Merge pull request #1417 from atom/ns-empty-panes

Support empty panes
This commit is contained in:
Nathan Sobo
2014-01-16 10:21:44 -08:00
13 changed files with 335 additions and 377 deletions

View File

@@ -56,17 +56,17 @@
},
"packageDependencies": {
"atom-dark-syntax": "0.10.0",
"atom-dark-ui": "0.19.0",
"atom-dark-ui": "0.20.0",
"atom-light-syntax": "0.10.0",
"atom-light-ui": "0.18.0",
"atom-light-ui": "0.19.0",
"base16-tomorrow-dark-theme": "0.8.0",
"solarized-dark-syntax": "0.6.0",
"solarized-light-syntax": "0.2.0",
"archive-view": "0.19.0",
"archive-view": "0.20.0",
"autocomplete": "0.20.0",
"autoflow": "0.12.0",
"autosave": "0.10.0",
"background-tips": "0.4.0",
"background-tips": "0.5.0",
"bookmarks": "0.16.0",
"bracket-matcher": "0.19.0",
"command-logger": "0.9.0",
@@ -95,10 +95,10 @@
"styleguide": "0.19.0",
"symbols-view": "0.29.0",
"tabs": "0.17.0",
"terminal": "0.24.0",
"terminal": "0.25.0",
"timecop": "0.13.0",
"to-the-hubs": "0.17.0",
"tree-view": "0.61.0",
"tree-view": "0.62.0",
"visual-bell": "0.6.0",
"welcome": "0.4.0",
"whitespace": "0.10.0",

View File

@@ -36,8 +36,8 @@ describe "PaneContainer", ->
[container, pane1, pane2] = []
beforeEach ->
pane1 = new Pane
container = new PaneContainer(root: pane1)
container = new PaneContainer
pane1 = container.root
it "references the first pane if no pane has been made active", ->
expect(container.activePane).toBe pane1
@@ -60,18 +60,8 @@ describe "PaneContainer", ->
pane2.destroy()
expect(container.activePane).toBe pane1
expect(pane1.active).toBe true
it "does not allow the root pane to be destroyed", ->
pane1.destroy()
expect(container.activePane).toBe null
describe "when the last pane is removed", ->
[container, pane, surrenderedFocusHandler] = []
beforeEach ->
pane = new Pane
container = new PaneContainer(root: pane)
container.on 'surrendered-focus', surrenderedFocusHandler = jasmine.createSpy("surrenderedFocusHandler")
it "assigns null to the root and the activePane", ->
pane.destroy()
expect(container.root).toBe null
expect(container.activePane).toBe null
expect(container.root).toBe pane1
expect(pane1.isDestroyed()).toBe false

View File

@@ -19,8 +19,8 @@ describe "PaneContainerView", ->
isEqual: (other) -> @name is other?.name
container = new PaneContainerView
pane1 = new PaneView(new TestView('1'))
container.setRoot(pane1)
pane1 = container.getRoot()
pane1.activateItem(new TestView('1'))
pane2 = pane1.splitRight(new TestView('2'))
pane3 = pane2.splitDown(new TestView('3'))
@@ -130,12 +130,23 @@ describe "PaneContainerView", ->
expect(newContainer.find('.pane-row > :contains(1)').width()).toBe 150
expect(newContainer.find('.pane-row > .pane-column > :contains(2)').height()).toBe 100
it "removes empty panes on deserialization", ->
# only deserialize pane 1's view successfully
TestView.deserialize = ({name}) -> new TestView(name) if name is '1'
newContainer = new PaneContainerView(container.model.testSerialization())
expect(newContainer.find('.pane-row, .pane-column')).not.toExist()
expect(newContainer.find('> :contains(1)')).toExist()
describe "if there are empty panes after deserialization", ->
beforeEach ->
# only deserialize pane 1's view successfully
TestView.deserialize = ({name}) -> new TestView(name) if name is '1'
describe "if the 'core.destroyEmptyPanes' config option is false (the default)", ->
it "leaves the empty panes intact", ->
newContainer = new PaneContainerView(container.model.testSerialization())
expect(newContainer.find('.pane-row > :contains(1)')).toExist()
expect(newContainer.find('.pane-row > .pane-column > .pane').length).toBe 2
describe "if the 'core.destroyEmptyPanes' config option is true", ->
it "removes empty panes on deserialization", ->
atom.config.set('core.destroyEmptyPanes', true)
newContainer = new PaneContainerView(container.model.testSerialization())
expect(newContainer.find('.pane-row, .pane-column')).not.toExist()
expect(newContainer.find('> :contains(1)')).toExist()
describe "pane-container:active-pane-item-changed", ->
[pane1, item1a, item1b, item2a, item2b, item3a, container, activeItemChangedHandler] = []
@@ -147,24 +158,13 @@ describe "PaneContainerView", ->
item3a = new TestView('3a')
container = new PaneContainerView
pane1 = container.getRoot()
pane1.activateItem(item1a)
container.attachToDom()
pane1 = new PaneView(item1a)
container.setRoot(pane1)
activeItemChangedHandler = jasmine.createSpy("activeItemChangedHandler")
container.on 'pane-container:active-pane-item-changed', activeItemChangedHandler
describe "when there are no panes", ->
it "is triggered when a new pane containing a pane item is added", ->
container.setRoot()
expect(container.getPanes().length).toBe 0
activeItemChangedHandler.reset()
pane = new PaneView(item1a)
container.setRoot(pane)
expect(activeItemChangedHandler.callCount).toBe 1
expect(activeItemChangedHandler.argsForCall[0][1]).toEqual item1a
describe "when there is one pane", ->
it "is triggered when a new pane item is added", ->
pane1.activateItem(item1b)
@@ -203,11 +203,6 @@ describe "PaneContainerView", ->
expect(activeItemChangedHandler.callCount).toBe 1
expect(activeItemChangedHandler.argsForCall[0][1]).toBe undefined
it "is triggered when the pane is destroyed", ->
pane1.remove()
expect(activeItemChangedHandler.callCount).toBe 1
expect(activeItemChangedHandler.argsForCall[0][1]).toBe undefined
describe "when there are two panes", ->
[pane2] = []

View File

@@ -23,6 +23,33 @@ describe "Pane", ->
pane = new Pane(items: [new Item("A"), new Item("B")])
expect(pane.activeItem).toBe pane.items[0]
it "compacts the items array", ->
pane = new Pane(items: [undefined, new Item("A"), null, new Item("B")])
expect(pane.items.length).toBe 2
expect(pane.activeItem).toBe pane.items[0]
describe "::addItem(item, index)", ->
it "adds the item at the given index", ->
pane = new Pane(items: [new Item("A"), new Item("B")])
[item1, item2] = pane.items
item3 = new Item("C")
pane.addItem(item3, 1)
expect(pane.items).toEqual [item1, item3, item2]
it "adds the item after the active item ", ->
pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")])
[item1, item2, item3] = pane.items
pane.activateItem(item2)
item4 = new Item("D")
pane.addItem(item4)
expect(pane.items).toEqual [item1, item2, item4, item3]
it "sets the active item if it is undefined", ->
pane = new Pane
item = new Item("A")
pane.addItem(item)
expect(pane.activeItem).toBe item
describe "::activateItem(item)", ->
pane = null
@@ -146,19 +173,39 @@ describe "Pane", ->
expect(item1.isDestroyed()).toBe false
describe "when the last item is destroyed", ->
it "destroys the pane", ->
pane.destroyItem(item) for item in pane.getItems()
expect(pane.isDestroyed()).toBe true
describe "when the 'core.destroyEmptyPanes' config option is false (the default)", ->
it "does not destroy the pane, but leaves it in place with empty items", ->
expect(atom.config.get('core.destroyEmptyPanes')).toBe false
pane.destroyItem(item) for item in pane.getItems()
expect(pane.isDestroyed()).toBe false
expect(pane.activeItem).toBeUndefined()
describe "when the 'core.destroyEmptyPanes' config option is true", ->
it "destroys the pane", ->
atom.config.set('core.destroyEmptyPanes', true)
pane.destroyItem(item) for item in pane.getItems()
expect(pane.isDestroyed()).toBe true
describe "::destroyActiveItem()", ->
it "destroys the active item", ->
pane = new Pane(items: [new Item("A"), new Item("B")])
activeItem = pane.activeItem
pane.destroyActiveItem()
expect(activeItem.isDestroyed()).toBe true
expect(activeItem in pane.items).toBe false
it "does not throw an exception if there are no more items", ->
pane = new Pane
pane.destroyActiveItem()
describe "::destroyItems()", ->
it "destroys all items and the pane", ->
it "destroys all items", ->
pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")])
[item1, item2, item3] = pane.items
pane.destroyItems()
expect(item1.isDestroyed()).toBe true
expect(item2.isDestroyed()).toBe true
expect(item3.isDestroyed()).toBe true
expect(pane.isDestroyed()).toBe true
expect(pane.items).toEqual []
describe "when an item emits a destroyed event", ->
@@ -280,11 +327,21 @@ describe "Pane", ->
expect(pane2.items).toEqual [item4, item2, item5]
describe "when the moved item the last item in the source pane", ->
it "destroys the pane, but not the item", ->
beforeEach ->
item5.destroy()
pane2.moveItemToPane(item4, pane1, 0)
expect(pane2.isDestroyed()).toBe true
expect(item4.isDestroyed()).toBe false
describe "when the 'core.destroyEmptyPanes' config option is false (the default)", ->
it "does not destroy the pane or the item", ->
pane2.moveItemToPane(item4, pane1, 0)
expect(pane2.isDestroyed()).toBe false
expect(item4.isDestroyed()).toBe false
describe "when the 'core.destroyEmptyPanes' config option is true", ->
it "destroys the pane, but not the item", ->
atom.config.set('core.destroyEmptyPanes', true)
pane2.moveItemToPane(item4, pane1, 0)
expect(pane2.isDestroyed()).toBe true
expect(item4.isDestroyed()).toBe false
describe "split methods", ->
[pane1, container] = []
@@ -367,11 +424,13 @@ describe "Pane", ->
expect(pane2.focused).toBe true
describe "::destroy()", ->
[pane1, container] = []
[container, pane1, pane2] = []
beforeEach ->
pane1 = new Pane(items: [new Model, new Model])
container = new PaneContainer(root: pane1)
container = new PaneContainer
pane1 = container.root
pane1.addItems([new Item("A"), new Item("B")])
pane2 = pane1.splitRight()
it "destroys the pane's destroyable items", ->
[item1, item2] = pane1.items
@@ -381,14 +440,12 @@ describe "Pane", ->
describe "if the pane is active", ->
it "makes the next pane active", ->
pane2 = pane1.splitRight()
expect(pane2.isActive()).toBe true
pane2.destroy()
expect(pane1.isActive()).to
describe "if the pane's parent has more than two children", ->
it "removes the pane from its parent", ->
pane2 = pane1.splitRight()
pane3 = pane2.splitRight()
expect(container.root.children).toEqual [pane1, pane2, pane3]
@@ -397,7 +454,6 @@ describe "Pane", ->
describe "if the pane's parent has two children", ->
it "replaces the parent with its last remaining child", ->
pane2 = pane1.splitRight()
pane3 = pane2.splitDown()
expect(container.root.children[0]).toBe pane1

View File

@@ -22,15 +22,15 @@ describe "PaneView", ->
view2 = new TestView(id: 'view-2', text: 'View 2')
editor1 = atom.project.openSync('sample.js')
editor2 = atom.project.openSync('sample.txt')
pane = new PaneView(view1, editor1, view2, editor2)
pane = container.getRoot()
paneModel = pane.model
container.setRoot(pane)
paneModel.addItems([view1, editor1, view2, editor2])
afterEach ->
atom.deserializers.remove(TestView)
describe "when the active pane item changes", ->
it "hides all item views except the one being shown and sets the activeItem", ->
it "hides all item views except the active one", ->
expect(pane.activeItem).toBe view1
expect(view1.css('display')).not.toBe 'none'
@@ -168,34 +168,28 @@ describe "PaneView", ->
pane.items.length == 4
describe "when a pane is destroyed", ->
[pane2, pane2Model] = []
beforeEach ->
pane2Model = paneModel.splitRight() # Can't destroy the last pane, so we add another
pane2 = pane2Model._view
it "triggers a 'pane:removed' event with the pane", ->
removedHandler = jasmine.createSpy("removedHandler")
container.on 'pane:removed', removedHandler
pane.remove()
paneModel.destroy()
expect(removedHandler).toHaveBeenCalled()
expect(removedHandler.argsForCall[0][1]).toBe pane
describe "if the destroyed pane has focus", ->
[paneToLeft, paneToRight] = []
describe "if it is not the last pane in the container", ->
it "focuses the next pane", ->
paneModel.activateItem(editor1)
pane2Model = paneModel.splitRight(items: [paneModel.copyActiveItem()])
pane2 = pane2Model._view
container.attachToDom()
expect(pane.hasFocus()).toBe false
pane2Model.destroy()
expect(pane.hasFocus()).toBe true
describe "if it is the last pane in the container", ->
it "shifts focus to the workspace view", ->
atom.workspaceView = {focus: jasmine.createSpy("atom.workspaceView.focus")}
container.attachToDom()
pane.focus()
expect(container.hasFocus()).toBe true
paneModel.destroy()
expect(atom.workspaceView.focus).toHaveBeenCalled()
it "focuses the next pane", ->
container.attachToDom()
expect(pane.hasFocus()).toBe false
expect(pane2.hasFocus()).toBe true
pane2Model.destroy()
expect(pane.hasFocus()).toBe true
describe "::getNextPane()", ->
it "returns the next pane if one exists, wrapping around from the last pane to the first", ->

View File

@@ -98,39 +98,12 @@ describe "WorkspaceView", ->
beforeEach ->
atom.workspaceView.attachToDom()
describe "when there is an active view", ->
it "hands off focus to the active view", ->
editorView = atom.workspaceView.getActiveView()
editorView.isFocused = false
atom.workspaceView.focus()
expect(editorView.isFocused).toBeTruthy()
describe "when there is no active view", ->
beforeEach ->
atom.workspaceView.getActivePane().remove()
expect(atom.workspaceView.getActiveView()).toBeUndefined()
atom.workspaceView.attachToDom()
expect(document.activeElement).toBe document.body
describe "when are visible focusable elements (with a -1 tabindex)", ->
it "passes focus to the first focusable element", ->
focusable1 = $$ -> @div "One", id: 'one', tabindex: -1
focusable2 = $$ -> @div "Two", id: 'two', tabindex: -1
atom.workspaceView.appendToLeft(focusable1, focusable2)
expect(document.activeElement).toBe document.body
atom.workspaceView.focus()
expect(document.activeElement).toBe focusable1[0]
describe "when there are no visible focusable elements", ->
it "surrenders focus to the body", ->
focusable = $$ -> @div "One", id: 'one', tabindex: -1
atom.workspaceView.appendToLeft(focusable)
focusable.hide()
expect(document.activeElement).toBe document.body
atom.workspaceView.focus()
expect(document.activeElement).toBe document.body
it "hands off focus to the active pane", ->
activePane = atom.workspaceView.getActivePane()
$('body').focus()
expect(activePane.hasFocus()).toBe false
atom.workspaceView.focus()
expect(activePane.hasFocus()).toBe true
describe "keymap wiring", ->
commandHandler = null
@@ -213,254 +186,168 @@ describe "WorkspaceView", ->
expect(atom.config.get('editor.fontSize')).toBe 1
describe ".openSync(filePath, options)", ->
describe "when there is no active pane", ->
beforeEach ->
spyOn(PaneView.prototype, 'focus')
atom.workspaceView.getActivePane().remove()
expect(atom.workspaceView.getActivePane()).toBeUndefined()
[activePane, initialItemCount] = []
beforeEach ->
activePane = atom.workspaceView.getActivePane()
spyOn(activePane, 'focus')
initialItemCount = activePane.getItems().length
describe "when called with no path", ->
it "creates a empty edit session as an item on a new pane, and focuses the pane", ->
editor = atom.workspaceView.openSync()
expect(atom.workspaceView.getActivePane().activeItem).toBe editor
expect(editor.getPath()).toBeUndefined()
expect(atom.workspaceView.getActivePane().focus).toHaveBeenCalled()
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()
it "can create multiple empty edit sessions as an item on a new pane", ->
editor = atom.workspaceView.openSync()
editor2 = atom.workspaceView.openSync()
expect(atom.workspaceView.getActivePane().getItems().length).toBe 2
expect(editor).not.toBe editor2
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
describe "when called with a path", ->
it "creates an edit session for the given path as an item on a new pane, and focuses the pane", ->
editor = atom.workspaceView.openSync('b')
expect(atom.workspaceView.getActivePane().activeItem).toBe editor
expect(editor.getPath()).toBe require.resolve('./fixtures/dir/b')
expect(atom.workspaceView.getActivePane().focus).toHaveBeenCalled()
expect(activePane.activeItem).toBe editor
expect(editor).not.toBe previousEditor
describe "when the changeFocus option is false", ->
it "does not focus the new pane", ->
editor = atom.workspaceView.openSync('b', changeFocus: false)
expect(atom.workspaceView.getActivePane().focus).not.toHaveBeenCalled()
editor = atom.workspaceView.openSync(previousEditor.getPath())
expect(editor).toBe previousEditor
expect(activePane.activeItem).toBe editor
describe "when the split option is 'right'", ->
it "creates a new pane and opens the file in said pane", ->
editor = atom.workspaceView.openSync('b', split: 'right')
expect(atom.workspaceView.getActivePane().activeItem).toBe editor
expect(editor.getPath()).toBe require.resolve('./fixtures/dir/b')
expect(activePane.focus).toHaveBeenCalled()
describe "when there is an active pane", ->
[activePane, initialItemCount] = []
beforeEach ->
activePane = atom.workspaceView.getActivePane()
spyOn(activePane, 'focus')
initialItemCount = activePane.getItems().length
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 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
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 edit session item for the path being opened", ->
it "shows the existing edit session in the pane", ->
previousEditor = activePane.activeItem
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 = 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)", ->
describe "when there is an active pane", ->
[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)", ->
beforeEach ->
spyOn(PaneView.prototype, 'focus')
describe "when there is no active pane", ->
beforeEach ->
atom.workspaceView.getActivePane().remove()
expect(atom.workspaceView.getActivePane()).toBeUndefined()
describe "when called with no path", ->
it "creates a empty edit session as an item on a new pane, and focuses the pane", ->
editor = null
waitsForPromise ->
atom.workspaceView.open().then (o) -> editor = o
runs ->
expect(atom.workspaceView.getActivePane().activeItem).toBe editor
expect(editor.getPath()).toBeUndefined()
expect(atom.workspaceView.getActivePane().focus).toHaveBeenCalled()
it "can create multiple empty edit sessions as items on a pane", ->
editor1 = null
editor2 = null
waitsForPromise ->
atom.workspaceView.open()
.then (o) ->
editor1 = o
atom.workspaceView.open()
.then (o) ->
editor2 = o
runs ->
expect(atom.workspaceView.getActivePane().getItems().length).toBe 2
expect(editor1).not.toBe editor2
describe "when called with a path", ->
it "creates an edit session for the given path as an item on a new pane, and focuses the pane", ->
editor = null
waitsForPromise ->
atom.workspaceView.open('b').then (o) -> editor = o
runs ->
expect(atom.workspaceView.getActivePane().activeItem).toBe editor
expect(editor.getPath()).toBe require.resolve('./fixtures/dir/b')
expect(atom.workspaceView.getActivePane().focus).toHaveBeenCalled()
expect(activePane.activeItem).toBe editor
expect(editor).not.toBe previousEditor
describe "when there is an active pane", ->
[activePane] = []
waitsForPromise ->
atom.workspaceView.open(previousEditor.getPath()).then (o) -> editor = o
beforeEach ->
activePane = atom.workspaceView.getActivePane()
runs ->
expect(editor).toBe previousEditor
expect(activePane.activeItem).toBe editor
expect(activePane.focus).toHaveBeenCalled()
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", ->
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().then (o) -> editor = o
atom.workspaceView.open('b').then (o) -> editor = o
runs ->
expect(activePane.getItems().length).toBe 2
expect(activePane.activeItem).toBe editor
expect(editor.getPath()).toBeUndefined()
expect(activePane.getItems().length).toBe 2
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)
@@ -561,13 +448,12 @@ describe "WorkspaceView", ->
expect(workspace.getActivePaneItem().getUri()).toBe 'a'
describe "core:close", ->
it "closes the active editor until there are none", ->
it "closes the active pane item until all that remains is a single empty pane", ->
atom.config.set('core.destroyEmptyPanes', true)
atom.project.openSync('../sample.txt')
expect(atom.workspaceView.getActivePane().getItems()).toHaveLength 1
atom.workspaceView.trigger('core:close')
expect(atom.workspaceView.getActivePane()).not.toBeDefined()
atom.workspaceView.trigger('core:close')
expect(atom.workspaceView.getActivePane()).not.toBeDefined()
expect(atom.workspaceView.getActivePane().getItems()).toHaveLength 0
describe "core:save", ->
it "saves active editor until there are none", ->

