diff --git a/docs/your-first-package.md b/docs/your-first-package.md index 02cb98379..fe9550bd9 100644 --- a/docs/your-first-package.md +++ b/docs/your-first-package.md @@ -53,7 +53,7 @@ module.exports = convert: -> # This assumes the active pane item is an editor - editor = atom.workspace.activePaneItem + editor = atom.workspace.getActivePaneItem() editor.insertText('Hello, World!') ``` @@ -131,7 +131,7 @@ inserting 'Hello, World!' convert the selected text to ASCII art. ```coffeescript convert: -> # This assumes the active pane item is an editor - editor = atom.workspace.activePaneItem + editor = atom.workspace.getActivePaneItem() selection = editor.getLastSelection() figlet = require 'figlet' diff --git a/package.json b/package.json index d24ca30de..6dc56fa54 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "coffee-script": "1.7.0", "coffeestack": "0.7.0", "delegato": "^1", - "emissary": "^1.2.2", + "emissary": "^1.3.1", + "event-kit": "0.5.0", "first-mate": "^2.0.5", "fs-plus": "^2.2.6", "fstream": "0.1.24", @@ -109,7 +110,6 @@ "welcome": "0.18.0", "whitespace": "0.25.0", "wrap-guide": "0.21.0", - "language-c": "0.28.0", "language-coffee-script": "0.30.0", "language-css": "0.17.0", diff --git a/spec/pane-container-spec.coffee b/spec/pane-container-spec.coffee index df9921e69..1701e3f03 100644 --- a/spec/pane-container-spec.coffee +++ b/spec/pane-container-spec.coffee @@ -27,42 +27,91 @@ describe "PaneContainer", -> it "preserves the active pane across serialization, independent of focus", -> pane3A.activate() - expect(containerA.activePane).toBe pane3A + expect(containerA.getActivePane()).toBe pane3A containerB = containerA.testSerialization() [pane1B, pane2B, pane3B] = containerB.getPanes() - expect(containerB.activePane).toBe pane3B + expect(containerB.getActivePane()).toBe pane3B - describe "::activePane", -> + it "does not allow the root pane to be destroyed", -> + container = new PaneContainer + container.getRoot().destroy() + expect(container.getRoot()).toBeDefined() + expect(container.getRoot().isDestroyed()).toBe false + + describe "::getActivePane()", -> [container, pane1, pane2] = [] beforeEach -> container = new PaneContainer - pane1 = container.root + pane1 = container.getRoot() - it "references the first pane if no pane has been made active", -> - expect(container.activePane).toBe pane1 - expect(pane1.active).toBe true + it "returns the first pane if no pane has been made active", -> + expect(container.getActivePane()).toBe pane1 + expect(pane1.isActive()).toBe true - it "references the most pane on which ::activate was most recently called", -> + it "returns the most pane on which ::activate() was most recently called", -> pane2 = pane1.splitRight() pane2.activate() - expect(container.activePane).toBe pane2 - expect(pane1.active).toBe false - expect(pane2.active).toBe true + expect(container.getActivePane()).toBe pane2 + expect(pane1.isActive()).toBe false + expect(pane2.isActive()).toBe true pane1.activate() - expect(container.activePane).toBe pane1 - expect(pane1.active).toBe true - expect(pane2.active).toBe false + expect(container.getActivePane()).toBe pane1 + expect(pane1.isActive()).toBe true + expect(pane2.isActive()).toBe false - it "is reassigned to the next pane if the current active pane is destroyed", -> + it "returns the next pane if the current active pane is destroyed", -> pane2 = pane1.splitRight() pane2.activate() pane2.destroy() - expect(container.activePane).toBe pane1 - expect(pane1.active).toBe true + expect(container.getActivePane()).toBe pane1 + expect(pane1.isActive()).toBe true - it "does not allow the root pane to be destroyed", -> - pane1.destroy() - expect(container.root).toBe pane1 - expect(pane1.isDestroyed()).toBe false + describe "::onDidChangeActivePaneItem()", -> + [container, pane1, pane2, observed] = [] + + beforeEach -> + container = new PaneContainer(root: new Pane(items: [new Object, new Object])) + container.getRoot().splitRight(items: [new Object, new Object]) + [pane1, pane2] = container.getPanes() + + observed = [] + container.onDidChangeActivePaneItem (item) -> observed.push(item) + + it "invokes observers when the active item of the active pane changes", -> + pane2.activateNextItem() + pane2.activateNextItem() + expect(observed).toEqual [pane2.itemAtIndex(1), pane2.itemAtIndex(0)] + + it "invokes observers when the active pane changes", -> + pane1.activate() + pane2.activate() + expect(observed).toEqual [pane1.itemAtIndex(0), pane2.itemAtIndex(0)] + + describe "::observePanes()", -> + it "invokes observers with all current and future panes", -> + container = new PaneContainer + container.getRoot().splitRight() + [pane1, pane2] = container.getPanes() + + observed = [] + container.observePanes (pane) -> observed.push(pane) + + pane3 = pane2.splitDown() + pane4 = pane2.splitRight() + + expect(observed).toEqual [pane1, pane2, pane3, pane4] + + describe "::observePaneItems()", -> + it "invokes observers with all current and future pane items", -> + container = new PaneContainer(root: new Pane(items: [new Object, new Object])) + container.getRoot().splitRight(items: [new Object]) + [pane1, pane2] = container.getPanes() + observed = [] + container.observePaneItems (pane) -> observed.push(pane) + + pane3 = pane2.splitDown(items: [new Object]) + pane3.addItems([new Object, new Object]) + + expect(observed).toEqual container.getPaneItems() diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index 04b1c2474..c66b88850 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -21,39 +21,83 @@ describe "Pane", -> describe "construction", -> it "sets the active item to the first item", -> pane = new Pane(items: [new Item("A"), new Item("B")]) - expect(pane.activeItem).toBe pane.items[0] + expect(pane.getActiveItem()).toBe pane.itemAtIndex(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] + expect(pane.getItems().length).toBe 2 + expect(pane.getActiveItem()).toBe pane.itemAtIndex(0) + + describe "::activate()", -> + [container, pane1, pane2] = [] + + beforeEach -> + container = new PaneContainer(root: new Pane) + container.getRoot().splitRight() + [pane1, pane2] = container.getPanes() + + it "changes the active pane on the container", -> + expect(container.getActivePane()).toBe pane2 + pane1.activate() + expect(container.getActivePane()).toBe pane1 + pane2.activate() + expect(container.getActivePane()).toBe pane2 + + it "invokes ::onDidChangeActivePane observers on the container", -> + observed = [] + container.onDidChangeActivePane (activePane) -> observed.push(activePane) + + pane1.activate() + pane1.activate() + pane2.activate() + pane1.activate() + expect(observed).toEqual [pane1, pane2, pane1] + + it "invokes ::onDidChangeActive observers on the relevant panes", -> + observed = [] + pane1.onDidChangeActive (active) -> observed.push(active) + pane1.activate() + pane2.activate() + expect(observed).toEqual [true, false] + + it "invokes ::onDidActivate() observers", -> + eventCount = 0 + pane1.onDidActivate -> eventCount++ + pane1.activate() + pane1.activate() + pane2.activate() + expect(eventCount).toBe 2 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 + [item1, item2] = pane.getItems() item3 = new Item("C") pane.addItem(item3, 1) - expect(pane.items).toEqual [item1, item3, item2] + expect(pane.getItems()).toEqual [item1, item3, item2] - it "adds the item after the active item ", -> + it "adds the item after the active item if no index is provided", -> pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) - [item1, item2, item3] = pane.items + [item1, item2, item3] = pane.getItems() pane.activateItem(item2) item4 = new Item("D") pane.addItem(item4) - expect(pane.items).toEqual [item1, item2, item4, item3] + expect(pane.getItems()).toEqual [item1, item2, item4, item3] it "sets the active item after adding the first item", -> pane = new Pane item = new Item("A") - events = [] - pane.on 'item-added', -> events.push('item-added') - pane.$activeItem.changes.onValue -> events.push('active-item-changed') - pane.addItem(item) - expect(pane.activeItem).toBe item - expect(events).toEqual ['item-added', 'active-item-changed'] + expect(pane.getActiveItem()).toBe item + + it "invokes ::onDidAddItem() observers", -> + pane = new Pane(items: [new Item("A"), new Item("B")]) + events = [] + pane.onDidAddItem (event) -> events.push(event) + + item = new Item("C") + pane.addItem(item, 1) + expect(events).toEqual [{item, index: 1}] describe "::activateItem(item)", -> pane = null @@ -62,83 +106,102 @@ describe "Pane", -> pane = new Pane(items: [new Item("A"), new Item("B")]) it "changes the active item to the current item", -> - expect(pane.activeItem).toBe pane.items[0] - pane.activateItem(pane.items[1]) - expect(pane.activeItem).toBe pane.items[1] + expect(pane.getActiveItem()).toBe pane.itemAtIndex(0) + pane.activateItem(pane.itemAtIndex(1)) + expect(pane.getActiveItem()).toBe pane.itemAtIndex(1) it "adds the given item if it isn't present in ::items", -> item = new Item("C") pane.activateItem(item) - expect(item in pane.items).toBe true - expect(pane.activeItem).toBe item + expect(item in pane.getItems()).toBe true + expect(pane.getActiveItem()).toBe item + + it "invokes ::onDidChangeActiveItem() observers", -> + observed = [] + pane.onDidChangeActiveItem (item) -> observed.push(item) + pane.activateItem(pane.itemAtIndex(1)) + expect(observed).toEqual [pane.itemAtIndex(1)] describe "::activateNextItem() and ::activatePreviousItem()", -> it "sets the active item to the next/previous item, looping around at either end", -> pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) - [item1, item2, item3] = pane.items + [item1, item2, item3] = pane.getItems() - expect(pane.activeItem).toBe item1 + expect(pane.getActiveItem()).toBe item1 pane.activatePreviousItem() - expect(pane.activeItem).toBe item3 + expect(pane.getActiveItem()).toBe item3 pane.activatePreviousItem() - expect(pane.activeItem).toBe item2 + expect(pane.getActiveItem()).toBe item2 pane.activateNextItem() - expect(pane.activeItem).toBe item3 + expect(pane.getActiveItem()).toBe item3 pane.activateNextItem() - expect(pane.activeItem).toBe item1 + expect(pane.getActiveItem()).toBe item1 describe "::activateItemAtIndex(index)", -> it "activates the item at the given index", -> pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) - [item1, item2, item3] = pane.items + [item1, item2, item3] = pane.getItems() pane.activateItemAtIndex(2) - expect(pane.activeItem).toBe item3 + expect(pane.getActiveItem()).toBe item3 pane.activateItemAtIndex(1) - expect(pane.activeItem).toBe item2 + expect(pane.getActiveItem()).toBe item2 pane.activateItemAtIndex(0) - expect(pane.activeItem).toBe item1 + expect(pane.getActiveItem()).toBe item1 # Doesn't fail with out-of-bounds indices pane.activateItemAtIndex(100) - expect(pane.activeItem).toBe item1 + expect(pane.getActiveItem()).toBe item1 pane.activateItemAtIndex(-1) - expect(pane.activeItem).toBe item1 + expect(pane.getActiveItem()).toBe item1 describe "::destroyItem(item)", -> [pane, item1, item2, item3] = [] beforeEach -> pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) - [item1, item2, item3] = pane.items + [item1, item2, item3] = pane.getItems() - it "removes the item from the items list", -> - expect(pane.activeItem).toBe item1 + it "removes the item from the items list and destroyes it", -> + expect(pane.getActiveItem()).toBe item1 pane.destroyItem(item2) - expect(item2 in pane.items).toBe false - expect(pane.activeItem).toBe item1 + expect(item2 in pane.getItems()).toBe false + expect(item2.isDestroyed()).toBe true + expect(pane.getActiveItem()).toBe item1 pane.destroyItem(item1) - expect(item1 in pane.items).toBe false + expect(item1 in pane.getItems()).toBe false + expect(item1.isDestroyed()).toBe true + + it "invokes ::onWillDestroyItem() observers before destroying the item", -> + events = [] + pane.onWillDestroyItem (event) -> + expect(item2.isDestroyed()).toBe false + events.push(event) + + pane.destroyItem(item2) + expect(item2.isDestroyed()).toBe true + expect(events).toEqual [{item: item2, index: 1}] + + it "invokes ::onDidRemoveItem() observers", -> + events = [] + pane.onDidRemoveItem (event) -> events.push(event) + pane.destroyItem(item2) + expect(events).toEqual [{item: item2, index: 1, destroyed: true}] describe "when the destroyed item is the active item and is the first item", -> it "activates the next item", -> - expect(pane.activeItem).toBe item1 + expect(pane.getActiveItem()).toBe item1 pane.destroyItem(item1) - expect(pane.activeItem).toBe item2 + expect(pane.getActiveItem()).toBe item2 describe "when the destroyed item is the active item and is not the first item", -> beforeEach -> pane.activateItem(item2) it "activates the previous item", -> - expect(pane.activeItem).toBe item2 + expect(pane.getActiveItem()).toBe item2 pane.destroyItem(item2) - expect(pane.activeItem).toBe item1 - - it "emits 'item-removed' with the item, its index, and true indicating the item is being destroyed", -> - pane.on 'item-removed', itemRemovedHandler = jasmine.createSpy("itemRemovedHandler") - pane.destroyItem(item2) - expect(itemRemovedHandler).toHaveBeenCalledWith(item2, 1, true) + expect(pane.getActiveItem()).toBe item1 describe "if the item is modified", -> itemUri = null @@ -157,7 +220,7 @@ describe "Pane", -> pane.destroyItem(item1) expect(item1.save).toHaveBeenCalled() - expect(item1 in pane.items).toBe false + expect(item1 in pane.getItems()).toBe false expect(item1.isDestroyed()).toBe true describe "when the item has no uri", -> @@ -170,7 +233,7 @@ describe "Pane", -> expect(atom.showSaveDialogSync).toHaveBeenCalled() expect(item1.saveAs).toHaveBeenCalledWith("/selected/path") - expect(item1 in pane.items).toBe false + expect(item1 in pane.getItems()).toBe false expect(item1.isDestroyed()).toBe true describe "if the [Don't Save] option is selected", -> @@ -179,7 +242,7 @@ describe "Pane", -> pane.destroyItem(item1) expect(item1.save).not.toHaveBeenCalled() - expect(item1 in pane.items).toBe false + expect(item1 in pane.getItems()).toBe false expect(item1.isDestroyed()).toBe true describe "if the [Cancel] option is selected", -> @@ -188,7 +251,7 @@ describe "Pane", -> pane.destroyItem(item1) expect(item1.save).not.toHaveBeenCalled() - expect(item1 in pane.items).toBe true + expect(item1 in pane.getItems()).toBe true expect(item1.isDestroyed()).toBe false describe "when the last item is destroyed", -> @@ -197,7 +260,7 @@ describe "Pane", -> 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() + expect(pane.getActiveItem()).toBeUndefined() expect(-> pane.saveActiveItem()).not.toThrow() expect(-> pane.saveActiveItemAs()).not.toThrow() @@ -210,10 +273,10 @@ describe "Pane", -> describe "::destroyActiveItem()", -> it "destroys the active item", -> pane = new Pane(items: [new Item("A"), new Item("B")]) - activeItem = pane.activeItem + activeItem = pane.getActiveItem() pane.destroyActiveItem() expect(activeItem.isDestroyed()).toBe true - expect(activeItem in pane.items).toBe false + expect(activeItem in pane.getItems()).toBe false it "does not throw an exception if there are no more items", -> pane = new Pane @@ -222,27 +285,40 @@ describe "Pane", -> describe "::destroyItems()", -> it "destroys all items", -> pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) - [item1, item2, item3] = pane.items + [item1, item2, item3] = pane.getItems() pane.destroyItems() expect(item1.isDestroyed()).toBe true expect(item2.isDestroyed()).toBe true expect(item3.isDestroyed()).toBe true - expect(pane.items).toEqual [] + expect(pane.getItems()).toEqual [] + + describe "::observeItems()", -> + it "invokes the observer with all current and future items", -> + pane = new Pane(items: [new Item, new Item]) + [item1, item2] = pane.getItems() + + observed = [] + pane.observeItems (item) -> observed.push(item) + + item3 = new Item + pane.addItem(item3) + + expect(observed).toEqual [item1, item2, item3] describe "when an item emits a destroyed event", -> it "removes it from the list of items", -> pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) - [item1, item2, item3] = pane.items - pane.items[1].destroy() - expect(pane.items).toEqual [item1, item3] + [item1, item2, item3] = pane.getItems() + pane.itemAtIndex(1).destroy() + expect(pane.getItems()).toEqual [item1, item3] describe "::destroyInactiveItems()", -> it "destroys all items but the active item", -> pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) - [item1, item2, item3] = pane.items + [item1, item2, item3] = pane.getItems() pane.activateItem(item2) pane.destroyInactiveItems() - expect(pane.items).toEqual [item2] + expect(pane.getItems()).toEqual [item2] describe "::saveActiveItem()", -> pane = null @@ -253,30 +329,30 @@ describe "Pane", -> describe "when the active item has a uri", -> beforeEach -> - pane.activeItem.uri = "test" + pane.getActiveItem().uri = "test" describe "when the active item has a save method", -> it "saves the current item", -> - pane.activeItem.save = jasmine.createSpy("save") + pane.getActiveItem().save = jasmine.createSpy("save") pane.saveActiveItem() - expect(pane.activeItem.save).toHaveBeenCalled() + expect(pane.getActiveItem().save).toHaveBeenCalled() describe "when the current item has no save method", -> it "does nothing", -> - expect(pane.activeItem.save).toBeUndefined() + expect(pane.getActiveItem().save).toBeUndefined() pane.saveActiveItem() describe "when the current item has no uri", -> describe "when the current item has a saveAs method", -> it "opens a save dialog and saves the current item as the selected path", -> - pane.activeItem.saveAs = jasmine.createSpy("saveAs") + pane.getActiveItem().saveAs = jasmine.createSpy("saveAs") pane.saveActiveItem() expect(atom.showSaveDialogSync).toHaveBeenCalled() - expect(pane.activeItem.saveAs).toHaveBeenCalledWith('/selected/path') + expect(pane.getActiveItem().saveAs).toHaveBeenCalledWith('/selected/path') describe "when the current item has no saveAs method", -> it "does nothing", -> - expect(pane.activeItem.saveAs).toBeUndefined() + expect(pane.getActiveItem().saveAs).toBeUndefined() pane.saveActiveItem() expect(atom.showSaveDialogSync).not.toHaveBeenCalled() @@ -289,22 +365,22 @@ describe "Pane", -> describe "when the current item has a saveAs method", -> it "opens the save dialog and calls saveAs on the item with the selected path", -> - pane.activeItem.path = __filename - pane.activeItem.saveAs = jasmine.createSpy("saveAs") + pane.getActiveItem().path = __filename + pane.getActiveItem().saveAs = jasmine.createSpy("saveAs") pane.saveActiveItemAs() expect(atom.showSaveDialogSync).toHaveBeenCalledWith(__filename) - expect(pane.activeItem.saveAs).toHaveBeenCalledWith('/selected/path') + expect(pane.getActiveItem().saveAs).toHaveBeenCalledWith('/selected/path') describe "when the current item does not have a saveAs method", -> it "does nothing", -> - expect(pane.activeItem.saveAs).toBeUndefined() + expect(pane.getActiveItem().saveAs).toBeUndefined() pane.saveActiveItemAs() expect(atom.showSaveDialogSync).not.toHaveBeenCalled() describe "::itemForUri(uri)", -> it "returns the item for which a call to .getUri() returns the given uri", -> pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D")]) - [item1, item2, item3] = pane.items + [item1, item2, item3] = pane.getItems() item1.uri = "a" item2.uri = "b" expect(pane.itemForUri("a")).toBe item1 @@ -312,24 +388,32 @@ describe "Pane", -> expect(pane.itemForUri("bogus")).toBeUndefined() describe "::moveItem(item, index)", -> - it "moves the item to the given index and emits an 'item-moved' event with the item and its new index", -> - pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D")]) - [item1, item2, item3, item4] = pane.items - pane.on 'item-moved', itemMovedHandler = jasmine.createSpy("itemMovedHandler") + [pane, item1, item2, item3, item4] = [] + beforeEach -> + pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D")]) + [item1, item2, item3, item4] = pane.getItems() + + it "moves the item to the given index and invokes ::onDidMoveItem observers", -> pane.moveItem(item1, 2) expect(pane.getItems()).toEqual [item2, item3, item1, item4] - expect(itemMovedHandler).toHaveBeenCalledWith(item1, 2) - itemMovedHandler.reset() pane.moveItem(item2, 3) expect(pane.getItems()).toEqual [item3, item1, item4, item2] - expect(itemMovedHandler).toHaveBeenCalledWith(item2, 3) - itemMovedHandler.reset() pane.moveItem(item2, 1) expect(pane.getItems()).toEqual [item3, item2, item1, item4] - expect(itemMovedHandler).toHaveBeenCalledWith(item2, 1) + + it "invokes ::onDidMoveItem() observers", -> + events = [] + pane.onDidMoveItem (event) -> events.push(event) + + pane.moveItem(item1, 2) + pane.moveItem(item2, 3) + expect(events).toEqual [ + {item: item1, oldIndex: 0, newIndex: 2} + {item: item2, oldIndex: 0, newIndex: 3} + ] describe "::moveItemToPane(item, pane, index)", -> [container, pane1, pane2] = [] @@ -339,13 +423,20 @@ describe "Pane", -> pane1 = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) container = new PaneContainer(root: pane1) pane2 = pane1.splitRight(items: [new Item("D"), new Item("E")]) - [item1, item2, item3] = pane1.items - [item4, item5] = pane2.items + [item1, item2, item3] = pane1.getItems() + [item4, item5] = pane2.getItems() it "moves the item to the given pane at the given index", -> pane1.moveItemToPane(item2, pane2, 1) - expect(pane1.items).toEqual [item1, item3] - expect(pane2.items).toEqual [item4, item2, item5] + expect(pane1.getItems()).toEqual [item1, item3] + expect(pane2.getItems()).toEqual [item4, item2, item5] + + it "invokes ::onDidRemoveItem() observers", -> + events = [] + pane1.onDidRemoveItem (event) -> events.push(event) + pane1.moveItemToPane(item2, pane2, 1) + + expect(events).toEqual [{item: item2, index: 1, destroyed: false}] describe "when the moved item the last item in the source pane", -> beforeEach -> @@ -455,7 +546,7 @@ describe "Pane", -> pane2 = pane1.splitRight() it "destroys the pane's destroyable items", -> - [item1, item2] = pane1.items + [item1, item2] = pane1.getItems() pane1.destroy() expect(item1.isDestroyed()).toBe true expect(item2.isDestroyed()).toBe true @@ -493,12 +584,12 @@ describe "Pane", -> it "can serialize and deserialize the pane and all its items", -> newPane = pane.testSerialization() - expect(newPane.items).toEqual pane.items + expect(newPane.getItems()).toEqual pane.getItems() it "restores the active item on deserialization", -> pane.activateItemAtIndex(1) newPane = pane.testSerialization() - expect(newPane.activeItem).toEqual newPane.items[1] + expect(newPane.getActiveItem()).toEqual newPane.itemAtIndex(1) it "does not include items that cannot be deserialized", -> spyOn(console, 'warn') @@ -506,8 +597,8 @@ describe "Pane", -> pane.activateItem(unserializable) newPane = pane.testSerialization() - expect(newPane.activeItem).toEqual pane.items[0] - expect(newPane.items.length).toBe pane.items.length - 1 + expect(newPane.getActiveItem()).toEqual pane.itemAtIndex(0) + expect(newPane.getItems().length).toBe pane.getItems().length - 1 it "includes the pane's focus state in the serialized state", -> pane.focus() diff --git a/spec/pane-view-spec.coffee b/spec/pane-view-spec.coffee index 9f73884b5..4862e639b 100644 --- a/spec/pane-view-spec.coffee +++ b/spec/pane-view-spec.coffee @@ -37,7 +37,7 @@ describe "PaneView", -> describe "when the active pane item changes", -> it "hides all item views except the active one", -> - expect(pane.activeItem).toBe view1 + expect(pane.getActiveItem()).toBe view1 expect(view1.css('display')).not.toBe 'none' pane.activateItem(view2) @@ -48,7 +48,7 @@ describe "PaneView", -> itemChangedHandler = jasmine.createSpy("itemChangedHandler") container.on 'pane:active-item-changed', itemChangedHandler - expect(pane.activeItem).toBe view1 + expect(pane.getActiveItem()).toBe view1 paneModel.activateItem(view2) paneModel.activateItem(view2) @@ -149,7 +149,7 @@ describe "PaneView", -> activeItemTitleChangedHandler = jasmine.createSpy("activeItemTitleChangedHandler") pane.on 'pane:active-item-title-changed', activeItemTitleChangedHandler - expect(pane.activeItem).toBe view1 + expect(pane.getActiveItem()).toBe view1 view2.trigger 'title-changed' expect(activeItemTitleChangedHandler).not.toHaveBeenCalled() @@ -246,7 +246,7 @@ describe "PaneView", -> it "transfers focus to the active view", -> focusHandler = jasmine.createSpy("focusHandler") - pane.activeItem.on 'focus', focusHandler + pane.getActiveItem().on 'focus', focusHandler pane.focus() expect(focusHandler).toHaveBeenCalled() diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index a75896582..88bcdf49c 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -8,8 +8,12 @@ describe "Workspace", -> atom.workspace = workspace = new Workspace describe "::open(uri, options)", -> + openEvents = null + beforeEach -> - spyOn(workspace.activePane, 'activate').andCallThrough() + openEvents = [] + workspace.onDidOpen (event) -> openEvents.push(event) + spyOn(workspace.getActivePane(), 'activate').andCallThrough() describe "when the 'searchAllPanes' option is false (default)", -> describe "when called without a uri", -> @@ -21,18 +25,21 @@ describe "Workspace", -> runs -> expect(editor1.getPath()).toBeUndefined() - expect(workspace.activePane.items).toEqual [editor1] - expect(workspace.activePaneItem).toBe editor1 - expect(workspace.activePane.activate).toHaveBeenCalled() + expect(workspace.getActivePane().items).toEqual [editor1] + expect(workspace.getActivePaneItem()).toBe editor1 + expect(workspace.getActivePane().activate).toHaveBeenCalled() + expect(openEvents).toEqual [{uri: undefined, pane: workspace.getActivePane(), item: editor1, index: 0}] + openEvents = [] waitsForPromise -> workspace.open().then (editor) -> editor2 = editor runs -> expect(editor2.getPath()).toBeUndefined() - expect(workspace.activePane.items).toEqual [editor1, editor2] - expect(workspace.activePaneItem).toBe editor2 - expect(workspace.activePane.activate).toHaveBeenCalled() + expect(workspace.getActivePane().items).toEqual [editor1, editor2] + expect(workspace.getActivePaneItem()).toBe editor2 + expect(workspace.getActivePane().activate).toHaveBeenCalled() + expect(openEvents).toEqual [{uri: undefined, pane: workspace.getActivePane(), item: editor2, index: 1}] describe "when called with a uri", -> describe "when the active pane already has an editor for the given uri", -> @@ -51,8 +58,29 @@ describe "Workspace", -> runs -> expect(editor).toBe editor1 - expect(workspace.activePaneItem).toBe editor - expect(workspace.activePane.activate).toHaveBeenCalled() + expect(workspace.getActivePaneItem()).toBe editor + expect(workspace.getActivePane().activate).toHaveBeenCalled() + + expect(openEvents).toEqual [ + { + uri: atom.project.resolve('a') + item: editor1 + pane: atom.workspace.getActivePane() + index: 0 + } + { + uri: atom.project.resolve('b') + item: editor2 + pane: atom.workspace.getActivePane() + index: 1 + } + { + uri: atom.project.resolve('a') + item: editor1 + pane: atom.workspace.getActivePane() + index: 0 + } + ] 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", -> @@ -62,9 +90,9 @@ describe "Workspace", -> runs -> expect(editor.getUri()).toBe atom.project.resolve('a') - expect(workspace.activePaneItem).toBe editor - expect(workspace.activePane.items).toEqual [editor] - expect(workspace.activePane.activate).toHaveBeenCalled() + expect(workspace.getActivePaneItem()).toBe editor + expect(workspace.getActivePane().items).toEqual [editor] + expect(workspace.getActivePane().activate).toHaveBeenCalled() describe "when the 'searchAllPanes' option is true", -> describe "when an editor for the given uri is already open on an inactive pane", -> @@ -83,14 +111,14 @@ describe "Workspace", -> workspace.open('b').then (o) -> editor2 = o runs -> - expect(workspace.activePaneItem).toBe editor2 + expect(workspace.getActivePaneItem()).toBe editor2 waitsForPromise -> workspace.open('a', searchAllPanes: true) runs -> - expect(workspace.activePane).toBe pane1 - expect(workspace.activePaneItem).toBe editor1 + expect(workspace.getActivePane()).toBe pane1 + expect(workspace.getActivePaneItem()).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", -> @@ -99,21 +127,21 @@ describe "Workspace", -> workspace.open('a', searchAllPanes: true).then (o) -> editor = o runs -> - expect(workspace.activePaneItem).toBe editor + expect(workspace.getActivePaneItem()).toBe editor describe "when the 'split' option is set", -> describe "when the 'split' option is 'left'", -> it "opens the editor in the leftmost pane of the current pane axis", -> - pane1 = workspace.activePane + pane1 = workspace.getActivePane() pane2 = pane1.splitRight() - expect(workspace.activePane).toBe pane2 + expect(workspace.getActivePane()).toBe pane2 editor = null waitsForPromise -> workspace.open('a', split: 'left').then (o) -> editor = o runs -> - expect(workspace.activePane).toBe pane1 + expect(workspace.getActivePane()).toBe pane1 expect(pane1.items).toEqual [editor] expect(pane2.items).toEqual [] @@ -123,37 +151,37 @@ describe "Workspace", -> workspace.open('a', split: 'left').then (o) -> editor = o runs -> - expect(workspace.activePane).toBe pane1 + expect(workspace.getActivePane()).toBe pane1 expect(pane1.items).toEqual [editor] expect(pane2.items).toEqual [] describe "when a pane axis is the leftmost sibling of the current pane", -> it "opens the new item in the current pane", -> editor = null - pane1 = workspace.activePane + pane1 = workspace.getActivePane() pane2 = pane1.splitLeft() pane3 = pane2.splitDown() pane1.activate() - expect(workspace.activePane).toBe pane1 + expect(workspace.getActivePane()).toBe pane1 waitsForPromise -> workspace.open('a', split: 'left').then (o) -> editor = o runs -> - expect(workspace.activePane).toBe pane1 + expect(workspace.getActivePane()).toBe pane1 expect(pane1.items).toEqual [editor] describe "when the 'split' option is 'right'", -> it "opens the editor in the rightmost pane of the current pane axis", -> editor = null - pane1 = workspace.activePane + pane1 = workspace.getActivePane() pane2 = null waitsForPromise -> workspace.open('a', split: 'right').then (o) -> editor = o runs -> pane2 = workspace.getPanes().filter((p) -> p != pane1)[0] - expect(workspace.activePane).toBe pane2 + expect(workspace.getActivePane()).toBe pane2 expect(pane1.items).toEqual [] expect(pane2.items).toEqual [editor] @@ -163,18 +191,18 @@ describe "Workspace", -> workspace.open('a', split: 'right').then (o) -> editor = o runs -> - expect(workspace.activePane).toBe pane2 + expect(workspace.getActivePane()).toBe pane2 expect(pane1.items).toEqual [] expect(pane2.items).toEqual [editor] describe "when a pane axis is the rightmost sibling of the current pane", -> it "opens the new item in a new pane split to the right of the current pane", -> editor = null - pane1 = workspace.activePane + pane1 = workspace.getActivePane() pane2 = pane1.splitRight() pane3 = pane2.splitDown() pane1.activate() - expect(workspace.activePane).toBe pane1 + expect(workspace.getActivePane()).toBe pane1 pane4 = null waitsForPromise -> @@ -182,7 +210,7 @@ describe "Workspace", -> runs -> pane4 = workspace.getPanes().filter((p) -> p != pane1)[0] - expect(workspace.activePane).toBe pane4 + expect(workspace.getActivePane()).toBe pane4 expect(pane4.items).toEqual [editor] expect(workspace.paneContainer.root.children[0]).toBe pane1 expect(workspace.paneContainer.root.children[1]).toBe pane4 @@ -203,21 +231,21 @@ describe "Workspace", -> workspace.open("bar://baz").then (item) -> expect(item).toEqual { bar: "bar://baz" } - it "emits an 'editor-created' event", -> + it "notifies ::onDidAddTextEditor observers", -> absolutePath = require.resolve('./fixtures/dir/a') newEditorHandler = jasmine.createSpy('newEditorHandler') - workspace.on 'editor-created', newEditorHandler + workspace.onDidAddTextEditor newEditorHandler editor = null waitsForPromise -> workspace.open(absolutePath).then (e) -> editor = e runs -> - expect(newEditorHandler).toHaveBeenCalledWith editor + expect(newEditorHandler.argsForCall[0][0].textEditor).toBe editor describe "::reopenItem()", -> it "opens the uri associated with the last closed pane that isn't currently open", -> - pane = workspace.activePane + pane = workspace.getActivePane() waitsForPromise -> workspace.open('a').then -> workspace.open('b').then -> @@ -226,44 +254,44 @@ describe "Workspace", -> runs -> # does not reopen items with no uri - expect(workspace.activePaneItem.getUri()).toBeUndefined() + expect(workspace.getActivePaneItem().getUri()).toBeUndefined() pane.destroyActiveItem() waitsForPromise -> workspace.reopenItem() runs -> - expect(workspace.activePaneItem.getUri()).not.toBeUndefined() + expect(workspace.getActivePaneItem().getUri()).not.toBeUndefined() # destroy all items - expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('file1') + expect(workspace.getActivePaneItem().getUri()).toBe atom.project.resolve('file1') pane.destroyActiveItem() - expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('b') + expect(workspace.getActivePaneItem().getUri()).toBe atom.project.resolve('b') pane.destroyActiveItem() - expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('a') + expect(workspace.getActivePaneItem().getUri()).toBe atom.project.resolve('a') pane.destroyActiveItem() # reopens items with uris - expect(workspace.activePaneItem).toBeUndefined() + expect(workspace.getActivePaneItem()).toBeUndefined() waitsForPromise -> workspace.reopenItem() runs -> - expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('a') + expect(workspace.getActivePaneItem().getUri()).toBe atom.project.resolve('a') # does not reopen items that are already open waitsForPromise -> workspace.open('b') runs -> - expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('b') + expect(workspace.getActivePaneItem().getUri()).toBe atom.project.resolve('b') waitsForPromise -> workspace.reopenItem() runs -> - expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('file1') + expect(workspace.getActivePaneItem().getUri()).toBe atom.project.resolve('file1') describe "::increase/decreaseFontSize()", -> it "increases/decreases the font size without going below 1", -> @@ -282,7 +310,22 @@ describe "Workspace", -> describe "::openLicense()", -> it "opens the license as plain-text in a buffer", -> waitsForPromise -> workspace.openLicense() - runs -> expect(workspace.activePaneItem.getText()).toMatch /Copyright/ + runs -> expect(workspace.getActivePaneItem().getText()).toMatch /Copyright/ + + describe "::observeTextEditors()", -> + it "invokes the observer with current and future text editors", -> + observed = [] + + waitsForPromise -> workspace.open() + waitsForPromise -> workspace.open() + waitsForPromise -> workspace.openLicense() + + runs -> + workspace.observeTextEditors (editor) -> observed.push(editor) + + waitsForPromise -> workspace.open() + + expect(observed).toEqual workspace.getTextEditors() describe "when an editor is destroyed", -> it "removes the editor", -> @@ -292,23 +335,9 @@ describe "Workspace", -> workspace.open("a").then (e) -> editor = e runs -> - expect(workspace.getEditors()).toHaveLength 1 + expect(workspace.getTextEditors()).toHaveLength 1 editor.destroy() - expect(workspace.getEditors()).toHaveLength 0 - - describe "when an editor is copied", -> - it "emits an 'editor-created' event", -> - editor = null - handler = jasmine.createSpy('editorCreatedHandler') - workspace.on 'editor-created', handler - - waitsForPromise -> - workspace.open("a").then (o) -> editor = o - - runs -> - expect(handler.callCount).toBe 1 - editorCopy = editor.copy() - expect(handler.callCount).toBe 2 + expect(workspace.getTextEditors()).toHaveLength 0 it "stores the active grammars used by all the open editors", -> waitsForPromise -> diff --git a/src/pane-axis-view.coffee b/src/pane-axis-view.coffee index 3039600c0..0018ee6b1 100644 --- a/src/pane-axis-view.coffee +++ b/src/pane-axis-view.coffee @@ -1,11 +1,17 @@ +{CompositeDisposable} = require 'event-kit' {View} = require './space-pen-extensions' PaneView = null module.exports = class PaneAxisView extends View initialize: (@model) -> - @onChildAdded(child) for child in @model.children - @subscribe @model.children, 'changed', @onChildrenChanged + @subscriptions = new CompositeDisposable + + @onChildAdded({child, index}) for child, index in @model.getChildren() + + @subscriptions.add @model.onDidAddChild(@onChildAdded) + @subscriptions.add @model.onDidRemoveChild(@onChildRemoved) + @subscriptions.add @model.onDidReplaceChild(@onChildReplaced) afterAttach: -> @container = @closest('.panes').view() @@ -14,19 +20,22 @@ class PaneAxisView extends View viewClass = model.getViewClass() model._view ?= new viewClass(model) - onChildrenChanged: ({index, removedValues, insertedValues}) => + onChildReplaced: ({index, oldChild, newChild}) => focusedElement = document.activeElement if @hasFocus() - @onChildRemoved(child, index) for child in removedValues - @onChildAdded(child, index + i) for child, i in insertedValues + @onChildRemoved({child: oldChild, index}) + @onChildAdded({child: newChild, index}) focusedElement?.focus() if document.activeElement is document.body - onChildAdded: (child, index) => + onChildAdded: ({child, index}) => view = @viewForModel(child) @insertAt(index, view) - onChildRemoved: (child) => + onChildRemoved: ({child}) => view = @viewForModel(child) view.detach() PaneView ?= require './pane-view' if view instanceof PaneView and view.model.isDestroyed() @container?.trigger 'pane:removed', [view] + + beforeRemove: -> + @subscriptions.dispose() diff --git a/src/pane-axis.coffee b/src/pane-axis.coffee index 39657fcf7..9ef95bc8c 100644 --- a/src/pane-axis.coffee +++ b/src/pane-axis.coffee @@ -1,4 +1,5 @@ -{Model, Sequence} = require 'theorist' +{Model} = require 'theorist' +{Emitter, CompositeDisposable} = require 'event-kit' {flatten} = require 'underscore-plus' Serializable = require 'serializable' @@ -10,18 +11,17 @@ class PaneAxis extends Model atom.deserializers.add(this) Serializable.includeInto(this) + parent: null + container: null + orientation: null + constructor: ({@container, @orientation, children}) -> - @children = Sequence.fromArray(children ? []) - - @subscribe @children.onEach (child) => - child.parent = this - child.container = @container - @subscribe child, 'destroyed', => @removeChild(child) - - @subscribe @children.onRemoval (child) => @unsubscribe(child) - - @when @children.$length.becomesLessThan(2), 'reparentLastChild' - @when @children.$length.becomesLessThan(1), 'destroy' + @emitter = new Emitter + @subscriptionsByChild = new WeakMap + @subscriptions = new CompositeDisposable + @children = [] + if children? + @addChild(child) for child in children deserializeParams: (params) -> {container} = params @@ -32,35 +32,93 @@ class PaneAxis extends Model children: @children.map (child) -> child.serialize() orientation: @orientation + getParent: -> @parent + + setParent: (@parent) -> @parent + + getContainer: -> @container + + setContainer: (@container) -> @container + getViewClass: -> if @orientation is 'vertical' PaneColumnView ?= require './pane-column-view' else PaneRowView ?= require './pane-row-view' + getChildren: -> @children.slice() + getPanes: -> flatten(@children.map (child) -> child.getPanes()) - addChild: (child, index=@children.length) -> - @children.splice(index, 0, child) + getItems: -> + flatten(@children.map (child) -> child.getItems()) - removeChild: (child) -> + onDidAddChild: (fn) -> + @emitter.on 'did-add-child', fn + + onDidRemoveChild: (fn) -> + @emitter.on 'did-remove-child', fn + + onDidReplaceChild: (fn) -> + @emitter.on 'did-replace-child', fn + + onDidDestroy: (fn) -> + @emitter.on 'did-destroy', fn + + addChild: (child, index=@children.length) -> + child.setParent(this) + child.setContainer(@container) + + @subscribeToChild(child) + + @children.splice(index, 0, child) + @emitter.emit 'did-add-child', {child, index} + + removeChild: (child, replacing=false) -> index = @children.indexOf(child) throw new Error("Removing non-existent child") if index is -1 + + @unsubscribeFromChild(child) + @children.splice(index, 1) + @emitter.emit 'did-remove-child', {child, index} + @reparentLastChild() if not replacing and @children.length < 2 replaceChild: (oldChild, newChild) -> + @unsubscribeFromChild(oldChild) + @subscribeToChild(newChild) + + newChild.setParent(this) + newChild.setContainer(@container) + index = @children.indexOf(oldChild) - throw new Error("Replacing non-existent child") if index is -1 @children.splice(index, 1, newChild) + @emitter.emit 'did-replace-child', {oldChild, newChild, index} insertChildBefore: (currentChild, newChild) -> index = @children.indexOf(currentChild) - @children.splice(index, 0, newChild) + @addChild(newChild, index) insertChildAfter: (currentChild, newChild) -> index = @children.indexOf(currentChild) - @children.splice(index + 1, 0, newChild) + @addChild(newChild, index + 1) reparentLastChild: -> @parent.replaceChild(this, @children[0]) + @destroy() + + subscribeToChild: (child) -> + subscription = child.onDidDestroy => @removeChild(child) + @subscriptionsByChild.set(child, subscription) + @subscriptions.add(subscription) + + unsubscribeFromChild: (child) -> + subscription = @subscriptionsByChild.get(child) + @subscriptions.remove(subscription) + subscription.dispose() + + destroyed: -> + @subscriptions.dispose() + @emitter.emit 'did-destroy' + @emitter.dispose() diff --git a/src/pane-container-view.coffee b/src/pane-container-view.coffee index 77be49420..09a890d74 100644 --- a/src/pane-container-view.coffee +++ b/src/pane-container-view.coffee @@ -1,5 +1,6 @@ {deprecate} = require 'grim' Delegator = require 'delegato' +{CompositeDisposable} = require 'event-kit' {$, View} = require './space-pen-extensions' PaneView = require './pane-view' PaneContainer = require './pane-container' @@ -15,13 +16,15 @@ class PaneContainerView extends View @div class: 'panes' initialize: (params) -> + @subscriptions = new CompositeDisposable + if params instanceof PaneContainer @model = params else @model = new PaneContainer({root: params?.root?.model}) - @subscribe @model.$root, @onRootChanged - @subscribe @model.$activePaneItem.changes, @onActivePaneItemChanged + @subscriptions.add @model.observeRoot(@onRootChanged) + @subscriptions.add @model.onDidChangeActivePaneItem(@onActivePaneItemChanged) viewForModel: (model) -> if model? @@ -88,7 +91,7 @@ class PaneContainerView extends View @viewForModel(@model.activePane) getActivePaneItem: -> - @model.activePaneItem + @model.getActivePaneItem() getActiveView: -> @getActivePaneView()?.activeView @@ -153,3 +156,6 @@ class PaneContainerView extends View getPanes: -> deprecate("Use PaneContainerView::getPaneViews() instead") @getPaneViews() + + beforeRemove: -> + @subscriptions.dispose() diff --git a/src/pane-container.coffee b/src/pane-container.coffee index 07083263a..14eeae077 100644 --- a/src/pane-container.coffee +++ b/src/pane-container.coffee @@ -1,5 +1,6 @@ -{find} = require 'underscore-plus' +{find, flatten} = require 'underscore-plus' {Model} = require 'theorist' +{Emitter, CompositeDisposable} = require 'event-kit' Serializable = require 'serializable' Pane = require './pane' @@ -11,10 +12,9 @@ class PaneContainer extends Model @version: 1 @properties - root: -> new Pane activePane: null - previousRoot: null + root: null @behavior 'activePaneItem', -> @$activePane @@ -23,9 +23,16 @@ class PaneContainer extends Model constructor: (params) -> super - @subscribe @$root, @onRootChanged + + @emitter = new Emitter + @subscriptions = new CompositeDisposable + + @setRoot(params?.root ? new Pane) @destroyEmptyPanes() if params?.destroyEmptyPanes + @monitorActivePaneItem() + @monitorPaneItems() + deserializeParams: (params) -> params.root = atom.deserializers.deserialize(params.root, container: this) params.destroyEmptyPanes = atom.config.get('core.destroyEmptyPanes') @@ -36,16 +43,75 @@ class PaneContainer extends Model root: @root?.serialize() activePaneId: @activePane.id + onDidChangeRoot: (fn) -> + @emitter.on 'did-change-root', fn + + observeRoot: (fn) -> + fn(@getRoot()) + @onDidChangeRoot(fn) + + onDidAddPane: (fn) -> + @emitter.on 'did-add-pane', fn + + observePanes: (fn) -> + fn(pane) for pane in @getPanes() + @onDidAddPane ({pane}) -> fn(pane) + + onDidChangeActivePane: (fn) -> + @emitter.on 'did-change-active-pane', fn + + observeActivePane: (fn) -> + fn(@getActivePane()) + @onDidChangeActivePane(fn) + + onDidAddPaneItem: (fn) -> + @emitter.on 'did-add-pane-item', fn + + observePaneItems: (fn) -> + fn(item) for item in @getPaneItems() + @onDidAddPaneItem ({item}) -> fn(item) + + onDidChangeActivePaneItem: (fn) -> + @emitter.on 'did-change-active-pane-item', fn + + observeActivePaneItem: (fn) -> + fn(@getActivePaneItem()) + @onDidChangeActivePaneItem(fn) + + onDidDestroyPaneItem: (fn) -> + @emitter.on 'did-destroy-pane-item', fn + + getRoot: -> @root + + setRoot: (@root) -> + @root.setParent(this) + @root.setContainer(this) + @emitter.emit 'did-change-root', @root + if not @getActivePane()? and @root instanceof Pane + @setActivePane(@root) + replaceChild: (oldChild, newChild) -> throw new Error("Replacing non-existent child") if oldChild isnt @root - @root = newChild + @setRoot(newChild) getPanes: -> - @root?.getPanes() ? [] + @getRoot().getPanes() + + getPaneItems: -> + @getRoot().getItems() getActivePane: -> @activePane + setActivePane: (activePane) -> + if activePane isnt @activePane + @activePane = activePane + @emitter.emit 'did-change-active-pane', @activePane + @activePane + + getActivePaneItem: -> + @getActivePane().getActiveItem() + paneForUri: (uri) -> find @getPanes(), (pane) -> pane.itemForUri(uri)? @@ -73,26 +139,37 @@ class PaneContainer extends Model else false - onRootChanged: (root) => - @unsubscribe(@previousRoot) if @previousRoot? - @previousRoot = root - - unless root? - @activePane = null - return - - root.parent = this - root.container = this - - - @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 + paneItemDestroyed: (item) -> + @emitter.emit 'did-destroy-pane-item', item + + didAddPane: (pane) -> + @emitter.emit 'did-add-pane', pane # Called by Model superclass when destroyed destroyed: -> pane.destroy() for pane in @getPanes() + @subscriptions.dispose() + @emitter.dispose() + + monitorActivePaneItem: -> + childSubscription = null + @subscriptions.add @observeActivePane (activePane) => + if childSubscription? + @subscriptions.remove(childSubscription) + childSubscription.dispose() + + childSubscription = activePane.observeActiveItem (activeItem) => + @emitter.emit 'did-change-active-pane-item', activeItem + + @subscriptions.add(childSubscription) + + monitorPaneItems: -> + @subscriptions.add @observePanes (pane) => + for item, index in pane.getItems() + @emitter.emit 'did-add-pane-item', {item, pane, index} + + pane.onDidAddItem ({item, index}) => + @emitter.emit 'did-add-pane-item', {item, pane, index} diff --git a/src/pane-view.coffee b/src/pane-view.coffee index 0d641582e..ac79faaba 100644 --- a/src/pane-view.coffee +++ b/src/pane-view.coffee @@ -1,6 +1,7 @@ {$, View} = require './space-pen-extensions' Delegator = require 'delegato' {deprecate} = require 'grim' +{CompositeDisposable} = require 'event-kit' PropertyAccessors = require 'property-accessors' Pane = require './pane' @@ -33,6 +34,8 @@ class PaneView extends View previousActiveItem: null initialize: (args...) -> + @subscriptions = new CompositeDisposable + if args[0] instanceof Pane @model = args[0] else @@ -44,13 +47,13 @@ class PaneView extends View @handleEvents() handleEvents: -> - @subscribe @model.$activeItem, @onActiveItemChanged - @subscribe @model, 'item-added', @onItemAdded - @subscribe @model, 'item-removed', @onItemRemoved - @subscribe @model, 'item-moved', @onItemMoved - @subscribe @model, 'before-item-destroyed', @onBeforeItemDestroyed - @subscribe @model, 'activated', @onActivated - @subscribe @model.$active, @onActiveStatusChanged + @subscriptions.add @model.observeActiveItem(@onActiveItemChanged) + @subscriptions.add @model.onDidAddItem(@onItemAdded) + @subscriptions.add @model.onDidRemoveItem(@onItemRemoved) + @subscriptions.add @model.onDidMoveItem(@onItemMoved) + @subscriptions.add @model.onWillDestroyItem(@onBeforeItemDestroyed) + @subscriptions.add @model.onDidActivate(@onActivated) + @subscriptions.add @model.observeActive(@onActiveStatusChanged) @subscribe this, 'focusin', => @model.focus() @subscribe this, 'focusout', => @model.blur() @@ -160,10 +163,10 @@ class PaneView extends View @trigger 'pane:active-item-changed', [item] - onItemAdded: (item, index) => + onItemAdded: ({item, index}) => @trigger 'pane:item-added', [item, index] - onItemRemoved: (item, index, destroyed) => + onItemRemoved: ({item, index, destroyed}) => if item instanceof $ viewToRemove = item else if viewToRemove = @viewsByItem.get(item) @@ -177,7 +180,7 @@ class PaneView extends View @trigger 'pane:item-removed', [item, index] - onItemMoved: (item, newIndex) => + onItemMoved: ({item, newIndex}) => @trigger 'pane:item-moved', [item, newIndex] onBeforeItemDestroyed: (item) => @@ -219,6 +222,7 @@ class PaneView extends View @closest('.panes').view() beforeRemove: -> + @subscriptions.dispose() @model.destroy() unless @model.isDestroyed() remove: (selector, keepData) -> diff --git a/src/pane.coffee b/src/pane.coffee index 678e2156e..d56ae007b 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -1,47 +1,16 @@ {find, compact, extend, last} = require 'underscore-plus' -{Model, Sequence} = require 'theorist' +{Model} = require 'theorist' +{Emitter} = require 'event-kit' Serializable = require 'serializable' +Grim = require 'grim' PaneAxis = require './pane-axis' Editor = require './editor' PaneView = null -# Extended: A container for multiple items, one of which is *active* at a given -# time. With the default packages, a tab is displayed for each item and the -# active item's view is displayed. -# -# ## Events -# ### activated -# -# Extended: Emit when this pane as been activated -# -# ### item-added -# -# Extended: Emit when an item was added to the pane -# -# * `item` The pane item that has been added -# * `index` {Number} Index in the pane -# -# ### before-item-destroyed -# -# Extended: Emit before the item is destroyed -# -# * `item` The pane item that will be destoryed -# -# ### item-removed -# -# Extended: Emit when the item was removed from the pane -# -# * `item` The pane item that was removed -# * `index` {Number} Index in the pane -# * `destroying` {Boolean} `true` when the item is being removed because of destruction -# -# ### item-moved -# -# Extended: Emit when an item was moved within the pane -# -# * `item` The pane item that was moved -# * `newIndex` {Number} Index that the item was moved to -# +# 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. +# The view corresponding to the active item is displayed in the interface. In +# the default configuration, tabs are also displayed for each item. module.exports = class Pane extends Model atom.deserializers.add(this) @@ -64,15 +33,11 @@ class Pane extends Model constructor: (params) -> super - @items = Sequence.fromArray(compact(params?.items ? [])) - @activeItem ?= @items[0] + @emitter = new Emitter + @items = [] - @subscribe @items.onEach (item) => - if typeof item.on is 'function' - @subscribe item, 'destroyed', => @removeItem(item, true) - - @subscribe @items.onRemoval (item, index) => - @unsubscribe item if typeof item.on is 'function' + @addItems(compact(params?.items ? [])) + @setActiveItem(@items[0]) unless @getActiveItem()? # Called by the Serializable mixin during serialization. serializeParams: -> @@ -91,7 +56,174 @@ class Pane extends Model # Called by the view layer to construct a view for this model. getViewClass: -> PaneView ?= require './pane-view' - isActive: -> @active + getParent: -> @parent + + setParent: (@parent) -> @parent + + getContainer: -> @container + + setContainer: (container) -> + container.didAddPane({pane: this}) unless container is @container + @container = container + + ### + Section: Event Subscription + ### + + # Public: Invoke the given callback when the pane is activated. + # + # The given callback will be invoked whenever {::activate} is called on the + # pane, even if it is already active at the time. + # + # * `callback` {Function} to be called when the pane is activated. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidActivate: (callback) -> + @emitter.on 'did-activate', callback + + # Public: Invoke the given callback when the pane is destroyed. + # + # * `callback` {Function} to be called when the pane is destroyed. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy: (callback) -> + @emitter.on 'did-destroy', callback + + # Public: Invoke the given callback when the value of the {::isActive} + # property changes. + # + # * `callback` {Function} to be called when the value of the {::isActive} + # property changes. + # * `active` {Boolean} indicating whether the pane is active. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActive: (callback) -> + @container.onDidChangeActivePane (activePane) => + callback(this is activePane) + + # Public: Invoke the given callback with the current and future values of the + # {::isActive} property. + # + # * `callback` {Function} to be called with the current and future values of + # the {::isActive} property. + # * `active` {Boolean} indicating whether the pane is active. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActive: (callback) -> + callback(@isActive()) + @onDidChangeActive(callback) + + # Public: Invoke the given callback when an item is added to the pane. + # + # * `callback` {Function} to be called with when items are added. + # * `event` {Object} with the following keys: + # * `item` The added pane item. + # * `index` {Number} indicating where the item is located. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddItem: (callback) -> + @emitter.on 'did-add-item', callback + + # Public: Invoke the given callback when an item is removed from the pane. + # + # * `callback` {Function} to be called with when items are removed. + # * `event` {Object} with the following keys: + # * `item` The removed pane item. + # * `index` {Number} indicating where the item was located. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRemoveItem: (callback) -> + @emitter.on 'did-remove-item', callback + + # Public: Invoke the given callback when an item is moved within the pane. + # + # * `callback` {Function} to be called with when items are moved. + # * `event` {Object} with the following keys: + # * `item` The removed pane item. + # * `oldIndex` {Number} indicating where the item was located. + # * `newIndex` {Number} indicating where the item is now located. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidMoveItem: (callback) -> + @emitter.on 'did-move-item', callback + + # Public: Invoke the given callback with all current and future items. + # + # * `callback` {Function} to be called with current and future items. + # * `item` An item that is present in {::getItems} at the time of + # subscription or that is added at some later time. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeItems: (callback) -> + callback(item) for item in @getItems() + @onDidAddItem ({item}) -> callback(item) + + # Public: Invoke the given callback when the value of {::getActiveItem} + # changes. + # + # * `callback` {Function} to be called with when the active item changes. + # * `activeItem` The current active item. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActiveItem: (callback) -> + @emitter.on 'did-change-active-item', callback + + # Public: Invoke the given callback with the current and future values of + # {::getActiveItem}. + # + # * `callback` {Function} to be called with the current and future active + # items. + # * `activeItem` The current active item. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActiveItem: (callback) -> + callback(@getActiveItem()) + @onDidChangeActiveItem(callback) + + # Public: Invoke the given callback before items are destroyed. + # + # * `callback` {Function} to be called before items are destroyed. + # * `event` {Object} with the following keys: + # * `item` The item that will be destroyed. + # * `index` The location of the item. + # + # Returns a {Disposable} on which `.dispose()` can be called to + # unsubscribe. + onWillDestroyItem: (callback) -> + @emitter.on 'will-destroy-item', callback + + on: (eventName) -> + switch eventName + when 'activated' + Grim.deprecate("Use Pane::onDidActivate instead") + when 'destroyed' + Grim.deprecate("Use Pane::onDidDestroy instead") + when 'item-added' + Grim.deprecate("Use Pane::onDidAddItem instead") + when 'item-removed' + Grim.deprecate("Use Pane::onDidRemoveItem instead") + when 'item-moved' + Grim.deprecate("Use Pane::onDidMoveItem instead") + when 'before-item-destroyed' + Grim.deprecate("Use Pane::onWillDestroyItem instead") + else + Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.") + super + + behavior: (behaviorName) -> + switch behaviorName + when 'active' + Grim.deprecate("The $active behavior property is deprecated. Use ::observeActive or ::onDidChangeActive instead.") + when 'container' + Grim.deprecate("The $container behavior property is deprecated.") + when 'activeItem' + Grim.deprecate("The $activeItem behavior property is deprecated. Use ::observeActiveItem or ::onDidChangeActiveItem instead.") + when 'focused' + Grim.deprecate("The $focused behavior property is deprecated.") + else + Grim.deprecate("Pane::behavior is deprecated. Use event subscription methods instead.") + + super # Called by the view layer to indicate that the pane has gained focus. focus: -> @@ -103,14 +235,12 @@ class Pane extends Model @focused = false true # if this is called from an event handler, don't cancel it - # Public: Makes this pane the *active* pane, causing it to gain focus - # immediately. - activate: -> - @container?.activePane = this - @emit 'activated' - getPanes: -> [this] + ### + Section: Items + ### + # Public: Get the items in this pane. # # Returns an {Array} of items. @@ -120,15 +250,23 @@ class Pane extends Model # Public: Get the active pane item in this pane. # # Returns a pane item. - getActiveItem: -> + getActiveItem: -> @activeItem + + setActiveItem: (activeItem) -> + unless activeItem is @activeItem + @activeItem = activeItem + @emitter.emit 'did-change-active-item', @activeItem @activeItem - # Public: Returns an {Editor} if the pane item is an {Editor}, or null - # otherwise. + # Return an {Editor} if the pane item is an {Editor}, or null otherwise. getActiveEditor: -> @activeItem if @activeItem instanceof Editor - # Public: Returns the item at the specified index. + # Public: Return the item at the given index. + # + # * `index` {Number} + # + # Returns an item or `null` if no item exists at the given index. itemAtIndex: (index) -> @items[index] @@ -148,86 +286,115 @@ class Pane extends Model else @activateItemAtIndex(@items.length - 1) - # Returns the index of the current active item. + # Public: Get the index of the active item. + # + # Returns a {Number}. getActiveItemIndex: -> @items.indexOf(@activeItem) - # Makes the item at the given index active. + # Public: Activate the item at the given index. + # + # * `index` {Number} activateItemAtIndex: (index) -> @activateItem(@itemAtIndex(index)) - # Makes the given item active, adding the item if necessary. + # Public: Make the given item *active*, causing it to be displayed by + # the pane's view. activateItem: (item) -> if item? @addItem(item) - @activeItem = item + @setActiveItem(item) - # Public: Adds the item to the pane. + # Public: Add the given item to the pane. # - # * `item` The item to add. It can be a model with an associated view or a view. - # * `index` (optional) {Number} at which to add the item. If omitted, the item is - # added after the current active item. + # * `item` The item to add. It can be a model with an associated view or a + # view. + # * `index` (optional) {Number} indicating the index at which to add the item. + # If omitted, the item is added after the current active item. # - # Returns the added item + # Returns the added item. addItem: (item, index=@getActiveItemIndex() + 1) -> return if item in @items + if typeof item.on is 'function' + @subscribe item, 'destroyed', => @removeItem(item, true) + @items.splice(index, 0, item) @emit 'item-added', item, index - @activeItem ?= item + @emitter.emit 'did-add-item', {item, index} + @setActiveItem(item) unless @getActiveItem()? item - # Public: Adds the given items to the pane. + # Public: Add 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` (optional) {Number} index at which to add the item. If omitted, the item is - # added after the current active item. + # * `items` An {Array} of items to add. Items can be views or models with + # associated views. Any objects that are already present in the pane's + # current items will not be added again. + # * `index` (optional) {Number} index at which to add the items. If omitted, + # the item is # added after the current active item. # - # Returns an {Array} of the added items + # Returns an {Array} of 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 - removeItem: (item, destroying) -> + removeItem: (item, destroyed=false) -> index = @items.indexOf(item) return if index is -1 + + if typeof item.on is 'function' + @unsubscribe item + if item is @activeItem if @items.length is 1 - @activeItem = undefined + @setActiveItem(undefined) else if index is 0 @activateNextItem() else @activatePreviousItem() @items.splice(index, 1) - @emit 'item-removed', item, index, destroying - @container?.itemDestroyed(item) if destroying + @emit 'item-removed', item, index, destroyed + @emitter.emit 'did-remove-item', {item, index, destroyed} + @container?.paneItemDestroyed(item) if destroyed @destroy() if @items.length is 0 and atom.config.get('core.destroyEmptyPanes') - # Public: Moves the given item to the specified index. + # Public: Move the given item to the given index. + # + # * `item` The item to move. + # * `index` {Number} indicating the index to which to move the item. moveItem: (item, newIndex) -> oldIndex = @items.indexOf(item) @items.splice(oldIndex, 1) @items.splice(newIndex, 0, item) @emit 'item-moved', item, newIndex + @emitter.emit 'did-move-item', {item, oldIndex, newIndex} - # Public: Moves the given item to the given index at another pane. + # Public: Move the given item to the given index on another pane. + # + # * `item` The item to move. + # * `pane` {Pane} to which to move the item. + # * `index` {Number} indicating the index to which to move the item in the + # given pane. moveItemToPane: (item, pane, index) -> pane.addItem(item, index) @removeItem(item) - # Public: Destroys the currently active item and make the next item active. + # Public: Destroy the active item and activate the next item. destroyActiveItem: -> @destroyItem(@activeItem) false - # 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. + # Public: Destroy the given item. + # + # If the item is active, the next item will be activated. If the item is the + # last item, the pane will be destroyed if the `core.destroyEmptyPanes` config + # setting is `true`. destroyItem: (item) -> - if item? + index = @items.indexOf(item) + if index isnt -1 @emit 'before-item-destroyed', item + @emitter.emit 'will-destroy-item', {item, index} if @promptToSaveItem(item) @removeItem(item, true) item.destroy?() @@ -235,27 +402,14 @@ class Pane extends Model else false - # Public: Destroys all items and destroys the pane. + # Public: Destroy all items. destroyItems: -> @destroyItem(item) for item in @getItems() - # Public: Destroys all items but the active one. + # Public: Destroy all items except for the active item. destroyInactiveItems: -> @destroyItem(item) for item in @getItems() when item isnt @activeItem - destroy: -> - if @container?.isAlive() and @container.getPanes().length is 1 - @destroyItems() - else - super - - # Called by model superclass. - destroyed: -> - @container.activateNextPane() if @isActive() - item.destroy?() for item in @items.slice() - - # Public: Prompts the user to save the given item if it can be saved and is - # currently unsaved. promptToSaveItem: (item) -> return true unless item.shouldPromptToSave?() @@ -270,18 +424,23 @@ class Pane extends Model when 1 then false when 2 then true - # Public: Saves the active item. - saveActiveItem: -> - @saveItem(@activeItem) + # Public: Save the active item. + saveActiveItem: (nextAction) -> + @saveItem(@getActiveItem(), nextAction) - # Public: Saves the active item at a prompted-for location. - saveActiveItemAs: -> - @saveItemAs(@activeItem) + # Public: Prompt the user for a location and save the active item with the + # path they select. + # + # * `nextAction` (optional) {Function} which will be called after the item is + # successfully saved. + saveActiveItemAs: (nextAction) -> + @saveItemAs(@getActiveItem(), nextAction) - # Public: Saves the specified item. + # Public: Save the given item. # # * `item` The item to save. - # * `nextAction` (optional) {Function} which will be called after the item is saved. + # * `nextAction` (optional) {Function} which will be called after the item is + # successfully saved. saveItem: (item, nextAction) -> if item?.getUri?() item.save?() @@ -289,10 +448,12 @@ class Pane extends Model else @saveItemAs(item, nextAction) - # Public: Saves the given item at a prompted-for location. + # Public: Prompt the user for a location and save the active item with the + # path they select. # # * `item` The item to save. - # * `nextAction` (optional) {Function} which will be called after the item is saved. + # * `nextAction` (optional) {Function} which will be called after the item is + # successfully saved. saveItemAs: (item, nextAction) -> return unless item?.saveAs? @@ -302,17 +463,20 @@ class Pane extends Model item.saveAs(newItemPath) nextAction?() - # Public: Saves all items. + # Public: Save all items. saveItems: -> @saveItem(item) for item in @getItems() - # Public: Returns the first item that matches the given URI or undefined if + # Public: Return the first item that matches the given URI or undefined if # none exists. + # + # * `uri` {String} containing a URI. itemForUri: (uri) -> find @items, (item) -> item.getUri?() is uri - # Public: Activates the first item that matches the given URI. Returns a - # boolean indicating whether a matching item was found. + # Public: Activate the first item that matches the given URI. + # + # Returns a {Boolean} indicating whether an item matching the URI was found. activateItemForUri: (uri) -> if item = @itemForUri(uri) @activateItem(item) @@ -324,19 +488,55 @@ class Pane extends Model if @activeItem? @activeItem.copy?() ? atom.deserializers.deserialize(@activeItem.serialize()) - # Public: Creates a new pane to the left of the receiver. + ### + Section: Lifecycle + ### + + # Public: Determine whether the pane is active. # - # * `params` {Object} with keys - # * `items` (optional) {Array} of items with which to construct the new pane. + # Returns a {Boolean}. + isActive: -> + @container?.getActivePane() is this + + # Public: Makes this pane the *active* pane, causing it to gain focus. + activate: -> + @container?.setActivePane(this) + @emit 'activated' + @emitter.emit 'did-activate' + + # Public: Close the pane and destroy all its items. + # + # If this is the last pane, all the items will be destroyed but the pane + # itself will not be destroyed. + destroy: -> + if @container?.isAlive() and @container.getPanes().length is 1 + @destroyItems() + else + super + + # Called by model superclass. + destroyed: -> + @container.activateNextPane() if @isActive() + @emitter.emit 'did-destroy' + item.destroy?() for item in @items.slice() + + ### + Section: Splitting + ### + + # Public: Create a new pane to the left of this pane. + # + # * `params` (optional) {Object} with the following keys: + # * `items` (optional) {Array} of items to add to the new pane. # # Returns the new {Pane}. splitLeft: (params) -> @split('horizontal', 'before', params) - # Public: Creates a new pane to the right of the receiver. + # Public: Create a new pane to the right of this pane. # - # * `params` {Object} with keys: - # * `items` (optional) {Array} of items with which to construct the new pane. + # * `params` (optional) {Object} with the following keys: + # * `items` (optional) {Array} of items to add to the new pane. # # Returns the new {Pane}. splitRight: (params) -> @@ -344,8 +544,8 @@ class Pane extends Model # Public: Creates a new pane above the receiver. # - # * `params` {Object} with keys: - # * `items` (optional) {Array} of items with which to construct the new pane. + # * `params` (optional) {Object} with the following keys: + # * `items` (optional) {Array} of items to add to the new pane. # # Returns the new {Pane}. splitUp: (params) -> @@ -353,8 +553,8 @@ class Pane extends Model # Public: Creates a new pane below the receiver. # - # * `params` {Object} with keys: - # * `items` (optional) {Array} of items with which to construct the new pane. + # * `params` (optional) {Object} with the following keys: + # * `items` (optional) {Array} of items to add to the new pane. # # Returns the new {Pane}. splitDown: (params) -> diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index 9da0d7b90..b4ae47318 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -84,7 +84,7 @@ class WorkspaceView extends View @panes.replaceWith(panes) @panes = panes - @subscribe @model, 'uri-opened', => @trigger 'uri-opened' + @subscribe @model.onDidOpen => @trigger 'uri-opened' @subscribe scrollbarStyle, (style) => @removeClass('scrollbars-visible-always scrollbars-visible-when-scrolling') @@ -409,4 +409,4 @@ class WorkspaceView extends View # Deprecated: Call {Workspace::getActivePaneItem} instead. getActivePaneItem: -> deprecate("Use Workspace::getActivePaneItem instead") - @model.activePaneItem + @model.getActivePaneItem() diff --git a/src/workspace.coffee b/src/workspace.coffee index 85870ef1b..97fff29e1 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -5,6 +5,7 @@ _ = require 'underscore-plus' Q = require 'q' Serializable = require 'serializable' Delegator = require 'delegato' +{Emitter} = require 'event-kit' Editor = require './editor' PaneContainer = require './pane-container' Pane = require './pane' @@ -16,17 +17,6 @@ Pane = require './pane' # editors, and manipulate panes. To add panels, you'll need to use the # {WorkspaceView} class for now until we establish APIs at the model layer. # -# ## Events -# -# ### uri-opened -# -# Extended: Emit when something has been opened. This can be anything, from an -# editor to the settings view. You can get the new item via {::getActivePaneItem} -# -# ### editor-created -# -# Extended: Emit when an editor is created (a file opened). -# # * `editor` {Editor} the new editor # module.exports = @@ -44,9 +34,11 @@ class Workspace extends Model constructor: -> super + @emitter = new Emitter @openers = [] - @subscribe @paneContainer, 'item-destroyed', @onPaneItemDestroyed + @paneContainer.onDidDestroyPaneItem(@onPaneItemDestroyed) + @registerOpener (filePath) => switch filePath when 'atom://.atom/stylesheet' @@ -83,34 +75,130 @@ class Workspace extends Model for scopeName in includedGrammarScopes ? [] addGrammar(atom.syntax.grammarForScopeName(scopeName)) - addGrammar(editor.getGrammar()) for editor in @getEditors() + addGrammar(editor.getGrammar()) for editor in @getTextEditors() _.uniq(packageNames) editorAdded: (editor) -> @emit 'editor-created', editor - # Public: Register a function to be called for every current and future - # {Editor} in the workspace. + ### + Section: Event Subscription + ### + + # Extended: Invoke the given callback when a pane is added to the workspace. # - # * `callback` A {Function} with an {Editor} as its only argument. + # * `callback` {Function} to be called panes are added. + # * `event` {Object} with the following keys: + # * `pane` The added pane. # - # Returns a subscription object with an `.off` method that you can call to - # unregister the callback. + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddPane: (callback) -> @paneContainer.onDidAddPane(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) -> @paneContainer.observePanes(callback) + + # Extended: Invoke the given callback when a pane item is added to the + # workspace. + # + # * `callback` {Function} to be called panes 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) -> @paneContainer.onDidAddPaneItem(callback) + + # Extended: 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) -> @paneContainer.observePaneItems(callback) + + # Extended: Invoke the given callback when a text editor is added to the + # workspace. + # + # * `callback` {Function} to be called panes are added. + # * `event` {Object} with the following keys: + # * `textEditor` {Editor} that was added. + # * `pane` {Pane} containing the added text editor. + # * `index` {Number} indicating the index of the added text editor in its + # pane. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddTextEditor: (callback) -> + @onDidAddPaneItem ({item, pane, index}) -> + callback({textEditor: item, pane, index}) if item instanceof Editor + + # 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 {Editor} 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) -> + callback(textEditor) for textEditor in @getTextEditors() + @onDidAddTextEditor ({textEditor}) -> callback(textEditor) + + # Essential: Invoke the given callback whenever an item is opened. Unlike + # ::onDidAddPaneItem, observers will be notified for items that are already + # present in the workspace when they are reopened. + # + # * `callback` {Function} to be called whenever an item is opened. + # * `event` {Object} with the following keys: + # * `uri` {String} representing the opened URI. Could be `undefined`. + # * `item` The opened item. + # * `pane` The pane in which the item was opened. + # * `index` The index of the opened item on its pane. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidOpen: (callback) -> + @emitter.on 'did-open', callback + eachEditor: (callback) -> + deprecate("Use Workspace::observeTextEditors instead") + callback(editor) for editor in @getEditors() @subscribe this, 'editor-created', (editor) -> callback(editor) - # Public: Get all current editors in the workspace. - # - # Returns an {Array} of {Editor}s. getEditors: -> + deprecate("Use Workspace::getTextEditors instead") + editors = [] for pane in @paneContainer.getPanes() editors.push(item) for item in pane.getItems() when item instanceof Editor editors - # Public: Open a given a URI in Atom asynchronously. + on: (eventName) -> + switch eventName + when 'editor-created' + deprecate("Use Workspace::onDidAddTextEditor or Workspace::observeTextEditors instead.") + when 'uri-opened' + deprecate("Use Workspace::onDidAddPaneItem instead.") + else + deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.") + + super + + ### + Section: Opening + ### + + # Essential: Open a given a URI in Atom asynchronously. # # * `uri` A {String} containing a URI. # * `options` (optional) {Object} @@ -137,11 +225,11 @@ class Workspace extends Model pane = @paneContainer.paneForUri(uri) if searchAllPanes pane ?= switch split when 'left' - @activePane.findLeftmostSibling() + @getActivePane().findLeftmostSibling() when 'right' - @activePane.findOrCreateRightmostSibling() + @getActivePane().findOrCreateRightmostSibling() else - @activePane + @getActivePane() @openUriInPane(uri, pane, options) @@ -195,12 +283,14 @@ class Workspace extends Model @itemOpened(item) pane.activateItem(item) pane.activate() if changeFocus + index = pane.getActiveItemIndex() @emit "uri-opened" + @emitter.emit 'did-open', {uri, pane, item, index} item .catch (error) -> console.error(error.stack ? error) - # Public: Asynchronously reopens the last-closed item's URI if it hasn't already been + # Extended: Asynchronously reopens the last-closed item's URI if it hasn't already been # reopened. # # Returns a promise that is resolved when the item is opened @@ -216,7 +306,7 @@ class Workspace extends Model if uri = @destroyedItemUris.pop() @openSync(uri) - # Public: Register an opener for a uri. + # Extended: Register an opener for a uri. # # An {Editor} will be used if no openers return a value. # @@ -232,52 +322,52 @@ class Workspace extends Model registerOpener: (opener) -> @openers.push(opener) - # Public: Unregister an opener registered with {::registerOpener}. + # Extended: Unregister an opener registered with {::registerOpener}. unregisterOpener: (opener) -> _.remove(@openers, opener) getOpeners: -> @openers - # Public: Get the active {Pane}. + ### + Section: Pane Items + ### + + # Essential: Get all pane items in the workspace. # - # Returns a {Pane}. - getActivePane: -> - @paneContainer.activePane + # Returns an {Array} of items. + getPaneItems: -> + @paneContainer.getPaneItems() - # Public: Get all {Pane}s. - # - # Returns an {Array} of {Pane}s. - getPanes: -> - @paneContainer.getPanes() - - # Public: Save all pane items. - saveAll: -> - @paneContainer.saveAll() - - # Public: Make the next pane active. - activateNextPane: -> - @paneContainer.activateNextPane() - - # Public: Make the previous pane active. - activatePreviousPane: -> - @paneContainer.activatePreviousPane() - - # Public: Get the first pane {Pane} with an item for the given URI. - # - # * `uri` {String} uri - # - # Returns a {Pane} or `undefined` if no pane exists for the given URI. - paneForUri: (uri) -> - @paneContainer.paneForUri(uri) - - # Public: Get the active {Pane}'s active item. + # Essential: Get the active {Pane}'s active item. # # Returns an pane item {Object}. getActivePaneItem: -> - @paneContainer.getActivePane().getActiveItem() + @paneContainer.getActivePaneItem() - # Public: Save the active pane item. + # Essential: Get all text editors in the workspace. + # + # Returns an {Array} of {Editor}s. + getTextEditors: -> + @getPaneItems().filter (item) -> item instanceof Editor + + # Essential: Get the active item if it is an {Editor}. + # + # Returns an {Editor} or `undefined` if the current active item is not an + # {Editor}. + getActiveTextEditor: -> + activeItem = @getActiveItem() + activeItem if activeItem instanceof Editor + + # Deprecated: + getActiveEditor: -> + @activePane?.getActiveEditor() + + # Extended: Save all pane items. + saveAll: -> + @paneContainer.saveAll() + + # Save the active pane item. # # If the active pane item currently has a URI according to the item's # `.getUri` method, calls `.save` on the item. Otherwise @@ -286,7 +376,7 @@ class Workspace extends Model saveActivePaneItem: -> @activePane?.saveActiveItem() - # Public: Prompt the user for a path and save the active pane item to it. + # Prompt the user for a path and save the active pane item to it. # # Opens a native dialog where the user selects a path on disk, then calls # `.saveAs` on the item with the selected path. This method does nothing if @@ -294,34 +384,59 @@ class Workspace extends Model saveActivePaneItemAs: -> @activePane?.saveActiveItemAs() - # Public: Destroy (close) the active pane item. + # Destroy (close) the active pane item. # # Removes the active pane item and calls the `.destroy` method on it if one is # defined. destroyActivePaneItem: -> @activePane?.destroyActiveItem() - # Public: Destroy (close) the active pane. + ### + Section: Panes + ### + + # Extended: Get all panes in the workspace. + # + # Returns an {Array} of {Pane}s. + getPanes: -> + @paneContainer.getPanes() + + # Extended: Get the active {Pane}. + # + # Returns a {Pane}. + getActivePane: -> + @paneContainer.getActivePane() + + # Extended: Make the next pane active. + activateNextPane: -> + @paneContainer.activateNextPane() + + # Extended: Make the previous pane active. + activatePreviousPane: -> + @paneContainer.activatePreviousPane() + + # Extended: Get the first pane {Pane} with an item for the given URI. + # + # * `uri` {String} uri + # + # Returns a {Pane} or `undefined` if no pane exists for the given URI. + paneForUri: (uri) -> + @paneContainer.paneForUri(uri) + + # Destroy (close) the active pane. destroyActivePane: -> @activePane?.destroy() - # Public: Get the active item if it is an {Editor}. - # - # Returns an {Editor} or `undefined` if the current active item is not an - # {Editor}. - getActiveEditor: -> - @activePane?.getActiveEditor() - - # Public: Increase the editor font size by 1px. + # Increase the editor font size by 1px. increaseFontSize: -> atom.config.set("editor.fontSize", atom.config.get("editor.fontSize") + 1) - # Public: Decrease the editor font size by 1px. + # Decrease the editor font size by 1px. decreaseFontSize: -> fontSize = atom.config.get("editor.fontSize") atom.config.set("editor.fontSize", fontSize - 1) if fontSize > 1 - # Public: Restore to a default editor font size. + # Restore to a default editor font size. resetFontSize: -> atom.config.restoreDefault("editor.fontSize")