diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee new file mode 100644 index 000000000..937069482 --- /dev/null +++ b/spec/workspace-spec.coffee @@ -0,0 +1,195 @@ +Workspace = require '../src/workspace' + +describe "Workspace", -> + workspace = null + + beforeEach -> + atom.project.setPath(atom.project.resolve('dir')) + workspace = new Workspace + + describe "::open(uri)", -> + beforeEach -> + spyOn(workspace.activePane, 'activate') + + describe "when called without a uri", -> + it "adds and activates an empty editor on the active pane", -> + editor = null + waitsForPromise -> + workspace.open().then (o) -> editor = o + + runs -> + expect(editor.getPath()).toBeUndefined() + expect(workspace.activePane.items).toEqual [editor] + expect(workspace.activePaneItem).toBe editor + expect(workspace.activePane.activate).toHaveBeenCalled() + + describe "when called with a uri", -> + describe "when the active pane already has an editor for the given uri", -> + it "activates the existing editor on the active pane", -> + editor1 = workspace.openSync('a') + editor2 = workspace.openSync('b') + + editor = null + waitsForPromise -> + workspace.open('a').then (o) -> editor = o + + runs -> + expect(editor).toBe editor1 + expect(workspace.activePaneItem).toBe editor + expect(workspace.activePane.activate).toHaveBeenCalled() + + describe "when the active pane does not have an editor for the given uri", -> + it "adds and activates a new editor for the given path on the active pane", -> + editor = null + waitsForPromise -> + workspace.open('a').then (o) -> editor = o + + runs -> + expect(editor.getUri()).toBe 'a' + expect(workspace.activePaneItem).toBe editor + expect(workspace.activePane.items).toEqual [editor] + expect(workspace.activePane.activate).toHaveBeenCalled() + + describe "::openSync(uri, options)", -> + [activePane, initialItemCount] = [] + + beforeEach -> + activePane = workspace.activePane + spyOn(activePane, 'activate') + initialItemCount = activePane.items.length + + describe "when called without a uri", -> + it "adds and activates an empty editor on the active pane", -> + editor = workspace.openSync() + expect(activePane.items.length).toBe initialItemCount + 1 + expect(activePane.activeItem).toBe editor + expect(editor.getPath()).toBeUndefined() + expect(activePane.activate).toHaveBeenCalled() + + describe "when called with a uri", -> + describe "when the active pane already has an editor for the given uri", -> + it "activates the existing editor on the active pane", -> + editor1 = workspace.openSync('a') + editor2 = workspace.openSync('b') + expect(activePane.activeItem).toBe editor2 + expect(activePane.items.length).toBe 2 + + editor = workspace.openSync(editor1.getPath()) + expect(editor).toBe editor1 + expect(activePane.activeItem).toBe editor + expect(activePane.activate).toHaveBeenCalled() + expect(activePane.items.length).toBe 2 + + describe "when the active pane does not have an editor for the given uri", -> + it "adds and activates a new editor for the given path on the active pane", -> + editor = workspace.openSync('a') + expect(activePane.items.length).toBe 1 + expect(activePane.activeItem).toBe editor + expect(activePane.activate).toHaveBeenCalled() + + describe "when the 'activatePane' option is false", -> + it "does not activate the active pane", -> + workspace.openSync('b', activatePane: false) + expect(activePane.activate).not.toHaveBeenCalled() + + describe "::openSingletonSync(uri, options)", -> + describe "when an editor for the given uri is already open on the active pane", -> + it "activates the existing editor", -> + editor1 = workspace.openSync('a') + editor2 = workspace.openSync('b') + expect(workspace.activePaneItem).toBe editor2 + workspace.openSingletonSync('a') + expect(workspace.activePaneItem).toBe editor1 + + describe "when an editor for the given uri is already open on an inactive pane", -> + it "activates the existing editor on the inactive pane, then activates that pane", -> + editor1 = workspace.openSync('a') + pane1 = workspace.activePane + pane2 = workspace.activePane.splitRight() + editor2 = workspace.openSync('b') + expect(workspace.activePaneItem).toBe editor2 + workspace.openSingletonSync('a') + expect(workspace.activePane).toBe pane1 + expect(workspace.activePaneItem).toBe editor1 + + describe "when no editor for the given uri is open in any pane", -> + it "opens an editor for the given uri in the active pane", -> + editor1 = workspace.openSingletonSync('a') + expect(workspace.activePaneItem).toBe editor1 + + describe "when the 'split' option is 'left'", -> + it "opens the editor in the leftmost pane of the current pane axis", -> + pane1 = workspace.activePane + pane2 = pane1.splitRight() + expect(workspace.activePane).toBe pane2 + editor1 = workspace.openSingletonSync('a', split: 'left') + expect(workspace.activePane).toBe pane1 + expect(pane1.items).toEqual [editor1] + expect(pane2.items).toEqual [] + + describe "when the 'split' option is 'right'", -> + describe "when the active pane is in a horizontal pane axis", -> + it "activates the editor on the rightmost pane of the current pane axis", -> + pane1 = workspace.activePane + pane2 = pane1.splitRight() + pane1.activate() + editor1 = workspace.openSingletonSync('a', split: 'right') + expect(workspace.activePane).toBe pane2 + expect(pane2.items).toEqual [editor1] + expect(pane1.items).toEqual [] + + describe "when the active pane is not in a horizontal pane axis", -> + it "splits the current pane to the right, then activates the editor on the right pane", -> + pane1 = workspace.activePane + editor1 = workspace.openSingletonSync('a', split: 'right') + pane2 = workspace.activePane + expect(workspace.paneContainer.root.children).toEqual [pane1, pane2] + expect(pane2.items).toEqual [editor1] + expect(pane1.items).toEqual [] + + describe "::reopenItemSync()", -> + it "opens the uri associated with the last closed pane that isn't currently open", -> + pane = workspace.activePane + workspace.openSync('a') + workspace.openSync('b') + workspace.openSync('file1') + workspace.openSync() + + # does not reopen items with no uri + expect(workspace.activePaneItem.getUri()).toBeUndefined() + pane.destroyActiveItem() + workspace.reopenItemSync() + expect(workspace.activePaneItem.getUri()).not.toBeUndefined() + + # destroy all items + expect(workspace.activePaneItem.getUri()).toBe 'file1' + pane.destroyActiveItem() + expect(workspace.activePaneItem.getUri()).toBe 'b' + pane.destroyActiveItem() + expect(workspace.activePaneItem.getUri()).toBe 'a' + pane.destroyActiveItem() + + # reopens items with uris + expect(workspace.activePaneItem).toBeUndefined() + workspace.reopenItemSync() + expect(workspace.activePaneItem.getUri()).toBe 'a' + + # does not reopen items that are already open + workspace.openSync('b') + expect(workspace.activePaneItem.getUri()).toBe 'b' + workspace.reopenItemSync() + expect(workspace.activePaneItem.getUri()).toBe 'file1' + + describe "::increase/decreaseFontSize()", -> + it "increases/decreases the font size without going below 1", -> + atom.config.set('editor.fontSize', 1) + workspace.increaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe 2 + workspace.increaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe 3 + workspace.decreaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe 2 + workspace.decreaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe 1 + workspace.decreaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe 1 diff --git a/spec/workspace-view-spec.coffee b/spec/workspace-view-spec.coffee index 5af9caffd..d13ca3323 100644 --- a/spec/workspace-view-spec.coffee +++ b/spec/workspace-view-spec.coffee @@ -168,186 +168,6 @@ describe "WorkspaceView", -> expect(workspaceView2.title).toBe "#{item.getTitle()} - #{atom.project.getPath()}" workspaceView2.remove() - describe "font size adjustment", -> - it "increases/decreases font size when increase/decrease-font-size events are triggered", -> - fontSizeBefore = atom.config.get('editor.fontSize') - atom.workspaceView.trigger 'window:increase-font-size' - expect(atom.config.get('editor.fontSize')).toBe fontSizeBefore + 1 - atom.workspaceView.trigger 'window:increase-font-size' - expect(atom.config.get('editor.fontSize')).toBe fontSizeBefore + 2 - atom.workspaceView.trigger 'window:decrease-font-size' - expect(atom.config.get('editor.fontSize')).toBe fontSizeBefore + 1 - atom.workspaceView.trigger 'window:decrease-font-size' - expect(atom.config.get('editor.fontSize')).toBe fontSizeBefore - - it "does not allow the font size to be less than 1", -> - atom.config.set("editor.fontSize", 1) - atom.workspaceView.trigger 'window:decrease-font-size' - expect(atom.config.get('editor.fontSize')).toBe 1 - - describe ".openSync(filePath, options)", -> - [activePane, initialItemCount] = [] - beforeEach -> - activePane = atom.workspaceView.getActivePane() - spyOn(activePane, 'focus') - initialItemCount = activePane.getItems().length - - describe "when called with no path", -> - it "opens an edit session with an empty buffer as an item in the active pane and focuses it", -> - editor = atom.workspaceView.openSync() - expect(activePane.getItems().length).toBe initialItemCount + 1 - expect(activePane.activeItem).toBe editor - expect(editor.getPath()).toBeUndefined() - expect(activePane.focus).toHaveBeenCalled() - - describe "when called with a path", -> - describe "when the active pane already has an edit session item for the path being opened", -> - it "shows the existing edit session in the pane", -> - previousEditor = activePane.activeItem - - editor = atom.workspaceView.openSync('b') - expect(activePane.activeItem).toBe editor - expect(editor).not.toBe previousEditor - - editor = atom.workspaceView.openSync(previousEditor.getPath()) - expect(editor).toBe previousEditor - expect(activePane.activeItem).toBe editor - - expect(activePane.focus).toHaveBeenCalled() - - describe "when the active pane does not have an edit session item for the path being opened", -> - it "creates a new edit session for the given path in the active editor", -> - editor = atom.workspaceView.openSync('b') - expect(activePane.items.length).toBe 2 - expect(activePane.activeItem).toBe editor - expect(activePane.focus).toHaveBeenCalled() - - describe "when the changeFocus option is false", -> - it "does not focus the active pane", -> - editor = atom.workspaceView.openSync('b', changeFocus: false) - expect(activePane.focus).not.toHaveBeenCalled() - - describe "when the split option is 'right'", -> - it "creates a new pane and opens the file in said pane", -> - pane1 = atom.workspaceView.getActivePane() - - editor = atom.workspaceView.openSync('b', split: 'right') - pane2 = atom.workspaceView.getActivePane() - expect(pane2[0]).not.toBe pane1[0] - expect(editor.getPath()).toBe require.resolve('./fixtures/dir/b') - - expect(atom.workspaceView.panes.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0]] - - editor = atom.workspaceView.openSync('file1', split: 'right') - pane3 = atom.workspaceView.getActivePane() - expect(pane3[0]).toBe pane2[0] - expect(editor.getPath()).toBe require.resolve('./fixtures/dir/file1') - - expect(atom.workspaceView.panes.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0]] - - describe ".openSingletonSync(filePath, options)", -> - [pane1] = [] - beforeEach -> - pane1 = atom.workspaceView.getActivePane() - - it "creates a new pane and reuses the file when already open", -> - atom.workspaceView.openSingletonSync('b', split: 'right') - pane2 = atom.workspaceView.getActivePane() - expect(pane2[0]).not.toBe pane1[0] - expect(pane1.itemForUri('b')).toBeFalsy() - expect(pane2.itemForUri('b')).not.toBeFalsy() - expect(atom.workspaceView.panes.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0]] - - pane1.activate() - expect(atom.workspaceView.getActivePane()[0]).toBe pane1[0] - - atom.workspaceView.openSingletonSync('b', split: 'right') - pane3 = atom.workspaceView.getActivePane() - expect(pane3[0]).toBe pane2[0] - expect(pane1.itemForUri('b')).toBeFalsy() - expect(pane2.itemForUri('b')).not.toBeFalsy() - expect(atom.workspaceView.panes.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0]] - - it "handles split: left by opening to the left pane when necessary", -> - atom.workspaceView.openSingletonSync('b', split: 'right') - pane2 = atom.workspaceView.getActivePane() - expect(pane2[0]).not.toBe pane1[0] - - atom.workspaceView.openSingletonSync('file1', split: 'left') - - activePane = atom.workspaceView.getActivePane() - expect(activePane[0]).toBe pane1[0] - - expect(pane1.itemForUri('file1')).toBeTruthy() - expect(pane2.itemForUri('file1')).toBeFalsy() - expect(atom.workspaceView.panes.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0]] - - pane2.activate() - expect(atom.workspaceView.getActivePane()[0]).toBe pane2[0] - - atom.workspaceView.openSingletonSync('file1', split: 'left') - activePane = atom.workspaceView.getActivePane() - expect(activePane[0]).toBe pane1[0] - expect(atom.workspaceView.panes.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0]] - - it "reuses the file when already open", -> - atom.workspaceView.openSync('b') - atom.workspaceView.openSingletonSync('b', split: 'right') - expect(atom.workspaceView.panes.find('.pane').toArray()).toEqual [pane1[0]] - - describe ".open(filePath)", -> - [activePane] = [] - - beforeEach -> - spyOn(PaneView.prototype, 'focus') - activePane = atom.workspaceView.getActivePane() - - describe "when called with no path", -> - it "opens an edit session with an empty buffer as an item in the active pane and focuses it", -> - editor = null - - waitsForPromise -> - atom.workspaceView.open().then (o) -> editor = o - - runs -> - expect(activePane.getItems().length).toBe 2 - expect(activePane.activeItem).toBe editor - expect(editor.getPath()).toBeUndefined() - expect(activePane.focus).toHaveBeenCalled() - - describe "when called with a path", -> - describe "when the active pane already has an item for the given path", -> - it "shows the existing edit session in the pane", -> - previousEditor = activePane.activeItem - - editor = null - waitsForPromise -> - atom.workspaceView.open('b').then (o) -> editor = o - - runs -> - expect(activePane.activeItem).toBe editor - expect(editor).not.toBe previousEditor - - waitsForPromise -> - atom.workspaceView.open(previousEditor.getPath()).then (o) -> editor = o - - runs -> - expect(editor).toBe previousEditor - expect(activePane.activeItem).toBe editor - expect(activePane.focus).toHaveBeenCalled() - - describe "when the active pane does not have an existing item for the given path", -> - it "creates a new edit session for the given path in the active pane", -> - editor = null - - waitsForPromise -> - atom.workspaceView.open('b').then (o) -> editor = o - - runs -> - expect(activePane.activeItem).toBe editor - expect(activePane.getItems().length).toBe 2 - expect(activePane.focus).toHaveBeenCalled() - describe "window:toggle-invisibles event", -> it "shows/hides invisibles in all open and future editors", -> atom.workspaceView.height(200) @@ -414,39 +234,6 @@ describe "WorkspaceView", -> atom.workspaceView.getActiveView().splitRight() expect(count).toBe 2 - describe ".reopenItemSync()", -> - it "opens the uri associated with the last closed pane that isn't currently open", -> - workspace = atom.workspaceView - pane = workspace.getActivePane() - workspace.openSync('b') - workspace.openSync('file1') - workspace.openSync() - - # does not reopen items with no uri - expect(workspace.getActivePaneItem().getUri()).toBeUndefined() - pane.destroyActiveItem() - workspace.reopenItemSync() - expect(workspace.getActivePaneItem().getUri()).not.toBeUndefined() - - # destroy all items - expect(workspace.getActivePaneItem().getUri()).toBe 'file1' - pane.destroyActiveItem() - expect(workspace.getActivePaneItem().getUri()).toBe 'b' - pane.destroyActiveItem() - expect(workspace.getActivePaneItem().getUri()).toBe 'a' - pane.destroyActiveItem() - - # reopens items with uris - expect(workspace.getActivePaneItem()).toBeUndefined() - workspace.reopenItemSync() - expect(workspace.getActivePaneItem().getUri()).toBe 'a' - - # does not reopen items that are already open - workspace.openSync('b') - expect(workspace.getActivePaneItem().getUri()).toBe 'b' - workspace.reopenItemSync() - expect(workspace.getActivePaneItem().getUri()).toBe 'file1' - describe "core:close", -> it "closes the active pane item until all that remains is a single empty pane", -> atom.config.set('core.destroyEmptyPanes', true) @@ -454,22 +241,3 @@ describe "WorkspaceView", -> expect(atom.workspaceView.getActivePane().getItems()).toHaveLength 1 atom.workspaceView.trigger('core:close') expect(atom.workspaceView.getActivePane().getItems()).toHaveLength 0 - - describe "core:save", -> - it "saves active editor until there are none", -> - editor = atom.project.openSync('../sample.txt') - spyOn(editor, 'save') - atom.workspaceView.getActivePane().activateItem(editor) - atom.workspaceView.trigger('core:save') - expect(editor.save).toHaveBeenCalled() - - describe "core:save-as", -> - beforeEach -> - spyOn(atom, 'showSaveDialogSync').andReturn('/selected/path') - - it "saves active editor until there are none", -> - editor = atom.project.openSync('../sample.txt') - spyOn(editor, 'saveAs') - atom.workspaceView.getActivePane().activateItem(editor) - atom.workspaceView.trigger('core:save-as') - expect(editor.saveAs).toHaveBeenCalled() diff --git a/src/pane.coffee b/src/pane.coffee index 2c1bdfd67..4375ce588 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -1,4 +1,4 @@ -{find, compact, extend} = require 'underscore-plus' +{find, compact, extend, last} = require 'underscore-plus' {dirname} = require 'path' {Model, Sequence} = require 'theorist' Serializable = require 'serializable' @@ -332,3 +332,19 @@ class Pane extends Model newPane.activate() newPane + + # Private: If the parent is a horizontal axis, returns its first child; + # otherwise this pane. + findLeftmostSibling: -> + if @parent.orientation is 'horizontal' + @parent.children[0] + else + this + + # Private: If the parent is a horizontal axis, returns its last child; + # otherwise returns a new pane created by splitting this pane rightward. + findOrCreateRightmostSibling: -> + if @parent.orientation is 'horizontal' + last(@parent.children) + else + @splitRight() diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index 3e94d730b..a45b86b04 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -50,7 +50,7 @@ class WorkspaceView extends View @delegatesProperty 'fullScreen', 'destroyedItemUris', toProperty: 'model' @delegatesMethods 'open', 'openSync', 'openSingletonSync', 'reopenItemSync', 'saveActivePaneItem', 'saveActivePaneItemAs', 'saveAll', 'destroyActivePaneItem', - 'destroyActivePane', toProperty: 'model' + 'destroyActivePane', 'increaseFontSize', 'decreaseFontSize', toProperty: 'model' @version: 4 @@ -113,12 +113,8 @@ class WorkspaceView extends View @command 'window:install-shell-commands', => @installShellCommands() @command 'window:run-package-specs', => ipc.sendChannel('run-package-specs', path.join(atom.project.getPath(), 'spec')) - @command 'window:increase-font-size', => - atom.config.set("editor.fontSize", atom.config.get("editor.fontSize") + 1) - - @command 'window:decrease-font-size', => - fontSize = atom.config.get "editor.fontSize" - atom.config.set("editor.fontSize", fontSize - 1) if fontSize > 1 + @command 'window:increase-font-size', => @increaseFontSize() + @command 'window:decrease-font-size', => @decreaseFontSize() @command 'window:focus-next-pane', => @focusNextPane() @command 'window:focus-previous-pane', => @focusPreviousPane() diff --git a/src/workspace.coffee b/src/workspace.coffee index d09d8842d..21663bb59 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -77,51 +77,45 @@ class Workspace extends Model console.error(error.stack ? error) # Private: Only used in specs - openSync: (uri, {changeFocus, initialLine, pane, split}={}) -> - changeFocus ?= true - pane ?= @activePane + openSync: (uri, options={}) -> + {initialLine} = options + # TODO: Remove deprecated changeFocus option + activatePane = options.activatePane ? options.changeFocus ? true uri = atom.project.relativize(uri) - if pane - if uri - paneItem = pane.itemForUri(uri) ? atom.project.openSync(uri, {initialLine}) - else - paneItem = atom.project.openSync() - - if split == 'right' - panes = @getPanes() - if panes.length == 1 - pane = panes[0].splitRight() - else - pane = last(panes) - else if split == 'left' - pane = @getPanes()[0] - - pane.activateItem(paneItem) + if uri? + editor = @activePane.itemForUri(uri) ? atom.project.openSync(uri, {initialLine}) else - paneItem = atom.project.openSync(uri, {initialLine}) - pane = new Pane(items: [paneItem]) - @paneContainer.root = pane + editor = atom.project.openSync() - @itemOpened(paneItem) - - pane.activate() if changeFocus - paneItem + @activePane.activateItem(editor) + @itemOpened(editor) + @activePane.activate() if activatePane + editor # Public: Synchronously open an editor for the given URI or activate an existing # editor in any pane if one already exists. - openSingletonSync: (uri, {changeFocus, initialLine, split}={}) -> - changeFocus ?= true + openSingletonSync: (uri, options={}) -> + {initialLine, split} = options + # TODO: Remove deprecated changeFocus option + activatePane = options.activatePane ? options.changeFocus ? true uri = atom.project.relativize(uri) - pane = @paneContainer.paneForUri(uri) - if pane - paneItem = pane.itemForUri(uri) - pane.activateItem(paneItem) - pane.activate() if changeFocus - paneItem + if pane = @paneContainer.paneForUri(uri) + editor = pane.itemForUri(uri) else - @openSync(uri, {changeFocus, initialLine, split}) + pane = switch split + when 'left' + @activePane.findLeftmostSibling() + when 'right' + @activePane.findOrCreateRightmostSibling() + else + @activePane + editor = atom.project.openSync(uri, {initialLine}) + + pane.activateItem(editor) + pane.activate() if activatePane + editor # Public: Reopens the last-closed item uri if it hasn't already been reopened. reopenItemSync: -> @@ -144,6 +138,13 @@ class Workspace extends Model destroyActivePane: -> @activePane?.destroy() + increaseFontSize: -> + atom.config.set("editor.fontSize", atom.config.get("editor.fontSize") + 1) + + decreaseFontSize: -> + fontSize = atom.config.get("editor.fontSize") + atom.config.set("editor.fontSize", fontSize - 1) if fontSize > 1 + # Private: Removes the item's uri from the list of potential items to reopen. itemOpened: (item) -> if uri = item.getUri?()