View File

@@ -8,6 +8,9 @@ class PaneAxisView extends View
@onChildAdded(child) for child in @model.children
@subscribe @model.children, 'changed', @onChildrenChanged
afterAttach: ->
@container = @closest('.panes').view()
viewForModel: (model) ->
viewClass = model.getViewClass()
model._view ?= new viewClass(model)
@@ -26,9 +29,5 @@ class PaneAxisView extends View
view = @viewForModel(child)
view.detach()
PaneView ?= require './pane-view'
if view instanceof PaneView and view.model.isDestroyed()
@getContainer()?.trigger 'pane:removed', [view]
getContainer: ->
@closest('.panes').view()
@container?.trigger 'pane:removed', [view]

View File

@@ -32,9 +32,6 @@ class PaneContainerView extends View
getRoot: ->
@children().first().view()
setRoot: (root) ->
@model.root = root?.model
onRootChanged: (root) =>
focusedElement = document.activeElement if @hasFocus()

View File

@@ -9,13 +9,15 @@ class PaneContainer extends Model
Serializable.includeInto(this)
@properties
root: null
root: -> new Pane
activePane: null
previousRoot: null
@behavior 'activePaneItem', ->
@$activePane.switch (activePane) -> activePane?.$activeItem
@$activePane
.switch((activePane) -> activePane?.$activeItem)
.distinctUntilChanged()
constructor: (params) ->
super
@@ -24,7 +26,7 @@ class PaneContainer extends Model
deserializeParams: (params) ->
params.root = atom.deserializers.deserialize(params.root, container: this)
params.destroyEmptyPanes = true
params.destroyEmptyPanes = atom.config.get('core.destroyEmptyPanes')
params
serializeParams: (params) ->
@@ -75,14 +77,15 @@ class PaneContainer extends Model
root.parent = this
root.container = this
if root instanceof Pane
@activePane ?= root
@subscribe root, 'destroyed', =>
@activePane = null
@root = null
@activePane ?= root if root instanceof Pane
destroyEmptyPanes: ->
pane.destroy() for pane in @getPanes() when pane.items.length is 0
itemDestroyed: (item) ->
@emit 'item-destroyed', item
# Private: Called by Model superclass when destroyed
destroyed: ->
pane.destroy() for pane in @getPanes()

