From 42dda1a771a2a625a910253f61a102ef8cd4aa0c Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 6 Feb 2017 22:40:00 -0500 Subject: [PATCH 01/73] Disable soft wrap on mini editors --- src/text-editor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 10a6c9783..af9b7a341 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -169,7 +169,7 @@ class TextEditor extends Model unless @displayLayer? displayLayerParams = { invisibles: @getInvisibles(), - softWrapColumn: @getSoftWrapColumn(), + softWrapColumn: not @isMini() and @getSoftWrapColumn(), showIndentGuides: not @isMini() and @doesShowIndentGuide(), atomicSoftTabs: params.atomicSoftTabs ? true, tabLength: tabLength, From e5382215e5f35143fc91991bd67bb284c67f1ca3 Mon Sep 17 00:00:00 2001 From: Wliu Date: Mon, 6 Feb 2017 22:59:15 -0500 Subject: [PATCH 02/73] Add supplementary logic --- src/text-editor.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index af9b7a341..756af23f0 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -285,6 +285,7 @@ class TextEditor extends Model @mini = value @emitter.emit 'did-change-mini', value displayLayerParams.invisibles = @getInvisibles() + displayLayerParams.softWrapColumn = @getSoftWrapColumn() displayLayerParams.showIndentGuides = @doesShowIndentGuide() when 'placeholderText' @@ -2958,7 +2959,7 @@ class TextEditor extends Model # Essential: Gets the column at which column will soft wrap getSoftWrapColumn: -> - if @isSoftWrapped() + if @isSoftWrapped() and not @mini if @softWrapAtPreferredLineLength Math.min(@getEditorWidthInChars(), @preferredLineLength) else From 29c956e66f7962ed249d0b2a7e0732a13ec58be9 Mon Sep 17 00:00:00 2001 From: Wliu Date: Mon, 6 Feb 2017 22:59:47 -0500 Subject: [PATCH 03/73] Spec! --- spec/text-editor-spec.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 911270d16..9a2a44d6c 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5961,7 +5961,7 @@ describe "TextEditor", -> expect(editor.getGrammar().name).toBe 'CoffeeScript' describe "softWrapAtPreferredLineLength", -> - it "soft wraps the editor at the preferred line length unless the editor is narrower", -> + it "soft wraps the editor at the preferred line length unless the editor is narrower or the editor is mini", -> editor.update({ editorWidthInChars: 30 softWrapped: true @@ -5974,6 +5974,9 @@ describe "TextEditor", -> editor.update({editorWidthInChars: 10}) expect(editor.lineTextForScreenRow(0)).toBe 'var ' + editor.update({mini: true}) + expect(editor.lineTextForScreenRow(0)).toBe 'var quicksort = function () {' + describe "softWrapHangingIndentLength", -> it "controls how much extra indentation is applied to soft-wrapped lines", -> editor.setText('123456789') From d1c14e4f39dc0915cd6f226e2ab50e513f5f1ed5 Mon Sep 17 00:00:00 2001 From: Wliu Date: Tue, 7 Feb 2017 09:17:09 -0500 Subject: [PATCH 04/73] Remove un-necessary mini checks when initializing In addition, the boolean check I added for softWrapColumn was incorrect. --- src/text-editor.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 756af23f0..505e8fdec 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -169,8 +169,8 @@ class TextEditor extends Model unless @displayLayer? displayLayerParams = { invisibles: @getInvisibles(), - softWrapColumn: not @isMini() and @getSoftWrapColumn(), - showIndentGuides: not @isMini() and @doesShowIndentGuide(), + softWrapColumn: @getSoftWrapColumn(), + showIndentGuides: @doesShowIndentGuide(), atomicSoftTabs: params.atomicSoftTabs ? true, tabLength: tabLength, ratioForCharacter: @ratioForCharacter.bind(this), From b701bc790edfb0bc56fb894775e97c0204476fd4 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 8 Mar 2017 14:12:42 -0800 Subject: [PATCH 05/73] Add optional state key to AtomEnvironment::saveState and ::loadState --- src/atom-environment.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index d7fab1924..26831400d 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -874,12 +874,12 @@ class AtomEnvironment extends Model @blobStore.save() - saveState: (options) -> + saveState: (options, storageKey) -> new Promise (resolve, reject) => if @enablePersistence and @project state = @serialize(options) savePromise = - if storageKey = @getStateKey(@project?.getPaths()) + if storageKey ?= @getStateKey(@project?.getPaths()) @stateStore.save(storageKey, state) else @applicationDelegate.setTemporaryWindowState(state) @@ -887,9 +887,9 @@ class AtomEnvironment extends Model else resolve() - loadState: -> + loadState: (stateKey) -> if @enablePersistence - if stateKey = @getStateKey(@getLoadSettings().initialPaths) + if stateKey ?= @getStateKey(@getLoadSettings().initialPaths) @stateStore.load(stateKey).then (state) => if state state From 12ea7a7300eb235716ab71726952966814add27f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 8 Mar 2017 14:15:36 -0800 Subject: [PATCH 06/73] Add AtomEnvironment::restoreStateIntoEnvironment This takes an existing Atom environment and restores a saved state from the IndexDB state storage into it by: 1. Serializing the existing pane items 2. Restoring the saved state 3. Deserializing the saved pane items into the newly restored environment --- src/atom-environment.coffee | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 26831400d..51727fb30 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -863,6 +863,22 @@ class AtomEnvironment extends Model @pickFolder (selectedPaths = []) => @project.addPath(selectedPath) for selectedPath in selectedPaths + restoreStateIntoEnvironment: (state) -> + shouldSerializeItem = (item) -> + return true unless item instanceof TextEditor + item.getPath() or item.isModified() + serializedOpenItems = (item.serialize() for item in @workspace.getPaneItems() when shouldSerializeItem(item)) + serializedBuffers = (buffer.serialize() for buffer in @project.buffers) + + state.fullScreen = @isFullScreen() + pane.destroy() for pane in @workspace.getPanes() + @deserialize(state) + savedBuffers = (TextBuffer.deserialize(serializedBuffer) for serializedBuffer in serializedBuffers) + @project.buffers = @project.buffers.concat(savedBuffers) + + items = (@deserializers.deserialize(itemState) for itemState in serializedOpenItems) + @workspace.getPanes()[0].addItems(items, 0) + showSaveDialog: (callback) -> callback(@showSaveDialogSync()) From d891f2376ca1ad6e4453fdb417be40a7e52f02c2 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 8 Mar 2017 14:40:58 -0800 Subject: [PATCH 07/73] Restore saved state when adding folders to empty project --- spec/atom-environment-spec.coffee | 119 ++++++++++++++++++++++++++++-- src/atom-environment.coffee | 6 +- 2 files changed, 116 insertions(+), 9 deletions(-) diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index d1eabf2c8..17b9f51b0 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -321,14 +321,6 @@ describe "AtomEnvironment", -> expect(atom.workspace.open).not.toHaveBeenCalled() describe "adding a project folder", -> - it "adds a second path to the project", -> - initialPaths = atom.project.getPaths() - tempDirectory = temp.mkdirSync("a-new-directory") - spyOn(atom, "pickFolder").andCallFake (callback) -> - callback([tempDirectory]) - atom.addProjectFolder() - expect(atom.project.getPaths()).toEqual(initialPaths.concat([tempDirectory])) - it "does nothing if the user dismisses the file picker", -> initialPaths = atom.project.getPaths() tempDirectory = temp.mkdirSync("a-new-directory") @@ -336,6 +328,117 @@ describe "AtomEnvironment", -> atom.addProjectFolder() expect(atom.project.getPaths()).toEqual(initialPaths) + describe "when the project contains no folders", -> + describe "when there is saved state for the added folders", -> + projectPath = null + + beforeEach -> + atom.enablePersistence = true + [projectPath] = atom.project.getPaths() + waitsForPromise -> + Promise.all([ + atom.workspace.open(path.join(projectPath, 'script.js')) + atom.workspace.open(path.join(projectPath, 'sample.js')) + .then (e) -> e.insertText('changes') + ]) + + runs -> atom.workspace.getActivePane().splitRight() + waitsForPromise -> atom.workspace.open().then((e) -> e.setText('new editor')) + waitsForPromise -> atom.saveState() + runs -> atom.reset() + + afterEach -> + atom.enablePersistence = false + + it "restores the saved state", -> + spyOn(atom, "pickFolder").andCallFake (callback) -> + callback([projectPath]) + + waitsForPromise -> + atom.addProjectFolder() + + runs -> + expect(atom.project.getPaths()).toEqual([projectPath]) + expect(atom.workspace.getPanes().length).toEqual(2) + items = atom.workspace.getPaneItems() + expect(items.length).toEqual(3) + [unmodifiedNamedItem, modifiedNamedItem, modifiedUnnamedItem] = items + expect(unmodifiedNamedItem.getPath()).toEqual(path.join(projectPath, 'script.js')) + expect(unmodifiedNamedItem.isModified()).toBe(false) + expect(modifiedNamedItem.getPath()).toEqual(path.join(projectPath, 'sample.js')) + waitsFor -> modifiedNamedItem.isModified() + runs -> + expect(modifiedNamedItem.getText()).toMatch(/^changes/) + expect(modifiedUnnamedItem.getPath()).toEqual(undefined) + waitsFor -> modifiedUnnamedItem.isModified() + runs -> + expect(modifiedUnnamedItem.getText()).toEqual('new editor') + + it "maintains any existing dirty or named pane items", -> + # # TODO handle collisions + # waitsForPromise -> + # atom.workspace.open(path.join(projectPath, 'script.js')) + + waitsForPromise -> + Promise.all([ + atom.workspace.open(path.join(projectPath, 'css.css')) + atom.workspace.open(path.join(projectPath, 'lorem.txt')) + .then (e) -> e.insertText('changes') + atom.workspace.open().then (e) -> e.setText('another new editor') + atom.workspace.open() + ]) + + spyOn(atom, "pickFolder").andCallFake (callback) -> + callback([projectPath]) + + waitsForPromise -> + atom.addProjectFolder() + + runs -> + expect(atom.project.getPaths()).toEqual([projectPath]) + expect(atom.workspace.getPanes().length).toEqual(2) + items = atom.workspace.getPaneItems() + expect(items.length).toEqual(6) # 3 existing pane items, 3 from saved state + # discarded the empty, unnamed item (likely opened due to the "open empty editor on start" config option) + [modifiedUnnamedItem, unmodifiedNamedItem, modifiedNamedItem] = items + expect(unmodifiedNamedItem.getPath()).toEqual(path.join(projectPath, 'css.css')) + expect(unmodifiedNamedItem.isModified()).toBe(false) + expect(modifiedNamedItem.getPath()).toEqual(path.join(projectPath, 'lorem.txt')) + waitsFor -> modifiedNamedItem.isModified() + runs -> + expect(modifiedNamedItem.getText()).toMatch(/^changes/) + expect(modifiedUnnamedItem.getPath()).toEqual(undefined) + waitsFor -> modifiedUnnamedItem.isModified() + runs -> + expect(modifiedUnnamedItem.getText()).toEqual('another new editor') + + describe "when there is no saved state for the added folders", -> + beforeEach -> + spyOn(atom, 'loadState').andReturn(Promise.resolve(null)) + spyOn(atom, 'restoreStateIntoEnvironment') + + it "adds the selected folder to the project", -> + initialPaths = atom.project.setPaths([]) + tempDirectory = temp.mkdirSync("a-new-directory") + spyOn(atom, "pickFolder").andCallFake (callback) -> + callback([tempDirectory]) + waitsForPromise -> + atom.addProjectFolder() + runs -> + expect(atom.project.getPaths()).toEqual([tempDirectory]) + expect(atom.restoreStateIntoEnvironment).not.toHaveBeenCalled() + + describe "when the project already contains at least one folder", -> + it "adds a second path to the project", -> + initialPaths = atom.project.getPaths() + tempDirectory = temp.mkdirSync("a-new-directory") + spyOn(atom, "pickFolder").andCallFake (callback) -> + callback([tempDirectory]) + waitsForPromise -> + atom.addProjectFolder() + runs -> + expect(atom.project.getPaths()).toEqual(initialPaths.concat([tempDirectory])) + describe "::unloadEditorWindow()", -> it "saves the BlobStore so it can be loaded after reload", -> configDirPath = temp.mkdirSync('atom-spec-environment') diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 51727fb30..0ae9779cd 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -861,7 +861,11 @@ class AtomEnvironment extends Model addProjectFolder: -> @pickFolder (selectedPaths = []) => - @project.addPath(selectedPath) for selectedPath in selectedPaths + @loadState(@getStateKey(selectedPaths)).then (state) => + if state && @project.getPaths().length is 0 + @restoreStateIntoEnvironment(state) + else + @project.addPath(selectedPath) for selectedPath in selectedPaths restoreStateIntoEnvironment: (state) -> shouldSerializeItem = (item) -> From 2e2438ca27a2f7783e6074cec718ea44eb2090d8 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 8 Mar 2017 14:41:41 -0800 Subject: [PATCH 08/73] Restore saved state when adding folder to empty project via open command --- src/atom-environment.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 0ae9779cd..5cf6cf44d 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -1008,6 +1008,10 @@ class AtomEnvironment extends Model unless fs.isDirectorySync(pathToOpen) @workspace?.open(pathToOpen, {initialLine, initialColumn}) + if needsProjectPaths + @loadState(@getStateKey(@project.getPaths())).then (state) => + @restoreStateIntoEnvironment(state) if state + return # Preserve this deprecation until 2.0. Sorry. Should have removed Q sooner. From 62926e6b5b722670d2543a901f3f2f791e0142b7 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 16 Mar 2017 14:42:10 -0700 Subject: [PATCH 09/73] Don't mutate list during iteration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I saw a situation where this was calling `destroy()` on `undefined`— presumably because destroying one caused the list to be mutated elsewhere and the indexes to shift. --- src/project.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/project.coffee b/src/project.coffee index a02f27dac..f47f7033b 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -30,8 +30,8 @@ class Project extends Model @consumeServices(packageManager) destroyed: -> - buffer.destroy() for buffer in @buffers - repository?.destroy() for repository in @repositories + buffer.destroy() for buffer in @buffers.slice() + repository?.destroy() for repository in @repositories.slice() @rootDirectories = [] @repositories = [] From 7f5ad9a359aebfc6bc76df718f71bd20b34ed9e6 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Fri, 10 Mar 2017 16:42:11 -0800 Subject: [PATCH 10/73] Add workspace center --- src/workspace-center.js | 279 ++++++++++++++++++++++++++++++++++++++++ src/workspace.js | 11 ++ 2 files changed, 290 insertions(+) create mode 100644 src/workspace-center.js diff --git a/src/workspace-center.js b/src/workspace-center.js new file mode 100644 index 000000000..370d37b75 --- /dev/null +++ b/src/workspace-center.js @@ -0,0 +1,279 @@ +'use strict' + +const TextEditor = require('./text-editor') + +module.exports = class WorkspaceCenter { + constructor (paneContainer) { + this.paneContainer = paneContainer + } + + /* + Section: Event Subscription + */ + + // Essential: Invoke the given callback with all current and future text + // editors in the workspace. + // + // * `callback` {Function} to be called with current and future text editors. + // * `editor` An {TextEditor} that is present in {::getTextEditors} at the time + // of subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeTextEditors (callback) { + for (let textEditor of this.getTextEditors()) { callback(textEditor) } + return this.onDidAddTextEditor(({textEditor}) => callback(textEditor)) + } + + // Essential: Invoke the given callback with all current and future panes items + // in the workspace. + // + // * `callback` {Function} to be called with current and future pane items. + // * `item` An item that is present in {::getPaneItems} at the time of + // subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observePaneItems (callback) { return this.paneContainer.observePaneItems(callback) } + + // Essential: Invoke the given callback when the active pane item changes. + // + // Because observers are invoked synchronously, it's important not to perform + // any expensive operations via this method. Consider + // {::onDidStopChangingActivePaneItem} to delay operations until after changes + // stop occurring. + // + // * `callback` {Function} to be called when the active pane item changes. + // * `item` The active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActivePaneItem (callback) { + return this.paneContainer.onDidChangeActivePaneItem(callback) + } + + // Essential: Invoke the given callback when the active pane item stops + // changing. + // + // Observers are called asynchronously 100ms after the last active pane item + // change. Handling changes here rather than in the synchronous + // {::onDidChangeActivePaneItem} prevents unneeded work if the user is quickly + // changing or closing tabs and ensures critical UI feedback, like changing the + // highlighted tab, gets priority over work that can be done asynchronously. + // + // * `callback` {Function} to be called when the active pane item stopts + // changing. + // * `item` The active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidStopChangingActivePaneItem (callback) { + return this.paneContainer.onDidStopChangingActivePaneItem(callback) + } + + // Essential: Invoke the given callback with the current active pane item and + // with all future active pane items in the workspace. + // + // * `callback` {Function} to be called when the active pane item changes. + // * `item` The current active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActivePaneItem (callback) { + return this.paneContainer.observeActivePaneItem(callback) + } + + // Extended: Invoke the given callback when a pane is added to the workspace. + // + // * `callback` {Function} to be called panes are added. + // * `event` {Object} with the following keys: + // * `pane` The added pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddPane (callback) { + return this.paneContainer.onDidAddPane(callback) + } + + // Extended: Invoke the given callback before a pane is destroyed in the + // workspace. + // + // * `callback` {Function} to be called before panes are destroyed. + // * `event` {Object} with the following keys: + // * `pane` The pane to be destroyed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onWillDestroyPane (callback) { + return this.paneContainer.onWillDestroyPane(callback) + } + + // Extended: Invoke the given callback when a pane is destroyed in the + // workspace. + // + // * `callback` {Function} to be called panes are destroyed. + // * `event` {Object} with the following keys: + // * `pane` The destroyed pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroyPane (callback) { + return this.paneContainer.onDidDestroyPane(callback) + } + + // Extended: Invoke the given callback with all current and future panes in the + // workspace. + // + // * `callback` {Function} to be called with current and future panes. + // * `pane` A {Pane} that is present in {::getPanes} at the time of + // subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observePanes (callback) { + return this.paneContainer.observePanes(callback) + } + + // Extended: Invoke the given callback when the active pane changes. + // + // * `callback` {Function} to be called when the active pane changes. + // * `pane` A {Pane} that is the current return value of {::getActivePane}. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActivePane (callback) { + return this.paneContainer.onDidChangeActivePane(callback) + } + + // Extended: Invoke the given callback with the current active pane and when + // the active pane changes. + // + // * `callback` {Function} to be called with the current and future active# + // panes. + // * `pane` A {Pane} that is the current return value of {::getActivePane}. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActivePane (callback) { + return this.paneContainer.observeActivePane(callback) + } + + // Extended: Invoke the given callback when a pane item is added to the + // workspace. + // + // * `callback` {Function} to be called when pane items are added. + // * `event` {Object} with the following keys: + // * `item` The added pane item. + // * `pane` {Pane} containing the added item. + // * `index` {Number} indicating the index of the added item in its pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddPaneItem (callback) { + return this.paneContainer.onDidAddPaneItem(callback) + } + + // Extended: Invoke the given callback when a pane item is about to be + // destroyed, before the user is prompted to save it. + // + // * `callback` {Function} to be called before pane items are destroyed. + // * `event` {Object} with the following keys: + // * `item` The item to be destroyed. + // * `pane` {Pane} containing the item to be destroyed. + // * `index` {Number} indicating the index of the item to be destroyed in + // its pane. + // + // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. + onWillDestroyPaneItem (callback) { + return this.paneContainer.onWillDestroyPaneItem(callback) + } + + // Extended: Invoke the given callback when a pane item is destroyed. + // + // * `callback` {Function} to be called when pane items are destroyed. + // * `event` {Object} with the following keys: + // * `item` The destroyed item. + // * `pane` {Pane} containing the destroyed item. + // * `index` {Number} indicating the index of the destroyed item in its + // pane. + // + // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. + onDidDestroyPaneItem (callback) { + return this.paneContainer.onDidDestroyPaneItem(callback) + } + + /* + Section: Pane Items + */ + + // Essential: Get all pane items in the workspace. + // + // Returns an {Array} of items. + getPaneItems () { + return this.paneContainer.getPaneItems() + } + + // Essential: Get the active {Pane}'s active item. + // + // Returns an pane item {Object}. + getActivePaneItem () { + return this.paneContainer.getActivePaneItem() + } + + // Essential: Get all text editors in the workspace. + // + // Returns an {Array} of {TextEditor}s. + getTextEditors () { + return this.getPaneItems().filter(item => item instanceof TextEditor) + } + + // Essential: Get the active item if it is an {TextEditor}. + // + // Returns an {TextEditor} or `undefined` if the current active item is not an + // {TextEditor}. + getActiveTextEditor () { + const activeItem = this.getActivePaneItem() + if (activeItem instanceof TextEditor) { return activeItem } + } + + // Save all pane items. + saveAll () { + this.paneContainer.saveAll() + } + + confirmClose (options) { + return this.paneContainer.confirmClose(options) + } + + /* + Section: Panes + */ + + // Extended: Get all panes in the workspace. + // + // Returns an {Array} of {Pane}s. + getPanes () { + return this.paneContainer.getPanes() + } + + // Extended: Get the active {Pane}. + // + // Returns a {Pane}. + getActivePane () { + return this.paneContainer.getActivePane() + } + + // Extended: Make the next pane active. + activateNextPane () { + return this.paneContainer.activateNextPane() + } + + // Extended: Make the previous pane active. + activatePreviousPane () { + return this.paneContainer.activatePreviousPane() + } + + paneForURI (uri) { + return this.paneContainer.paneForURI(uri) + } + + paneForItem (item) { + return this.paneContainer.paneForItem(item) + } + + // Destroy (close) the active pane. + destroyActivePane () { + const activePane = this.getActivePane() + if (activePane != null) { + activePane.destroy() + } + } +} diff --git a/src/workspace.js b/src/workspace.js index 6c5daba12..04b3f89e5 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -13,6 +13,7 @@ const PaneContainer = require('./pane-container') const Panel = require('./panel') const PanelContainer = require('./panel-container') const Task = require('./task') +const WorkspaceCenter = require('./workspace-center') // Essential: Represents the state of the user interface for the entire window. // An instance of this class is available via the `atom.workspace` global. @@ -57,6 +58,8 @@ module.exports = class Workspace extends Model { this.defaultDirectorySearcher = new DefaultDirectorySearcher() this.consumeServices(this.packageManager) + this.center = new WorkspaceCenter(this.paneContainer) + this.panelContainers = { top: new PanelContainer({location: 'top'}), left: new PanelContainer({location: 'left'}), @@ -1008,6 +1011,14 @@ module.exports = class Workspace extends Model { } } + /* + Section: Pane Locations + */ + + getCenter () { + return this.center + } + /* Section: Panels From bf39947eee2f84d6db8a19740a117d568744fedc Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 9 Mar 2017 11:26:10 -0800 Subject: [PATCH 11/73] Add Dock component --- src/dock.js | 506 +++++++++++++++++++++++++++++++++ src/panel-container-element.js | 6 + src/panel-container.js | 3 +- src/workspace-element.js | 108 ++++++- src/workspace.js | 53 +++- static/atom.less | 1 + static/docks.less | 214 ++++++++++++++ 7 files changed, 882 insertions(+), 9 deletions(-) create mode 100644 src/dock.js create mode 100644 static/docks.less diff --git a/src/dock.js b/src/dock.js new file mode 100644 index 000000000..9bcea6e57 --- /dev/null +++ b/src/dock.js @@ -0,0 +1,506 @@ +'use strict' + +const _ = require('underscore-plus') +const {CompositeDisposable} = require('event-kit') +const PaneContainer = require('./pane-container') +const TextEditor = require('./text-editor') + +const MINIMUM_SIZE = 100 +const DEFAULT_INITIAL_SIZE = 300 +const HANDLE_SIZE = 4 +const SHOULD_ANIMATE_CLASS = 'atom-dock-should-animate' +const OPEN_CLASS = 'atom-dock-open' +const RESIZE_HANDLE_RESIZABLE_CLASS = 'atom-dock-resize-handle-resizable' +const TOGGLE_BUTTON_VISIBLE_CLASS = 'atom-dock-toggle-button-visible' +const CURSOR_OVERLAY_VISIBLE_CLASS = 'atom-dock-cursor-overlay-visible' + +// Extended: A container at the edges of the editor window capable of holding items. +// You should not create a `Dock` directly, instead use {Workspace::open}. +module.exports = class Dock { + constructor (params) { + this.handleResizeHandleDragStart = this.handleResizeHandleDragStart.bind(this) + this.handleMouseMove = this.handleMouseMove.bind(this) + this.handleMouseUp = this.handleMouseUp.bind(this) + this.handleDrag = _.throttle(this.handleDrag.bind(this), 30) + this.handleDragEnd = this.handleDragEnd.bind(this) + + this.location = params.location + this.widthOrHeight = getWidthOrHeight(this.location) + this.config = params.config + this.applicationDelegate = params.applicationDelegate + this.deserializerManager = params.deserializerManager + this.notificationManager = params.notificationManager + this.viewRegistry = params.viewRegistry + + this.paneContainer = new PaneContainer({ + config: this.config, + applicationDelegate: this.applicationDelegate, + deserializerManager: this.deserializerManager, + notificationManager: this.notificationManager, + views: this.viewRegistry + }) + + this.state = { + open: false, + shouldAnimate: false + } + + this.subscriptions = new CompositeDisposable( + this.paneContainer.observePanes(pane => { + pane.onDidAddItem(this.handleDidAddPaneItem.bind(this)) + }), + this.paneContainer.observePanes(pane => { + pane.onDidRemoveItem(this.handleDidRemovePaneItem.bind(this)) + }) + ) + } + + // FIXME(matthewwithanm: This is kinda gross. We need to get a view for the pane container so we + // have to make sure that this is called after its view provider is registered. But we really + // don't have any guarantees about that. + getElement () { + this.render(this.state) + return this.element + } + + getLocation () { + return this.location + } + + destroy () { + this.subscriptions.dispose() + this.paneContainer.destroy() + this.resizeHandle.destroy() + this.toggleButton.destroy() + window.removeEventListener('mousemove', this.handleMouseMove) + window.removeEventListener('mouseup', this.handleMouseUp) + window.removeEventListener('drag', this.handleDrag) + window.removeEventListener('dragend', this.handleDragEnd) + } + + setHovered (hovered) { + if (hovered === this.state.hovered) return + this.setState({hovered}) + } + + setDraggingItem (draggingItem) { + if (draggingItem === this.state.draggingItem) return + this.setState({draggingItem}) + } + + toggle () { + this.setState({open: !this.state.open}) + } + + setState (newState) { + const prevState = this.state + const nextState = Object.assign({}, prevState, newState) + + // Update the `shouldAnimate` state. This needs to be written to the DOM before updating the + // class that changes the animated property. Normally we'd have to defer the class change a + // frame to ensure the property is animated (or not) appropriately, however we luck out in this + // case because the drag start always happens before the item is dragged into the toggle button. + if (nextState.open !== prevState.open) { + // Never animate toggling visiblity... + nextState.shouldAnimate = false + } else if (!nextState.open && nextState.draggingItem && !prevState.draggingItem) { + // ...but do animate if you start dragging while the panel is hidden. + nextState.shouldAnimate = true + } + + this.state = nextState + this.render(this.state) + } + + render (state) { + if (this.element == null) { + this.element = document.createElement('atom-dock') + this.element.classList.add(this.location) + this.innerElement = document.createElement('div') + this.innerElement.classList.add('atom-dock-inner', this.location) + this.maskElement = document.createElement('div') + this.maskElement.classList.add('atom-dock-mask') + this.wrapperElement = document.createElement('div') + this.wrapperElement.classList.add('atom-dock-content-wrapper', this.location) + this.resizeHandle = new DockResizeHandle({ + location: this.location, + onResizeStart: this.handleResizeHandleDragStart, + toggle: this.toggle.bind(this) + }) + this.toggleButton = new DockToggleButton({ + onDragEnter: this.handleToggleButtonDragEnter.bind(this), + location: this.location, + toggle: this.toggle.bind(this) + }) + this.cursorOverlayElement = document.createElement('div') + this.cursorOverlayElement.classList.add('atom-dock-cursor-overlay', this.location) + + // Add the children to the DOM tree + this.element.appendChild(this.innerElement) + this.innerElement.appendChild(this.maskElement) + this.maskElement.appendChild(this.wrapperElement) + this.wrapperElement.appendChild(this.viewRegistry.getView(this.resizeHandle)) + this.wrapperElement.appendChild(this.viewRegistry.getView(this.paneContainer)) + this.wrapperElement.appendChild(this.cursorOverlayElement) + // The toggle button must be rendered outside the mask because (1) it shouldn't be masked and + // (2) if we made the mask larger to avoid masking it, the mask would block mouse events. + this.innerElement.appendChild(this.viewRegistry.getView(this.toggleButton)) + } + + if (state.open) { + this.innerElement.classList.add(OPEN_CLASS) + } else { + this.innerElement.classList.remove(OPEN_CLASS) + } + + if (state.shouldAnimate) { + this.maskElement.classList.add(SHOULD_ANIMATE_CLASS) + } else { + this.maskElement.classList.remove(SHOULD_ANIMATE_CLASS) + } + + if (state.resizing) { + this.cursorOverlayElement.classList.add(CURSOR_OVERLAY_VISIBLE_CLASS) + } else { + this.cursorOverlayElement.classList.remove(CURSOR_OVERLAY_VISIBLE_CLASS) + } + + const shouldBeVisible = state.open || state.showDropTarget + const size = Math.max(MINIMUM_SIZE, state.size == null ? this.getInitialSize() : state.size) + + // We need to change the size of the mask... + this.maskElement.style[this.widthOrHeight] = `${shouldBeVisible ? size : HANDLE_SIZE}px` + // ...but the content needs to maintain a constant size. + this.wrapperElement.style[this.widthOrHeight] = `${size}px` + + this.resizeHandle.update({dockIsOpen: this.state.open}) + this.toggleButton.update({ + open: shouldBeVisible, + visible: state.hovered || (state.draggingItem && !shouldBeVisible) + }) + } + + handleDidAddPaneItem () { + // Show the dock if you drop an item into it. + if (this.paneContainer.getPaneItems().length === 1) { + this.setState({open: true}) + } + } + + handleDidRemovePaneItem () { + // Hide the dock if you remove the last item. + if (this.paneContainer.getPaneItems().length === 0) { + this.setState({open: false}) + } + } + + handleResizeHandleDragStart () { + window.addEventListener('mousemove', this.handleMouseMove) + window.addEventListener('mouseup', this.handleMouseUp) + this.setState({resizing: true}) + } + + handleMouseMove (event) { + if (event.buttons === 0) { // We missed the mouseup event. For some reason it happens on Windows + this.handleMouseUp(event) + return + } + + let size = 0 + switch (this.location) { + case 'left': + size = event.pageX - this.element.getBoundingClientRect().left + break + case 'bottom': + size = this.element.getBoundingClientRect().bottom - event.pageY + break + case 'right': + size = this.element.getBoundingClientRect().right - event.pageX + break + } + this.setState({size}) + } + + handleMouseUp (event) { + window.removeEventListener('mousemove', this.handleMouseMove) + window.removeEventListener('mouseup', this.handleMouseUp) + this.setState({resizing: false}) + } + + handleToggleButtonDragEnter () { + this.setState({showDropTarget: true}) + window.addEventListener('drag', this.handleDrag) + window.addEventListener('dragend', this.handleDragEnd) + } + + handleDrag (event) { + if (!this.pointWithinHoverArea({x: event.pageX, y: event.pageY}, false)) { + this.draggedOut() + } + } + + handleDragEnd () { + this.draggedOut() + } + + draggedOut () { + this.setState({showDropTarget: false}) + window.removeEventListener('drag', this.handleDrag) + window.removeEventListener('dragend', this.handleDragEnd) + } + + // Determine whether the cursor is within the dock hover area. This isn't as simple as just using + // mouseenter/leave because we want to be a little more forgiving. For example, if the cursor is + // over the footer, we want to show the bottom dock's toggle button. + pointWithinHoverArea (point, includeButtonWidth) { + const dockBounds = this.innerElement.getBoundingClientRect() + // Copy the bounds object since we can't mutate it. + const bounds = { + top: dockBounds.top, + right: dockBounds.right, + bottom: dockBounds.bottom, + left: dockBounds.left + } + + // Include all panels that are closer to the edge than the dock in our calculations. + switch (this.location) { + case 'right': + bounds.right = Number.POSITIVE_INFINITY + break + case 'bottom': + bounds.bottom = Number.POSITIVE_INFINITY + break + case 'left': + bounds.left = 0 + break + } + + // The area used when detecting "leave" events is actually larger than when detecting entrances. + if (includeButtonWidth) { + const affordance = 20 + const toggleButtonSize = 50 / 2 // This needs to match the value in the CSS. + switch (this.location) { + case 'right': + bounds.left -= toggleButtonSize + affordance + break + case 'bottom': + bounds.top -= toggleButtonSize + affordance + break + case 'left': + bounds.right += toggleButtonSize + affordance + break + } + } + return rectContainsPoint(bounds, point) + } + + getInitialSize () { + let initialSize + // The item may not have been activated yet. If that's the case, just use the first item. + const activePaneItem = this.paneContainer.getActivePaneItem() || this.paneContainer.getPaneItems()[0] + if (activePaneItem != null) { + initialSize = getPreferredInitialSize(activePaneItem, this.location) + } + return initialSize == null ? DEFAULT_INITIAL_SIZE : initialSize + } + + // PaneContainer-delegating methods + + getPanes () { + return this.paneContainer.getPanes() + } + + observePanes (fn) { + return this.paneContainer.observePanes(fn) + } + + onDidAddPane (fn) { + return this.paneContainer.onDidAddPane(fn) + } + + onWillDestroyPane (fn) { + return this.paneContainer.onWillDestroyPane(fn) + } + + onDidDestroyPane (fn) { + return this.paneContainer.onDidDestroyPane(fn) + } + + paneForURI (uri) { + return this.paneContainer.paneForURI(uri) + } + + paneForItem (item) { + return this.paneContainer.paneForItem(item) + } + + getActivePane () { + return this.paneContainer.getActivePane() + } + + getPaneItems () { + return this.paneContainer.getPaneItems() + } + + getActivePaneItem () { + return this.paneContainer.getActivePaneItem() + } + + getTextEditors () { + return this.paneContainer.getTextEditors() + } + + getActiveTextEditor () { + const activeItem = this.getActivePaneItem() + if (activeItem instanceof TextEditor) { return activeItem } + } + + observePaneItems (fn) { + return this.paneContainer.observePaneItems(fn) + } + + onDidAddPaneItem (fn) { + return this.paneContainer.onDidAddPaneItem(fn) + } + + onWillDestroyPaneItem (fn) { + return this.paneContainer.onWillDestroyPaneItem(fn) + } + + onDidDestroyPaneItem (fn) { + return this.paneContainer.onDidDestroyPaneItem(fn) + } + + saveAll () { + this.paneContainer.saveAll() + } + + confirmClose (options) { + return this.paneContainer.confirmClose(options) + } +} + +class DockResizeHandle { + constructor (props) { + this.handleMouseDown = this.handleMouseDown.bind(this) + this.handleClick = this.handleClick.bind(this) + + this.element = document.createElement('div') + this.element.classList.add('atom-dock-resize-handle', props.location) + this.element.addEventListener('mousedown', this.handleMouseDown) + this.element.addEventListener('click', this.handleClick) + const widthOrHeight = getWidthOrHeight(props.location) + this.element.style[widthOrHeight] = `${HANDLE_SIZE}px` + this.props = props + this.update(props) + } + + update (newProps) { + this.props = Object.assign({}, this.props, newProps) + + if (this.props.dockIsOpen) { + this.element.classList.add(RESIZE_HANDLE_RESIZABLE_CLASS) + } else { + this.element.classList.remove(RESIZE_HANDLE_RESIZABLE_CLASS) + } + } + + destroy () { + this.element.removeEventListener('mousedown', this.handleMouseDown) + this.element.removeEventListener('click', this.handleClick) + } + + handleClick () { + if (!this.props.dockIsOpen) { + this.props.toggle() + } + } + + handleMouseDown () { + if (this.props.dockIsOpen) { + this.props.onResizeStart() + } + } +} + +class DockToggleButton { + constructor (props) { + this.handleClick = this.handleClick.bind(this) + this.handleDragEnter = this.handleDragEnter.bind(this) + + this.element = document.createElement('div') + this.element.classList.add('atom-dock-toggle-button', props.location) + this.element.classList.add(props.location) + this.innerElement = document.createElement('div') + this.innerElement.classList.add('atom-dock-toggle-button-inner', props.location) + this.innerElement.addEventListener('click', this.handleClick) + this.innerElement.addEventListener('dragenter', this.handleDragEnter) + this.iconElement = document.createElement('span') + this.innerElement.appendChild(this.iconElement) + this.element.appendChild(this.innerElement) + + this.props = props + this.update(props) + } + + destroy () { + this.innerElement.removeEventListener('click', this.handleClick) + this.innerElement.removeEventListener('dragenter', this.handleDragEnter) + } + + update (newProps) { + this.props = Object.assign({}, this.props, newProps) + + if (this.props.visible) { + this.element.classList.add(TOGGLE_BUTTON_VISIBLE_CLASS) + } else { + this.element.classList.remove(TOGGLE_BUTTON_VISIBLE_CLASS) + } + + this.iconElement.className = 'icon ' + getIconName(this.props.location, this.props.open) + } + + handleClick () { + this.props.toggle() + } + + handleDragEnter () { + this.props.onDragEnter() + } +} + +function getWidthOrHeight (location) { + return location === 'left' || location === 'right' ? 'width' : 'height' +} + +function getPreferredInitialSize (item, location) { + switch (location) { + case 'left': + case 'right': + return typeof item.getPreferredInitialWidth === 'function' + ? item.getPreferredInitialWidth() + : null + default: + return typeof item.getPreferredInitialHeight === 'function' + ? item.getPreferredInitialHeight() + : null + } +} + +function getIconName (location, open) { + switch (location) { + case 'right': return open ? 'icon-chevron-right' : 'icon-chevron-left' + case 'bottom': return open ? 'icon-chevron-down' : 'icon-chevron-up' + case 'left': return open ? 'icon-chevron-left' : 'icon-chevron-right' + default: throw new Error(`Invalid location: ${location}`) + } +} + +function rectContainsPoint (rect, point) { + return ( + point.x >= rect.left && + point.y >= rect.top && + point.x <= rect.right && + point.y <= rect.bottom + ) +} diff --git a/src/panel-container-element.js b/src/panel-container-element.js index dbc595186..2571d9875 100644 --- a/src/panel-container-element.js +++ b/src/panel-container-element.js @@ -19,6 +19,12 @@ class PanelContainerElement extends HTMLElement { this.subscriptions.add(this.model.onDidAddPanel(this.panelAdded.bind(this))) this.subscriptions.add(this.model.onDidDestroy(this.destroyed.bind(this))) this.classList.add(this.model.getLocation()) + + // Add the dock. + if (this.model.dock != null) { + this.appendChild(this.views.getView(this.model.dock)) + } + return this } diff --git a/src/panel-container.js b/src/panel-container.js index 377b4cd97..943fda4c4 100644 --- a/src/panel-container.js +++ b/src/panel-container.js @@ -3,11 +3,12 @@ const {Emitter, CompositeDisposable} = require('event-kit') module.exports = class PanelContainer { - constructor ({location} = {}) { + constructor ({location, dock} = {}) { this.location = location this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.panels = [] + this.dock = dock } destroy () { diff --git a/src/workspace-element.js b/src/workspace-element.js index 65333e7d3..641e58992 100644 --- a/src/workspace-element.js +++ b/src/workspace-element.js @@ -5,8 +5,9 @@ const {ipcRenderer} = require('electron') const path = require('path') const fs = require('fs-plus') -const {CompositeDisposable} = require('event-kit') +const {CompositeDisposable, Disposable} = require('event-kit') const scrollbarStyle = require('scrollbar-style') +const _ = require('underscore-plus') class WorkspaceElement extends HTMLElement { attachedCallback () { @@ -64,6 +65,14 @@ class WorkspaceElement extends HTMLElement { } initialize (model, {views, workspace, project, config, styles}) { + this.handleCenterEnter = this.handleCenterEnter.bind(this) + this.handleCenterLeave = this.handleCenterLeave.bind(this) + this.handleEdgesMouseMove = _.throttle(this.handleEdgesMouseMove.bind(this), 100) + this.handleDockDragEnd = this.handleDockDragEnd.bind(this) + this.handleDragStart = this.handleDragStart.bind(this) + this.handleDragEnd = this.handleDragEnd.bind(this) + this.handleDrop = this.handleDrop.bind(this) + this.model = model this.views = views this.workspace = workspace @@ -76,7 +85,17 @@ class WorkspaceElement extends HTMLElement { if (this.config == null) { throw new Error('Must pass a config parameter when initializing WorskpaceElements') } if (this.styles == null) { throw new Error('Must pass a styles parameter when initializing WorskpaceElements') } - this.subscriptions = new CompositeDisposable() + this.subscriptions = new CompositeDisposable( + new Disposable(() => { + window.removeEventListener('mouseenter', this.handleCenterEnter) + window.removeEventListener('mouseleave', this.handleCenterLeave) + window.removeEventListener('mousemove', this.handleEdgesMouseMove) + window.removeEventListener('dragend', this.handleDockDragEnd) + window.removeEventListener('dragstart', this.handleDragStart) + window.removeEventListener('dragend', this.handleDragEnd, true) + window.removeEventListener('drop', this.handleDrop, true) + }) + ) this.initializeContent() this.observeScrollbarStyle() this.observeTextEditorFontConfig() @@ -86,6 +105,7 @@ class WorkspaceElement extends HTMLElement { this.addEventListener('focus', this.handleFocus.bind(this)) this.addEventListener('mousewheel', this.handleMousewheel.bind(this), true) + window.addEventListener('dragstart', this.handleDragStart) this.panelContainers = { top: this.views.getView(this.model.panelContainers.top), @@ -108,11 +128,86 @@ class WorkspaceElement extends HTMLElement { this.appendChild(this.panelContainers.modal) + this.paneContainer.addEventListener('mouseenter', this.handleCenterEnter) + this.paneContainer.addEventListener('mouseleave', this.handleCenterLeave) + return this } getModel () { return this.model } + handleDragStart (event) { + if (!isTab(event.target)) return + this.model.setDraggingItem(true) + window.addEventListener('dragend', this.handleDragEnd, true) + window.addEventListener('drop', this.handleDrop, true) + } + + handleDragEnd (event) { + this.dragEnded() + } + + handleDrop (event) { + this.dragEnded() + } + + dragEnded () { + this.model.setDraggingItem(false) + window.removeEventListener('dragend', this.handleDragEnd, true) + window.removeEventListener('drop', this.handleDrop, true) + } + + handleCenterEnter (event) { + // Just re-entering the center isn't enough to hide the dock toggle buttons, since they poke + // into the center and we want to give an affordance. + this.cursorInCenter = true + this.checkCleanupDockHoverEvents() + } + + handleCenterLeave (event) { + // If the cursor leaves the center, we start listening to determine whether one of the docs is + // being hovered. + this.cursorInCenter = false + this.updateHoveredDock({x: event.pageX, y: event.pageY}) + window.addEventListener('mousemove', this.handleEdgesMouseMove) + window.addEventListener('dragend', this.handleDockDragEnd) + } + + handleEdgesMouseMove (event) { + this.updateHoveredDock({x: event.pageX, y: event.pageY}) + } + + handleDockDragEnd (event) { + this.updateHoveredDock({x: event.pageX, y: event.pageY}) + } + + updateHoveredDock (mousePosition) { + // See if we've left the currently hovered dock's area. + if (this.model.hoveredDock) { + const hideToggleButton = !this.model.hoveredDock.pointWithinHoverArea(mousePosition, true) + if (hideToggleButton) { + this.model.setHoveredDock(null) + } + } + // See if we've moved over a dock. + if (this.model.hoveredDock == null) { + const hoveredDock = _.values(this.model.docks).find( + dock => dock.pointWithinHoverArea(mousePosition, false) + ) + if (hoveredDock != null) { + this.model.setHoveredDock(hoveredDock) + } + } + this.checkCleanupDockHoverEvents() + } + + checkCleanupDockHoverEvents () { + if (this.cursorInCenter && !this.model.hoveredDock) { + window.removeEventListener('mousemove', this.handleEdgesMouseMove) + window.removeEventListener('dragend', this.handleDockDragEnd) + } + } + handleMousewheel (event) { if (event.ctrlKey && this.config.get('editor.zoomFontWhenCtrlScrolling') && (event.target.closest('atom-text-editor') != null)) { if (event.wheelDeltaY > 0) { @@ -182,3 +277,12 @@ class WorkspaceElement extends HTMLElement { } module.exports = document.registerElement('atom-workspace', {prototype: WorkspaceElement.prototype}) + +function isTab (element) { + let el = element + while (el != null) { + if (el.getAttribute('is') === 'tabs-tab') { return true } + el = el.parentElement + } + return false +} diff --git a/src/workspace.js b/src/workspace.js index 04b3f89e5..98e945b18 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -7,6 +7,7 @@ const {Emitter, Disposable, CompositeDisposable} = require('event-kit') const fs = require('fs-plus') const {Directory} = require('pathwatcher') const DefaultDirectorySearcher = require('./default-directory-searcher') +const Dock = require('./dock') const Model = require('./model') const TextEditor = require('./text-editor') const PaneContainer = require('./pane-container') @@ -42,6 +43,8 @@ module.exports = class Workspace extends Model { this.assert = params.assert this.deserializerManager = params.deserializerManager this.textEditorRegistry = params.textEditorRegistry + this.hoveredDock = null + this.draggingItem = false this.emitter = new Emitter() this.openers = [] @@ -59,12 +62,17 @@ module.exports = class Workspace extends Model { this.consumeServices(this.packageManager) this.center = new WorkspaceCenter(this.paneContainer) + this.docks = { + left: this.createDock('left'), + right: this.createDock('right'), + bottom: this.createDock('bottom') + } this.panelContainers = { top: new PanelContainer({location: 'top'}), - left: new PanelContainer({location: 'left'}), - right: new PanelContainer({location: 'right'}), - bottom: new PanelContainer({location: 'bottom'}), + left: new PanelContainer({location: 'left', dock: this.docks.left}), + right: new PanelContainer({location: 'right', dock: this.docks.right}), + bottom: new PanelContainer({location: 'bottom', dock: this.docks.bottom}), header: new PanelContainer({location: 'header'}), footer: new PanelContainer({location: 'footer'}), modal: new PanelContainer({location: 'modal'}) @@ -73,6 +81,19 @@ module.exports = class Workspace extends Model { this.subscribeToEvents() } + createDock (location) { + const dock = new Dock({ + location, + config: this.config, + applicationDelegate: this.applicationDelegate, + deserializerManager: this.deserializerManager, + notificationManager: this.notificationManager, + viewRegistry: this.viewRegistry + }) + dock.onDidDestroyPaneItem(this.didDestroyPaneItem) + return dock + } + reset (packageManager) { this.packageManager = packageManager this.emitter.dispose() @@ -89,11 +110,18 @@ module.exports = class Workspace extends Model { }) this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem) + this.center = new WorkspaceCenter(this.paneContainer) + this.docks = { + left: this.createDock('left'), + right: this.createDock('right'), + bottom: this.createDock('bottom') + } + this.panelContainers = { top: new PanelContainer({location: 'top'}), - left: new PanelContainer({location: 'left'}), - right: new PanelContainer({location: 'right'}), - bottom: new PanelContainer({location: 'bottom'}), + left: new PanelContainer({location: 'left', dock: this.docks.left}), + right: new PanelContainer({location: 'right', dock: this.docks.right}), + bottom: new PanelContainer({location: 'bottom', dock: this.docks.bottom}), header: new PanelContainer({location: 'header'}), footer: new PanelContainer({location: 'footer'}), modal: new PanelContainer({location: 'modal'}) @@ -172,6 +200,19 @@ module.exports = class Workspace extends Model { return _.uniq(packageNames) } + setHoveredDock (hoveredDock) { + this.hoveredDock = hoveredDock + _.values(this.docks).forEach(dock => { + dock.setHovered(dock === hoveredDock) + }) + } + + setDraggingItem (draggingItem) { + _.values(this.docks).forEach(dock => { + dock.setDraggingItem(draggingItem) + }) + } + subscribeToActiveItem () { this.updateWindowTitle() this.updateDocumentEdited() diff --git a/static/atom.less b/static/atom.less index 6a8c688e2..caa1e1c6b 100644 --- a/static/atom.less +++ b/static/atom.less @@ -18,6 +18,7 @@ // Core components @import "cursors"; @import "panels"; +@import "docks"; @import "panes"; @import "syntax"; @import "text-editor-light"; diff --git a/static/docks.less b/static/docks.less new file mode 100644 index 000000000..c8761707a --- /dev/null +++ b/static/docks.less @@ -0,0 +1,214 @@ +@import 'ui-variables'; +@import 'syntax-variables'; + +@atom-dock-toggle-button-size: 50px; + +// Dock -------------- + +// The actual dock element is used as a kind of placeholder in the DOM, relative +// to which its children can be positioned. +atom-dock { + display: flex; + position: relative; +} + +.atom-dock-inner { + display: flex; + + &.bottom { width: 100%; } + &.left, &.right { height: 100%; } + + // Make sure to center the toggle buttons + &.bottom { flex-direction: column; } + align-items: center; + + // Position the docks flush with their side of the editor. + &.right { right: 0; } + &.bottom { bottom: 0; } + &.left { left: 0; } + + // Position the docks flush with their side of the editor. + &.right { right: 0; } + &.bottom { bottom: 0; } + &.left { left: 0; } + + &:not(.atom-dock-open) { + // The dock should only take up space when it's active (i.e. it shouldn't + // take up space when you're dragging something into it). + position: absolute; + z-index: 10; // An arbitrary number. Seems high enough. ¯\_(ツ)_/¯ + } +} + +.atom-dock-mask { + position: relative; + background-color: @tool-panel-background-color; + overflow: hidden; // Mask the content. + + // This shouldn't technically be necessary. Apparently, there's a bug in + // Chrome whereby the 100% width (in the bottom dock) and height (in left and + // right docks) won't actually take effect when the docks are given more + // space because another dock is hidden. Unsetting and resetting the width + // will correct the issue, as will changing its "display." However, only this + // seems to fix it without an actual runtime change occurring. + flex: 1; + + // One of these will be overridden by the component with an explicit size. + // Which depends on the position of the dock. + width: 100%; + height: 100%; + + transition: none; + &.atom-dock-should-animate { + transition: width 0.2s ease-out, height 0.2s ease-out; + } +} + +.atom-dock-content-wrapper { + position: absolute; + display: flex; + flex: 1; + align-items: stretch; + width: 100%; + height: 100%; + cursor: default; + -webkit-user-select: none; + white-space: nowrap; + + // The contents of the dock should be "stuck" to the moving edge of the mask, + // so it looks like they're sliding in (instead of being unmasked in place). + &.right { left: 0; } + &.bottom { top: 0; } + &.left { right: 0; } + + // Use flex-direction to put the resize handle in the correct place. + &.left { flex-direction: row-reverse; } + &.bottom { flex-direction: column; } + &.right { flex-direction: row; } +} + +// Toggle button -------------- + +.atom-dock-toggle-button { + position: absolute; + overflow: hidden; // Mask half of the circle. + + // Must be > .scrollbar-content and inactive atom-dock + z-index: 11; + + // Position the toggle button target at the edge of the dock. It's important + // that this is absolutely positioned so that it doesn't expand the area of + // its container (which would block mouse events). + &.right { right: 100%; } + &.bottom { bottom: 100%; } + &.left { left: 100%; } + + width: @atom-dock-toggle-button-size; + height: @atom-dock-toggle-button-size; + &.bottom { height: @atom-dock-toggle-button-size / 2; } + &.left, &.right { width: @atom-dock-toggle-button-size / 2; } + + .atom-dock-toggle-button-inner { + width: @atom-dock-toggle-button-size; + height: @atom-dock-toggle-button-size; + border-radius: @atom-dock-toggle-button-size / 2; + + position: absolute; + display: flex; + text-align: center; + justify-content: center; + align-items: center; + cursor: pointer; + + &.right { left: 0; } + &.bottom { top: 0; } + &.left { right: 0; } + } + + // Hide the button. + &:not(.atom-dock-toggle-button-visible) { + .atom-dock-toggle-button-inner { + &.right { transform: translateX(50%); } + &.bottom { transform: translateY(50%); } + &.left { transform: translateX(-50%); } + } + } + + // Center the icon. + @offset: 8px; + .atom-dock-toggle-button-inner { + &.right .icon { transform: translateX(-@offset); } + &.bottom .icon { transform: translateY(-@offset); } + &.left .icon { transform: translateX(@offset); } + } + + // Animate the icon. + .icon { + transition: opacity 0.1s ease-in 0.1s; // intro + opacity: 1; + + &::before { + // Shrink the icon element to the size of the character. + width: auto; + margin: 0; + } + } + &:not(.atom-dock-toggle-button-visible) .icon { + opacity: 0; + transition: opacity 0.2s ease-out 0s; // outro + } + + .atom-dock-toggle-button-inner { + background-color: @tool-panel-background-color; + border: 1px solid @pane-item-border-color; + transition: transform 0.2s ease-out 0s; // intro + } + + &:not(.atom-dock-toggle-button-visible) { + // Don't contribute to mouseenter/drag events when not visible. + pointer-events: none; + + .atom-dock-toggle-button-inner { + transition: transform 0.2s ease-out 0.1s; // outro + } + } +} + +// Resize handle -------------- + +.atom-dock-resize-handle { + width: auto; + height: auto; + flex: 0 0 auto; + cursor: pointer; + + // Use the resize cursor when the handle's resizable + &.atom-dock-resize-handle-resizable { + &.left, &.right { cursor: col-resize; } + &.bottom { cursor: row-resize; } + } +} + +// Cursor overlay -------------- + +.atom-dock-cursor-overlay { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 4; + + &.left, + &.right { + cursor: col-resize; + } + + &.bottom { + cursor: row-resize; + } + + &:not(.atom-dock-cursor-overlay-visible) { + display: none; + } +} From 3ff830102f4ea620431950ce69ba183f6b794e50 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 9 Mar 2017 11:26:48 -0800 Subject: [PATCH 12/73] Serialize docks --- src/atom-environment.coffee | 2 ++ src/dock.js | 18 ++++++++++++++++++ src/workspace.js | 15 +++++++++++++-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 7594f5de9..9db5330d3 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -38,6 +38,7 @@ Panel = require './panel' PaneContainer = require './pane-container' PaneAxis = require './pane-axis' Pane = require './pane' +Dock = require './dock' Project = require './project' TextEditor = require './text-editor' TextBuffer = require 'text-buffer' @@ -254,6 +255,7 @@ class AtomEnvironment extends Model @deserializers.add(PaneContainer) @deserializers.add(PaneAxis) @deserializers.add(Pane) + @deserializers.add(Dock) @deserializers.add(Project) @deserializers.add(TextEditor) @deserializers.add(TextBuffer) diff --git a/src/dock.js b/src/dock.js index 9bcea6e57..e72b1415f 100644 --- a/src/dock.js +++ b/src/dock.js @@ -304,6 +304,24 @@ module.exports = class Dock { return initialSize == null ? DEFAULT_INITIAL_SIZE : initialSize } + serialize () { + return { + deserializer: 'Dock', + size: this.state.size, + paneContainer: this.paneContainer.serialize(), + open: this.state.open + } + } + + deserialize (serialized, deserializerManager) { + this.paneContainer.deserialize(serialized.paneContainer, deserializerManager) + this.setState({ + size: serialized.size, + // If no items could be deserialized, we don't want to show the dock (even if it was open last time) + open: serialized.open && (this.paneContainer.getPaneItems().length > 0) + }) + } + // PaneContainer-delegating methods getPanes () { diff --git a/src/workspace.js b/src/workspace.js index 98e945b18..33fec9d67 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -154,7 +154,12 @@ module.exports = class Workspace extends Model { deserializer: 'Workspace', paneContainer: this.paneContainer.serialize(), packagesWithActiveGrammars: this.getPackageNamesWithActiveGrammars(), - destroyedItemURIs: this.destroyedItemURIs.slice() + destroyedItemURIs: this.destroyedItemURIs.slice(), + docks: { + left: this.docks.left.serialize(), + right: this.docks.right.serialize(), + bottom: this.docks.bottom.serialize() + } } } @@ -170,7 +175,13 @@ module.exports = class Workspace extends Model { if (state.destroyedItemURIs != null) { this.destroyedItemURIs = state.destroyedItemURIs } - return this.paneContainer.deserialize(state.paneContainer, deserializerManager) + this.paneContainer.deserialize(state.paneContainer, deserializerManager) + for (let location in this.docks) { + const serialized = state.docks && state.docks[location] + if (serialized) { + this.docks[location].deserialize(serialized, deserializerManager) + } + } } getPackageNamesWithActiveGrammars () { From 939ebb3ddfcd6c35ebf362312e23478e75789eff Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 9 Mar 2017 17:12:16 -0800 Subject: [PATCH 13/73] Add findRightmostSibling and findBottommostSibling methods --- src/pane.coffee | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/pane.coffee b/src/pane.coffee index c55c9f043..467775f45 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -841,17 +841,21 @@ class Pane extends Model else this - # If the parent is a horizontal axis, returns its last child if it is a pane; - # otherwise returns a new pane created by splitting this pane rightward. - findOrCreateRightmostSibling: -> + findRightmostSibling: -> if @parent.orientation is 'horizontal' rightmostSibling = last(@parent.children) if rightmostSibling instanceof PaneAxis - @splitRight() + this else rightmostSibling else - @splitRight() + this + + # If the parent is a horizontal axis, returns its last child if it is a pane; + # otherwise returns a new pane created by splitting this pane rightward. + findOrCreateRightmostSibling: -> + rightmostSibling = @findRightmostSibling() + if rightmostSibling is this then @splitRight() else rightmostSibling # If the parent is a vertical axis, returns its first child if it is a pane; # otherwise returns this pane. @@ -865,17 +869,21 @@ class Pane extends Model else this - # If the parent is a vertical axis, returns its last child if it is a pane; - # otherwise returns a new pane created by splitting this pane bottomward. - findOrCreateBottommostSibling: -> + findBottommostSibling: -> if @parent.orientation is 'vertical' bottommostSibling = last(@parent.children) if bottommostSibling instanceof PaneAxis - @splitDown() + this else bottommostSibling else - @splitDown() + this + + # If the parent is a vertical axis, returns its last child if it is a pane; + # otherwise returns a new pane created by splitting this pane bottomward. + findOrCreateBottommostSibling: -> + bottommostSibling = @findBottommostSibling() + if bottommostSibling is this then @splitDown() else bottommostSibling close: -> @destroy() if @confirmClose() From a6424a795eb3a7aa97971c2ef18ed096f0165989 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 9 Mar 2017 17:13:39 -0800 Subject: [PATCH 14/73] Separate searching panes from creation --- src/workspace.js | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/workspace.js b/src/workspace.js index 33fec9d67..ff8b0d593 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -587,13 +587,13 @@ module.exports = class Workspace extends Model { pane = this.getActivePane().findLeftmostSibling() break case 'right': - pane = this.getActivePane().findOrCreateRightmostSibling() + pane = this.getActivePane().findRightmostSibling() break case 'up': pane = this.getActivePane().findTopmostSibling() break case 'down': - pane = this.getActivePane().findOrCreateBottommostSibling() + pane = this.getActivePane().findBottommostSibling() break default: pane = this.getActivePane() @@ -602,11 +602,12 @@ module.exports = class Workspace extends Model { } let item - if (uri != null) { + if (uri != null && pane != null) { item = pane.itemForURI(uri) } if (item == null) { item = this.createItemForURI(uri, options) + pane = null } return Promise.resolve(item) @@ -712,10 +713,29 @@ module.exports = class Workspace extends Model { } openItem (item, options = {}) { - const {pane} = options + let {pane} = options + const {split} = options if (item == null) return undefined - if (pane.isDestroyed()) return item + if (pane != null && pane.isDestroyed()) return item + + if (pane == null) { + pane = this.getActivePane() + switch (split) { + case 'left': + pane = pane.findLeftmostSibling() + break + case 'right': + pane = pane.findOrCreateRightmostSibling() + break + case 'up': + pane = pane.findTopmostSibling() + break + case 'down': + pane = pane.findOrCreateBottommostSibling() + break + } + } if (!options.pending && (pane.getPendingItem() === item)) { pane.clearPendingItem() From 238ce1d8cd45a577ad07772ecaf965b5241bc84f Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Tue, 14 Mar 2017 11:20:42 -0700 Subject: [PATCH 15/73] Make workspace inspection methods location-aware --- src/workspace.js | 92 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 77 insertions(+), 15 deletions(-) diff --git a/src/workspace.js b/src/workspace.js index ff8b0d593..4f2e1c9be 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -363,7 +363,11 @@ module.exports = class Workspace extends Model { // subscription or that is added at some later time. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observePaneItems (callback) { return this.paneContainer.observePaneItems(callback) } + observePaneItems (callback) { + return new CompositeDisposable( + ...this.getPaneLocations().map(location => location.observePaneItems(callback)) + ) + } // Essential: Invoke the given callback when the active pane item changes. // @@ -430,7 +434,11 @@ module.exports = class Workspace extends Model { // * `pane` The added pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddPane (callback) { return this.paneContainer.onDidAddPane(callback) } + onDidAddPane (callback) { + return new CompositeDisposable( + ...this.getPaneLocations().map(location => location.onDidAddPane(callback)) + ) + } // Extended: Invoke the given callback before a pane is destroyed in the // workspace. @@ -440,7 +448,11 @@ module.exports = class Workspace extends Model { // * `pane` The pane to be destroyed. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onWillDestroyPane (callback) { return this.paneContainer.onWillDestroyPane(callback) } + onWillDestroyPane (callback) { + return new CompositeDisposable( + ...this.getPaneLocations().map(location => location.onWillDestroyPane(callback)) + ) + } // Extended: Invoke the given callback when a pane is destroyed in the // workspace. @@ -450,7 +462,11 @@ module.exports = class Workspace extends Model { // * `pane` The destroyed pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroyPane (callback) { return this.paneContainer.onDidDestroyPane(callback) } + onDidDestroyPane (callback) { + return new CompositeDisposable( + ...this.getPaneLocations().map(location => location.onDidDestroyPane(callback)) + ) + } // Extended: Invoke the given callback with all current and future panes in the // workspace. @@ -460,7 +476,11 @@ module.exports = class Workspace extends Model { // subscription or that is added at some later time. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observePanes (callback) { return this.paneContainer.observePanes(callback) } + observePanes (callback) { + return new CompositeDisposable( + ...this.getPaneLocations().map(location => location.observePanes(callback)) + ) + } // Extended: Invoke the given callback when the active pane changes. // @@ -490,7 +510,11 @@ module.exports = class Workspace extends Model { // * `index` {Number} indicating the index of the added item in its pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddPaneItem (callback) { return this.paneContainer.onDidAddPaneItem(callback) } + onDidAddPaneItem (callback) { + return new CompositeDisposable( + ...this.getPaneLocations().map(location => location.onDidAddPaneItem(callback)) + ) + } // Extended: Invoke the given callback when a pane item is about to be // destroyed, before the user is prompted to save it. @@ -503,7 +527,11 @@ module.exports = class Workspace extends Model { // its pane. // // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. - onWillDestroyPaneItem (callback) { return this.paneContainer.onWillDestroyPaneItem(callback) } + onWillDestroyPaneItem (callback) { + return new CompositeDisposable( + ...this.getPaneLocations().map(location => location.onWillDestroyPaneItem(callback)) + ) + } // Extended: Invoke the given callback when a pane item is destroyed. // @@ -515,7 +543,11 @@ module.exports = class Workspace extends Model { // pane. // // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. - onDidDestroyPaneItem (callback) { return this.paneContainer.onDidDestroyPaneItem(callback) } + onDidDestroyPaneItem (callback) { + return new CompositeDisposable( + ...this.getPaneLocations().map(location => location.onDidDestroyPaneItem(callback)) + ) + } // Extended: Invoke the given callback when a text editor is added to the // workspace. @@ -891,7 +923,7 @@ module.exports = class Workspace extends Model { // // Returns an {Array} of items. getPaneItems () { - return this.paneContainer.getPaneItems() + return _.flatten(this.getPaneLocations().map(location => location.getPaneItems())) } // Essential: Get the active {Pane}'s active item. @@ -919,11 +951,15 @@ module.exports = class Workspace extends Model { // Save all pane items. saveAll () { - return this.paneContainer.saveAll() + this.getPaneLocations().forEach(location => { + location.saveAll() + }) } confirmClose (options) { - return this.paneContainer.confirmClose(options) + return this.getPaneLocations() + .map(location => location.confirmClose(options)) + .every(saved => saved) } // Save the active pane item. @@ -961,7 +997,7 @@ module.exports = class Workspace extends Model { // // Returns an {Array} of {Pane}s. getPanes () { - return this.paneContainer.getPanes() + return _.flatten(this.getPaneLocations().map(location => location.getPanes())) } // Extended: Get the active {Pane}. @@ -987,7 +1023,12 @@ module.exports = class Workspace extends Model { // // Returns a {Pane} or `undefined` if no pane exists for the given URI. paneForURI (uri) { - return this.paneContainer.paneForURI(uri) + for (let location of this.getPaneLocations()) { + const pane = location.paneForURI(uri) + if (pane != null) { + return pane + } + } } // Extended: Get the {Pane} containing the given item. @@ -996,7 +1037,12 @@ module.exports = class Workspace extends Model { // // Returns a {Pane} or `undefined` if no pane exists for the given item. paneForItem (item) { - return this.paneContainer.paneForItem(item) + for (let location of this.getPaneLocations()) { + const pane = location.paneForItem(item) + if (pane != null) { + return pane + } + } } // Destroy (close) the active pane. @@ -1012,7 +1058,7 @@ module.exports = class Workspace extends Model { closeActivePaneItemOrEmptyPaneOrWindow () { if (this.getActivePaneItem() != null) { this.destroyActivePaneItem() - } else if (this.getPanes().length > 1) { + } else if (this.getCenter().getPanes().length > 1) { this.destroyActivePane() } else if (this.config.get('core.closeEmptyWindows')) { atom.close() @@ -1091,6 +1137,22 @@ module.exports = class Workspace extends Model { return this.center } + getLeftDock () { + return this.docks.left + } + + getRightDock () { + return this.docks.right + } + + getBottomDock () { + return this.docks.bottom + } + + getPaneLocations () { + return [this.getCenter(), ..._.values(this.docks)] + } + /* Section: Panels From 64e290b57fa17e7472437478a540c678df581c66 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 9 Mar 2017 23:01:50 -0800 Subject: [PATCH 16/73] Make open() location aware --- src/workspace.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/workspace.js b/src/workspace.js index 4f2e1c9be..064a54210 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -745,14 +745,32 @@ module.exports = class Workspace extends Model { } openItem (item, options = {}) { - let {pane} = options - const {split} = options + let {pane, split} = options if (item == null) return undefined if (pane != null && pane.isDestroyed()) return item if (pane == null) { - pane = this.getActivePane() + // If this is a new item, we want to determine where to put it in the following way: + // - If you provided a split, you want to put it in that split of the center location + // (legacy behavior) + // - If the item specifies a default location, use that. + let locationInfo, location + if (split == null) { + if (locationInfo == null && typeof item.getDefaultLocation === 'function') { + locationInfo = item.getDefaultLocation() + } + if (locationInfo != null) { + if (typeof locationInfo === 'string') { + location = locationInfo + } else { + location = locationInfo.location + split = locationInfo.split + } + } + } + + pane = this.docks[location] == null ? this.getActivePane() : this.docks[location].getActivePane() switch (split) { case 'left': pane = pane.findLeftmostSibling() From 5c7bd668967291958b65bf9186d7b5f5c009081c Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 9 Mar 2017 18:28:53 -0800 Subject: [PATCH 17/73] Remember previous item locations --- src/workspace.js | 151 ++++++++++++++++++++++++++--------------------- 1 file changed, 84 insertions(+), 67 deletions(-) diff --git a/src/workspace.js b/src/workspace.js index 064a54210..dbbba1a97 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -9,6 +9,7 @@ const {Directory} = require('pathwatcher') const DefaultDirectorySearcher = require('./default-directory-searcher') const Dock = require('./dock') const Model = require('./model') +const StateStore = require('./state-store') const TextEditor = require('./text-editor') const PaneContainer = require('./pane-container') const Panel = require('./panel') @@ -45,6 +46,7 @@ module.exports = class Workspace extends Model { this.textEditorRegistry = params.textEditorRegistry this.hoveredDock = null this.draggingItem = false + this.previousLocations = new StateStore('AtomPreviousItemLocations', 1) this.emitter = new Emitter() this.openers = [] @@ -137,6 +139,7 @@ module.exports = class Workspace extends Model { this.subscribeToActiveItem() this.subscribeToFontSize() this.subscribeToAddedItems() + this.subscribeToMovedItems() } consumeServices ({serviceHub}) { @@ -282,6 +285,27 @@ module.exports = class Workspace extends Model { }) } + subscribeToMovedItems () { + if (this.movedItemSubscription != null) { + this.movedItemSubscription.dispose() + } + const paneLocations = Object.assign({center: this}, this.docks) + this.movedItemSubscription = new CompositeDisposable( + ..._.map(paneLocations, (host, location) => ( + host.observePanes(pane => { + pane.onDidAddItem(({item}) => { + if (typeof item.getURI === 'function') { + const uri = item.getURI() + if (uri != null) { + this.previousLocations.save(item.getURI(), location) + } + } + }) + }) + )) + ) + } + // Updates the application's title and proxy icon based on whichever file is // open. updateWindowTitle () { @@ -747,78 +771,68 @@ module.exports = class Workspace extends Model { openItem (item, options = {}) { let {pane, split} = options - if (item == null) return undefined - if (pane != null && pane.isDestroyed()) return item + if (item == null) return Promise.resolve() + if (pane != null && pane.isDestroyed()) return Promise.resolve(item) - if (pane == null) { - // If this is a new item, we want to determine where to put it in the following way: - // - If you provided a split, you want to put it in that split of the center location - // (legacy behavior) - // - If the item specifies a default location, use that. - let locationInfo, location - if (split == null) { - if (locationInfo == null && typeof item.getDefaultLocation === 'function') { - locationInfo = item.getDefaultLocation() + const uri = options.uri == null && typeof item.getURI === 'function' ? item.getURI() : options.uri + + let location + // If a split was provided, make sure it goes in the center location (legacy behavior) + if (pane == null && split == null) { + if (uri != null) { + location = this.previousLocations.load(uri) + } + if (location == null && typeof item.getDefaultLocation === 'function') { + location = item.getDefaultLocation() + } + } + + return Promise.resolve(location) + .then(location => { + if (pane != null) return pane + + pane = this.docks[location] == null ? this.getActivePane() : this.docks[location].getActivePane() + switch (split) { + case 'left': return pane.findLeftmostSibling() + case 'right': return pane.findOrCreateRightmostSibling() + case 'up': return pane.findTopmostSibling() + case 'down': return pane.findOrCreateBottommostSibling() + default: return pane } - if (locationInfo != null) { - if (typeof locationInfo === 'string') { - location = locationInfo - } else { - location = locationInfo.location - split = locationInfo.split + }) + .then(pane => { + if (!options.pending && (pane.getPendingItem() === item)) { + pane.clearPendingItem() + } + + const activatePane = options.activatePane != null ? options.activatePane : true + const activateItem = options.activateItem != null ? options.activateItem : true + this.itemOpened(item) + if (activateItem) { + pane.activateItem(item, {pending: options.pending}) + } + if (activatePane) { + pane.activate() + } + + let initialColumn = 0 + let initialLine = 0 + if (!Number.isNaN(options.initialLine)) { + initialLine = options.initialLine + } + if (!Number.isNaN(options.initialColumn)) { + initialColumn = options.initialColumn + } + if ((initialLine >= 0) || (initialColumn >= 0)) { + if (typeof item.setCursorBufferPosition === 'function') { + item.setCursorBufferPosition([initialLine, initialColumn]) } } - } - pane = this.docks[location] == null ? this.getActivePane() : this.docks[location].getActivePane() - switch (split) { - case 'left': - pane = pane.findLeftmostSibling() - break - case 'right': - pane = pane.findOrCreateRightmostSibling() - break - case 'up': - pane = pane.findTopmostSibling() - break - case 'down': - pane = pane.findOrCreateBottommostSibling() - break - } - } - - if (!options.pending && (pane.getPendingItem() === item)) { - pane.clearPendingItem() - } - - const activatePane = options.activatePane != null ? options.activatePane : true - const activateItem = options.activateItem != null ? options.activateItem : true - this.itemOpened(item) - if (activateItem) { - pane.activateItem(item, {pending: options.pending}) - } - if (activatePane) { - pane.activate() - } - - let initialColumn = 0 - let initialLine = 0 - if (!Number.isNaN(options.initialLine)) { - initialLine = options.initialLine - } - if (!Number.isNaN(options.initialColumn)) { - initialColumn = options.initialColumn - } - if ((initialLine >= 0) || (initialColumn >= 0)) { - if (typeof item.setCursorBufferPosition === 'function') { - item.setCursorBufferPosition([initialLine, initialColumn]) - } - } - - const index = pane.getActiveItemIndex() - const uri = options.uri == null && typeof item.getURI === 'function' ? item.getURI() : options.uri - this.emitter.emit('did-open', {uri, pane, item, index}) - return item + const index = pane.getActiveItemIndex() + this.emitter.emit('did-open', {uri, pane, item, index}) + return item + }) } openTextFile (uri, options) { @@ -1145,6 +1159,9 @@ module.exports = class Workspace extends Model { if (this.activeItemSubscriptions != null) { this.activeItemSubscriptions.dispose() } + if (this.movedItemSubscription != null) { + this.movedItemSubscription.dispose() + } } /* From 5b4f4022784397d1b7c669f09e478b824f5c19b2 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 9 Mar 2017 17:12:26 -0800 Subject: [PATCH 18/73] Add toggle commands --- src/register-default-commands.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index 8196d9237..22f9acb36 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -55,6 +55,9 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage 'application:open-license': -> @getModel().openLicense() 'window:run-package-specs': -> @runPackageSpecs() 'window:run-benchmarks': -> @runBenchmarks() + 'window:toggle-left-dock': -> @getModel().getLeftDock().toggle() + 'window:toggle-right-dock': -> @getModel().getRightDock().toggle() + 'window:toggle-bottom-dock': -> @getModel().getBottomDock().toggle() 'window:focus-next-pane': -> @getModel().activateNextPane() 'window:focus-previous-pane': -> @getModel().activatePreviousPane() 'window:focus-pane-above': -> @focusPaneViewAbove() From d854a88dbbe3613ea2d31b83cfffe4a47e6f86f9 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Tue, 14 Mar 2017 16:31:06 -0700 Subject: [PATCH 19/73] Add `workspace.toggle()` method --- src/dock.js | 12 +++++++++ src/workspace-center.js | 2 ++ src/workspace.js | 60 ++++++++++++++++++++++++++++++++++------- 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/dock.js b/src/dock.js index e72b1415f..c4d95eecb 100644 --- a/src/dock.js +++ b/src/dock.js @@ -88,10 +88,22 @@ module.exports = class Dock { this.setState({draggingItem}) } + activate () { + this.setState({open: true}) + } + + hide () { + this.setState({open: false}) + } + toggle () { this.setState({open: !this.state.open}) } + isOpen () { + return this.state.open + } + setState (newState) { const prevState = this.state const nextState = Object.assign({}, prevState, newState) diff --git a/src/workspace-center.js b/src/workspace-center.js index 370d37b75..c2c875b74 100644 --- a/src/workspace-center.js +++ b/src/workspace-center.js @@ -7,6 +7,8 @@ module.exports = class WorkspaceCenter { this.paneContainer = paneContainer } + activate () {} + /* Section: Event Subscription */ diff --git a/src/workspace.js b/src/workspace.js index dbbba1a97..786ec85ee 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -776,22 +776,30 @@ module.exports = class Workspace extends Model { const uri = options.uri == null && typeof item.getURI === 'function' ? item.getURI() : options.uri + let paneLocation + if (pane != null) { + paneLocation = this.getPaneLocations().find(location => location.getPanes().includes(pane)) + } + + // Determine which location to use, unless a split was provided. In that case, make sure it goes + // in the center location (legacy behavior) let location - // If a split was provided, make sure it goes in the center location (legacy behavior) - if (pane == null && split == null) { - if (uri != null) { - location = this.previousLocations.load(uri) - } - if (location == null && typeof item.getDefaultLocation === 'function') { - location = item.getDefaultLocation() - } + if (paneLocation == null && pane == null && split == null && uri != null) { + location = this.previousLocations.load(uri) } return Promise.resolve(location) .then(location => { + if (paneLocation == null) { + if (location == null && typeof item.getDefaultLocation === 'function') { + location = item.getDefaultLocation() + } + paneLocation = this.docks[location] || this.getCenter() + } + }) + .then(() => { if (pane != null) return pane - - pane = this.docks[location] == null ? this.getActivePane() : this.docks[location].getActivePane() + pane = paneLocation.getActivePane() switch (split) { case 'left': return pane.findLeftmostSibling() case 'right': return pane.findOrCreateRightmostSibling() @@ -814,6 +822,7 @@ module.exports = class Workspace extends Model { if (activatePane) { pane.activate() } + paneLocation.activate() let initialColumn = 0 let initialLine = 0 @@ -1188,6 +1197,37 @@ module.exports = class Workspace extends Model { return [this.getCenter(), ..._.values(this.docks)] } + toggle (uri) { + let foundItems = false + + // If any visible item has the given URI, hide it + for (const location of this.getPaneLocations()) { + const isCenter = location === this.getCenter() + if (isCenter || location.isOpen()) { + for (const pane of location.getPanes()) { + const activeItem = pane.getActiveItem() + if (activeItem != null && typeof activeItem.getURI === 'function') { + const itemURI = activeItem.getURI() + if (itemURI === uri) { + foundItems = true + // We can't really hide the center so we just destroy the item. + if (isCenter) { + pane.destroyItem(activeItem) + } else { + location.hide() + } + } + } + } + } + } + + // If no visible items had the URI, show it. + if (!foundItems) { + this.open(uri, {searchAllPanes: true}) + } + } + /* Section: Panels From 47cdc74b6a923e433b4f87fc099871197a778d7b Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Wed, 15 Mar 2017 14:46:03 -0700 Subject: [PATCH 20/73] Only count panes in center in workspace test --- spec/workspace-spec.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index b04d8cbd7..4bc2c3799 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -1985,24 +1985,24 @@ i = /test/; #FIXME\ const pane1 = atom.workspace.getActivePane() const pane2 = pane1.splitRight({copyActiveItem: true}) - expect(atom.workspace.getPanes().length).toBe(2) + expect(atom.workspace.getCenter().getPanes().length).toBe(2) expect(pane2.getItems().length).toBe(1) atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - expect(atom.workspace.getPanes().length).toBe(2) + expect(atom.workspace.getCenter().getPanes().length).toBe(2) expect(pane2.getItems().length).toBe(0) atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - expect(atom.workspace.getPanes().length).toBe(1) + expect(atom.workspace.getCenter().getPanes().length).toBe(1) expect(pane1.getItems().length).toBe(1) atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - expect(atom.workspace.getPanes().length).toBe(1) + expect(atom.workspace.getCenter().getPanes().length).toBe(1) expect(pane1.getItems().length).toBe(0) atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - expect(atom.workspace.getPanes().length).toBe(1) + expect(atom.workspace.getCenter().getPanes().length).toBe(1) atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() expect(atom.close).toHaveBeenCalled() From 417e9c697985c28812a140d25a9486c4028191e7 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 16 Mar 2017 11:48:38 -0700 Subject: [PATCH 21/73] Add tests for open() and docks --- spec/workspace-spec.js | 134 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index 4bc2c3799..d5fb333f0 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -24,6 +24,8 @@ describe('Workspace', () => { setDocumentEdited = spyOn(atom.applicationDelegate, 'setWindowDocumentEdited') atom.project.setPaths([atom.project.getDirectories()[0].resolve('dir')]) waits(1) + + waitsForPromise(() => atom.workspace.previousLocations.clear()) }) afterEach(() => temp.cleanupSync()) @@ -116,6 +118,63 @@ describe('Workspace', () => { expect(atom.workspace.getTextEditors().length).toBe(0) }) }) + + describe('where a dock contains an editor', () => { + afterEach(() => { + atom.workspace.getRightDock().paneContainer.destroy() + }) + + it('constructs the view with the same panes', () => { + const getActivePane = () => atom.workspace.getRightDock().getActivePane() + const pane1 = atom.workspace.getRightDock().getActivePane() + const pane2 = pane1.splitRight({copyActiveItem: true}) + const pane3 = pane2.splitRight({copyActiveItem: true}) + let pane4 = null + + waitsForPromise(() => + atom.workspace.open(null, {pane: getActivePane()}).then(editor => editor.setText('An untitled editor.')) + ) + + waitsForPromise(() => + atom.workspace.open('b', {pane: getActivePane()}).then(editor => pane2.activateItem(editor.copy())) + ) + + waitsForPromise(() => + atom.workspace.open('../sample.js', {pane: getActivePane()}).then(editor => pane3.activateItem(editor)) + ) + + runs(() => { + pane3.activeItem.setCursorScreenPosition([2, 4]) + pane4 = pane2.splitDown() + }) + + waitsForPromise(() => + atom.workspace.open('../sample.txt', {pane: getActivePane()}).then(editor => pane4.activateItem(editor)) + ) + + runs(() => { + pane4.getActiveItem().setCursorScreenPosition([0, 2]) + pane2.activate() + + simulateReload() + + expect(atom.workspace.getTextEditors().length).toBe(5) + const [editor1, editor2, untitledEditor, editor3, editor4] = atom.workspace.getTextEditors() + const firstDirectory = atom.project.getDirectories()[0] + expect(firstDirectory).toBeDefined() + expect(editor1.getPath()).toBe(firstDirectory.resolve('b')) + expect(editor2.getPath()).toBe(firstDirectory.resolve('../sample.txt')) + expect(editor2.getCursorScreenPosition()).toEqual([0, 2]) + expect(editor3.getPath()).toBe(firstDirectory.resolve('b')) + expect(editor4.getPath()).toBe(firstDirectory.resolve('../sample.js')) + expect(editor4.getCursorScreenPosition()).toEqual([2, 4]) + expect(untitledEditor.getPath()).toBeUndefined() + expect(untitledEditor.getText()).toBe('An untitled editor.') + + expect(atom.workspace.getRightDock().getActiveTextEditor().getPath()).toBe(editor3.getPath()) + }) + }) + }) }) describe('::open(uri, options)', () => { @@ -201,6 +260,25 @@ describe('Workspace', () => { ]) }) }) + + it('finds items in docks', () => { + const dock = atom.workspace.getRightDock() + const ITEM_URI = 'atom://test' + const item = { + getURI: () => ITEM_URI, + getDefaultLocation: jasmine.createSpy().andReturn('left'), + getElement: () => document.createElement('div') + } + dock.getActivePane().addItem(item) + expect(dock.getPaneItems()).toHaveLength(1) + waitsForPromise(() => atom.workspace.open(ITEM_URI, {searchAllPanes: true})) + runs(() => { + expect(item.getDefaultLocation).not.toHaveBeenCalled() + expect(atom.workspace.getPaneItems()).toHaveLength(1) + expect(dock.getPaneItems()).toHaveLength(1) + expect(dock.getPaneItems()[0]).toBe(item) + }) + }) }) describe('when the active pane does not have an editor for the given uri', () => { @@ -217,6 +295,46 @@ describe('Workspace', () => { expect(workspace.getActivePane().activate).toHaveBeenCalled() }) }) + + it("uses the location specified by the model's `getDefaultLocation()` method", () => { + const item = { + getDefaultLocation: jasmine.createSpy().andReturn('right'), + getElement: () => document.createElement('div') + } + const opener = jasmine.createSpy().andReturn(item) + const dock = atom.workspace.getRightDock() + spyOn(atom.workspace.previousLocations, 'load').andReturn(Promise.resolve()) + spyOn(atom.workspace, 'getOpeners').andReturn([opener]) + expect(dock.getPaneItems()).toHaveLength(0) + waitsForPromise(() => atom.workspace.open('a')) + runs(() => { + expect(dock.getPaneItems()).toHaveLength(1) + expect(opener).toHaveBeenCalled() + expect(item.getDefaultLocation).toHaveBeenCalled() + }) + }) + + it('prefers the last location the user used for that item', () => { + const ITEM_URI = 'atom://test' + const item = { + getURI: () => ITEM_URI, + getDefaultLocation: jasmine.createSpy().andReturn('left'), + getElement: () => document.createElement('div') + } + const opener = uri => uri === ITEM_URI ? item : null + const dock = atom.workspace.getRightDock() + spyOn(atom.workspace.previousLocations, 'load').andCallFake(uri => + uri === 'atom://test' ? Promise.resolve('right') : Promise.resolve() + ) + spyOn(atom.workspace, 'getOpeners').andReturn([opener]) + expect(dock.getPaneItems()).toHaveLength(0) + waitsForPromise(() => atom.workspace.open(ITEM_URI)) + runs(() => { + expect(dock.getPaneItems()).toHaveLength(1) + expect(dock.getPaneItems()[0]).toBe(item) + expect(item.getDefaultLocation).not.toHaveBeenCalled() + }) + }) }) }) }) @@ -248,6 +366,22 @@ describe('Workspace', () => { expect(workspace.getActivePaneItem()).toBe(editor1) }) }) + + it('activates the dock with the matching item', () => { + const dock = atom.workspace.getRightDock() + const ITEM_URI = 'atom://test' + const item = { + getURI: () => ITEM_URI, + getDefaultLocation: jasmine.createSpy().andReturn('left'), + getElement: () => document.createElement('div') + } + dock.getActivePane().addItem(item) + spyOn(dock, 'activate') + waitsForPromise(() => atom.workspace.open(ITEM_URI, {searchAllPanes: true})) + runs(() => { + expect(dock.activate).toHaveBeenCalled() + }) + }) }) describe('when no editor for the given uri is open in any pane', () => { From 6f9893d77da26acac9d5fe741f881192c630a07b Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 20 Mar 2017 19:50:35 -0700 Subject: [PATCH 22/73] Rename "getActivePane" in tests to clarify intent --- spec/workspace-spec.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index d5fb333f0..5b6ecc7ba 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -125,22 +125,22 @@ describe('Workspace', () => { }) it('constructs the view with the same panes', () => { - const getActivePane = () => atom.workspace.getRightDock().getActivePane() + const getRightDockActivePane = () => atom.workspace.getRightDock().getActivePane() const pane1 = atom.workspace.getRightDock().getActivePane() const pane2 = pane1.splitRight({copyActiveItem: true}) const pane3 = pane2.splitRight({copyActiveItem: true}) let pane4 = null waitsForPromise(() => - atom.workspace.open(null, {pane: getActivePane()}).then(editor => editor.setText('An untitled editor.')) + atom.workspace.open(null, {pane: getRightDockActivePane()}).then(editor => editor.setText('An untitled editor.')) ) waitsForPromise(() => - atom.workspace.open('b', {pane: getActivePane()}).then(editor => pane2.activateItem(editor.copy())) + atom.workspace.open('b', {pane: getRightDockActivePane()}).then(editor => pane2.activateItem(editor.copy())) ) waitsForPromise(() => - atom.workspace.open('../sample.js', {pane: getActivePane()}).then(editor => pane3.activateItem(editor)) + atom.workspace.open('../sample.js', {pane: getRightDockActivePane()}).then(editor => pane3.activateItem(editor)) ) runs(() => { @@ -149,7 +149,7 @@ describe('Workspace', () => { }) waitsForPromise(() => - atom.workspace.open('../sample.txt', {pane: getActivePane()}).then(editor => pane4.activateItem(editor)) + atom.workspace.open('../sample.txt', {pane: getRightDockActivePane()}).then(editor => pane4.activateItem(editor)) ) runs(() => { From 98e7fcc50511800ffd4211e5edb796606e0178fa Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 20 Mar 2017 19:52:01 -0700 Subject: [PATCH 23/73] Prefer `getElement()` to view registry for Docks, PaneContainer & Pane --- spec/pane-axis-element-spec.coffee | 5 +++-- spec/pane-container-element-spec.coffee | 16 +++++++++++----- spec/pane-container-spec.coffee | 3 ++- spec/pane-element-spec.coffee | 2 +- src/atom-environment.coffee | 6 ------ src/dock.js | 22 ++++++++++++++-------- src/pane-container.coffee | 8 ++++++-- src/pane.coffee | 11 ++++++++--- src/panel-container-element.js | 2 +- src/workspace.js | 6 ++++-- 10 files changed, 50 insertions(+), 31 deletions(-) diff --git a/spec/pane-axis-element-spec.coffee b/spec/pane-axis-element-spec.coffee index 702e9c5fc..e6aa2ed5f 100644 --- a/spec/pane-axis-element-spec.coffee +++ b/spec/pane-axis-element-spec.coffee @@ -7,12 +7,13 @@ buildPane = -> applicationDelegate: atom.applicationDelegate, config: atom.config, deserializerManager: atom.deserializers, - notificationManager: atom.notifications + notificationManager: atom.notifications, + viewRegistry: atom.views }) describe "PaneAxisElement", -> it "correctly subscribes and unsubscribes to the underlying model events on attach/detach", -> - container = new PaneContainer(config: atom.config, applicationDelegate: atom.applicationDelegate) + container = new PaneContainer(config: atom.config, applicationDelegate: atom.applicationDelegate, viewRegistry: atom.views) axis = new PaneAxis axis.setContainer(container) axisElement = atom.views.getView(axis) diff --git a/spec/pane-container-element-spec.coffee b/spec/pane-container-element-spec.coffee index fe57e89af..e986e5d6c 100644 --- a/spec/pane-container-element-spec.coffee +++ b/spec/pane-container-element-spec.coffee @@ -2,6 +2,12 @@ PaneContainer = require '../src/pane-container' PaneAxisElement = require '../src/pane-axis-element' PaneAxis = require '../src/pane-axis' +params = + config: atom.config + confirm: atom.confirm.bind(atom) + viewRegistry: atom.views + applicationDelegate: atom.applicationDelegate + describe "PaneContainerElement", -> describe "when panes are added or removed", -> it "inserts or removes resize elements", -> @@ -42,7 +48,7 @@ describe "PaneContainerElement", -> ] it "transfers focus to the next pane if a focused pane is removed", -> - container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) + container = new PaneContainer(params) containerElement = atom.views.getView(container) leftPane = container.getActivePane() leftPaneElement = atom.views.getView(leftPane) @@ -58,7 +64,7 @@ describe "PaneContainerElement", -> describe "when a pane is split", -> it "builds appropriately-oriented atom-pane-axis elements", -> - container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) + container = new PaneContainer(params) containerElement = atom.views.getView(container) pane1 = container.getActivePane() @@ -84,7 +90,7 @@ describe "PaneContainerElement", -> [container, containerElement] = [] beforeEach -> - container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) + container = new PaneContainer(params) containerElement = atom.views.getView(container) document.querySelector('#jasmine-content').appendChild(containerElement) @@ -201,7 +207,7 @@ describe "PaneContainerElement", -> [leftPane, rightPane] = [] beforeEach -> - container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) + container = new PaneContainer(params) leftPane = container.getActivePane() rightPane = leftPane.splitRight() @@ -258,7 +264,7 @@ describe "PaneContainerElement", -> element.cloneNode(true) element - container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) + container = new PaneContainer(params) [item1, item2, item3, item4, item5, item6, item7, item8, item9] = [buildElement('1'), buildElement('2'), buildElement('3'), diff --git a/spec/pane-container-spec.coffee b/spec/pane-container-spec.coffee index 84c6c4fc9..c1c0b11b5 100644 --- a/spec/pane-container-spec.coffee +++ b/spec/pane-container-spec.coffee @@ -9,7 +9,8 @@ describe "PaneContainer", -> params = { config: atom.config, deserializerManager: atom.deserializers - applicationDelegate: atom.applicationDelegate + applicationDelegate: atom.applicationDelegate, + viewRegistry: atom.views } describe "serialization", -> diff --git a/spec/pane-element-spec.coffee b/spec/pane-element-spec.coffee index f5a059c49..f6dc4d535 100644 --- a/spec/pane-element-spec.coffee +++ b/spec/pane-element-spec.coffee @@ -6,7 +6,7 @@ describe "PaneElement", -> beforeEach -> spyOn(atom.applicationDelegate, "open") - container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) + container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom), viewRegistry: atom.views, applicationDelegate: atom.applicationDelegate) containerElement = atom.views.getView(container) pane = container.getActivePane() paneElement = atom.views.getView(pane) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 74f90e4f8..cca66f05b 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -49,9 +49,7 @@ AutoUpdateManager = require './auto-update-manager' WorkspaceElement = require './workspace-element' PanelContainerElement = require './panel-container-element' PanelElement = require './panel-element' -PaneContainerElement = require './pane-container-element' PaneAxisElement = require './pane-axis-element' -PaneElement = require './pane-element' {createGutterView} = require './gutter-component-helpers' # Essential: Atom global for dealing with packages, themes, menus, and the window. @@ -270,12 +268,8 @@ class AtomEnvironment extends Model new PanelContainerElement().initialize(model, env) @views.addViewProvider Panel, (model, env) -> new PanelElement().initialize(model, env) - @views.addViewProvider PaneContainer, (model, env) -> - new PaneContainerElement().initialize(model, env) @views.addViewProvider PaneAxis, (model, env) -> new PaneAxisElement().initialize(model, env) - @views.addViewProvider Pane, (model, env) -> - new PaneElement().initialize(model, env) @views.addViewProvider(Gutter, createGutterView) registerDefaultOpeners: -> diff --git a/src/dock.js b/src/dock.js index c4d95eecb..0aeab7336 100644 --- a/src/dock.js +++ b/src/dock.js @@ -37,7 +37,7 @@ module.exports = class Dock { applicationDelegate: this.applicationDelegate, deserializerManager: this.deserializerManager, notificationManager: this.notificationManager, - views: this.viewRegistry + viewRegistry: this.viewRegistry }) this.state = { @@ -53,13 +53,11 @@ module.exports = class Dock { pane.onDidRemoveItem(this.handleDidRemovePaneItem.bind(this)) }) ) + + this.render(this.state) } - // FIXME(matthewwithanm: This is kinda gross. We need to get a view for the pane container so we - // have to make sure that this is called after its view provider is registered. But we really - // don't have any guarantees about that. getElement () { - this.render(this.state) return this.element } @@ -151,12 +149,12 @@ module.exports = class Dock { this.element.appendChild(this.innerElement) this.innerElement.appendChild(this.maskElement) this.maskElement.appendChild(this.wrapperElement) - this.wrapperElement.appendChild(this.viewRegistry.getView(this.resizeHandle)) - this.wrapperElement.appendChild(this.viewRegistry.getView(this.paneContainer)) + this.wrapperElement.appendChild(this.resizeHandle.getElement()) + this.wrapperElement.appendChild(this.paneContainer.getElement()) this.wrapperElement.appendChild(this.cursorOverlayElement) // The toggle button must be rendered outside the mask because (1) it shouldn't be masked and // (2) if we made the mask larger to avoid masking it, the mask would block mouse events. - this.innerElement.appendChild(this.viewRegistry.getView(this.toggleButton)) + this.innerElement.appendChild(this.toggleButton.getElement()) } if (state.open) { @@ -425,6 +423,10 @@ class DockResizeHandle { this.update(props) } + getElement () { + return this.element + } + update (newProps) { this.props = Object.assign({}, this.props, newProps) @@ -473,6 +475,10 @@ class DockToggleButton { this.update(props) } + getElement () { + return this.element + } + destroy () { this.innerElement.removeEventListener('click', this.handleClick) this.innerElement.removeEventListener('dragenter', this.handleDragEnter) diff --git a/src/pane-container.coffee b/src/pane-container.coffee index fc092122e..590f9847d 100644 --- a/src/pane-container.coffee +++ b/src/pane-container.coffee @@ -3,6 +3,7 @@ Model = require './model' Pane = require './pane' ItemRegistry = require './item-registry' +PaneContainerElement = require './pane-container-element' module.exports = class PaneContainer extends Model @@ -14,16 +15,19 @@ class PaneContainer extends Model constructor: (params) -> super - {@config, applicationDelegate, notificationManager, deserializerManager} = params + {@config, applicationDelegate, notificationManager, deserializerManager, @viewRegistry} = params @emitter = new Emitter @subscriptions = new CompositeDisposable @itemRegistry = new ItemRegistry - @setRoot(new Pane({container: this, @config, applicationDelegate, notificationManager, deserializerManager})) + @setRoot(new Pane({container: this, @config, applicationDelegate, notificationManager, deserializerManager, @viewRegistry})) @setActivePane(@getRoot()) @monitorActivePaneItem() @monitorPaneItems() + getElement: -> + @element ?= new PaneContainerElement().initialize(this, {views: @viewRegistry}) + serialize: (params) -> deserializer: 'PaneContainer' version: @serializationVersion diff --git a/src/pane.coffee b/src/pane.coffee index 467775f45..5dfc59374 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -4,6 +4,7 @@ Grim = require 'grim' Model = require './model' PaneAxis = require './pane-axis' TextEditor = require './text-editor' +PaneElement = require './pane-element' # Extended: A container for presenting content in the center of the workspace. # Panes can contain multiple items, one of which is *active* at a given time. @@ -20,7 +21,7 @@ class Pane extends Model activeItem: undefined focused: false - @deserialize: (state, {deserializers, applicationDelegate, config, notifications}) -> + @deserialize: (state, {deserializers, applicationDelegate, config, notifications, views}) -> {items, activeItemIndex, activeItemURI, activeItemUri} = state activeItemURI ?= activeItemUri items = items.map (itemState) -> deserializers.deserialize(itemState) @@ -34,6 +35,7 @@ class Pane extends Model new Pane(extend(state, { deserializerManager: deserializers, notificationManager: notifications, + viewRegistry: views, config, applicationDelegate })) @@ -42,7 +44,7 @@ class Pane extends Model { @activeItem, @focused, @applicationDelegate, @notificationManager, @config, - @deserializerManager + @deserializerManager, @viewRegistry } = params @emitter = new Emitter @@ -55,6 +57,9 @@ class Pane extends Model @addItemsToStack(params?.itemStackIndices ? []) @setFlexScale(params?.flexScale ? 1) + getElement: -> + @element ?= new PaneElement().initialize(this, {views: @viewRegistry, @applicationDelegate}) + serialize: -> itemsToBeSerialized = compact(@items.map((item) -> item if typeof item.serialize is 'function')) itemStackIndices = (itemsToBeSerialized.indexOf(item) for item in @itemStack when typeof item.serialize is 'function') @@ -819,7 +824,7 @@ class Pane extends Model @parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this], @flexScale})) @setFlexScale(1) - newPane = new Pane(extend({@applicationDelegate, @notificationManager, @deserializerManager, @config}, params)) + newPane = new Pane(extend({@applicationDelegate, @notificationManager, @deserializerManager, @config, @viewRegistry}, params)) switch side when 'before' then @parent.insertChildBefore(this, newPane) when 'after' then @parent.insertChildAfter(this, newPane) diff --git a/src/panel-container-element.js b/src/panel-container-element.js index 2571d9875..f78a2e352 100644 --- a/src/panel-container-element.js +++ b/src/panel-container-element.js @@ -22,7 +22,7 @@ class PanelContainerElement extends HTMLElement { // Add the dock. if (this.model.dock != null) { - this.appendChild(this.views.getView(this.model.dock)) + this.appendChild(this.model.dock.getElement()) } return this diff --git a/src/workspace.js b/src/workspace.js index 786ec85ee..9910153d1 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -56,7 +56,8 @@ module.exports = class Workspace extends Model { config: this.config, applicationDelegate: this.applicationDelegate, notificationManager: this.notificationManager, - deserializerManager: this.deserializerManager + deserializerManager: this.deserializerManager, + viewRegistry: this.viewRegistry }) this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem) @@ -108,7 +109,8 @@ module.exports = class Workspace extends Model { config: this.config, applicationDelegate: this.applicationDelegate, notificationManager: this.notificationManager, - deserializerManager: this.deserializerManager + deserializerManager: this.deserializerManager, + viewRegistry: this.viewRegistry }) this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem) From cd62357f0f7ca574a2fd2c945592637d6a1408e6 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 20 Mar 2017 20:58:58 -0700 Subject: [PATCH 24/73] Mention dock getters in Dock docs --- src/dock.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/dock.js b/src/dock.js index 0aeab7336..a67ad3ab9 100644 --- a/src/dock.js +++ b/src/dock.js @@ -15,7 +15,9 @@ const TOGGLE_BUTTON_VISIBLE_CLASS = 'atom-dock-toggle-button-visible' const CURSOR_OVERLAY_VISIBLE_CLASS = 'atom-dock-cursor-overlay-visible' // Extended: A container at the edges of the editor window capable of holding items. -// You should not create a `Dock` directly, instead use {Workspace::open}. +// You should not create a Dock directly. Instead, access one of the three docks of the workspace +// via {::getLeftDock}, {::getRightDock}, and {::getBottomDock} or add an item to a dock via +// {Workspace::open}. module.exports = class Dock { constructor (params) { this.handleResizeHandleDragStart = this.handleResizeHandleDragStart.bind(this) From 910fef97a0b74e5ed487f961adeb5d1f1473d858 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 22 Mar 2017 20:24:50 -0700 Subject: [PATCH 25/73] Restore state when adding folders to applicable windows Note: "clean window" is defined as 1) having an empty project and 2) having no pane items or only empty unnamed buffers Adding folder(s) * If we have a clean window, restore project state in window * If window is dirty, prompt user to * add folder to the existing window LOSING state * OR open project folder in a new window --- spec/atom-environment-spec.coffee | 197 ++++++++++++++---------------- src/atom-environment.coffee | 55 ++++++--- 2 files changed, 132 insertions(+), 120 deletions(-) diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 17b9f51b0..6fc5ab1a8 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -328,116 +328,105 @@ describe "AtomEnvironment", -> atom.addProjectFolder() expect(atom.project.getPaths()).toEqual(initialPaths) - describe "when the project contains no folders", -> - describe "when there is saved state for the added folders", -> - projectPath = null + describe "when there is no saved state for the added folders", -> + beforeEach -> + spyOn(atom, 'loadState').andReturn(Promise.resolve(null)) + spyOn(atom, 'attemptRestoreProjectStateForPaths') - beforeEach -> - atom.enablePersistence = true - [projectPath] = atom.project.getPaths() - waitsForPromise -> - Promise.all([ - atom.workspace.open(path.join(projectPath, 'script.js')) - atom.workspace.open(path.join(projectPath, 'sample.js')) - .then (e) -> e.insertText('changes') - ]) - - runs -> atom.workspace.getActivePane().splitRight() - waitsForPromise -> atom.workspace.open().then((e) -> e.setText('new editor')) - waitsForPromise -> atom.saveState() - runs -> atom.reset() - - afterEach -> - atom.enablePersistence = false - - it "restores the saved state", -> - spyOn(atom, "pickFolder").andCallFake (callback) -> - callback([projectPath]) - - waitsForPromise -> - atom.addProjectFolder() - - runs -> - expect(atom.project.getPaths()).toEqual([projectPath]) - expect(atom.workspace.getPanes().length).toEqual(2) - items = atom.workspace.getPaneItems() - expect(items.length).toEqual(3) - [unmodifiedNamedItem, modifiedNamedItem, modifiedUnnamedItem] = items - expect(unmodifiedNamedItem.getPath()).toEqual(path.join(projectPath, 'script.js')) - expect(unmodifiedNamedItem.isModified()).toBe(false) - expect(modifiedNamedItem.getPath()).toEqual(path.join(projectPath, 'sample.js')) - waitsFor -> modifiedNamedItem.isModified() - runs -> - expect(modifiedNamedItem.getText()).toMatch(/^changes/) - expect(modifiedUnnamedItem.getPath()).toEqual(undefined) - waitsFor -> modifiedUnnamedItem.isModified() - runs -> - expect(modifiedUnnamedItem.getText()).toEqual('new editor') - - it "maintains any existing dirty or named pane items", -> - # # TODO handle collisions - # waitsForPromise -> - # atom.workspace.open(path.join(projectPath, 'script.js')) - - waitsForPromise -> - Promise.all([ - atom.workspace.open(path.join(projectPath, 'css.css')) - atom.workspace.open(path.join(projectPath, 'lorem.txt')) - .then (e) -> e.insertText('changes') - atom.workspace.open().then (e) -> e.setText('another new editor') - atom.workspace.open() - ]) - - spyOn(atom, "pickFolder").andCallFake (callback) -> - callback([projectPath]) - - waitsForPromise -> - atom.addProjectFolder() - - runs -> - expect(atom.project.getPaths()).toEqual([projectPath]) - expect(atom.workspace.getPanes().length).toEqual(2) - items = atom.workspace.getPaneItems() - expect(items.length).toEqual(6) # 3 existing pane items, 3 from saved state - # discarded the empty, unnamed item (likely opened due to the "open empty editor on start" config option) - [modifiedUnnamedItem, unmodifiedNamedItem, modifiedNamedItem] = items - expect(unmodifiedNamedItem.getPath()).toEqual(path.join(projectPath, 'css.css')) - expect(unmodifiedNamedItem.isModified()).toBe(false) - expect(modifiedNamedItem.getPath()).toEqual(path.join(projectPath, 'lorem.txt')) - waitsFor -> modifiedNamedItem.isModified() - runs -> - expect(modifiedNamedItem.getText()).toMatch(/^changes/) - expect(modifiedUnnamedItem.getPath()).toEqual(undefined) - waitsFor -> modifiedUnnamedItem.isModified() - runs -> - expect(modifiedUnnamedItem.getText()).toEqual('another new editor') - - describe "when there is no saved state for the added folders", -> - beforeEach -> - spyOn(atom, 'loadState').andReturn(Promise.resolve(null)) - spyOn(atom, 'restoreStateIntoEnvironment') - - it "adds the selected folder to the project", -> - initialPaths = atom.project.setPaths([]) - tempDirectory = temp.mkdirSync("a-new-directory") - spyOn(atom, "pickFolder").andCallFake (callback) -> - callback([tempDirectory]) - waitsForPromise -> - atom.addProjectFolder() - runs -> - expect(atom.project.getPaths()).toEqual([tempDirectory]) - expect(atom.restoreStateIntoEnvironment).not.toHaveBeenCalled() - - describe "when the project already contains at least one folder", -> - it "adds a second path to the project", -> - initialPaths = atom.project.getPaths() + fit "adds the selected folder to the project", -> + initialPaths = atom.project.setPaths([]) tempDirectory = temp.mkdirSync("a-new-directory") spyOn(atom, "pickFolder").andCallFake (callback) -> callback([tempDirectory]) waitsForPromise -> atom.addProjectFolder() - runs -> - expect(atom.project.getPaths()).toEqual(initialPaths.concat([tempDirectory])) + # runs -> + # expect(atom.project.getPaths()).toEqual([tempDirectory]) + # expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled() + + describe "when there is saved state for the relevant directories", -> + state = Symbol('savedState') + + beforeEach -> + spyOn(atom, "getStateKey").andCallFake (dirs) -> dirs.join(':') + spyOn(atom, "loadState").andCallFake (key) -> + if key == __dirname then Promise.resolve(state) else Promise.resolve(null) + spyOn(atom, "attemptRestoreProjectStateForPaths") + spyOn(atom, "pickFolder").andCallFake (callback) -> + callback([__dirname]) + atom.project.setPaths([]) + + describe "when there are no project folders", -> + it "attempts to restore the project state", -> + waitsForPromise -> + atom.addProjectFolder() + runs -> + expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname]) + expect(atom.project.getPaths()).toEqual([]) + + describe "when there are already project folders", -> + openedPath = path.join(__dirname, 'fixtures') + beforeEach -> + atom.project.setPaths([openedPath]) + + it "does not attempt to restore the project state, instead adding the project paths", -> + waitsForPromise -> + atom.addProjectFolder() + runs -> + expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled() + expect(atom.project.getPaths()).toEqual([openedPath, __dirname]) + + describe "attemptRestoreProjectStateForPaths(state, projectPaths, filesToOpen)", -> + describe "when the window is clean (empty or has only unnamed, unmodified buffers)", -> + beforeEach -> + # Unnamed, unmodified buffer doesn't count toward "clean"-ness + waitsForPromise -> atom.workspace.open() + + it "automatically restores the saved state into the current environment", -> + state = Symbol() + spyOn(atom.workspace, 'open') + spyOn(atom, 'restoreStateIntoThisEnvironment') + + atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename]) + expect(atom.restoreStateIntoThisEnvironment).toHaveBeenCalledWith(state) + expect(atom.workspace.open.callCount).toBe(1) + expect(atom.workspace.open).toHaveBeenCalledWith(__filename) + + describe "when the window is dirty", -> + editor = null + + beforeEach -> + waitsForPromise -> atom.workspace.open().then (e) -> + editor = e + editor.setText('new editor') + + it "prompts the user to restore the state in a new window, discarding it and adding folder to current window", -> + spyOn(atom, "confirm").andReturn(1) + spyOn(atom.project, 'addPath') + spyOn(atom.workspace, 'open') + state = Symbol() + + atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename]) + expect(atom.confirm).toHaveBeenCalled() + expect(atom.project.addPath.callCount).toBe(1) + expect(atom.project.addPath).toHaveBeenCalledWith(__dirname) + expect(atom.workspace.open.callCount).toBe(1) + expect(atom.workspace.open).toHaveBeenCalledWith(__filename) + + it "prompts the user to restore the state in a new window, opening a new window", -> + spyOn(atom, "confirm").andReturn(0) + spyOn(atom, "open") + state = Symbol() + + atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename]) + expect(atom.confirm).toHaveBeenCalled() + expect(atom.open).toHaveBeenCalledWith + pathsToOpen: [__dirname, __filename] + newWindow: true + devMode: atom.inDevMode() + safeMode: atom.inSafeMode() + + describe "::unloadEditorWindow()", -> it "saves the BlobStore so it can be loaded after reload", -> diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 5cf6cf44d..58d7ee431 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -861,27 +861,50 @@ class AtomEnvironment extends Model addProjectFolder: -> @pickFolder (selectedPaths = []) => - @loadState(@getStateKey(selectedPaths)).then (state) => - if state && @project.getPaths().length is 0 - @restoreStateIntoEnvironment(state) - else - @project.addPath(selectedPath) for selectedPath in selectedPaths + @addToProject(selectedPaths) - restoreStateIntoEnvironment: (state) -> - shouldSerializeItem = (item) -> - return true unless item instanceof TextEditor - item.getPath() or item.isModified() - serializedOpenItems = (item.serialize() for item in @workspace.getPaneItems() when shouldSerializeItem(item)) - serializedBuffers = (buffer.serialize() for buffer in @project.buffers) + addToProject: (projectPaths) -> + @loadState(@getStateKey(projectPaths)).then (state) => + if state and @project.getPaths().length is 0 + @attemptRestoreProjectStateForPaths(state, projectPaths) + else + @project.addPath(folder) for folder in projectPaths + attemptRestoreProjectStateForPaths: (state, projectPaths, filesToOpen = []) -> + paneItemIsEmptyUnnamedTextEditor = (item) -> + return false unless item instanceof TextEditor + return false if item.getPath() or item.isModified() + true + + windowIsUnused = @workspace.getPaneItems().every(paneItemIsEmptyUnnamedTextEditor) + if windowIsUnused + @restoreStateIntoThisEnvironment(state) + @workspace.open(file) for file in filesToOpen + else + nouns = if projectPaths.length is 1 then 'folder' else 'folders' + btn = @confirm + message: 'Previous automatically-saved project state detected' + detailedMessage: "There is previously saved state for the selected #{nouns}. " + + "Would you like to add the #{nouns} to this window, permanently discarding the saved state, " + + "or open the #{nouns} in a new window, restoring the saved state?" + buttons: [ + 'Open in new window and recover state' + 'Add to this window and discard state' + ] + if btn is 0 + @open + pathsToOpen: projectPaths.concat(filesToOpen) + newWindow: true + devMode: @inDevMode() + safeMode: @inSafeMode() + else if btn is 1 + @project.addPath(selectedPath) for selectedPath in projectPaths + @workspace.open(file) for file in filesToOpen + + restoreStateIntoThisEnvironment: (state) -> state.fullScreen = @isFullScreen() pane.destroy() for pane in @workspace.getPanes() @deserialize(state) - savedBuffers = (TextBuffer.deserialize(serializedBuffer) for serializedBuffer in serializedBuffers) - @project.buffers = @project.buffers.concat(savedBuffers) - - items = (@deserializers.deserialize(itemState) for itemState in serializedOpenItems) - @workspace.getPanes()[0].addItems(items, 0) showSaveDialog: (callback) -> callback(@showSaveDialogSync()) From d9b73fa6454f1bd30ac03bbcbbf9d3725cdb5f07 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 22 Mar 2017 20:25:57 -0700 Subject: [PATCH 26/73] Restore state when opening folders to applicable windows Note: "clean window" is defined as 1) having an empty project and 2) having no pane items or only empty unnamed buffers When project is empty and there is saved state associated with the opened/added folders... * Open a file or folder (from command line or Open menu) * If we have a clean window, restore project state in window * If window is dirty, restore saved state in new window --- spec/atom-environment-spec.coffee | 113 +++++++++++++++++++++--------- src/atom-environment.coffee | 33 ++++++--- src/project.coffee | 13 ++-- 3 files changed, 112 insertions(+), 47 deletions(-) diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 6fc5ab1a8..52e4af91a 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -461,44 +461,91 @@ describe "AtomEnvironment", -> spyOn(atom.workspace, 'open') atom.project.setPaths([]) - describe "when the opened path exists", -> - it "adds it to the project's paths", -> - pathToOpen = __filename - atom.openLocations([{pathToOpen}]) - expect(atom.project.getPaths()[0]).toBe __dirname + describe "when there is no saved state", -> + beforeEach -> + spyOn(atom, "loadState").andReturn(Promise.resolve(null)) - describe "then a second path is opened with forceAddToWindow", -> - it "adds the second path to the project's paths", -> - firstPathToOpen = __dirname - secondPathToOpen = path.resolve(__dirname, './fixtures') - atom.openLocations([{pathToOpen: firstPathToOpen}]) - atom.openLocations([{pathToOpen: secondPathToOpen, forceAddToWindow: true}]) - expect(atom.project.getPaths()).toEqual([firstPathToOpen, secondPathToOpen]) + describe "when the opened path exists", -> + it "adds it to the project's paths", -> + pathToOpen = __filename + waitsForPromise -> atom.openLocations([{pathToOpen}]) + runs -> expect(atom.project.getPaths()[0]).toBe __dirname - describe "when the opened path does not exist but its parent directory does", -> - it "adds the parent directory to the project paths", -> - pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt') - atom.openLocations([{pathToOpen}]) - expect(atom.project.getPaths()[0]).toBe __dirname + describe "then a second path is opened with forceAddToWindow", -> + it "adds the second path to the project's paths", -> + firstPathToOpen = __dirname + secondPathToOpen = path.resolve(__dirname, './fixtures') + waitsForPromise -> atom.openLocations([{pathToOpen: firstPathToOpen}]) + waitsForPromise -> atom.openLocations([{pathToOpen: secondPathToOpen, forceAddToWindow: true}]) + runs -> expect(atom.project.getPaths()).toEqual([firstPathToOpen, secondPathToOpen]) - describe "when the opened path is a file", -> - it "opens it in the workspace", -> - pathToOpen = __filename - atom.openLocations([{pathToOpen}]) - expect(atom.workspace.open.mostRecentCall.args[0]).toBe __filename + describe "when the opened path does not exist but its parent directory does", -> + it "adds the parent directory to the project paths", -> + pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt') + waitsForPromise -> atom.openLocations([{pathToOpen}]) + runs -> expect(atom.project.getPaths()[0]).toBe __dirname - describe "when the opened path is a directory", -> - it "does not open it in the workspace", -> - pathToOpen = __dirname - atom.openLocations([{pathToOpen}]) - expect(atom.workspace.open.callCount).toBe 0 + describe "when the opened path is a file", -> + it "opens it in the workspace", -> + pathToOpen = __filename + waitsForPromise -> atom.openLocations([{pathToOpen}]) + runs -> expect(atom.workspace.open.mostRecentCall.args[0]).toBe __filename - describe "when the opened path is a uri", -> - it "adds it to the project's paths as is", -> - pathToOpen = 'remote://server:7644/some/dir/path' - spyOn(atom.project, 'addPath') - atom.openLocations([{pathToOpen}]) - expect(atom.project.addPath).toHaveBeenCalledWith(pathToOpen) + describe "when the opened path is a directory", -> + it "does not open it in the workspace", -> + pathToOpen = __dirname + waitsForPromise -> atom.openLocations([{pathToOpen}]) + runs -> expect(atom.workspace.open.callCount).toBe 0 + + describe "when the opened path is a uri", -> + it "adds it to the project's paths as is", -> + pathToOpen = 'remote://server:7644/some/dir/path' + spyOn(atom.project, 'addPath') + waitsForPromise -> atom.openLocations([{pathToOpen}]) + runs -> expect(atom.project.addPath).toHaveBeenCalledWith(pathToOpen) + + describe "when there is saved state for the relevant directories", -> + state = Symbol('savedState') + + beforeEach -> + spyOn(atom, "getStateKey").andCallFake (dirs) -> dirs.join(':') + spyOn(atom, "loadState").andCallFake (key) -> + if key == __dirname then Promise.resolve(state) else Promise.resolve(null) + spyOn(atom, "attemptRestoreProjectStateForPaths") + + describe "when there are no project folders", -> + it "attempts to restore the project state", -> + pathToOpen = __dirname + waitsForPromise -> atom.openLocations([{pathToOpen}]) + runs -> + expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [pathToOpen], []) + expect(atom.project.getPaths()).toEqual([]) + + it "opens the specified files", -> + waitsForPromise -> atom.openLocations([{pathToOpen: __dirname}, {pathToOpen: __filename}]) + runs -> + expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname], [__filename]) + expect(atom.project.getPaths()).toEqual([]) + + + describe "when there are already project folders", -> + beforeEach -> + atom.project.setPaths([__dirname]) + + it "does not attempt to restore the project state, instead adding the project paths", -> + pathToOpen = path.join(__dirname, 'fixtures') + waitsForPromise -> atom.openLocations([{pathToOpen, forceAddToWindow: true}]) + runs -> + expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled() + expect(atom.project.getPaths()).toEqual([__dirname, pathToOpen]) + + it "opens the specified files", -> + pathToOpen = path.join(__dirname, 'fixtures') + fileToOpen = path.join(pathToOpen, 'michelle-is-awesome.txt') + waitsForPromise -> atom.openLocations([{pathToOpen}, {pathToOpen: fileToOpen}]) + runs -> + expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalledWith(state, [pathToOpen], [fileToOpen]) + expect(atom.project.getPaths()).toEqual([__dirname]) describe "::updateAvailable(info) (called via IPC from browser process)", -> subscription = null diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 58d7ee431..ad4a2103d 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -1019,23 +1019,38 @@ class AtomEnvironment extends Model openLocations: (locations) -> needsProjectPaths = @project?.getPaths().length is 0 + foldersToAddToProject = [] + fileLocationsToOpen = [] + + pushFolderToOpen = (folder) -> + if folder not in foldersToAddToProject + foldersToAddToProject.push(folder) + for {pathToOpen, initialLine, initialColumn, forceAddToWindow} in locations if pathToOpen? and (needsProjectPaths or forceAddToWindow) if fs.existsSync(pathToOpen) - @project.addPath(pathToOpen) + pushFolderToOpen @project.getDirectoryForProjectPath(pathToOpen).getPath() else if fs.existsSync(path.dirname(pathToOpen)) - @project.addPath(path.dirname(pathToOpen)) + pushFolderToOpen @project.getDirectoryForProjectPath(path.dirname(pathToOpen)).getPath() else - @project.addPath(pathToOpen) + pushFolderToOpen @project.getDirectoryForProjectPath(pathToOpen).getPath() unless fs.isDirectorySync(pathToOpen) + fileLocationsToOpen.push({pathToOpen, initialLine, initialColumn}) + + if foldersToAddToProject.length > 0 + @loadState(@getStateKey(foldersToAddToProject)).then (state) => + if state and needsProjectPaths # only load state if this is the first path added to the project + files = (location.pathToOpen for location in fileLocationsToOpen) + @attemptRestoreProjectStateForPaths(state, foldersToAddToProject, files) + else + @project.addPath(folder) for folder in foldersToAddToProject + for {pathToOpen, initialLine, initialColumn} in fileLocationsToOpen + @workspace?.open(pathToOpen, {initialLine, initialColumn}) + else + for {pathToOpen, initialLine, initialColumn} in fileLocationsToOpen @workspace?.open(pathToOpen, {initialLine, initialColumn}) - - if needsProjectPaths - @loadState(@getStateKey(@project.getPaths())).then (state) => - @restoreStateIntoEnvironment(state) if state - - return + Promise.resolve(null) # Preserve this deprecation until 2.0. Sorry. Should have removed Q sooner. Promise.prototype.done = (callback) -> diff --git a/src/project.coffee b/src/project.coffee index a02f27dac..b9d8be32d 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -184,11 +184,7 @@ class Project extends Model # # * `projectPath` {String} The path to the directory to add. addPath: (projectPath, options) -> - directory = null - for provider in @directoryProviders - break if directory = provider.directoryForURISync?(projectPath) - directory ?= @defaultDirectoryProvider.directoryForURISync(projectPath) - + directory = @getDirectoryForProjectPath(projectPath) return unless directory.existsSync() for existingDirectory in @getDirectories() return if existingDirectory.getPath() is directory.getPath() @@ -203,6 +199,13 @@ class Project extends Model unless options?.emitEvent is false @emitter.emit 'did-change-paths', @getPaths() + getDirectoryForProjectPath: (projectPath) -> + directory = null + for provider in @directoryProviders + break if directory = provider.directoryForURISync?(projectPath) + directory ?= @defaultDirectoryProvider.directoryForURISync(projectPath) + directory + # Public: remove a path from the project's list of root paths. # # * `projectPath` {String} The path to remove. From 01175d774c857b5d476486f6e859fe0d41933d0a Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 22 Mar 2017 20:30:08 -0700 Subject: [PATCH 27/73] :fire: fit --- spec/atom-environment-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 52e4af91a..7b6dc57d0 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -333,7 +333,7 @@ describe "AtomEnvironment", -> spyOn(atom, 'loadState').andReturn(Promise.resolve(null)) spyOn(atom, 'attemptRestoreProjectStateForPaths') - fit "adds the selected folder to the project", -> + it "adds the selected folder to the project", -> initialPaths = atom.project.setPaths([]) tempDirectory = temp.mkdirSync("a-new-directory") spyOn(atom, "pickFolder").andCallFake (callback) -> From 3fcec8b8cda071b3bc0a55c55c70672302492760 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 23 Mar 2017 10:39:02 -0700 Subject: [PATCH 28/73] previousLocations -> itemLocationStore --- spec/workspace-spec.js | 6 +++--- src/workspace.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index 5b6ecc7ba..a4dbf65df 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -25,7 +25,7 @@ describe('Workspace', () => { atom.project.setPaths([atom.project.getDirectories()[0].resolve('dir')]) waits(1) - waitsForPromise(() => atom.workspace.previousLocations.clear()) + waitsForPromise(() => atom.workspace.itemLocationStore.clear()) }) afterEach(() => temp.cleanupSync()) @@ -303,7 +303,7 @@ describe('Workspace', () => { } const opener = jasmine.createSpy().andReturn(item) const dock = atom.workspace.getRightDock() - spyOn(atom.workspace.previousLocations, 'load').andReturn(Promise.resolve()) + spyOn(atom.workspace.itemLocationStore, 'load').andReturn(Promise.resolve()) spyOn(atom.workspace, 'getOpeners').andReturn([opener]) expect(dock.getPaneItems()).toHaveLength(0) waitsForPromise(() => atom.workspace.open('a')) @@ -323,7 +323,7 @@ describe('Workspace', () => { } const opener = uri => uri === ITEM_URI ? item : null const dock = atom.workspace.getRightDock() - spyOn(atom.workspace.previousLocations, 'load').andCallFake(uri => + spyOn(atom.workspace.itemLocationStore, 'load').andCallFake(uri => uri === 'atom://test' ? Promise.resolve('right') : Promise.resolve() ) spyOn(atom.workspace, 'getOpeners').andReturn([opener]) diff --git a/src/workspace.js b/src/workspace.js index 9910153d1..106d20526 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -46,7 +46,7 @@ module.exports = class Workspace extends Model { this.textEditorRegistry = params.textEditorRegistry this.hoveredDock = null this.draggingItem = false - this.previousLocations = new StateStore('AtomPreviousItemLocations', 1) + this.itemLocationStore = new StateStore('AtomPreviousItemLocations', 1) this.emitter = new Emitter() this.openers = [] @@ -299,7 +299,7 @@ module.exports = class Workspace extends Model { if (typeof item.getURI === 'function') { const uri = item.getURI() if (uri != null) { - this.previousLocations.save(item.getURI(), location) + this.itemLocationStore.save(item.getURI(), location) } } }) @@ -787,7 +787,7 @@ module.exports = class Workspace extends Model { // in the center location (legacy behavior) let location if (paneLocation == null && pane == null && split == null && uri != null) { - location = this.previousLocations.load(uri) + location = this.itemLocationStore.load(uri) } return Promise.resolve(location) From 3d9ce1610d140739bb8a72aa75df6e952060a162 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 23 Mar 2017 10:40:20 -0700 Subject: [PATCH 29/73] :shirt: --- spec/atom-environment-spec.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 5fba1ae48..d1eda732d 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -350,7 +350,7 @@ describe "AtomEnvironment", -> beforeEach -> spyOn(atom, "getStateKey").andCallFake (dirs) -> dirs.join(':') spyOn(atom, "loadState").andCallFake (key) -> - if key == __dirname then Promise.resolve(state) else Promise.resolve(null) + if key is __dirname then Promise.resolve(state) else Promise.resolve(null) spyOn(atom, "attemptRestoreProjectStateForPaths") spyOn(atom, "pickFolder").andCallFake (callback) -> callback([__dirname]) @@ -512,7 +512,7 @@ describe "AtomEnvironment", -> beforeEach -> spyOn(atom, "getStateKey").andCallFake (dirs) -> dirs.join(':') spyOn(atom, "loadState").andCallFake (key) -> - if key == __dirname then Promise.resolve(state) else Promise.resolve(null) + if key is __dirname then Promise.resolve(state) else Promise.resolve(null) spyOn(atom, "attemptRestoreProjectStateForPaths") describe "when there are no project folders", -> From e80220ab1ebb17fdf12a8587365390bc07ce8ed3 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 23 Mar 2017 11:18:54 -0700 Subject: [PATCH 30/73] Oops, let's put that back --- static/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/static/index.html b/static/index.html index 81e19d4da..39c7d80c1 100644 --- a/static/index.html +++ b/static/index.html @@ -1,6 +1,7 @@ + From d307c791c4eb9a10e87593086292fcf1a2d22ce8 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 23 Mar 2017 10:54:24 -0700 Subject: [PATCH 31/73] Be consistent about what "location" refers to --- src/workspace.js | 54 ++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/workspace.js b/src/workspace.js index 106d20526..6008f60f1 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -291,9 +291,9 @@ module.exports = class Workspace extends Model { if (this.movedItemSubscription != null) { this.movedItemSubscription.dispose() } - const paneLocations = Object.assign({center: this}, this.docks) + const paneContainers = Object.assign({center: this}, this.docks) this.movedItemSubscription = new CompositeDisposable( - ..._.map(paneLocations, (host, location) => ( + ..._.map(paneContainers, (host, location) => ( host.observePanes(pane => { pane.onDidAddItem(({item}) => { if (typeof item.getURI === 'function') { @@ -391,7 +391,7 @@ module.exports = class Workspace extends Model { // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observePaneItems (callback) { return new CompositeDisposable( - ...this.getPaneLocations().map(location => location.observePaneItems(callback)) + ...this.getPaneContainers().map(container => container.observePaneItems(callback)) ) } @@ -462,7 +462,7 @@ module.exports = class Workspace extends Model { // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddPane (callback) { return new CompositeDisposable( - ...this.getPaneLocations().map(location => location.onDidAddPane(callback)) + ...this.getPaneContainers().map(container => container.onDidAddPane(callback)) ) } @@ -476,7 +476,7 @@ module.exports = class Workspace extends Model { // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onWillDestroyPane (callback) { return new CompositeDisposable( - ...this.getPaneLocations().map(location => location.onWillDestroyPane(callback)) + ...this.getPaneContainers().map(container => container.onWillDestroyPane(callback)) ) } @@ -490,7 +490,7 @@ module.exports = class Workspace extends Model { // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroyPane (callback) { return new CompositeDisposable( - ...this.getPaneLocations().map(location => location.onDidDestroyPane(callback)) + ...this.getPaneContainers().map(container => container.onDidDestroyPane(callback)) ) } @@ -504,7 +504,7 @@ module.exports = class Workspace extends Model { // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observePanes (callback) { return new CompositeDisposable( - ...this.getPaneLocations().map(location => location.observePanes(callback)) + ...this.getPaneContainers().map(container => container.observePanes(callback)) ) } @@ -538,7 +538,7 @@ module.exports = class Workspace extends Model { // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddPaneItem (callback) { return new CompositeDisposable( - ...this.getPaneLocations().map(location => location.onDidAddPaneItem(callback)) + ...this.getPaneContainers().map(container => container.onDidAddPaneItem(callback)) ) } @@ -555,7 +555,7 @@ module.exports = class Workspace extends Model { // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. onWillDestroyPaneItem (callback) { return new CompositeDisposable( - ...this.getPaneLocations().map(location => location.onWillDestroyPaneItem(callback)) + ...this.getPaneContainers().map(container => container.onWillDestroyPaneItem(callback)) ) } @@ -571,7 +571,7 @@ module.exports = class Workspace extends Model { // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. onDidDestroyPaneItem (callback) { return new CompositeDisposable( - ...this.getPaneLocations().map(location => location.onDidDestroyPaneItem(callback)) + ...this.getPaneContainers().map(container => container.onDidDestroyPaneItem(callback)) ) } @@ -778,30 +778,30 @@ module.exports = class Workspace extends Model { const uri = options.uri == null && typeof item.getURI === 'function' ? item.getURI() : options.uri - let paneLocation + let paneContainer if (pane != null) { - paneLocation = this.getPaneLocations().find(location => location.getPanes().includes(pane)) + paneContainer = this.getPaneContainers().find(container => container.getPanes().includes(pane)) } // Determine which location to use, unless a split was provided. In that case, make sure it goes // in the center location (legacy behavior) let location - if (paneLocation == null && pane == null && split == null && uri != null) { + if (paneContainer == null && pane == null && split == null && uri != null) { location = this.itemLocationStore.load(uri) } return Promise.resolve(location) .then(location => { - if (paneLocation == null) { + if (paneContainer == null) { if (location == null && typeof item.getDefaultLocation === 'function') { location = item.getDefaultLocation() } - paneLocation = this.docks[location] || this.getCenter() + paneContainer = this.docks[location] || this.getCenter() } }) .then(() => { if (pane != null) return pane - pane = paneLocation.getActivePane() + pane = paneContainer.getActivePane() switch (split) { case 'left': return pane.findLeftmostSibling() case 'right': return pane.findOrCreateRightmostSibling() @@ -824,7 +824,7 @@ module.exports = class Workspace extends Model { if (activatePane) { pane.activate() } - paneLocation.activate() + paneContainer.activate() let initialColumn = 0 let initialLine = 0 @@ -966,7 +966,7 @@ module.exports = class Workspace extends Model { // // Returns an {Array} of items. getPaneItems () { - return _.flatten(this.getPaneLocations().map(location => location.getPaneItems())) + return _.flatten(this.getPaneContainers().map(container => container.getPaneItems())) } // Essential: Get the active {Pane}'s active item. @@ -994,14 +994,14 @@ module.exports = class Workspace extends Model { // Save all pane items. saveAll () { - this.getPaneLocations().forEach(location => { - location.saveAll() + this.getPaneContainers().forEach(container => { + container.saveAll() }) } confirmClose (options) { - return this.getPaneLocations() - .map(location => location.confirmClose(options)) + return this.getPaneContainers() + .map(container => container.confirmClose(options)) .every(saved => saved) } @@ -1040,7 +1040,7 @@ module.exports = class Workspace extends Model { // // Returns an {Array} of {Pane}s. getPanes () { - return _.flatten(this.getPaneLocations().map(location => location.getPanes())) + return _.flatten(this.getPaneContainers().map(container => container.getPanes())) } // Extended: Get the active {Pane}. @@ -1066,7 +1066,7 @@ module.exports = class Workspace extends Model { // // Returns a {Pane} or `undefined` if no pane exists for the given URI. paneForURI (uri) { - for (let location of this.getPaneLocations()) { + for (let location of this.getPaneContainers()) { const pane = location.paneForURI(uri) if (pane != null) { return pane @@ -1080,7 +1080,7 @@ module.exports = class Workspace extends Model { // // Returns a {Pane} or `undefined` if no pane exists for the given item. paneForItem (item) { - for (let location of this.getPaneLocations()) { + for (let location of this.getPaneContainers()) { const pane = location.paneForItem(item) if (pane != null) { return pane @@ -1195,7 +1195,7 @@ module.exports = class Workspace extends Model { return this.docks.bottom } - getPaneLocations () { + getPaneContainers () { return [this.getCenter(), ..._.values(this.docks)] } @@ -1203,7 +1203,7 @@ module.exports = class Workspace extends Model { let foundItems = false // If any visible item has the given URI, hide it - for (const location of this.getPaneLocations()) { + for (const location of this.getPaneContainers()) { const isCenter = location === this.getCenter() if (isCenter || location.isOpen()) { for (const pane of location.getPanes()) { From e01bc40a78c1352a7883306ad9e054540cb0df26 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 23 Mar 2017 10:55:31 -0700 Subject: [PATCH 32/73] "affordance" -> "hoverMargin" --- src/dock.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dock.js b/src/dock.js index a67ad3ab9..0908d8311 100644 --- a/src/dock.js +++ b/src/dock.js @@ -289,17 +289,17 @@ module.exports = class Dock { // The area used when detecting "leave" events is actually larger than when detecting entrances. if (includeButtonWidth) { - const affordance = 20 + const hoverMargin = 20 const toggleButtonSize = 50 / 2 // This needs to match the value in the CSS. switch (this.location) { case 'right': - bounds.left -= toggleButtonSize + affordance + bounds.left -= toggleButtonSize + hoverMargin break case 'bottom': - bounds.top -= toggleButtonSize + affordance + bounds.top -= toggleButtonSize + hoverMargin break case 'left': - bounds.right += toggleButtonSize + affordance + bounds.right += toggleButtonSize + hoverMargin break } } From 37a3c9b59c8e3d73bc4a53afed58877eb5efc245 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 23 Mar 2017 11:04:52 -0700 Subject: [PATCH 33/73] Measure toggle button size instead of hardcoding it --- src/dock.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/dock.js b/src/dock.js index 0908d8311..7ddc55883 100644 --- a/src/dock.js +++ b/src/dock.js @@ -290,16 +290,16 @@ module.exports = class Dock { // The area used when detecting "leave" events is actually larger than when detecting entrances. if (includeButtonWidth) { const hoverMargin = 20 - const toggleButtonSize = 50 / 2 // This needs to match the value in the CSS. + const {width, height} = this.toggleButton.getSize() switch (this.location) { case 'right': - bounds.left -= toggleButtonSize + hoverMargin + bounds.left -= width + hoverMargin break case 'bottom': - bounds.top -= toggleButtonSize + hoverMargin + bounds.top -= height + hoverMargin break case 'left': - bounds.right += toggleButtonSize + hoverMargin + bounds.right += width + hoverMargin break } } @@ -481,6 +481,13 @@ class DockToggleButton { return this.element } + getSize () { + if (this.size == null) { + this.size = this.element.getBoundingClientRect() + } + return this.size + } + destroy () { this.innerElement.removeEventListener('click', this.handleClick) this.innerElement.removeEventListener('dragenter', this.handleDragEnter) From 791457d9a710a59f21c8a40dc347aa86badb160f Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 23 Mar 2017 11:19:54 -0700 Subject: [PATCH 34/73] Add remaining pane container methods and documentation to docks --- src/dock.js | 303 ++++++++++++++++++++++++++++++++-------- src/workspace-center.js | 23 +-- 2 files changed, 258 insertions(+), 68 deletions(-) diff --git a/src/dock.js b/src/dock.js index 7ddc55883..585b2441d 100644 --- a/src/dock.js +++ b/src/dock.js @@ -336,24 +336,250 @@ module.exports = class Dock { // PaneContainer-delegating methods + /* + Section: Event Subscription + */ + + // Essential: Invoke the given callback with all current and future text + // editors in the dock. + // + // * `callback` {Function} to be called with current and future text editors. + // * `editor` An {TextEditor} that is present in {::getTextEditors} at the time + // of subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeTextEditors (callback) { + for (const textEditor of this.getTextEditors()) { + callback(textEditor) + } + return this.onDidAddTextEditor(({textEditor}) => callback(textEditor)) + } + + // Essential: Invoke the given callback with all current and future panes items + // in the dock. + // + // * `callback` {Function} to be called with current and future pane items. + // * `item` An item that is present in {::getPaneItems} at the time of + // subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observePaneItems (callback) { + return this.paneContainer.observePaneItems(callback) + } + + // Essential: Invoke the given callback when the active pane item changes. + // + // Because observers are invoked synchronously, it's important not to perform + // any expensive operations via this method. Consider + // {::onDidStopChangingActivePaneItem} to delay operations until after changes + // stop occurring. + // + // * `callback` {Function} to be called when the active pane item changes. + // * `item` The active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActivePaneItem (callback) { + return this.paneContainer.onDidChangeActivePaneItem(callback) + } + + // Essential: Invoke the given callback when the active pane item stops + // changing. + // + // Observers are called asynchronously 100ms after the last active pane item + // change. Handling changes here rather than in the synchronous + // {::onDidChangeActivePaneItem} prevents unneeded work if the user is quickly + // changing or closing tabs and ensures critical UI feedback, like changing the + // highlighted tab, gets priority over work that can be done asynchronously. + // + // * `callback` {Function} to be called when the active pane item stopts + // changing. + // * `item` The active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidStopChangingActivePaneItem (callback) { + return this.paneContainer.onDidStopChangingActivePaneItem(callback) + } + + // Essential: Invoke the given callback with the current active pane item and + // with all future active pane items in the dock. + // + // * `callback` {Function} to be called when the active pane item changes. + // * `item` The current active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActivePaneItem (callback) { + return this.paneContainer.observeActivePaneItem(callback) + } + + // Extended: Invoke the given callback when a pane is added to the dock. + // + // * `callback` {Function} to be called panes are added. + // * `event` {Object} with the following keys: + // * `pane` The added pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddPane (callback) { + return this.paneContainer.onDidAddPane(callback) + } + + // Extended: Invoke the given callback before a pane is destroyed in the + // dock. + // + // * `callback` {Function} to be called before panes are destroyed. + // * `event` {Object} with the following keys: + // * `pane` The pane to be destroyed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onWillDestroyPane (callback) { + return this.paneContainer.onWillDestroyPane(callback) + } + + // Extended: Invoke the given callback when a pane is destroyed in the dock. + // + // * `callback` {Function} to be called panes are destroyed. + // * `event` {Object} with the following keys: + // * `pane` The destroyed pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroyPane (callback) { + return this.paneContainer.onDidDestroyPane(callback) + } + + // Extended: Invoke the given callback with all current and future panes in the + // dock. + // + // * `callback` {Function} to be called with current and future panes. + // * `pane` A {Pane} that is present in {::getPanes} at the time of + // subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observePanes (callback) { + return this.paneContainer.observePanes(callback) + } + + // Extended: Invoke the given callback when the active pane changes. + // + // * `callback` {Function} to be called when the active pane changes. + // * `pane` A {Pane} that is the current return value of {::getActivePane}. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActivePane (callback) { + return this.paneContainer.onDidChangeActivePane(callback) + } + + // Extended: Invoke the given callback with the current active pane and when + // the active pane changes. + // + // * `callback` {Function} to be called with the current and future active# + // panes. + // * `pane` A {Pane} that is the current return value of {::getActivePane}. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActivePane (callback) { + return this.paneContainer.observeActivePane(callback) + } + + // Extended: Invoke the given callback when a pane item is added to the dock. + // + // * `callback` {Function} to be called when pane items are added. + // * `event` {Object} with the following keys: + // * `item` The added pane item. + // * `pane` {Pane} containing the added item. + // * `index` {Number} indicating the index of the added item in its pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddPaneItem (callback) { + return this.paneContainer.onDidAddPaneItem(callback) + } + + // Extended: Invoke the given callback when a pane item is about to be + // destroyed, before the user is prompted to save it. + // + // * `callback` {Function} to be called before pane items are destroyed. + // * `event` {Object} with the following keys: + // * `item` The item to be destroyed. + // * `pane` {Pane} containing the item to be destroyed. + // * `index` {Number} indicating the index of the item to be destroyed in + // its pane. + // + // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. + onWillDestroyPaneItem (callback) { + return this.paneContainer.onWillDestroyPaneItem(callback) + } + + // Extended: Invoke the given callback when a pane item is destroyed. + // + // * `callback` {Function} to be called when pane items are destroyed. + // * `event` {Object} with the following keys: + // * `item` The destroyed item. + // * `pane` {Pane} containing the destroyed item. + // * `index` {Number} indicating the index of the destroyed item in its + // pane. + // + // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. + onDidDestroyPaneItem (callback) { + return this.paneContainer.onDidDestroyPaneItem(callback) + } + + /* + Section: Pane Items + */ + + // Essential: Get all pane items in the dock. + // + // Returns an {Array} of items. + getPaneItems () { + return this.paneContainer.getPaneItems() + } + + // Essential: Get the active {Pane}'s active item. + // + // Returns an pane item {Object}. + getActivePaneItem () { + return this.paneContainer.getActivePaneItem() + } + + // Essential: Get all text editors in the dock. + // + // Returns an {Array} of {TextEditor}s. + getTextEditors () { + return this.paneContainer.getTextEditors() + } + + // Essential: Get the active item if it is an {TextEditor}. + // + // Returns an {TextEditor} or `undefined` if the current active item is not an + // {TextEditor}. + getActiveTextEditor () { + const activeItem = this.getActivePaneItem() + if (activeItem instanceof TextEditor) { return activeItem } + } + + // Save all pane items. + saveAll () { + this.paneContainer.saveAll() + } + + confirmClose (options) { + return this.paneContainer.confirmClose(options) + } + + /* + Section: Panes + */ + + // Extended: Get all panes in the dock. + // + // Returns an {Array} of {Pane}s. getPanes () { return this.paneContainer.getPanes() } - observePanes (fn) { - return this.paneContainer.observePanes(fn) - } - - onDidAddPane (fn) { - return this.paneContainer.onDidAddPane(fn) - } - - onWillDestroyPane (fn) { - return this.paneContainer.onWillDestroyPane(fn) - } - - onDidDestroyPane (fn) { - return this.paneContainer.onDidDestroyPane(fn) + // Extended: Get the active {Pane}. + // + // Returns a {Pane}. + getActivePane () { + return this.paneContainer.getActivePane() } paneForURI (uri) { @@ -364,49 +590,12 @@ module.exports = class Dock { return this.paneContainer.paneForItem(item) } - getActivePane () { - return this.paneContainer.getActivePane() - } - - getPaneItems () { - return this.paneContainer.getPaneItems() - } - - getActivePaneItem () { - return this.paneContainer.getActivePaneItem() - } - - getTextEditors () { - return this.paneContainer.getTextEditors() - } - - getActiveTextEditor () { - const activeItem = this.getActivePaneItem() - if (activeItem instanceof TextEditor) { return activeItem } - } - - observePaneItems (fn) { - return this.paneContainer.observePaneItems(fn) - } - - onDidAddPaneItem (fn) { - return this.paneContainer.onDidAddPaneItem(fn) - } - - onWillDestroyPaneItem (fn) { - return this.paneContainer.onWillDestroyPaneItem(fn) - } - - onDidDestroyPaneItem (fn) { - return this.paneContainer.onDidDestroyPaneItem(fn) - } - - saveAll () { - this.paneContainer.saveAll() - } - - confirmClose (options) { - return this.paneContainer.confirmClose(options) + // Destroy (close) the active pane. + destroyActivePane () { + const activePane = this.getActivePane() + if (activePane != null) { + activePane.destroy() + } } } diff --git a/src/workspace-center.js b/src/workspace-center.js index c2c875b74..4979f4a6f 100644 --- a/src/workspace-center.js +++ b/src/workspace-center.js @@ -14,7 +14,7 @@ module.exports = class WorkspaceCenter { */ // Essential: Invoke the given callback with all current and future text - // editors in the workspace. + // editors in the workspace center. // // * `callback` {Function} to be called with current and future text editors. // * `editor` An {TextEditor} that is present in {::getTextEditors} at the time @@ -27,7 +27,7 @@ module.exports = class WorkspaceCenter { } // Essential: Invoke the given callback with all current and future panes items - // in the workspace. + // in the workspace center. // // * `callback` {Function} to be called with current and future pane items. // * `item` An item that is present in {::getPaneItems} at the time of @@ -70,7 +70,7 @@ module.exports = class WorkspaceCenter { } // Essential: Invoke the given callback with the current active pane item and - // with all future active pane items in the workspace. + // with all future active pane items in the workspace center. // // * `callback` {Function} to be called when the active pane item changes. // * `item` The current active pane item. @@ -80,7 +80,8 @@ module.exports = class WorkspaceCenter { return this.paneContainer.observeActivePaneItem(callback) } - // Extended: Invoke the given callback when a pane is added to the workspace. + // Extended: Invoke the given callback when a pane is added to the workspace + // center. // // * `callback` {Function} to be called panes are added. // * `event` {Object} with the following keys: @@ -92,7 +93,7 @@ module.exports = class WorkspaceCenter { } // Extended: Invoke the given callback before a pane is destroyed in the - // workspace. + // workspace center. // // * `callback` {Function} to be called before panes are destroyed. // * `event` {Object} with the following keys: @@ -104,7 +105,7 @@ module.exports = class WorkspaceCenter { } // Extended: Invoke the given callback when a pane is destroyed in the - // workspace. + // workspace center. // // * `callback` {Function} to be called panes are destroyed. // * `event` {Object} with the following keys: @@ -116,7 +117,7 @@ module.exports = class WorkspaceCenter { } // Extended: Invoke the given callback with all current and future panes in the - // workspace. + // workspace center. // // * `callback` {Function} to be called with current and future panes. // * `pane` A {Pane} that is present in {::getPanes} at the time of @@ -150,7 +151,7 @@ module.exports = class WorkspaceCenter { } // Extended: Invoke the given callback when a pane item is added to the - // workspace. + // workspace center. // // * `callback` {Function} to be called when pane items are added. // * `event` {Object} with the following keys: @@ -196,7 +197,7 @@ module.exports = class WorkspaceCenter { Section: Pane Items */ - // Essential: Get all pane items in the workspace. + // Essential: Get all pane items in the workspace center. // // Returns an {Array} of items. getPaneItems () { @@ -210,7 +211,7 @@ module.exports = class WorkspaceCenter { return this.paneContainer.getActivePaneItem() } - // Essential: Get all text editors in the workspace. + // Essential: Get all text editors in the workspace center. // // Returns an {Array} of {TextEditor}s. getTextEditors () { @@ -239,7 +240,7 @@ module.exports = class WorkspaceCenter { Section: Panes */ - // Extended: Get all panes in the workspace. + // Extended: Get all panes in the workspace center. // // Returns an {Array} of {Pane}s. getPanes () { From 3e826591fda6b51096fd42d1b3d7f08f4d9325e3 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 23 Mar 2017 11:27:37 -0700 Subject: [PATCH 35/73] Clean up storage of most recent location --- src/workspace-center.js | 4 ++++ src/workspace.js | 29 +++++++++-------------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/workspace-center.js b/src/workspace-center.js index 4979f4a6f..efaf0654c 100644 --- a/src/workspace-center.js +++ b/src/workspace-center.js @@ -9,6 +9,10 @@ module.exports = class WorkspaceCenter { activate () {} + getLocation () { + return 'center' + } + /* Section: Event Subscription */ diff --git a/src/workspace.js b/src/workspace.js index 6008f60f1..9a6f40204 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -288,24 +288,16 @@ module.exports = class Workspace extends Model { } subscribeToMovedItems () { - if (this.movedItemSubscription != null) { - this.movedItemSubscription.dispose() + for (const paneContainer of this.getPaneContainers()) { + paneContainer.onDidAddPaneItem(({item}) => { + if (typeof item.getURI === 'function') { + const uri = item.getURI() + if (uri != null) { + this.itemLocationStore.save(item.getURI(), paneContainer.getLocation()) + } + } + }) } - const paneContainers = Object.assign({center: this}, this.docks) - this.movedItemSubscription = new CompositeDisposable( - ..._.map(paneContainers, (host, location) => ( - host.observePanes(pane => { - pane.onDidAddItem(({item}) => { - if (typeof item.getURI === 'function') { - const uri = item.getURI() - if (uri != null) { - this.itemLocationStore.save(item.getURI(), location) - } - } - }) - }) - )) - ) } // Updates the application's title and proxy icon based on whichever file is @@ -1170,9 +1162,6 @@ module.exports = class Workspace extends Model { if (this.activeItemSubscriptions != null) { this.activeItemSubscriptions.dispose() } - if (this.movedItemSubscription != null) { - this.movedItemSubscription.dispose() - } } /* From 77ea97e623cdef220de6e933b56ef30539e2c94b Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 23 Mar 2017 11:43:06 -0700 Subject: [PATCH 36/73] Use async/await in `openItem()` --- src/workspace.js | 115 ++++++++++++++++++++++++----------------------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/src/workspace.js b/src/workspace.js index 9a6f40204..49f69ff5e 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -1,4 +1,4 @@ -'use strict' +'use babel' const _ = require('underscore-plus') const url = require('url') @@ -762,11 +762,11 @@ module.exports = class Workspace extends Model { } } - openItem (item, options = {}) { + async openItem (item, options = {}) { let {pane, split} = options - if (item == null) return Promise.resolve() - if (pane != null && pane.isDestroyed()) return Promise.resolve(item) + if (item == null) return undefined + if (pane != null && pane.isDestroyed()) return item const uri = options.uri == null && typeof item.getURI === 'function' ? item.getURI() : options.uri @@ -779,63 +779,66 @@ module.exports = class Workspace extends Model { // in the center location (legacy behavior) let location if (paneContainer == null && pane == null && split == null && uri != null) { - location = this.itemLocationStore.load(uri) + location = await this.itemLocationStore.load(uri) } - return Promise.resolve(location) - .then(location => { - if (paneContainer == null) { - if (location == null && typeof item.getDefaultLocation === 'function') { - location = item.getDefaultLocation() - } - paneContainer = this.docks[location] || this.getCenter() - } - }) - .then(() => { - if (pane != null) return pane - pane = paneContainer.getActivePane() - switch (split) { - case 'left': return pane.findLeftmostSibling() - case 'right': return pane.findOrCreateRightmostSibling() - case 'up': return pane.findTopmostSibling() - case 'down': return pane.findOrCreateBottommostSibling() - default: return pane - } - }) - .then(pane => { - if (!options.pending && (pane.getPendingItem() === item)) { - pane.clearPendingItem() - } + if (paneContainer == null) { + if (location == null && typeof item.getDefaultLocation === 'function') { + location = item.getDefaultLocation() + } + paneContainer = this.docks[location] || this.getCenter() + } - const activatePane = options.activatePane != null ? options.activatePane : true - const activateItem = options.activateItem != null ? options.activateItem : true - this.itemOpened(item) - if (activateItem) { - pane.activateItem(item, {pending: options.pending}) - } - if (activatePane) { - pane.activate() - } - paneContainer.activate() + if (pane == null) { + pane = paneContainer.getActivePane() + switch (split) { + case 'left': + pane = pane.findLeftmostSibling() + break + case 'right': + pane = pane.findOrCreateRightmostSibling() + break + case 'up': + pane = pane.findTopmostSibling() + break + case 'down': + pane = pane.findOrCreateBottommostSibling() + break + } + } - let initialColumn = 0 - let initialLine = 0 - if (!Number.isNaN(options.initialLine)) { - initialLine = options.initialLine - } - if (!Number.isNaN(options.initialColumn)) { - initialColumn = options.initialColumn - } - if ((initialLine >= 0) || (initialColumn >= 0)) { - if (typeof item.setCursorBufferPosition === 'function') { - item.setCursorBufferPosition([initialLine, initialColumn]) - } - } + if (!options.pending && (pane.getPendingItem() === item)) { + pane.clearPendingItem() + } - const index = pane.getActiveItemIndex() - this.emitter.emit('did-open', {uri, pane, item, index}) - return item - }) + const activatePane = options.activatePane != null ? options.activatePane : true + const activateItem = options.activateItem != null ? options.activateItem : true + this.itemOpened(item) + if (activateItem) { + pane.activateItem(item, {pending: options.pending}) + } + if (activatePane) { + pane.activate() + } + paneContainer.activate() + + let initialColumn = 0 + let initialLine = 0 + if (!Number.isNaN(options.initialLine)) { + initialLine = options.initialLine + } + if (!Number.isNaN(options.initialColumn)) { + initialColumn = options.initialColumn + } + if ((initialLine >= 0) || (initialColumn >= 0)) { + if (typeof item.setCursorBufferPosition === 'function') { + item.setCursorBufferPosition([initialLine, initialColumn]) + } + } + + const index = pane.getActiveItemIndex() + this.emitter.emit('did-open', {uri, pane, item, index}) + return item } openTextFile (uri, options) { From ed2c39999751fb8419352e957c69f3c141389e7e Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 23 Mar 2017 16:19:03 -0700 Subject: [PATCH 37/73] :white_check_mark: Fix main process tests --- spec/main-process/atom-application.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index d30900b99..116a85249 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -367,6 +367,7 @@ describe('AtomApplication', function () { const [app2Window1, app2Window2] = atomApplication2.launch(parseCommandLine([])) await app2Window1.loadedPromise await app2Window2.loadedPromise + await new Promise(resolve => setTimeout(resolve, 500)) // session restoration is async assert.deepEqual(await getTreeViewRootDirectories(app2Window1), [tempDirPath1]) assert.deepEqual(await getTreeViewRootDirectories(app2Window2), [tempDirPath2]) From 648055c5a97c636c172b258bf23ff7aa6a638a2a Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 23 Mar 2017 16:29:01 -0700 Subject: [PATCH 38/73] Just to be sure, let's use a longer timeout --- spec/main-process/atom-application.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 116a85249..a3ebd3645 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -367,7 +367,7 @@ describe('AtomApplication', function () { const [app2Window1, app2Window2] = atomApplication2.launch(parseCommandLine([])) await app2Window1.loadedPromise await app2Window2.loadedPromise - await new Promise(resolve => setTimeout(resolve, 500)) // session restoration is async + await new Promise(resolve => setTimeout(resolve, 5000)) // session restoration is async assert.deepEqual(await getTreeViewRootDirectories(app2Window1), [tempDirPath1]) assert.deepEqual(await getTreeViewRootDirectories(app2Window2), [tempDirPath2]) From 0f6489e3479af7929b42a80ed7657d5066d1c5c1 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 23 Mar 2017 16:51:59 -0700 Subject: [PATCH 39/73] Use test-until for more flexible test timeout --- package.json | 1 + spec/main-process/atom-application.test.js | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5568e2424..f032fdf4c 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "sinon": "1.17.4", "source-map-support": "^0.3.2", "temp": "0.8.1", + "test-until": "^1.1.1", "text-buffer": "11.4.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index a3ebd3645..279b39035 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -5,6 +5,7 @@ import dedent from 'dedent' import electron from 'electron' import fs from 'fs-plus' import path from 'path' +import until from 'test-until' import AtomApplication from '../../src/main-process/atom-application' import parseCommandLine from '../../src/main-process/parse-command-line' import {timeoutPromise, conditionPromise} from '../async-spec-helpers' @@ -369,8 +370,14 @@ describe('AtomApplication', function () { await app2Window2.loadedPromise await new Promise(resolve => setTimeout(resolve, 5000)) // session restoration is async - assert.deepEqual(await getTreeViewRootDirectories(app2Window1), [tempDirPath1]) - assert.deepEqual(await getTreeViewRootDirectories(app2Window2), [tempDirPath2]) + await until(`app2Window1 contains tempDirPath1 (${tempDirPath1})`, () => { + const dirs = await getTreeViewRootDirectories(app2Window1) + return dirs.length === 1 && dirs[0] == tempDirPath1 + }, 15000); + await until(`app2Window2 contains tempDirPath2 (${tempDirPath2})`, () => { + const dirs = await getTreeViewRootDirectories(app2Window2) + return dirs.length === 1 && dirs[0] == tempDirPath2 + }, 15000); }) it('does not reopen any previously opened windows when launched with no path and `core.restorePreviousWindowsOnStart` is false', async function () { From 52606171bfa837071c2c18ceeae9bc316b110475 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 23 Mar 2017 18:28:12 -0700 Subject: [PATCH 40/73] Add "location" param to `open()` --- spec/workspace-spec.js | 9 ++++----- src/workspace.js | 20 ++++++++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index 5c2cfb1b9..ae692087f 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -126,22 +126,21 @@ describe('Workspace', () => { }) it('constructs the view with the same panes', () => { - const getRightDockActivePane = () => atom.workspace.getRightDock().getActivePane() const pane1 = atom.workspace.getRightDock().getActivePane() const pane2 = pane1.splitRight({copyActiveItem: true}) const pane3 = pane2.splitRight({copyActiveItem: true}) let pane4 = null waitsForPromise(() => - atom.workspace.open(null, {pane: getRightDockActivePane()}).then(editor => editor.setText('An untitled editor.')) + atom.workspace.open(null, {location: 'right'}).then(editor => editor.setText('An untitled editor.')) ) waitsForPromise(() => - atom.workspace.open('b', {pane: getRightDockActivePane()}).then(editor => pane2.activateItem(editor.copy())) + atom.workspace.open('b', {location: 'right'}).then(editor => pane2.activateItem(editor.copy())) ) waitsForPromise(() => - atom.workspace.open('../sample.js', {pane: getRightDockActivePane()}).then(editor => pane3.activateItem(editor)) + atom.workspace.open('../sample.js', {location: 'right'}).then(editor => pane3.activateItem(editor)) ) runs(() => { @@ -150,7 +149,7 @@ describe('Workspace', () => { }) waitsForPromise(() => - atom.workspace.open('../sample.txt', {pane: getRightDockActivePane()}).then(editor => pane4.activateItem(editor)) + atom.workspace.open('../sample.txt', {location: 'right'}).then(editor => pane4.activateItem(editor)) ) runs(() => { diff --git a/src/workspace.js b/src/workspace.js index ef92e0da4..75cb0dd42 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -617,6 +617,12 @@ module.exports = class Workspace extends Model { // activate an existing item for the given URI on any pane. // If `false`, only the active pane will be searched for // an existing item for the same URI. Defaults to `false`. + // * `location` (optional) A {String} containing the name of the location + // in which this item should be opened (one of "left", "right", "bottom", + // or "center"). If omitted, Atom will fall back to the last location in + // which a user has placed an item with the same URI or, if this is a new + // URI, the default location specified by the item. NOTE: This option + // should almost always be omitted to honor user preference. // // Returns a {Promise} that resolves to the {TextEditor} for the file URI. open (uri_, options = {}) { @@ -767,7 +773,7 @@ module.exports = class Workspace extends Model { } async openItem (item, options = {}) { - let {pane, split} = options + let {pane, split, location} = options if (item == null) return undefined if (pane != null && pane.isDestroyed()) return item @@ -779,14 +785,12 @@ module.exports = class Workspace extends Model { paneContainer = this.getPaneContainers().find(container => container.getPanes().includes(pane)) } - // Determine which location to use, unless a split was provided. In that case, make sure it goes - // in the center location (legacy behavior) - let location - if (paneContainer == null && pane == null && split == null && uri != null) { - location = await this.itemLocationStore.load(uri) - } - if (paneContainer == null) { + // Determine which location to use, unless a split was provided. In that case, make sure it goes + // in the center location (legacy behavior) + if (location == null && pane == null && split == null && uri != null) { + location = await this.itemLocationStore.load(uri) + } if (location == null && typeof item.getDefaultLocation === 'function') { location = item.getDefaultLocation() } From 3c47b775d26e262e8b9a8b492fb0fc9e7aa86e33 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 23 Mar 2017 21:28:12 -0700 Subject: [PATCH 41/73] Let's make that async pls --- spec/main-process/atom-application.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 279b39035..879c4675b 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -370,11 +370,11 @@ describe('AtomApplication', function () { await app2Window2.loadedPromise await new Promise(resolve => setTimeout(resolve, 5000)) // session restoration is async - await until(`app2Window1 contains tempDirPath1 (${tempDirPath1})`, () => { + await until(`app2Window1 contains tempDirPath1 (${tempDirPath1})`, async () => { const dirs = await getTreeViewRootDirectories(app2Window1) return dirs.length === 1 && dirs[0] == tempDirPath1 }, 15000); - await until(`app2Window2 contains tempDirPath2 (${tempDirPath2})`, () => { + await until(`app2Window2 contains tempDirPath2 (${tempDirPath2})`, async () => { const dirs = await getTreeViewRootDirectories(app2Window2) return dirs.length === 1 && dirs[0] == tempDirPath2 }, 15000); From cc2cbfbb0acdd35ae505256d91a8f63569df8e78 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 23 Mar 2017 23:36:53 -0700 Subject: [PATCH 42/73] Emit event from AtomWindow when locations are loaded --- src/atom-environment.coffee | 22 +++++++++++++++------- src/main-process/atom-window.coffee | 3 +++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 1de044a52..a94c72102 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -912,7 +912,7 @@ class AtomEnvironment extends Model windowIsUnused = @workspace.getPaneItems().every(paneItemIsEmptyUnnamedTextEditor) if windowIsUnused @restoreStateIntoThisEnvironment(state) - @workspace.open(file) for file in filesToOpen + Promise.all (@workspace.open(file) for file in filesToOpen) else nouns = if projectPaths.length is 1 then 'folder' else 'folders' btn = @confirm @@ -930,9 +930,10 @@ class AtomEnvironment extends Model newWindow: true devMode: @inDevMode() safeMode: @inSafeMode() + Promise.resolve(null) else if btn is 1 @project.addPath(selectedPath) for selectedPath in projectPaths - @workspace.open(file) for file in filesToOpen + Promise.all (@workspace.open(file) for file in filesToOpen) restoreStateIntoThisEnvironment: (state) -> state.fullScreen = @isFullScreen() @@ -1066,19 +1067,26 @@ class AtomEnvironment extends Model unless fs.isDirectorySync(pathToOpen) fileLocationsToOpen.push({pathToOpen, initialLine, initialColumn}) + promise = Promise.resolve(null) if foldersToAddToProject.length > 0 - @loadState(@getStateKey(foldersToAddToProject)).then (state) => + promise = @loadState(@getStateKey(foldersToAddToProject)).then (state) => if state and needsProjectPaths # only load state if this is the first path added to the project files = (location.pathToOpen for location in fileLocationsToOpen) - @attemptRestoreProjectStateForPaths(state, foldersToAddToProject, files) + @attemptRestoreProjectStateForPaths(state, foldersToAddToProject, files).then => else + promises = [] @project.addPath(folder) for folder in foldersToAddToProject for {pathToOpen, initialLine, initialColumn} in fileLocationsToOpen - @workspace?.open(pathToOpen, {initialLine, initialColumn}) + promises.push @workspace?.open(pathToOpen, {initialLine, initialColumn}) + Promise.all(promises) else + promises = [] for {pathToOpen, initialLine, initialColumn} in fileLocationsToOpen - @workspace?.open(pathToOpen, {initialLine, initialColumn}) - Promise.resolve(null) + promises.push @workspace?.open(pathToOpen, {initialLine, initialColumn}) + promise = Promise.all(promises) + + promise.then => + ipcRenderer.send 'window-command', 'window:locations-opened' resolveProxy: (url) -> return new Promise (resolve, reject) => diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index bbc235bc5..d4ffb2481 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -89,6 +89,9 @@ class AtomWindow @emit 'window:loaded' @resolveLoadedPromise() + @browserWindow.on 'window:locations-opened', => + @emit 'window:locations-opened' + @browserWindow.on 'enter-full-screen', => @browserWindow.webContents.send('did-enter-full-screen') From 6b92bd041a0203a4380dff938daa1fda00cd87e3 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 23 Mar 2017 23:37:06 -0700 Subject: [PATCH 43/73] Add emitterEventPromise helper --- spec/async-spec-helpers.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/spec/async-spec-helpers.js b/spec/async-spec-helpers.js index 115f944b5..d00971e2c 100644 --- a/spec/async-spec-helpers.js +++ b/spec/async-spec-helpers.js @@ -1,5 +1,7 @@ /** @babel */ +import until from 'test-until' + export function beforeEach (fn) { global.beforeEach(function () { const result = fn() @@ -60,3 +62,12 @@ function waitsForPromise (fn) { }) }) } + +export function emitterEventPromise (emitter, event, timeout = 5000) { + let called = false + emitter.once(event, () => { + called = true + // disposable.dispose() + }) + return until(`${event} is emitted`, () => called, timeout) +} From 132f199fae529d2b1be70a93a4184ec5bad77244 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 23 Mar 2017 23:37:18 -0700 Subject: [PATCH 44/73] Fix main process test race conditions --- spec/main-process/atom-application.test.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 879c4675b..04752f263 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -5,10 +5,9 @@ import dedent from 'dedent' import electron from 'electron' import fs from 'fs-plus' import path from 'path' -import until from 'test-until' import AtomApplication from '../../src/main-process/atom-application' import parseCommandLine from '../../src/main-process/parse-command-line' -import {timeoutPromise, conditionPromise} from '../async-spec-helpers' +import {timeoutPromise, conditionPromise, emitterEventPromise} from '../async-spec-helpers' const ATOM_RESOURCE_PATH = path.resolve(__dirname, '..', '..') @@ -366,6 +365,9 @@ describe('AtomApplication', function () { const atomApplication2 = buildAtomApplication() const [app2Window1, app2Window2] = atomApplication2.launch(parseCommandLine([])) + const p1 = emitterEventPromise(app2Window1, 'window:locations-opened') + const p2 = emitterEventPromise(app2Window2, 'window:locations-opened') + await Promise.all([p1, p2]) await app2Window1.loadedPromise await app2Window2.loadedPromise await new Promise(resolve => setTimeout(resolve, 5000)) // session restoration is async @@ -428,6 +430,7 @@ describe('AtomApplication', function () { const atomApplication = buildAtomApplication() const window = atomApplication.launch(parseCommandLine([dirA, dirB])) + await emitterEventPromise(window, 'window:locations-opened', 15000) await focusWindow(window) assert.deepEqual(await getTreeViewRootDirectories(window), [dirA, dirB]) From 3ab08754db8d2739b7cedd6632106e5ad9699e79 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 23 Mar 2017 23:42:55 -0700 Subject: [PATCH 45/73] These should probably be here --- spec/atom-environment-spec.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index d1eda732d..e8de966a8 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -340,9 +340,9 @@ describe "AtomEnvironment", -> callback([tempDirectory]) waitsForPromise -> atom.addProjectFolder() - # runs -> - # expect(atom.project.getPaths()).toEqual([tempDirectory]) - # expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled() + runs -> + expect(atom.project.getPaths()).toEqual([tempDirectory]) + expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled() describe "when there is saved state for the relevant directories", -> state = Symbol('savedState') From 2ee692d3cf43293b43db32b57a4ec5ab026561cf Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 23 Mar 2017 23:44:27 -0700 Subject: [PATCH 46/73] Fix up helpers --- spec/async-spec-helpers.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/spec/async-spec-helpers.js b/spec/async-spec-helpers.js index d00971e2c..99f063e44 100644 --- a/spec/async-spec-helpers.js +++ b/spec/async-spec-helpers.js @@ -64,10 +64,7 @@ function waitsForPromise (fn) { } export function emitterEventPromise (emitter, event, timeout = 5000) { - let called = false - emitter.once(event, () => { - called = true - // disposable.dispose() - }) - return until(`${event} is emitted`, () => called, timeout) + let emitted = false + emitter.once(event, () => { emitted = true }) + return until(`${event} is emitted`, () => emitted, timeout) } From f657bd13c646f68e4d521f457641d919bc24d756 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 23 Mar 2017 23:44:38 -0700 Subject: [PATCH 47/73] :shirt: --- src/atom-environment.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index a94c72102..1ae445750 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -1085,7 +1085,7 @@ class AtomEnvironment extends Model promises.push @workspace?.open(pathToOpen, {initialLine, initialColumn}) promise = Promise.all(promises) - promise.then => + promise.then -> ipcRenderer.send 'window-command', 'window:locations-opened' resolveProxy: (url) -> From 67a9e19bf9df6a0a023459ab3eeec8b05f0ff620 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 23 Mar 2017 23:48:19 -0700 Subject: [PATCH 48/73] Oh we can put this back now --- spec/main-process/atom-application.test.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 04752f263..9faae672b 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -370,16 +370,9 @@ describe('AtomApplication', function () { await Promise.all([p1, p2]) await app2Window1.loadedPromise await app2Window2.loadedPromise - await new Promise(resolve => setTimeout(resolve, 5000)) // session restoration is async - await until(`app2Window1 contains tempDirPath1 (${tempDirPath1})`, async () => { - const dirs = await getTreeViewRootDirectories(app2Window1) - return dirs.length === 1 && dirs[0] == tempDirPath1 - }, 15000); - await until(`app2Window2 contains tempDirPath2 (${tempDirPath2})`, async () => { - const dirs = await getTreeViewRootDirectories(app2Window2) - return dirs.length === 1 && dirs[0] == tempDirPath2 - }, 15000); + assert.deepEqual(await getTreeViewRootDirectories(app2Window1), [tempDirPath1]) + assert.deepEqual(await getTreeViewRootDirectories(app2Window2), [tempDirPath2]) }) it('does not reopen any previously opened windows when launched with no path and `core.restorePreviousWindowsOnStart` is false', async function () { From 1651f0fd762e0fd9e1be597739394c22320a3655 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 23 Mar 2017 23:57:53 -0700 Subject: [PATCH 49/73] :shirt: --- src/atom-environment.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 1ae445750..de516bb17 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -1072,7 +1072,7 @@ class AtomEnvironment extends Model promise = @loadState(@getStateKey(foldersToAddToProject)).then (state) => if state and needsProjectPaths # only load state if this is the first path added to the project files = (location.pathToOpen for location in fileLocationsToOpen) - @attemptRestoreProjectStateForPaths(state, foldersToAddToProject, files).then => + @attemptRestoreProjectStateForPaths(state, foldersToAddToProject, files) else promises = [] @project.addPath(folder) for folder in foldersToAddToProject From ae64b35dca55f22dab5e57740e82f3b102b1e15f Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 23 Mar 2017 23:58:27 -0700 Subject: [PATCH 50/73] We need more time --- spec/main-process/atom-application.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 9faae672b..4b0eb8165 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -365,8 +365,8 @@ describe('AtomApplication', function () { const atomApplication2 = buildAtomApplication() const [app2Window1, app2Window2] = atomApplication2.launch(parseCommandLine([])) - const p1 = emitterEventPromise(app2Window1, 'window:locations-opened') - const p2 = emitterEventPromise(app2Window2, 'window:locations-opened') + const p1 = emitterEventPromise(app2Window1, 'window:locations-opened', 15000) + const p2 = emitterEventPromise(app2Window2, 'window:locations-opened', 15000) await Promise.all([p1, p2]) await app2Window1.loadedPromise await app2Window2.loadedPromise From 42fb2cc55fc07b99243bb2d22b4e7d1634aeb17e Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Fri, 24 Mar 2017 00:32:15 -0700 Subject: [PATCH 51/73] Convert more tests to use emitterEventPromise --- spec/main-process/atom-application.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 4b0eb8165..7d1882654 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -121,6 +121,7 @@ describe('AtomApplication', function () { const atomApplication = buildAtomApplication() const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath, 'new-file')])) + await emitterEventPromise(window1, 'window:locations-opened') await focusWindow(window1) let activeEditorPath @@ -146,6 +147,7 @@ describe('AtomApplication', function () { // Opens new windows when opening directories const window2 = atomApplication.launch(parseCommandLine([dirCPath])) + await emitterEventPromise(window2, 'window:locations-opened') assert.notEqual(window2, window1) await focusWindow(window2) assert.deepEqual(await getTreeViewRootDirectories(window2), [dirCPath]) From a9a2409ff7f42a356ad693bfa8309e38254230b4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Mar 2017 14:55:04 +0100 Subject: [PATCH 52/73] Revert "Revert ":arrow_up: all packages that use atom-select-list"" This reverts commit ad27034f5de012af813c2355b285e0b28799ee42. --- package.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 302b5023d..1c6dcef77 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dependencies": { "async": "0.2.6", "atom-keymap": "8.1.1", - "atom-select-list": "0.0.15", + "atom-select-list": "^0.1.0", "atom-ui": "0.4.1", "babel-core": "5.8.38", "cached-run-in-this-context": "0.4.1", @@ -94,23 +94,23 @@ "autoflow": "0.29.0", "autosave": "0.24.1", "background-tips": "0.26.2", - "bookmarks": "0.44.2", + "bookmarks": "0.44.3", "bracket-matcher": "0.85.4", - "command-palette": "0.40.3", + "command-palette": "0.40.4", "dalek": "0.2.1", "deprecation-cop": "0.56.5", "dev-live-reload": "0.47.1", - "encoding-selector": "0.23.2", + "encoding-selector": "0.23.3", "exception-reporting": "0.41.3", "find-and-replace": "0.207.3", - "fuzzy-finder": "1.5.2", - "git-diff": "1.3.4", + "fuzzy-finder": "1.5.4", + "git-diff": "1.3.5", "go-to-line": "0.32.0", - "grammar-selector": "0.49.3", + "grammar-selector": "0.49.4", "image-view": "0.61.2", "incompatible-packages": "0.27.2", "keybinding-resolver": "0.37.0", - "line-ending-selector": "0.6.2", + "line-ending-selector": "0.6.3", "link": "0.31.3", "markdown-preview": "0.159.10", "metrics": "1.2.2", @@ -118,11 +118,11 @@ "open-on-github": "1.2.1", "package-generator": "1.1.1", "settings-view": "0.249.1", - "snippets": "1.1.3", - "spell-check": "0.71.3", + "snippets": "1.1.4", + "spell-check": "0.71.4", "status-bar": "1.8.5", - "styleguide": "0.49.4", - "symbols-view": "0.115.3", + "styleguide": "0.49.6", + "symbols-view": "0.115.5", "tabs": "0.104.4", "timecop": "0.36.0", "tree-view": "0.215.3", From b43253f3e1b687506dbb96bf1e881f0b22af2571 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Mar 2017 15:26:39 +0100 Subject: [PATCH 53/73] :arrow_up: packages with unnecessary deferred requires --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 1c6dcef77..7d72345e5 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "one-light-syntax": "1.7.1", "solarized-dark-syntax": "1.1.2", "solarized-light-syntax": "1.1.2", - "about": "1.7.5", + "about": "1.7.6", "archive-view": "0.63.1", "autocomplete-atom-api": "0.10.1", "autocomplete-css": "0.16.1", @@ -93,12 +93,12 @@ "autocomplete-snippets": "1.11.0", "autoflow": "0.29.0", "autosave": "0.24.1", - "background-tips": "0.26.2", + "background-tips": "0.27.0", "bookmarks": "0.44.3", - "bracket-matcher": "0.85.4", + "bracket-matcher": "0.85.5", "command-palette": "0.40.4", "dalek": "0.2.1", - "deprecation-cop": "0.56.5", + "deprecation-cop": "0.56.6", "dev-live-reload": "0.47.1", "encoding-selector": "0.23.3", "exception-reporting": "0.41.3", From 51b40edebd4e4808e14cce1d838aa98e07ae593f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 24 Mar 2017 12:02:49 -0700 Subject: [PATCH 54/73] :arrow_up: packages to fix test failures --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 302b5023d..090b6b190 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "solarized-dark-syntax": "1.1.2", "solarized-light-syntax": "1.1.2", "about": "1.7.5", - "archive-view": "0.63.1", + "archive-view": "0.63.2", "autocomplete-atom-api": "0.10.1", "autocomplete-css": "0.16.1", "autocomplete-html": "0.7.3", @@ -123,7 +123,7 @@ "status-bar": "1.8.5", "styleguide": "0.49.4", "symbols-view": "0.115.3", - "tabs": "0.104.4", + "tabs": "0.104.5", "timecop": "0.36.0", "tree-view": "0.215.3", "update-package-dependencies": "0.11.0", From 93ba6109fa1f8f970d9bd23ebebb2a6246e75e76 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 24 Mar 2017 15:29:46 -0700 Subject: [PATCH 55/73] Create Dock element lazily to be compatible w/ snapshotting --- src/dock.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/dock.js b/src/dock.js index 585b2441d..a171c5d66 100644 --- a/src/dock.js +++ b/src/dock.js @@ -55,11 +55,10 @@ module.exports = class Dock { pane.onDidRemoveItem(this.handleDidRemovePaneItem.bind(this)) }) ) - - this.render(this.state) } getElement () { + if (!this.element) this.render(this.state); return this.element } From 4082b67fb0f68351620fa79f7f839dc483e76610 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 24 Mar 2017 15:35:52 -0700 Subject: [PATCH 56/73] Refactor Workspace.open Signed-off-by: Nathan Sobo --- spec/workspace-spec.js | 10 +- src/workspace.js | 217 +++++++++++++++++++---------------------- 2 files changed, 106 insertions(+), 121 deletions(-) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index ae692087f..2237dc33e 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -818,9 +818,13 @@ describe('Workspace', () => { }) ) - it('creates a notification', () => { - const open = () => workspace.open('file1', workspace.getActivePane()) - expect(open).toThrow() + it('rejects the promise', () => { + waitsFor((done) => { + workspace.open('file1').catch(error => { + expect(error.message).toBe('I dont even know what is happening right now!!') + done() + }) + }) }) }) }) diff --git a/src/workspace.js b/src/workspace.js index 75cb0dd42..105820094 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -625,9 +625,8 @@ module.exports = class Workspace extends Model { // should almost always be omitted to honor user preference. // // Returns a {Promise} that resolves to the {TextEditor} for the file URI. - open (uri_, options = {}) { + async open (uri_, options = {}) { const uri = this.project.resolvePath(uri_) - const {searchAllPanes, split} = options if (!atom.config.get('core.allowPendingPaneItems')) { options.pending = false @@ -635,43 +634,110 @@ module.exports = class Workspace extends Model { // Avoid adding URLs as recent documents to work-around this Spotlight crash: // https://github.com/atom/atom/issues/10071 - if ((uri != null) && ((url.parse(uri).protocol == null) || (process.platform === 'win32'))) { + if (uri && (!url.parse(uri).protocol || process.platform === 'win32')) { this.applicationDelegate.addRecentDocument(uri) } - let pane - if (searchAllPanes) { pane = this.paneForURI(uri) } - if (pane == null) { - switch (split) { - case 'left': - pane = this.getActivePane().findLeftmostSibling() - break - case 'right': - pane = this.getActivePane().findRightmostSibling() - break - case 'up': - pane = this.getActivePane().findTopmostSibling() - break - case 'down': - pane = this.getActivePane().findBottommostSibling() - break - default: - pane = this.getActivePane() - break + let pane, item + + // Try to find an existing item with the given URI. + if (uri) { + if (options.pane) { + pane = options.pane + } else if (options.searchAllPanes) { + pane = this.paneForURI(uri) + } else { + + // The `split` option affects where we search for the item. + pane = this.getActivePane() + switch (options.split) { + case 'left': + pane = pane.findLeftmostSibling() + break + case 'right': + pane = pane.findRightmostSibling() + break + case 'up': + pane = pane.findTopmostSibling() + break + case 'down': + pane = pane.findBottommostSibling() + break + } + } + + if (pane) item = pane.itemForURI(uri) + } + + // Create an item if one was not found. + if (!item) { + item = await this.createItemForURI(uri, options) + if (!item) return + + if (options.pane) { + pane = options.pane + } else { + let location = options.location + if (!location && !options.split && uri) { + location = await this.itemLocationStore.load(uri) + } + if (!location && typeof item.getDefaultLocation === 'function') { + location = item.getDefaultLocation() + } + + const container = this.docks[location] || this.getCenter() + pane = container.getActivePane() + switch (options.split) { + case 'left': + pane = pane.findLeftmostSibling() + break + case 'right': + pane = pane.findOrCreateRightmostSibling() + break + case 'up': + pane = pane.findTopmostSibling() + break + case 'down': + pane = pane.findOrCreateBottommostSibling() + break + } } } - let item - if (uri != null && pane != null) { - item = pane.itemForURI(uri) - } - if (item == null) { - item = this.createItemForURI(uri, options) - pane = null + if (!options.pending && (pane.getPendingItem() === item)) { + pane.clearPendingItem() } - return Promise.resolve(item) - .then(item => this.openItem(item, Object.assign({pane, uri}, options))) + pane.addItem(item, options) + this.itemOpened(item) + + if (options.activateItem !== false) { + pane.activateItem(item, {pending: options.pending}) + } + + if (options.activatePane !== false) { + pane.activate() + const container = this.getPaneContainers().find(container => container.getPanes().includes(pane)) + container.activate() + } + + let initialColumn = 0 + let initialLine = 0 + if (!Number.isNaN(options.initialLine)) { + initialLine = options.initialLine + } + if (!Number.isNaN(options.initialColumn)) { + initialColumn = options.initialColumn + } + if (initialLine >= 0 || initialColumn >= 0) { + if (typeof item.setCursorBufferPosition === 'function') { + item.setCursorBufferPosition([initialLine, initialColumn]) + } + } + + const index = pane.getActiveItemIndex() + this.emitter.emit('did-open', {uri, pane, item, index}) + return item } // Open Atom's license in the active pane. @@ -720,16 +786,8 @@ module.exports = class Workspace extends Model { return item } - openURIInPane (uri, pane, options = {}) { - let item - if (uri != null) { - item = pane.itemForURI(uri) - } - if (item == null) { - item = this.createItemForURI(uri, options) - } - return Promise.resolve(item) - .then(item => this.openItem(item, Object.assign({pane, uri}, options))) + openURIInPane (uri, pane) { + return this.open(uri, {pane}) } // Returns a {Promise} that resolves to the {TextEditor} (or other item) for the given URI. @@ -772,83 +830,6 @@ module.exports = class Workspace extends Model { } } - async openItem (item, options = {}) { - let {pane, split, location} = options - - if (item == null) return undefined - if (pane != null && pane.isDestroyed()) return item - - const uri = options.uri == null && typeof item.getURI === 'function' ? item.getURI() : options.uri - - let paneContainer - if (pane != null) { - paneContainer = this.getPaneContainers().find(container => container.getPanes().includes(pane)) - } - - if (paneContainer == null) { - // Determine which location to use, unless a split was provided. In that case, make sure it goes - // in the center location (legacy behavior) - if (location == null && pane == null && split == null && uri != null) { - location = await this.itemLocationStore.load(uri) - } - if (location == null && typeof item.getDefaultLocation === 'function') { - location = item.getDefaultLocation() - } - paneContainer = this.docks[location] || this.getCenter() - } - - if (pane == null) { - pane = paneContainer.getActivePane() - switch (split) { - case 'left': - pane = pane.findLeftmostSibling() - break - case 'right': - pane = pane.findOrCreateRightmostSibling() - break - case 'up': - pane = pane.findTopmostSibling() - break - case 'down': - pane = pane.findOrCreateBottommostSibling() - break - } - } - - if (!options.pending && (pane.getPendingItem() === item)) { - pane.clearPendingItem() - } - - const activatePane = options.activatePane != null ? options.activatePane : true - const activateItem = options.activateItem != null ? options.activateItem : true - this.itemOpened(item) - if (activateItem) { - pane.activateItem(item, {pending: options.pending}) - } - if (activatePane) { - pane.activate() - } - paneContainer.activate() - - let initialColumn = 0 - let initialLine = 0 - if (!Number.isNaN(options.initialLine)) { - initialLine = options.initialLine - } - if (!Number.isNaN(options.initialColumn)) { - initialColumn = options.initialColumn - } - if ((initialLine >= 0) || (initialColumn >= 0)) { - if (typeof item.setCursorBufferPosition === 'function') { - item.setCursorBufferPosition([initialLine, initialColumn]) - } - } - - const index = pane.getActiveItemIndex() - this.emitter.emit('did-open', {uri, pane, item, index}) - return item - } - openTextFile (uri, options) { const filePath = this.project.resolvePath(uri) From bc872143cc28a2b7b99be6999647496dead17d7d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 24 Mar 2017 15:49:35 -0700 Subject: [PATCH 57/73] Avoid duplicate search for pane container in Workspace.open --- src/dock.js | 2 +- src/workspace.js | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/dock.js b/src/dock.js index a171c5d66..9d0fe8247 100644 --- a/src/dock.js +++ b/src/dock.js @@ -58,7 +58,7 @@ module.exports = class Dock { } getElement () { - if (!this.element) this.render(this.state); + if (!this.element) this.render(this.state) return this.element } diff --git a/src/workspace.js b/src/workspace.js index 105820094..24df8ec1f 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -638,7 +638,7 @@ module.exports = class Workspace extends Model { this.applicationDelegate.addRecentDocument(uri) } - let pane, item + let container, pane, item // Try to find an existing item with the given URI. if (uri) { @@ -647,7 +647,6 @@ module.exports = class Workspace extends Model { } else if (options.searchAllPanes) { pane = this.paneForURI(uri) } else { - // The `split` option affects where we search for the item. pane = this.getActivePane() switch (options.split) { @@ -685,7 +684,7 @@ module.exports = class Workspace extends Model { location = item.getDefaultLocation() } - const container = this.docks[location] || this.getCenter() + container = this.docks[location] || this.getCenter() pane = container.getActivePane() switch (options.split) { case 'left': @@ -717,7 +716,9 @@ module.exports = class Workspace extends Model { if (options.activatePane !== false) { pane.activate() - const container = this.getPaneContainers().find(container => container.getPanes().includes(pane)) + if (!container) { + container = this.getPaneContainers().find(container => container.getPanes().includes(pane)) + } container.activate() } From 11c06d44b8e6c1ab5992997d1fcee351f62f825d Mon Sep 17 00:00:00 2001 From: Matthew Brener Date: Sun, 26 Mar 2017 19:46:43 +1100 Subject: [PATCH 58/73] Fix typo in comments of text-editor-registry.js --- src/text-editor-registry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js index 3343ce89c..72aa8b364 100644 --- a/src/text-editor-registry.js +++ b/src/text-editor-registry.js @@ -190,7 +190,7 @@ export default class TextEditorRegistry { } // Set a {TextEditor}'s grammar based on its path and content, and continue - // to update its grammar as gramamrs are added or updated, or the editor's + // to update its grammar as grammars are added or updated, or the editor's // file path changes. // // * `editor` The editor whose grammar will be maintained. From d691c3e5aaf7cd9009dc109b806b2c804e06d200 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 27 Mar 2017 10:30:55 -0700 Subject: [PATCH 59/73] Docks: Don't change inherited presentation styles --- static/docks.less | 3 --- 1 file changed, 3 deletions(-) diff --git a/static/docks.less b/static/docks.less index c8761707a..04b548e31 100644 --- a/static/docks.less +++ b/static/docks.less @@ -71,9 +71,6 @@ atom-dock { align-items: stretch; width: 100%; height: 100%; - cursor: default; - -webkit-user-select: none; - white-space: nowrap; // The contents of the dock should be "stuck" to the moving edge of the mask, // so it looks like they're sliding in (instead of being unmasked in place). From 9d5d09f348cd1757246b365d2ff634c66456da13 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 27 Mar 2017 11:40:20 -0700 Subject: [PATCH 60/73] :arrow_up: tree-view@0.216.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 66d2015c3..be2b4f841 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "symbols-view": "0.115.5", "tabs": "0.104.4", "timecop": "0.36.0", - "tree-view": "0.215.3", + "tree-view": "0.216.0", "update-package-dependencies": "0.11.0", "welcome": "0.36.2", "whitespace": "0.36.2", From 13f0c8a977125e413e36f31d89b5863d791a516a Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 27 Mar 2017 12:04:30 -0700 Subject: [PATCH 61/73] Docks: define handle size in CSS; measure in JS --- src/dock.js | 28 +++++++++++++++++++--------- src/panel-container-element.js | 6 ++++++ static/docks.less | 4 ++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/dock.js b/src/dock.js index 9d0fe8247..1a409adf7 100644 --- a/src/dock.js +++ b/src/dock.js @@ -7,7 +7,6 @@ const TextEditor = require('./text-editor') const MINIMUM_SIZE = 100 const DEFAULT_INITIAL_SIZE = 300 -const HANDLE_SIZE = 4 const SHOULD_ANIMATE_CLASS = 'atom-dock-should-animate' const OPEN_CLASS = 'atom-dock-open' const RESIZE_HANDLE_RESIZABLE_CLASS = 'atom-dock-resize-handle-resizable' @@ -57,6 +56,12 @@ module.exports = class Dock { ) } + // This method is called explicitly by the object which adds the Dock to the document. + elementAttached () { + // Re-render when the dock is attached to make sure we remeasure sizes defined in CSS. + this.render(this.state) + } + getElement () { if (!this.element) this.render(this.state) return this.element @@ -180,7 +185,7 @@ module.exports = class Dock { const size = Math.max(MINIMUM_SIZE, state.size == null ? this.getInitialSize() : state.size) // We need to change the size of the mask... - this.maskElement.style[this.widthOrHeight] = `${shouldBeVisible ? size : HANDLE_SIZE}px` + this.maskElement.style[this.widthOrHeight] = `${shouldBeVisible ? size : this.resizeHandle.getSize()}px` // ...but the content needs to maintain a constant size. this.wrapperElement.style[this.widthOrHeight] = `${size}px` @@ -289,7 +294,7 @@ module.exports = class Dock { // The area used when detecting "leave" events is actually larger than when detecting entrances. if (includeButtonWidth) { const hoverMargin = 20 - const {width, height} = this.toggleButton.getSize() + const {width, height} = this.toggleButton.getBounds() switch (this.location) { case 'right': bounds.left -= width + hoverMargin @@ -607,8 +612,6 @@ class DockResizeHandle { this.element.classList.add('atom-dock-resize-handle', props.location) this.element.addEventListener('mousedown', this.handleMouseDown) this.element.addEventListener('click', this.handleClick) - const widthOrHeight = getWidthOrHeight(props.location) - this.element.style[widthOrHeight] = `${HANDLE_SIZE}px` this.props = props this.update(props) } @@ -617,6 +620,13 @@ class DockResizeHandle { return this.element } + getSize () { + if (!this.size) { + this.size = this.element.getBoundingClientRect()[getWidthOrHeight(this.props.location)] + } + return this.size + } + update (newProps) { this.props = Object.assign({}, this.props, newProps) @@ -669,11 +679,11 @@ class DockToggleButton { return this.element } - getSize () { - if (this.size == null) { - this.size = this.element.getBoundingClientRect() + getBounds () { + if (this.bounds == null) { + this.bounds = this.element.getBoundingClientRect() } - return this.size + return this.bounds } destroy () { diff --git a/src/panel-container-element.js b/src/panel-container-element.js index f78a2e352..c51321181 100644 --- a/src/panel-container-element.js +++ b/src/panel-container-element.js @@ -9,6 +9,12 @@ class PanelContainerElement extends HTMLElement { this.subscriptions = new CompositeDisposable() } + attachedCallback () { + if (this.model.dock) { + this.model.dock.elementAttached() + } + } + initialize (model, {views}) { this.model = model this.views = views diff --git a/static/docks.less b/static/docks.less index 04b548e31..583409726 100644 --- a/static/docks.less +++ b/static/docks.less @@ -2,6 +2,7 @@ @import 'syntax-variables'; @atom-dock-toggle-button-size: 50px; +@atom-dock-resize-handle-size: 4px; // Dock -------------- @@ -184,6 +185,9 @@ atom-dock { &.left, &.right { cursor: col-resize; } &.bottom { cursor: row-resize; } } + + &.left, &.right { width: @atom-dock-resize-handle-size; } + &.bottom { height: @atom-dock-resize-handle-size; } } // Cursor overlay -------------- From f3c3917825e08739f77ca8122072b3fd6a1a80f9 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 27 Mar 2017 14:20:47 -0700 Subject: [PATCH 62/73] Don't show the dock toggle button if it's closed and empty --- src/dock.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/dock.js b/src/dock.js index 1a409adf7..19e3528b2 100644 --- a/src/dock.js +++ b/src/dock.js @@ -192,7 +192,9 @@ module.exports = class Dock { this.resizeHandle.update({dockIsOpen: this.state.open}) this.toggleButton.update({ open: shouldBeVisible, - visible: state.hovered || (state.draggingItem && !shouldBeVisible) + // Don't show the toggle button if the dock is closed and empty. + visible: (state.hovered && (this.state.open || this.getPaneItems().length > 0)) || + (state.draggingItem && !shouldBeVisible) }) } @@ -206,7 +208,7 @@ module.exports = class Dock { handleDidRemovePaneItem () { // Hide the dock if you remove the last item. if (this.paneContainer.getPaneItems().length === 0) { - this.setState({open: false}) + this.setState({open: false, hovered: false}) } } From 41953ae7d6fc3d97a8fa0a83b2eba3cb0f1246a8 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 27 Mar 2017 15:09:32 -0700 Subject: [PATCH 63/73] Only show dock toggle buttons when dragging if item is allowed --- src/dock.js | 14 +++++++++++--- src/workspace-element.js | 6 ++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/dock.js b/src/dock.js index 19e3528b2..707aba2d4 100644 --- a/src/dock.js +++ b/src/dock.js @@ -192,9 +192,11 @@ module.exports = class Dock { this.resizeHandle.update({dockIsOpen: this.state.open}) this.toggleButton.update({ open: shouldBeVisible, - // Don't show the toggle button if the dock is closed and empty. - visible: (state.hovered && (this.state.open || this.getPaneItems().length > 0)) || - (state.draggingItem && !shouldBeVisible) + visible: + // Don't show the toggle button if the dock is closed and empty... + (state.hovered && (this.state.open || this.getPaneItems().length > 0)) || + // ...or if the item can't be dropped in that dock. + (!shouldBeVisible && state.draggingItem && isItemAllowed(state.draggingItem, this.location)) }) } @@ -749,3 +751,9 @@ function rectContainsPoint (rect, point) { point.y <= rect.bottom ) } + +// Is the item allowed in the given location? +function isItemAllowed (item, location) { + if (typeof item.getAllowedLocations !== 'function') return true + return item.getAllowedLocations().includes(location) +} diff --git a/src/workspace-element.js b/src/workspace-element.js index d18708ae4..a2fe39d5a 100644 --- a/src/workspace-element.js +++ b/src/workspace-element.js @@ -138,7 +138,9 @@ class WorkspaceElement extends HTMLElement { handleDragStart (event) { if (!isTab(event.target)) return - this.model.setDraggingItem(true) + const {item} = event.target + if (!item) return + this.model.setDraggingItem(item) window.addEventListener('dragend', this.handleDragEnd, true) window.addEventListener('drop', this.handleDrop, true) } @@ -152,7 +154,7 @@ class WorkspaceElement extends HTMLElement { } dragEnded () { - this.model.setDraggingItem(false) + this.model.setDraggingItem(null) window.removeEventListener('dragend', this.handleDragEnd, true) window.removeEventListener('drop', this.handleDrop, true) } From 401a549bf53b22e0cb13db39fc1abd7429d1558a Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 27 Mar 2017 15:21:22 -0700 Subject: [PATCH 64/73] Don't open items in disallowed locations --- src/workspace.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/workspace.js b/src/workspace.js index 24df8ec1f..3e94eccaf 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -684,6 +684,9 @@ module.exports = class Workspace extends Model { location = item.getDefaultLocation() } + const allowedLocations = typeof item.getAllowedLocations === 'function' ? item.getAllowedLocations() : ALL_LOCATIONS + location = allowedLocations.includes(location) ? location : allowedLocations[0] + container = this.docks[location] || this.getCenter() pane = container.getActivePane() switch (options.split) { @@ -1620,3 +1623,5 @@ module.exports = class Workspace extends Model { } } } + +const ALL_LOCATIONS = ['center', 'left', 'right', 'bottom'] From 3b23ab44bc0f1e3912e36193204ea322e1c1b815 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 27 Mar 2017 17:40:35 -0700 Subject: [PATCH 65/73] Add `getLocation()` to PaneContainer class This allows the location to be inspected without having to jump to the DOM and searching for a dock element. --- spec/pane-container-element-spec.coffee | 1 + spec/pane-container-spec.coffee | 1 + spec/pane-element-spec.coffee | 7 ++++++- spec/pane-spec.coffee | 5 ++++- src/dock.js | 1 + src/pane-container.coffee | 4 +++- src/workspace.js | 2 ++ 7 files changed, 18 insertions(+), 3 deletions(-) diff --git a/spec/pane-container-element-spec.coffee b/spec/pane-container-element-spec.coffee index e986e5d6c..55692c8e2 100644 --- a/spec/pane-container-element-spec.coffee +++ b/spec/pane-container-element-spec.coffee @@ -3,6 +3,7 @@ PaneAxisElement = require '../src/pane-axis-element' PaneAxis = require '../src/pane-axis' params = + location: 'center' config: atom.config confirm: atom.confirm.bind(atom) viewRegistry: atom.views diff --git a/spec/pane-container-spec.coffee b/spec/pane-container-spec.coffee index 939df49e5..153ef2bda 100644 --- a/spec/pane-container-spec.coffee +++ b/spec/pane-container-spec.coffee @@ -7,6 +7,7 @@ describe "PaneContainer", -> beforeEach -> confirm = spyOn(atom.applicationDelegate, 'confirm').andReturn(0) params = { + location: 'center', config: atom.config, deserializerManager: atom.deserializers applicationDelegate: atom.applicationDelegate, diff --git a/spec/pane-element-spec.coffee b/spec/pane-element-spec.coffee index f6dc4d535..d1725d11a 100644 --- a/spec/pane-element-spec.coffee +++ b/spec/pane-element-spec.coffee @@ -6,7 +6,12 @@ describe "PaneElement", -> beforeEach -> spyOn(atom.applicationDelegate, "open") - container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom), viewRegistry: atom.views, applicationDelegate: atom.applicationDelegate) + container = new PaneContainer + location: 'center' + config: atom.config + confirm: atom.confirm.bind(atom) + viewRegistry: atom.views + applicationDelegate: atom.applicationDelegate containerElement = atom.views.getView(container) pane = container.getActivePane() paneElement = atom.views.getView(pane) diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index 596b1ecea..53bccbf13 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -51,7 +51,10 @@ describe "Pane", -> [container, pane1, pane2] = [] beforeEach -> - container = new PaneContainer(config: atom.config, applicationDelegate: atom.applicationDelegate) + container = new PaneContainer + location: 'center' + config: atom.config + applicationDelegate: atom.applicationDelegate container.getActivePane().splitRight() [pane1, pane2] = container.getPanes() diff --git a/src/dock.js b/src/dock.js index 707aba2d4..47ad9b715 100644 --- a/src/dock.js +++ b/src/dock.js @@ -34,6 +34,7 @@ module.exports = class Dock { this.viewRegistry = params.viewRegistry this.paneContainer = new PaneContainer({ + location: this.location, config: this.config, applicationDelegate: this.applicationDelegate, deserializerManager: this.deserializerManager, diff --git a/src/pane-container.coffee b/src/pane-container.coffee index 20087c564..54d1d6cbe 100644 --- a/src/pane-container.coffee +++ b/src/pane-container.coffee @@ -15,7 +15,7 @@ class PaneContainer extends Model constructor: (params) -> super - {@config, applicationDelegate, notificationManager, deserializerManager, @viewRegistry} = params + {@config, applicationDelegate, notificationManager, deserializerManager, @viewRegistry, @location} = params @emitter = new Emitter @subscriptions = new CompositeDisposable @itemRegistry = new ItemRegistry @@ -27,6 +27,8 @@ class PaneContainer extends Model initialize: -> @monitorActivePaneItem() + getLocation: -> @location + getElement: -> @element ?= new PaneContainerElement().initialize(this, {views: @viewRegistry}) diff --git a/src/workspace.js b/src/workspace.js index 3e94eccaf..1a1786bbc 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -54,6 +54,7 @@ module.exports = class Workspace extends Model { this.destroyedItemURIs = [] this.paneContainer = new PaneContainer({ + location: 'center', config: this.config, applicationDelegate: this.applicationDelegate, notificationManager: this.notificationManager, @@ -111,6 +112,7 @@ module.exports = class Workspace extends Model { _.values(this.panelContainers).forEach(panelContainer => { panelContainer.destroy() }) this.paneContainer = new PaneContainer({ + location: 'center', config: this.config, applicationDelegate: this.applicationDelegate, notificationManager: this.notificationManager, From 08e8975a103f616d50715ab82912367abb82f109 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 27 Mar 2017 18:17:02 -0700 Subject: [PATCH 66/73] Always show the dock when an item is dropped into it Previously, we were only showing it when going from 0 -> 1 items (which is a bug). --- src/dock.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/dock.js b/src/dock.js index 47ad9b715..c04bbcb50 100644 --- a/src/dock.js +++ b/src/dock.js @@ -203,9 +203,7 @@ module.exports = class Dock { handleDidAddPaneItem () { // Show the dock if you drop an item into it. - if (this.paneContainer.getPaneItems().length === 1) { - this.setState({open: true}) - } + this.setState({open: true}) } handleDidRemovePaneItem () { From 45bd466384930dbc9a126bfd91f655456629afce Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 28 Mar 2017 15:00:13 -0600 Subject: [PATCH 67/73] =?UTF-8?q?Don=E2=80=99t=20add=20item=20in=20Workspa?= =?UTF-8?q?ce.open=20if=20activateItem=20is=20false?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We thought it was a bug that activateItem: false caused the item not to be added, but it turned out there were package tests that depended on this behavior. Ideally, we should have an addItem option that exhibits this behavior instead. Signed-off-by: Max Brunsfeld --- src/workspace.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/workspace.js b/src/workspace.js index 1a1786bbc..8d346e9bd 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -712,7 +712,6 @@ module.exports = class Workspace extends Model { pane.clearPendingItem() } - pane.addItem(item, options) this.itemOpened(item) if (options.activateItem !== false) { From 5b6cca41ed781a0016ceec2d89e51240be7c9bae Mon Sep 17 00:00:00 2001 From: simurai Date: Wed, 29 Mar 2017 12:09:29 +0900 Subject: [PATCH 68/73] :arrow_up: tabs@v0.104.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index be2b4f841..4d891b690 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "status-bar": "1.8.5", "styleguide": "0.49.6", "symbols-view": "0.115.5", - "tabs": "0.104.4", + "tabs": "0.104.6", "timecop": "0.36.0", "tree-view": "0.216.0", "update-package-dependencies": "0.11.0", From c7a47558084b200d1db3dbb79dba4ebca0bbe10e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 29 Mar 2017 09:27:21 -0600 Subject: [PATCH 69/73] If Workspace.open finds existing item, yield event loop This ensures that the function always behaves asynchronously regardless of the state of the workspace. /cc @maxbrunsfeld Signed-off-by: Antonio Scandurra --- src/workspace.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/workspace.js b/src/workspace.js index 8d346e9bd..6c9621b28 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -670,8 +670,12 @@ module.exports = class Workspace extends Model { if (pane) item = pane.itemForURI(uri) } - // Create an item if one was not found. - if (!item) { + // If an item is already present, yield the event loop to ensure this method + // is consistently asynchronous regardless of the workspace state. If no + // item is present, create one. + if (item) { + await Promise.resolve() + } else { item = await this.createItemForURI(uri, options) if (!item) return From 500cefb8d50b7bc7c6d19c854e78135a4e9731f9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 30 Mar 2017 10:19:05 +0200 Subject: [PATCH 70/73] Don't snapshot minimatch and fix package transpilation registry on win32 This module uses Node's `path` for determinining which path separator to use on the current platform. On browsers (and every other environment that does not support `require`, such as v8 snapshots) it falls back to always using a forward slash. As a result, `PackageTranspilationRegistry` (and potentially other bundled packages that depend on `minimatch`) couldn't match glob expressions against any given path on Windows, thus causing the custom transpiler code to not work properly. --- script/lib/generate-startup-snapshot.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index dec5b597c..26761fc69 100644 --- a/script/lib/generate-startup-snapshot.js +++ b/script/lib/generate-startup-snapshot.js @@ -46,17 +46,21 @@ module.exports = function (packagedAppPath) { relativePath == path.join('..', 'node_modules', 'less', 'lib', 'less', 'fs.js') || relativePath == path.join('..', 'node_modules', 'less', 'lib', 'less-node', 'index.js') || relativePath == path.join('..', 'node_modules', 'less', 'node_modules', 'graceful-fs', 'graceful-fs.js') || + relativePath == path.join('..', 'node_modules', 'minimatch', 'minimatch.js') || relativePath == path.join('..', 'node_modules', 'superstring', 'index.js') || relativePath == path.join('..', 'node_modules', 'oniguruma', 'lib', 'oniguruma.js') || relativePath == path.join('..', 'node_modules', 'request', 'index.js') || relativePath == path.join('..', 'node_modules', 'resolve', 'index.js') || relativePath == path.join('..', 'node_modules', 'resolve', 'lib', 'core.js') || + relativePath == path.join('..', 'node_modules', 'scandal', 'node_modules', 'minimatch', 'minimatch.js') || relativePath == path.join('..', 'node_modules', 'settings-view', 'node_modules', 'glob', 'glob.js') || + relativePath == path.join('..', 'node_modules', 'settings-view', 'node_modules', 'minimatch', 'minimatch.js') || relativePath == path.join('..', 'node_modules', 'spellchecker', 'lib', 'spellchecker.js') || relativePath == path.join('..', 'node_modules', 'spelling-manager', 'node_modules', 'natural', 'lib', 'natural', 'index.js') || relativePath == path.join('..', 'node_modules', 'tar', 'tar.js') || relativePath == path.join('..', 'node_modules', 'temp', 'lib', 'temp.js') || - relativePath == path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js') + relativePath == path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js') || + relativePath == path.join('..', 'node_modules', 'tree-view', 'node_modules', 'minimatch', 'minimatch.js') ) } }).then((snapshotScript) => { From 93766bb2568e59aa5370f85d1c53c9fef14a56d2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 30 Mar 2017 15:28:06 +0200 Subject: [PATCH 71/73] :arrow_up: temp --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4d891b690..407767630 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "service-hub": "^0.7.3", "sinon": "1.17.4", "source-map-support": "^0.3.2", - "temp": "0.8.1", + "temp": "^0.8.3", "test-until": "^1.1.1", "text-buffer": "11.4.0", "typescript-simple": "1.0.0", From 3e033250c8a5c19ad98efb41ffda7242a71e8fc6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Mar 2017 11:15:32 -0700 Subject: [PATCH 72/73] :arrow_up: deprecation-cop --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 407767630..334963f56 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "bracket-matcher": "0.85.5", "command-palette": "0.40.4", "dalek": "0.2.1", - "deprecation-cop": "0.56.6", + "deprecation-cop": "0.56.7", "dev-live-reload": "0.47.1", "encoding-selector": "0.23.3", "exception-reporting": "0.41.3", From 128f661e1e1e38e644975874fe2a7f216825430a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Mar 2017 13:17:58 -0700 Subject: [PATCH 73/73] :arrow_up: tabs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8642f6ade..15a604dda 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "status-bar": "1.8.5", "styleguide": "0.49.6", "symbols-view": "0.115.5", - "tabs": "0.104.6", + "tabs": "0.105.0", "timecop": "0.36.0", "tree-view": "0.216.0", "update-package-dependencies": "0.11.0",