View File

@@ -76,7 +76,7 @@ class PaneView extends View
@command 'pane:split-right', => @splitRight(@copyActiveItem())
@command 'pane:split-up', => @splitUp(@copyActiveItem())
@command 'pane:split-down', => @splitDown(@copyActiveItem())
@command 'pane:close', => @destroyItems()
@command 'pane:close', => @model.destroy()
@command 'pane:close-other-items', => @destroyInactiveItems()
# Deprecated: Use ::destroyItem
@@ -102,6 +102,7 @@ class PaneView extends View
@focus() if @model.focused and onDom
return if @attached
@container = @closest('.panes').view()
@attached = true
@trigger 'pane:attached', [this]
@@ -118,7 +119,7 @@ class PaneView extends View
# Public: Returns the next pane, ordered by creation.
getNextPane: ->
panes = @getContainer()?.getPanes()
panes = @container?.getPanes()
return unless panes.length > 1
nextIndex = (panes.indexOf(this) + 1) % panes.length
panes[nextIndex]
@@ -194,7 +195,7 @@ class PaneView extends View
splitDown: (items...) -> @model.splitDown({items})._view
# Private:
# Public:
getContainer: ->
@closest('.panes').view()

View File

@@ -14,8 +14,8 @@ class Pane extends Model
Serializable.includeInto(this)
@properties
container: null
activeItem: null
container: undefined
activeItem: undefined
focused: false
# Public: Only one pane is considered *active* at a time. A pane is activated
@@ -31,7 +31,7 @@ class Pane extends Model
constructor: (params) ->
super
@items = Sequence.fromArray(params?.items ? [])
@items = Sequence.fromArray(compact(params?.items ? []))
@activeItem ?= @items[0]
@subscribe @items.onEach (item) =>
@@ -125,25 +125,45 @@ class Pane extends Model
# The item to add. It can be a model with an associated view or a view.
# * index:
# An optional index at which to add the item. If omitted, the item is
# added to the end.
# added after the current active item.
#
# Returns the added item
addItem: (item, index=@getActiveItemIndex() + 1) ->
return if item in @items
@items.splice(index, 0, item)
@activeItem ?= item
@emit 'item-added', item, index
item
# Public: Adds the given items to the pane.
#
# * items:
# An {Array} of items to add. Items can be models with associated views
# or views. Any items that are already present in items will not be added.
# * index:
# An optional index at which to add the item. If omitted, the item is
# added after the current active item.
#
# Returns an {Array} of the added items
addItems: (items, index=@getActiveItemIndex() + 1) ->
items = items.filter (item) => not (item in @items)
@addItem(item, index + i) for item, i in items
items
# Private:
removeItem: (item, destroying) ->
index = @items.indexOf(item)
return if index is -1
@activateNextItem() if item is @activeItem and @items.length > 1
if item is @activeItem
if @items.length is 1
@activeItem = undefined
else
@activateNextItem()
@items.splice(index, 1)
@emit 'item-removed', item, index, destroying
@container?.itemDestroyed(item) if destroying
@destroy() if @items.length is 0
@destroy() if @items.length is 0 and atom.config.get('core.destroyEmptyPanes')
# Public: Moves the given item to the specified index.
moveItem: (item, newIndex) ->
@@ -165,13 +185,14 @@ class Pane extends Model
# Public: Destroys the given item. If it is the active item, activate the next
# one. If this is the last item, also destroys the pane.
destroyItem: (item) ->
@emit 'before-item-destroyed', item
if @promptToSaveItem(item)
@removeItem(item, true)
item.destroy?()
true
else
false
if item?
@emit 'before-item-destroyed', item
if @promptToSaveItem(item)
@removeItem(item, true)
item.destroy?()
true
else
false
# Public: Destroys all items and destroys the pane.
destroyItems: ->
@@ -181,6 +202,9 @@ class Pane extends Model
destroyInactiveItems: ->
@destroyItem(item) for item in @getItems() when item isnt @activeItem
destroy: ->
super unless @container?.isAlive() and @container?.getPanes().length is 1
# Private: Called by model superclass.
destroyed: ->
@container.activateNextPane() if @isActive()
@@ -255,7 +279,8 @@ class Pane extends Model
# Private:
copyActiveItem: ->
@activeItem.copy?() ? atom.deserializers.deserialize(@activeItem.serialize())
if @activeItem?
@activeItem.copy?() ? atom.deserializers.deserialize(@activeItem.serialize())
# Public: Creates a new pane to the left of the receiver.
#

View File

@@ -44,7 +44,7 @@ class WorkspaceView extends View
@delegatesProperty 'fullScreen', 'destroyedItemUris', toProperty: 'model'
@delegatesMethods 'open', 'openSync', 'openSingletonSync', 'reopenItemSync',
'saveActivePaneItem', 'saveActivePaneItemAs', 'saveAll', 'destroyActivePaneItem',
toProperty: 'model'
'destroyActivePane', toProperty: 'model'
@version: 4
@@ -55,6 +55,7 @@ class WorkspaceView extends View
themes: ['atom-dark-ui', 'atom-dark-syntax']
projectHome: path.join(fs.getHomeDirectory(), 'github')
audioBeep: true
destroyEmptyPanes: false
# Private:
@content: ->
@@ -118,7 +119,7 @@ class WorkspaceView extends View
@command 'pane:reopen-closed-item', => @reopenItemSync()
@command 'core:close', => @destroyActivePaneItem()
@command 'core:close', => if @getActivePaneItem()? then @destroyActivePaneItem() else @destroyActivePane()
@command 'core:save', => @saveActivePaneItem()
@command 'core:save-as', => @saveActivePaneItemAs()
@@ -237,8 +238,11 @@ class WorkspaceView extends View
@on('editor:attached', attachedCallback)
off: => @off('editor:attached', attachedCallback)
# Private: Called by SpacePen
beforeRemove: ->
@model.destroy()
# Private: Destroys everything.
remove: ->
@model.destroy()
editorView.remove() for editorView in @getEditorViews()
super

View File

@@ -132,6 +132,10 @@ class Workspace extends Model
destroyActivePaneItem: ->
@activePane?.destroyActiveItem()
# Public: destroy/close the active pane.
destroyActivePane: ->
@activePane?.destroy()
# Private: Removes the item's uri from the list of potential items to reopen.
itemOpened: (item) ->
if uri = item.getUri?()
@@ -141,3 +145,7 @@ class Workspace extends Model
onPaneItemDestroyed: (item) =>
if uri = item.getUri?()
@destroyedItemUris.push(uri)
# Private: Called by Model superclass when destroyed
destroyed: ->
@paneContainer.destroy()