mirror of
https://github.com/atom/atom.git
synced 2026-01-25 06:48:28 -05:00
Merge remote-tracking branch 'origin/master' into mkt-url-based-command-dispatch
This commit is contained in:
14
package.json
14
package.json
@@ -70,7 +70,7 @@
|
||||
"service-hub": "^0.7.4",
|
||||
"sinon": "1.17.4",
|
||||
"temp": "^0.8.3",
|
||||
"text-buffer": "13.3.1",
|
||||
"text-buffer": "13.3.4",
|
||||
"typescript-simple": "1.0.0",
|
||||
"underscore-plus": "^1.6.6",
|
||||
"winreg": "^1.2.1",
|
||||
@@ -97,7 +97,7 @@
|
||||
"autocomplete-plus": "2.35.10",
|
||||
"autocomplete-snippets": "1.11.1",
|
||||
"autoflow": "0.29.0",
|
||||
"autosave": "0.24.3",
|
||||
"autosave": "0.24.4",
|
||||
"background-tips": "0.27.1",
|
||||
"bookmarks": "0.44.4",
|
||||
"bracket-matcher": "0.88.0",
|
||||
@@ -112,7 +112,7 @@
|
||||
"github": "0.6.2",
|
||||
"git-diff": "1.3.6",
|
||||
"go-to-line": "0.32.1",
|
||||
"grammar-selector": "0.49.5",
|
||||
"grammar-selector": "0.49.6",
|
||||
"image-view": "0.62.3",
|
||||
"incompatible-packages": "0.27.3",
|
||||
"keybinding-resolver": "0.38.0",
|
||||
@@ -123,22 +123,22 @@
|
||||
"notifications": "0.69.2",
|
||||
"open-on-github": "1.2.1",
|
||||
"package-generator": "1.1.1",
|
||||
"settings-view": "0.251.8",
|
||||
"settings-view": "0.251.9",
|
||||
"snippets": "1.1.4",
|
||||
"spell-check": "0.72.2",
|
||||
"status-bar": "1.8.13",
|
||||
"styleguide": "0.49.7",
|
||||
"symbols-view": "0.118.0",
|
||||
"tabs": "0.107.2",
|
||||
"tabs": "0.107.3",
|
||||
"timecop": "0.36.0",
|
||||
"tree-view": "0.218.0",
|
||||
"update-package-dependencies": "0.12.0",
|
||||
"welcome": "0.36.5",
|
||||
"whitespace": "0.37.3",
|
||||
"whitespace": "0.37.4",
|
||||
"wrap-guide": "0.40.2",
|
||||
"language-c": "0.58.1",
|
||||
"language-clojure": "0.22.4",
|
||||
"language-coffee-script": "0.49.0",
|
||||
"language-coffee-script": "0.49.1",
|
||||
"language-csharp": "0.14.2",
|
||||
"language-css": "0.42.6",
|
||||
"language-gfm": "0.90.1",
|
||||
|
||||
@@ -322,6 +322,44 @@ describe "AtomEnvironment", ->
|
||||
expect(atom2.textEditors.getGrammarOverride(editor)).toBe('text.plain')
|
||||
atom2.destroy()
|
||||
|
||||
describe "deserialization failures", ->
|
||||
|
||||
it "propagates project state restoration failures", ->
|
||||
spyOn(atom.project, 'deserialize').andCallFake ->
|
||||
err = new Error('deserialization failure')
|
||||
err.missingProjectPaths = ['/foo']
|
||||
Promise.reject(err)
|
||||
spyOn(atom.notifications, 'addError')
|
||||
|
||||
waitsForPromise -> atom.deserialize({project: 'should work'})
|
||||
runs ->
|
||||
expect(atom.notifications.addError).toHaveBeenCalledWith 'Unable to open project directory',
|
||||
{description: 'Project directory `/foo` is no longer on disk.'}
|
||||
|
||||
it "accumulates and reports two errors with one notification", ->
|
||||
spyOn(atom.project, 'deserialize').andCallFake ->
|
||||
err = new Error('deserialization failure')
|
||||
err.missingProjectPaths = ['/foo', '/wat']
|
||||
Promise.reject(err)
|
||||
spyOn(atom.notifications, 'addError')
|
||||
|
||||
waitsForPromise -> atom.deserialize({project: 'should work'})
|
||||
runs ->
|
||||
expect(atom.notifications.addError).toHaveBeenCalledWith 'Unable to open 2 project directories',
|
||||
{description: 'Project directories `/foo` and `/wat` are no longer on disk.'}
|
||||
|
||||
it "accumulates and reports three+ errors with one notification", ->
|
||||
spyOn(atom.project, 'deserialize').andCallFake ->
|
||||
err = new Error('deserialization failure')
|
||||
err.missingProjectPaths = ['/foo', '/wat', '/stuff', '/things']
|
||||
Promise.reject(err)
|
||||
spyOn(atom.notifications, 'addError')
|
||||
|
||||
waitsForPromise -> atom.deserialize({project: 'should work'})
|
||||
runs ->
|
||||
expect(atom.notifications.addError).toHaveBeenCalledWith 'Unable to open 4 project directories',
|
||||
{description: 'Project directories `/foo`, `/wat`, `/stuff`, and `/things` are no longer on disk.'}
|
||||
|
||||
describe "openInitialEmptyEditorIfNecessary", ->
|
||||
describe "when there are no paths set", ->
|
||||
beforeEach ->
|
||||
|
||||
@@ -1,409 +0,0 @@
|
||||
PaneContainer = require '../src/pane-container'
|
||||
Pane = require '../src/pane'
|
||||
|
||||
describe "PaneContainer", ->
|
||||
[confirm, params] = []
|
||||
|
||||
beforeEach ->
|
||||
confirm = spyOn(atom.applicationDelegate, 'confirm').andReturn(0)
|
||||
params = {
|
||||
location: 'center',
|
||||
config: atom.config,
|
||||
deserializerManager: atom.deserializers
|
||||
applicationDelegate: atom.applicationDelegate,
|
||||
viewRegistry: atom.views
|
||||
}
|
||||
|
||||
describe "serialization", ->
|
||||
[containerA, pane1A, pane2A, pane3A] = []
|
||||
|
||||
beforeEach ->
|
||||
# This is a dummy item to prevent panes from being empty on deserialization
|
||||
class Item
|
||||
atom.deserializers.add(this)
|
||||
@deserialize: -> new this
|
||||
serialize: -> deserializer: 'Item'
|
||||
|
||||
containerA = new PaneContainer(params)
|
||||
pane1A = containerA.getActivePane()
|
||||
pane1A.addItem(new Item)
|
||||
pane2A = pane1A.splitRight(items: [new Item])
|
||||
pane3A = pane2A.splitDown(items: [new Item])
|
||||
pane3A.focus()
|
||||
|
||||
it "preserves the focused pane across serialization", ->
|
||||
expect(pane3A.focused).toBe true
|
||||
|
||||
containerB = new PaneContainer(params)
|
||||
containerB.deserialize(containerA.serialize(), atom.deserializers)
|
||||
[pane1B, pane2B, pane3B] = containerB.getPanes()
|
||||
expect(pane3B.focused).toBe true
|
||||
|
||||
it "preserves the active pane across serialization, independent of focus", ->
|
||||
pane3A.activate()
|
||||
expect(containerA.getActivePane()).toBe pane3A
|
||||
|
||||
containerB = new PaneContainer(params)
|
||||
containerB.deserialize(containerA.serialize(), atom.deserializers)
|
||||
[pane1B, pane2B, pane3B] = containerB.getPanes()
|
||||
expect(containerB.getActivePane()).toBe pane3B
|
||||
|
||||
it "makes the first pane active if no pane exists for the activePaneId", ->
|
||||
pane3A.activate()
|
||||
state = containerA.serialize()
|
||||
state.activePaneId = -22
|
||||
containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
expect(containerB.getActivePane()).toBe containerB.getPanes()[0]
|
||||
|
||||
describe "if there are empty panes after deserialization", ->
|
||||
beforeEach ->
|
||||
pane3A.getItems()[0].serialize = -> deserializer: 'Bogus'
|
||||
|
||||
describe "if the 'core.destroyEmptyPanes' config option is false (the default)", ->
|
||||
it "leaves the empty panes intact", ->
|
||||
state = containerA.serialize()
|
||||
containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
[leftPane, column] = containerB.getRoot().getChildren()
|
||||
[topPane, bottomPane] = column.getChildren()
|
||||
|
||||
expect(leftPane.getItems().length).toBe 1
|
||||
expect(topPane.getItems().length).toBe 1
|
||||
expect(bottomPane.getItems().length).toBe 0
|
||||
|
||||
describe "if the 'core.destroyEmptyPanes' config option is true", ->
|
||||
it "removes empty panes on deserialization", ->
|
||||
atom.config.set('core.destroyEmptyPanes', true)
|
||||
|
||||
state = containerA.serialize()
|
||||
containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
[leftPane, rightPane] = containerB.getRoot().getChildren()
|
||||
|
||||
expect(leftPane.getItems().length).toBe 1
|
||||
expect(rightPane.getItems().length).toBe 1
|
||||
|
||||
it "does not allow the root pane to be destroyed", ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().destroy()
|
||||
expect(container.getRoot()).toBeDefined()
|
||||
expect(container.getRoot().isDestroyed()).toBe false
|
||||
|
||||
describe "::getActivePane()", ->
|
||||
[container, pane1, pane2] = []
|
||||
|
||||
beforeEach ->
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
|
||||
it "returns the first pane if no pane has been made active", ->
|
||||
expect(container.getActivePane()).toBe pane1
|
||||
expect(pane1.isActive()).toBe true
|
||||
|
||||
it "returns the most pane on which ::activate() was most recently called", ->
|
||||
pane2 = pane1.splitRight()
|
||||
pane2.activate()
|
||||
expect(container.getActivePane()).toBe pane2
|
||||
expect(pane1.isActive()).toBe false
|
||||
expect(pane2.isActive()).toBe true
|
||||
pane1.activate()
|
||||
expect(container.getActivePane()).toBe pane1
|
||||
expect(pane1.isActive()).toBe true
|
||||
expect(pane2.isActive()).toBe false
|
||||
|
||||
it "returns the next pane if the current active pane is destroyed", ->
|
||||
pane2 = pane1.splitRight()
|
||||
pane2.activate()
|
||||
pane2.destroy()
|
||||
expect(container.getActivePane()).toBe pane1
|
||||
expect(pane1.isActive()).toBe true
|
||||
|
||||
describe "::onDidChangeActivePane()", ->
|
||||
[container, pane1, pane2, observed] = []
|
||||
|
||||
beforeEach ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([new Object, new Object])
|
||||
container.getRoot().splitRight(items: [new Object, new Object])
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.onDidChangeActivePane (pane) -> observed.push(pane)
|
||||
|
||||
it "invokes observers when the active pane changes", ->
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
expect(observed).toEqual [pane1, pane2]
|
||||
|
||||
describe "::onDidChangeActivePaneItem()", ->
|
||||
[container, pane1, pane2, observed] = []
|
||||
|
||||
beforeEach ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([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 "::onDidStopChangingActivePaneItem()", ->
|
||||
[container, pane1, pane2, observed] = []
|
||||
|
||||
beforeEach ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([new Object, new Object])
|
||||
container.getRoot().splitRight(items: [new Object, new Object])
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.onDidStopChangingActivePaneItem (item) -> observed.push(item)
|
||||
|
||||
it "invokes observers once when the active item of the active pane changes", ->
|
||||
pane2.activateNextItem()
|
||||
pane2.activateNextItem()
|
||||
expect(observed).toEqual []
|
||||
advanceClock 100
|
||||
expect(observed).toEqual [pane2.itemAtIndex(0)]
|
||||
|
||||
it "invokes observers once when the active pane changes", ->
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
expect(observed).toEqual []
|
||||
advanceClock 100
|
||||
expect(observed).toEqual [pane2.itemAtIndex(0)]
|
||||
|
||||
describe "::onDidActivatePane", ->
|
||||
it "invokes observers when a pane is activated (even if it was already active)", ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().splitRight()
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
activatedPanes = []
|
||||
container.onDidActivatePane (pane) -> activatedPanes.push(pane)
|
||||
|
||||
pane1.activate()
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
pane2.activate()
|
||||
expect(activatedPanes).toEqual([pane1, pane1, pane2, pane2])
|
||||
|
||||
describe "::observePanes()", ->
|
||||
it "invokes observers with all current and future panes", ->
|
||||
container = new PaneContainer(params)
|
||||
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(params)
|
||||
container.getRoot().addItems([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()
|
||||
|
||||
describe "::confirmClose()", ->
|
||||
[container, pane1, pane2] = []
|
||||
|
||||
beforeEach ->
|
||||
class TestItem
|
||||
shouldPromptToSave: -> true
|
||||
getURI: -> 'test'
|
||||
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().splitRight()
|
||||
[pane1, pane2] = container.getPanes()
|
||||
pane1.addItem(new TestItem)
|
||||
pane2.addItem(new TestItem)
|
||||
|
||||
it "returns true if the user saves all modified files when prompted", ->
|
||||
confirm.andReturn(0)
|
||||
waitsForPromise ->
|
||||
container.confirmClose().then (saved) ->
|
||||
expect(confirm).toHaveBeenCalled()
|
||||
expect(saved).toBeTruthy()
|
||||
|
||||
it "returns false if the user cancels saving any modified file", ->
|
||||
confirm.andReturn(1)
|
||||
waitsForPromise ->
|
||||
container.confirmClose().then (saved) ->
|
||||
expect(confirm).toHaveBeenCalled()
|
||||
expect(saved).toBeFalsy()
|
||||
|
||||
describe "::onDidAddPane(callback)", ->
|
||||
it "invokes the given callback when panes are added", ->
|
||||
container = new PaneContainer(params)
|
||||
events = []
|
||||
container.onDidAddPane (event) ->
|
||||
expect(event.pane in container.getPanes()).toBe true
|
||||
events.push(event)
|
||||
|
||||
pane1 = container.getActivePane()
|
||||
pane2 = pane1.splitRight()
|
||||
pane3 = pane2.splitDown()
|
||||
|
||||
expect(events).toEqual [{pane: pane2}, {pane: pane3}]
|
||||
|
||||
describe "::onWillDestroyPane(callback)", ->
|
||||
it "invokes the given callback before panes or their items are destroyed", ->
|
||||
class TestItem
|
||||
constructor: -> @_isDestroyed = false
|
||||
destroy: -> @_isDestroyed = true
|
||||
isDestroyed: -> @_isDestroyed
|
||||
|
||||
container = new PaneContainer(params)
|
||||
events = []
|
||||
container.onWillDestroyPane (event) ->
|
||||
itemsDestroyed = (item.isDestroyed() for item in event.pane.getItems())
|
||||
events.push([event, itemsDestroyed: itemsDestroyed])
|
||||
|
||||
pane1 = container.getActivePane()
|
||||
pane2 = pane1.splitRight()
|
||||
pane2.addItem(new TestItem)
|
||||
|
||||
pane2.destroy()
|
||||
|
||||
expect(events).toEqual [[{pane: pane2}, itemsDestroyed: [false]]]
|
||||
|
||||
describe "::onDidDestroyPane(callback)", ->
|
||||
it "invokes the given callback when panes are destroyed", ->
|
||||
container = new PaneContainer(params)
|
||||
events = []
|
||||
container.onDidDestroyPane (event) ->
|
||||
expect(event.pane in container.getPanes()).toBe false
|
||||
events.push(event)
|
||||
|
||||
pane1 = container.getActivePane()
|
||||
pane2 = pane1.splitRight()
|
||||
pane3 = pane2.splitDown()
|
||||
|
||||
pane2.destroy()
|
||||
pane3.destroy()
|
||||
|
||||
expect(events).toEqual [{pane: pane2}, {pane: pane3}]
|
||||
|
||||
it "invokes the given callback when the container is destroyed", ->
|
||||
container = new PaneContainer(params)
|
||||
events = []
|
||||
container.onDidDestroyPane (event) ->
|
||||
expect(event.pane in container.getPanes()).toBe false
|
||||
events.push(event)
|
||||
|
||||
pane1 = container.getActivePane()
|
||||
pane2 = pane1.splitRight()
|
||||
pane3 = pane2.splitDown()
|
||||
|
||||
container.destroy()
|
||||
|
||||
expect(events).toEqual [{pane: pane1}, {pane: pane2}, {pane: pane3}]
|
||||
|
||||
describe "::onWillDestroyPaneItem() and ::onDidDestroyPaneItem", ->
|
||||
it "invokes the given callbacks when an item will be destroyed on any pane", ->
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
item1 = new Object
|
||||
item2 = new Object
|
||||
item3 = new Object
|
||||
|
||||
pane1.addItem(item1)
|
||||
events = []
|
||||
container.onWillDestroyPaneItem (event) -> events.push(['will', event])
|
||||
container.onDidDestroyPaneItem (event) -> events.push(['did', event])
|
||||
pane2 = pane1.splitRight(items: [item2, item3])
|
||||
|
||||
pane1.destroyItem(item1)
|
||||
pane2.destroyItem(item3)
|
||||
pane2.destroyItem(item2)
|
||||
|
||||
expect(events).toEqual [
|
||||
['will', {item: item1, pane: pane1, index: 0}]
|
||||
['did', {item: item1, pane: pane1, index: 0}]
|
||||
['will', {item: item3, pane: pane2, index: 1}]
|
||||
['did', {item: item3, pane: pane2, index: 1}]
|
||||
['will', {item: item2, pane: pane2, index: 0}]
|
||||
['did', {item: item2, pane: pane2, index: 0}]
|
||||
]
|
||||
|
||||
describe "::saveAll()", ->
|
||||
it "saves all modified pane items", ->
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
pane2 = pane1.splitRight()
|
||||
|
||||
item1 = {
|
||||
saved: false
|
||||
getURI: -> ''
|
||||
isModified: -> true,
|
||||
save: -> @saved = true
|
||||
}
|
||||
item2 = {
|
||||
saved: false
|
||||
getURI: -> ''
|
||||
isModified: -> false,
|
||||
save: -> @saved = true
|
||||
}
|
||||
item3 = {
|
||||
saved: false
|
||||
getURI: -> ''
|
||||
isModified: -> true,
|
||||
save: -> @saved = true
|
||||
}
|
||||
|
||||
pane1.addItem(item1)
|
||||
pane1.addItem(item2)
|
||||
pane1.addItem(item3)
|
||||
|
||||
container.saveAll()
|
||||
|
||||
expect(item1.saved).toBe true
|
||||
expect(item2.saved).toBe false
|
||||
expect(item3.saved).toBe true
|
||||
|
||||
describe "::moveActiveItemToPane(destPane) and ::copyActiveItemToPane(destPane)", ->
|
||||
[container, pane1, pane2, item1] = []
|
||||
|
||||
beforeEach ->
|
||||
class TestItem
|
||||
constructor: (id) -> @id = id
|
||||
copy: -> new TestItem(@id)
|
||||
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
item1 = new TestItem('1')
|
||||
pane2 = pane1.splitRight(items: [item1])
|
||||
|
||||
describe "::::moveActiveItemToPane(destPane)", ->
|
||||
it "moves active item to given pane and focuses it", ->
|
||||
container.moveActiveItemToPane(pane1)
|
||||
expect(pane1.getActiveItem()).toBe item1
|
||||
|
||||
describe "::::copyActiveItemToPane(destPane)", ->
|
||||
it "copies active item to given pane and focuses it", ->
|
||||
container.copyActiveItemToPane(pane1)
|
||||
expect(container.paneForItem(item1)).toBe pane2
|
||||
expect(pane1.getActiveItem().id).toBe item1.id
|
||||
472
spec/pane-container-spec.js
Normal file
472
spec/pane-container-spec.js
Normal file
@@ -0,0 +1,472 @@
|
||||
const PaneContainer = require('../src/pane-container')
|
||||
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
|
||||
|
||||
describe('PaneContainer', () => {
|
||||
let confirm, params
|
||||
|
||||
beforeEach(() => {
|
||||
confirm = spyOn(atom.applicationDelegate, 'confirm').andReturn(0)
|
||||
params = {
|
||||
location: 'center',
|
||||
config: atom.config,
|
||||
deserializerManager: atom.deserializers,
|
||||
applicationDelegate: atom.applicationDelegate,
|
||||
viewRegistry: atom.views
|
||||
}
|
||||
})
|
||||
|
||||
describe('serialization', () => {
|
||||
let containerA, pane1A, pane2A, pane3A
|
||||
|
||||
beforeEach(() => {
|
||||
// This is a dummy item to prevent panes from being empty on deserialization
|
||||
class Item {
|
||||
static deserialize () { return new (this)() }
|
||||
serialize () { return {deserializer: 'Item'} }
|
||||
}
|
||||
atom.deserializers.add(Item)
|
||||
|
||||
containerA = new PaneContainer(params)
|
||||
pane1A = containerA.getActivePane()
|
||||
pane1A.addItem(new Item())
|
||||
pane2A = pane1A.splitRight({items: [new Item()]})
|
||||
pane3A = pane2A.splitDown({items: [new Item()]})
|
||||
pane3A.focus()
|
||||
})
|
||||
|
||||
it('preserves the focused pane across serialization', () => {
|
||||
expect(pane3A.focused).toBe(true)
|
||||
|
||||
const containerB = new PaneContainer(params)
|
||||
containerB.deserialize(containerA.serialize(), atom.deserializers)
|
||||
const pane3B = containerB.getPanes()[2]
|
||||
expect(pane3B.focused).toBe(true)
|
||||
})
|
||||
|
||||
it('preserves the active pane across serialization, independent of focus', () => {
|
||||
pane3A.activate()
|
||||
expect(containerA.getActivePane()).toBe(pane3A)
|
||||
|
||||
const containerB = new PaneContainer(params)
|
||||
containerB.deserialize(containerA.serialize(), atom.deserializers)
|
||||
const pane3B = containerB.getPanes()[2]
|
||||
expect(containerB.getActivePane()).toBe(pane3B)
|
||||
})
|
||||
|
||||
it('makes the first pane active if no pane exists for the activePaneId', () => {
|
||||
pane3A.activate()
|
||||
const state = containerA.serialize()
|
||||
state.activePaneId = -22
|
||||
const containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
expect(containerB.getActivePane()).toBe(containerB.getPanes()[0])
|
||||
})
|
||||
|
||||
describe('if there are empty panes after deserialization', () => {
|
||||
beforeEach(() => {
|
||||
pane3A.getItems()[0].serialize = () => ({deserializer: 'Bogus'})
|
||||
})
|
||||
|
||||
describe("if the 'core.destroyEmptyPanes' config option is false (the default)", () =>
|
||||
it('leaves the empty panes intact', () => {
|
||||
const state = containerA.serialize()
|
||||
const containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
const [leftPane, column] = containerB.getRoot().getChildren()
|
||||
const [topPane, bottomPane] = column.getChildren()
|
||||
|
||||
expect(leftPane.getItems().length).toBe(1)
|
||||
expect(topPane.getItems().length).toBe(1)
|
||||
expect(bottomPane.getItems().length).toBe(0)
|
||||
})
|
||||
)
|
||||
|
||||
describe("if the 'core.destroyEmptyPanes' config option is true", () =>
|
||||
it('removes empty panes on deserialization', () => {
|
||||
atom.config.set('core.destroyEmptyPanes', true)
|
||||
|
||||
const state = containerA.serialize()
|
||||
const containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
const [leftPane, rightPane] = containerB.getRoot().getChildren()
|
||||
|
||||
expect(leftPane.getItems().length).toBe(1)
|
||||
expect(rightPane.getItems().length).toBe(1)
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('does not allow the root pane to be destroyed', () => {
|
||||
const container = new PaneContainer(params)
|
||||
container.getRoot().destroy()
|
||||
expect(container.getRoot()).toBeDefined()
|
||||
expect(container.getRoot().isDestroyed()).toBe(false)
|
||||
})
|
||||
|
||||
describe('::getActivePane()', () => {
|
||||
let container, pane1, pane2
|
||||
|
||||
beforeEach(() => {
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
})
|
||||
|
||||
it('returns the first pane if no pane has been made active', () => {
|
||||
expect(container.getActivePane()).toBe(pane1)
|
||||
expect(pane1.isActive()).toBe(true)
|
||||
})
|
||||
|
||||
it('returns the most pane on which ::activate() was most recently called', () => {
|
||||
pane2 = pane1.splitRight()
|
||||
pane2.activate()
|
||||
expect(container.getActivePane()).toBe(pane2)
|
||||
expect(pane1.isActive()).toBe(false)
|
||||
expect(pane2.isActive()).toBe(true)
|
||||
pane1.activate()
|
||||
expect(container.getActivePane()).toBe(pane1)
|
||||
expect(pane1.isActive()).toBe(true)
|
||||
expect(pane2.isActive()).toBe(false)
|
||||
})
|
||||
|
||||
it('returns the next pane if the current active pane is destroyed', () => {
|
||||
pane2 = pane1.splitRight()
|
||||
pane2.activate()
|
||||
pane2.destroy()
|
||||
expect(container.getActivePane()).toBe(pane1)
|
||||
expect(pane1.isActive()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidChangeActivePane()', () => {
|
||||
let container, pane1, pane2, observed
|
||||
|
||||
beforeEach(() => {
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([{}, {}])
|
||||
container.getRoot().splitRight({items: [{}, {}]});
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.onDidChangeActivePane(pane => observed.push(pane))
|
||||
})
|
||||
|
||||
it('invokes observers when the active pane changes', () => {
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
expect(observed).toEqual([pane1, pane2])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidChangeActivePaneItem()', () => {
|
||||
let container, pane1, pane2, observed
|
||||
|
||||
beforeEach(() => {
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([{}, {}])
|
||||
container.getRoot().splitRight({items: [{}, {}]});
|
||||
[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('::onDidStopChangingActivePaneItem()', () => {
|
||||
let container, pane1, pane2, observed
|
||||
|
||||
beforeEach(() => {
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([{}, {}])
|
||||
container.getRoot().splitRight({items: [{}, {}]});
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.onDidStopChangingActivePaneItem(item => observed.push(item))
|
||||
})
|
||||
|
||||
it('invokes observers once when the active item of the active pane changes', () => {
|
||||
pane2.activateNextItem()
|
||||
pane2.activateNextItem()
|
||||
expect(observed).toEqual([])
|
||||
advanceClock(100)
|
||||
expect(observed).toEqual([pane2.itemAtIndex(0)])
|
||||
})
|
||||
|
||||
it('invokes observers once when the active pane changes', () => {
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
expect(observed).toEqual([])
|
||||
advanceClock(100)
|
||||
expect(observed).toEqual([pane2.itemAtIndex(0)])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidActivatePane', () => {
|
||||
it('invokes observers when a pane is activated (even if it was already active)', () => {
|
||||
const container = new PaneContainer(params)
|
||||
container.getRoot().splitRight()
|
||||
const [pane1, pane2] = container.getPanes()
|
||||
|
||||
const activatedPanes = []
|
||||
container.onDidActivatePane(pane => activatedPanes.push(pane))
|
||||
|
||||
pane1.activate()
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
pane2.activate()
|
||||
expect(activatedPanes).toEqual([pane1, pane1, pane2, pane2])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::observePanes()', () => {
|
||||
it('invokes observers with all current and future panes', () => {
|
||||
const container = new PaneContainer(params)
|
||||
container.getRoot().splitRight()
|
||||
const [pane1, pane2] = container.getPanes()
|
||||
|
||||
const observed = []
|
||||
container.observePanes(pane => observed.push(pane))
|
||||
|
||||
const pane3 = pane2.splitDown()
|
||||
const pane4 = pane2.splitRight()
|
||||
|
||||
expect(observed).toEqual([pane1, pane2, pane3, pane4])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::observePaneItems()', () =>
|
||||
it('invokes observers with all current and future pane items', () => {
|
||||
const container = new PaneContainer(params)
|
||||
container.getRoot().addItems([{}, {}])
|
||||
container.getRoot().splitRight({items: [{}]})
|
||||
const pane2 = container.getPanes()[1]
|
||||
const observed = []
|
||||
container.observePaneItems(pane => observed.push(pane))
|
||||
|
||||
const pane3 = pane2.splitDown({items: [{}]})
|
||||
pane3.addItems([{}, {}])
|
||||
|
||||
expect(observed).toEqual(container.getPaneItems())
|
||||
})
|
||||
)
|
||||
|
||||
describe('::confirmClose()', () => {
|
||||
let container, pane1, pane2
|
||||
|
||||
beforeEach(() => {
|
||||
class TestItem {
|
||||
shouldPromptToSave () { return true }
|
||||
getURI () { return 'test' }
|
||||
}
|
||||
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().splitRight();
|
||||
[pane1, pane2] = container.getPanes()
|
||||
pane1.addItem(new TestItem())
|
||||
pane2.addItem(new TestItem())
|
||||
})
|
||||
|
||||
it('returns true if the user saves all modified files when prompted', async () => {
|
||||
confirm.andReturn(0)
|
||||
const saved = await container.confirmClose()
|
||||
expect(confirm).toHaveBeenCalled()
|
||||
expect(saved).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns false if the user cancels saving any modified file', async () => {
|
||||
confirm.andReturn(1)
|
||||
const saved = await container.confirmClose()
|
||||
expect(confirm).toHaveBeenCalled()
|
||||
expect(saved).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidAddPane(callback)', () => {
|
||||
it('invokes the given callback when panes are added', () => {
|
||||
const container = new PaneContainer(params)
|
||||
const events = []
|
||||
container.onDidAddPane((event) => {
|
||||
expect(container.getPanes().includes(event.pane)).toBe(true)
|
||||
events.push(event)
|
||||
})
|
||||
|
||||
const pane1 = container.getActivePane()
|
||||
const pane2 = pane1.splitRight()
|
||||
const pane3 = pane2.splitDown()
|
||||
|
||||
expect(events).toEqual([{pane: pane2}, {pane: pane3}])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onWillDestroyPane(callback)', () => {
|
||||
it('invokes the given callback before panes or their items are destroyed', () => {
|
||||
class TestItem {
|
||||
constructor () { this._isDestroyed = false }
|
||||
destroy () { this._isDestroyed = true }
|
||||
isDestroyed () { return this._isDestroyed }
|
||||
}
|
||||
|
||||
const container = new PaneContainer(params)
|
||||
const events = []
|
||||
container.onWillDestroyPane((event) => {
|
||||
const itemsDestroyed = event.pane.getItems().map((item) => item.isDestroyed())
|
||||
events.push([event, {itemsDestroyed}])
|
||||
})
|
||||
|
||||
const pane1 = container.getActivePane()
|
||||
const pane2 = pane1.splitRight()
|
||||
pane2.addItem(new TestItem())
|
||||
|
||||
pane2.destroy()
|
||||
|
||||
expect(events).toEqual([[{pane: pane2}, {itemsDestroyed: [false]}]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidDestroyPane(callback)', () => {
|
||||
it('invokes the given callback when panes are destroyed', () => {
|
||||
const container = new PaneContainer(params)
|
||||
const events = []
|
||||
container.onDidDestroyPane((event) => {
|
||||
expect(container.getPanes().includes(event.pane)).toBe(false)
|
||||
events.push(event)
|
||||
})
|
||||
|
||||
const pane1 = container.getActivePane()
|
||||
const pane2 = pane1.splitRight()
|
||||
const pane3 = pane2.splitDown()
|
||||
|
||||
pane2.destroy()
|
||||
pane3.destroy()
|
||||
|
||||
expect(events).toEqual([{pane: pane2}, {pane: pane3}])
|
||||
})
|
||||
|
||||
it('invokes the given callback when the container is destroyed', () => {
|
||||
const container = new PaneContainer(params)
|
||||
const events = []
|
||||
container.onDidDestroyPane((event) => {
|
||||
expect(container.getPanes().includes(event.pane)).toBe(false)
|
||||
events.push(event)
|
||||
})
|
||||
|
||||
const pane1 = container.getActivePane()
|
||||
const pane2 = pane1.splitRight()
|
||||
const pane3 = pane2.splitDown()
|
||||
|
||||
container.destroy()
|
||||
|
||||
expect(events).toEqual([{pane: pane1}, {pane: pane2}, {pane: pane3}])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onWillDestroyPaneItem() and ::onDidDestroyPaneItem', () => {
|
||||
it('invokes the given callbacks when an item will be destroyed on any pane', async () => {
|
||||
const container = new PaneContainer(params)
|
||||
const pane1 = container.getRoot()
|
||||
const item1 = {}
|
||||
const item2 = {}
|
||||
const item3 = {}
|
||||
|
||||
pane1.addItem(item1)
|
||||
const events = []
|
||||
container.onWillDestroyPaneItem(event => events.push(['will', event]))
|
||||
container.onDidDestroyPaneItem(event => events.push(['did', event]))
|
||||
const pane2 = pane1.splitRight({items: [item2, item3]})
|
||||
|
||||
await pane1.destroyItem(item1)
|
||||
await pane2.destroyItem(item3)
|
||||
await pane2.destroyItem(item2)
|
||||
|
||||
expect(events).toEqual([
|
||||
['will', {item: item1, pane: pane1, index: 0}],
|
||||
['did', {item: item1, pane: pane1, index: 0}],
|
||||
['will', {item: item3, pane: pane2, index: 1}],
|
||||
['did', {item: item3, pane: pane2, index: 1}],
|
||||
['will', {item: item2, pane: pane2, index: 0}],
|
||||
['did', {item: item2, pane: pane2, index: 0}]
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::saveAll()', () =>
|
||||
it('saves all modified pane items', async () => {
|
||||
const container = new PaneContainer(params)
|
||||
const pane1 = container.getRoot()
|
||||
pane1.splitRight()
|
||||
|
||||
const item1 = {
|
||||
saved: false,
|
||||
getURI () { return '' },
|
||||
isModified () { return true },
|
||||
save () { this.saved = true }
|
||||
}
|
||||
const item2 = {
|
||||
saved: false,
|
||||
getURI () { return '' },
|
||||
isModified () { return false },
|
||||
save () { this.saved = true }
|
||||
}
|
||||
const item3 = {
|
||||
saved: false,
|
||||
getURI () { return '' },
|
||||
isModified () { return true },
|
||||
save () { this.saved = true }
|
||||
}
|
||||
|
||||
pane1.addItem(item1)
|
||||
pane1.addItem(item2)
|
||||
pane1.addItem(item3)
|
||||
|
||||
container.saveAll()
|
||||
|
||||
expect(item1.saved).toBe(true)
|
||||
expect(item2.saved).toBe(false)
|
||||
expect(item3.saved).toBe(true)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::moveActiveItemToPane(destPane) and ::copyActiveItemToPane(destPane)', () => {
|
||||
let container, pane1, pane2, item1
|
||||
|
||||
beforeEach(() => {
|
||||
class TestItem {
|
||||
constructor (id) { this.id = id }
|
||||
copy () { return new TestItem(this.id) }
|
||||
}
|
||||
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
item1 = new TestItem('1')
|
||||
pane2 = pane1.splitRight({items: [item1]})
|
||||
})
|
||||
|
||||
describe('::::moveActiveItemToPane(destPane)', () =>
|
||||
it('moves active item to given pane and focuses it', () => {
|
||||
container.moveActiveItemToPane(pane1)
|
||||
expect(pane1.getActiveItem()).toBe(item1)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::::copyActiveItemToPane(destPane)', () =>
|
||||
it('copies active item to given pane and focuses it', () => {
|
||||
container.copyActiveItemToPane(pane1)
|
||||
expect(container.paneForItem(item1)).toBe(pane2)
|
||||
expect(pane1.getActiveItem().id).toBe(item1.id)
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -491,23 +491,31 @@ describe('Pane', () => {
|
||||
expect(pane.getActiveItem()).toBeUndefined()
|
||||
})
|
||||
|
||||
it('invokes ::onWillDestroyItem() observers before destroying the item', async () => {
|
||||
it('invokes ::onWillDestroyItem() and PaneContainer::onWillDestroyPaneItem observers before destroying the item', async () => {
|
||||
jasmine.useRealClock()
|
||||
|
||||
let handlerDidFinish = false
|
||||
pane.container = new PaneContainer({config: atom.config, confirm})
|
||||
const events = []
|
||||
|
||||
pane.onWillDestroyItem(async (event) => {
|
||||
expect(item2.isDestroyed()).toBe(false)
|
||||
events.push(event)
|
||||
await timeoutPromise(50)
|
||||
expect(item2.isDestroyed()).toBe(false)
|
||||
handlerDidFinish = true
|
||||
events.push(['will-destroy-item', event])
|
||||
})
|
||||
|
||||
pane.container.onWillDestroyPaneItem(async (event) => {
|
||||
expect(item2.isDestroyed()).toBe(false)
|
||||
await timeoutPromise(50)
|
||||
expect(item2.isDestroyed()).toBe(false)
|
||||
events.push(['will-destroy-pane-item', event])
|
||||
})
|
||||
|
||||
await pane.destroyItem(item2)
|
||||
expect(handlerDidFinish).toBe(true)
|
||||
expect(item2.isDestroyed()).toBe(true)
|
||||
expect(events).toEqual([{item: item2, index: 1}])
|
||||
expect(events).toEqual([
|
||||
['will-destroy-item', {item: item2, index: 1}],
|
||||
['will-destroy-pane-item', {item: item2, index: 1, pane}]
|
||||
])
|
||||
})
|
||||
|
||||
it('invokes ::onWillRemoveItem() observers', () => {
|
||||
|
||||
@@ -16,20 +16,47 @@ describe "Project", ->
|
||||
|
||||
describe "serialization", ->
|
||||
deserializedProject = null
|
||||
notQuittingProject = null
|
||||
quittingProject = null
|
||||
|
||||
afterEach ->
|
||||
deserializedProject?.destroy()
|
||||
notQuittingProject?.destroy()
|
||||
quittingProject?.destroy()
|
||||
|
||||
it "does not deserialize paths to non directories", ->
|
||||
it "does not deserialize paths to directories that don't exist", ->
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
state = atom.project.serialize()
|
||||
state.paths.push('/directory/that/does/not/exist')
|
||||
|
||||
err = null
|
||||
waitsForPromise ->
|
||||
deserializedProject.deserialize(state, atom.deserializers)
|
||||
.catch (e) -> err = e
|
||||
|
||||
runs ->
|
||||
expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths())
|
||||
expect(err.missingProjectPaths).toEqual ['/directory/that/does/not/exist']
|
||||
|
||||
it "does not deserialize paths that are now files", ->
|
||||
childPath = path.join(temp.mkdirSync('atom-spec-project'), 'child')
|
||||
fs.mkdirSync(childPath)
|
||||
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
atom.project.setPaths([childPath])
|
||||
state = atom.project.serialize()
|
||||
|
||||
fs.rmdirSync(childPath)
|
||||
fs.writeFileSync(childPath, 'surprise!\n')
|
||||
|
||||
err = null
|
||||
waitsForPromise ->
|
||||
deserializedProject.deserialize(state, atom.deserializers)
|
||||
.catch (e) -> err = e
|
||||
|
||||
runs ->
|
||||
expect(deserializedProject.getPaths()).toEqual([])
|
||||
expect(err.missingProjectPaths).toEqual [childPath]
|
||||
|
||||
it "does not include unretained buffers in the serialized state", ->
|
||||
waitsForPromise ->
|
||||
@@ -62,7 +89,7 @@ describe "Project", ->
|
||||
deserializedProject.getBuffers()[0].destroy()
|
||||
expect(deserializedProject.getBuffers().length).toBe 0
|
||||
|
||||
it "does not deserialize buffers when their path is a directory that exists", ->
|
||||
it "does not deserialize buffers when their path is now a directory", ->
|
||||
pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
|
||||
|
||||
waitsForPromise ->
|
||||
@@ -72,7 +99,11 @@ describe "Project", ->
|
||||
expect(atom.project.getBuffers().length).toBe 1
|
||||
fs.mkdirSync(pathToOpen)
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
|
||||
waitsForPromise ->
|
||||
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
|
||||
|
||||
runs ->
|
||||
expect(deserializedProject.getBuffers().length).toBe 0
|
||||
|
||||
it "does not deserialize buffers when their path is inaccessible", ->
|
||||
@@ -87,12 +118,53 @@ describe "Project", ->
|
||||
expect(atom.project.getBuffers().length).toBe 1
|
||||
fs.chmodSync(pathToOpen, '000')
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
|
||||
waitsForPromise ->
|
||||
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
|
||||
|
||||
runs ->
|
||||
expect(deserializedProject.getBuffers().length).toBe 0
|
||||
|
||||
it "serializes marker layers and history only if Atom is quitting", ->
|
||||
it "does not deserialize buffers with their path is no longer present", ->
|
||||
pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
|
||||
fs.writeFileSync(pathToOpen, '')
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('a')
|
||||
atom.workspace.open(pathToOpen)
|
||||
|
||||
runs ->
|
||||
expect(atom.project.getBuffers().length).toBe 1
|
||||
fs.unlinkSync(pathToOpen)
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
|
||||
waitsForPromise ->
|
||||
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
|
||||
|
||||
runs ->
|
||||
expect(deserializedProject.getBuffers().length).toBe 0
|
||||
|
||||
it "deserializes buffers that have never been saved before", ->
|
||||
pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open(pathToOpen)
|
||||
|
||||
runs ->
|
||||
atom.workspace.getActiveTextEditor().setText('unsaved\n')
|
||||
expect(atom.project.getBuffers().length).toBe 1
|
||||
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
|
||||
waitsForPromise ->
|
||||
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
|
||||
|
||||
runs ->
|
||||
expect(deserializedProject.getBuffers().length).toBe 1
|
||||
expect(deserializedProject.getBuffers()[0].getPath()).toBe pathToOpen
|
||||
expect(deserializedProject.getBuffers()[0].getText()).toBe 'unsaved\n'
|
||||
|
||||
it "serializes marker layers and history only if Atom is quitting", ->
|
||||
waitsForPromise -> atom.workspace.open('a')
|
||||
|
||||
bufferA = null
|
||||
layerA = null
|
||||
@@ -103,18 +175,20 @@ describe "Project", ->
|
||||
layerA = bufferA.addMarkerLayer(persistent: true)
|
||||
markerA = layerA.markPosition([0, 3])
|
||||
bufferA.append('!')
|
||||
|
||||
waitsForPromise ->
|
||||
notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
notQuittingProject.deserialize(atom.project.serialize({isUnloading: false})).then ->
|
||||
expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined()
|
||||
expect(notQuittingProject.getBuffers()[0].undo()).toBe(false)
|
||||
|
||||
waitsForPromise ->
|
||||
waitsForPromise -> notQuittingProject.deserialize(atom.project.serialize({isUnloading: false}))
|
||||
|
||||
runs ->
|
||||
expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined()
|
||||
expect(notQuittingProject.getBuffers()[0].undo()).toBe(false)
|
||||
quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
quittingProject.deserialize(atom.project.serialize({isUnloading: true})).then ->
|
||||
expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).not.toBeUndefined()
|
||||
expect(quittingProject.getBuffers()[0].undo()).toBe(true)
|
||||
|
||||
waitsForPromise -> quittingProject.deserialize(atom.project.serialize({isUnloading: true}))
|
||||
|
||||
runs ->
|
||||
expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).not.toBeUndefined()
|
||||
expect(quittingProject.getBuffers()[0].undo()).toBe(true)
|
||||
|
||||
describe "when an editor is saved and the project has no path", ->
|
||||
it "sets the project's path to the saved file's parent directory", ->
|
||||
@@ -411,9 +485,9 @@ describe "Project", ->
|
||||
runs ->
|
||||
expect(repository.isDestroyed()).toBe(false)
|
||||
|
||||
describe ".setPaths(paths)", ->
|
||||
describe ".setPaths(paths, options)", ->
|
||||
describe "when path is a file", ->
|
||||
it "sets its path to the files parent directory and updates the root directory", ->
|
||||
it "sets its path to the file's parent directory and updates the root directory", ->
|
||||
filePath = require.resolve('./fixtures/dir/a')
|
||||
atom.project.setPaths([filePath])
|
||||
expect(atom.project.getPaths()[0]).toEqual path.dirname(filePath)
|
||||
@@ -448,6 +522,17 @@ describe "Project", ->
|
||||
expect(onDidChangePathsSpy.callCount).toBe 1
|
||||
expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths)
|
||||
|
||||
it "optionally throws an error with any paths that did not exist", ->
|
||||
paths = [temp.mkdirSync("exists0"), "/doesnt-exists/0", temp.mkdirSync("exists1"), "/doesnt-exists/1"]
|
||||
|
||||
try
|
||||
atom.project.setPaths paths, mustExist: true
|
||||
expect('no exception thrown').toBeUndefined()
|
||||
catch e
|
||||
expect(e.missingProjectPaths).toEqual [paths[1], paths[3]]
|
||||
|
||||
expect(atom.project.getPaths()).toEqual [paths[0], paths[2]]
|
||||
|
||||
describe "when no paths are given", ->
|
||||
it "clears its path", ->
|
||||
atom.project.setPaths([])
|
||||
@@ -459,7 +544,7 @@ describe "Project", ->
|
||||
expect(atom.project.getPaths()[0]).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
|
||||
expect(atom.project.getDirectories()[0].path).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
|
||||
|
||||
describe ".addPath(path)", ->
|
||||
describe ".addPath(path, options)", ->
|
||||
it "calls callbacks registered with ::onDidChangePaths", ->
|
||||
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
|
||||
atom.project.onDidChangePaths(onDidChangePathsSpy)
|
||||
@@ -498,6 +583,11 @@ describe "Project", ->
|
||||
atom.project.addPath('/this-definitely/does-not-exist')
|
||||
expect(atom.project.getPaths()).toEqual(previousPaths)
|
||||
|
||||
it "optionally throws on non-existent directories", ->
|
||||
expect ->
|
||||
atom.project.addPath '/this-definitely/does-not-exist', mustExist: true
|
||||
.toThrow()
|
||||
|
||||
describe ".removePath(path)", ->
|
||||
onDidChangePathsSpy = null
|
||||
|
||||
|
||||
@@ -76,6 +76,15 @@ describe "TextEditor", ->
|
||||
expect(editor2.displayLayer.tabLength).toBe(editor2.getTabLength())
|
||||
expect(editor2.displayLayer.softWrapColumn).toBe(editor2.getSoftWrapColumn())
|
||||
|
||||
it "ignores buffers with retired IDs", ->
|
||||
editor2 = TextEditor.deserialize(editor.serialize(), {
|
||||
assert: atom.assert,
|
||||
textEditors: atom.textEditors,
|
||||
project: {bufferForIdSync: -> null}
|
||||
})
|
||||
|
||||
expect(editor2).toBeNull()
|
||||
|
||||
describe "when the editor is constructed with the largeFileMode option set to true", ->
|
||||
it "loads the editor but doesn't tokenize", ->
|
||||
editor = null
|
||||
|
||||
@@ -831,6 +831,9 @@ class AtomEnvironment extends Model
|
||||
|
||||
# Essential: A flexible way to open a dialog akin to an alert dialog.
|
||||
#
|
||||
# If the dialog is closed (via `Esc` key or `X` in the top corner) without selecting a button
|
||||
# the first button will be clicked unless a "Cancel" or "No" button is provided.
|
||||
#
|
||||
# ## Examples
|
||||
#
|
||||
# ```coffee
|
||||
@@ -848,7 +851,7 @@ class AtomEnvironment extends Model
|
||||
# * `buttons` (optional) Either an array of strings or an object where keys are
|
||||
# button names and the values are callbacks to invoke when clicked.
|
||||
#
|
||||
# Returns the chosen button index {Number} if the buttons option was an array.
|
||||
# Returns the chosen button index {Number} if the buttons option is an array or the return value of the callback if the buttons option is an object.
|
||||
confirm: (params={}) ->
|
||||
@applicationDelegate.confirm(params)
|
||||
|
||||
@@ -1002,11 +1005,18 @@ class AtomEnvironment extends Model
|
||||
|
||||
@setFullScreen(state.fullScreen)
|
||||
|
||||
missingProjectPaths = []
|
||||
|
||||
@packages.packageStates = state.packageStates ? {}
|
||||
|
||||
startTime = Date.now()
|
||||
if state.project?
|
||||
projectPromise = @project.deserialize(state.project, @deserializers)
|
||||
.catch (err) =>
|
||||
if err.missingProjectPaths?
|
||||
missingProjectPaths.push(err.missingProjectPaths...)
|
||||
else
|
||||
@notifications.addError "Unable to deserialize project", description: err.message, stack: err.stack
|
||||
else
|
||||
projectPromise = Promise.resolve()
|
||||
|
||||
@@ -1019,6 +1029,19 @@ class AtomEnvironment extends Model
|
||||
@workspace.deserialize(state.workspace, @deserializers) if state.workspace?
|
||||
@deserializeTimings.workspace = Date.now() - startTime
|
||||
|
||||
if missingProjectPaths.length > 0
|
||||
count = if missingProjectPaths.length is 1 then '' else missingProjectPaths.length + ' '
|
||||
noun = if missingProjectPaths.length is 1 then 'directory' else 'directories'
|
||||
toBe = if missingProjectPaths.length is 1 then 'is' else 'are'
|
||||
escaped = missingProjectPaths.map (projectPath) -> "`#{projectPath}`"
|
||||
group = switch escaped.length
|
||||
when 1 then escaped[0]
|
||||
when 2 then "#{escaped[0]} and #{escaped[1]}"
|
||||
else escaped[..-2].join(", ") + ", and #{escaped[escaped.length - 1]}"
|
||||
|
||||
@notifications.addError "Unable to open #{count}project #{noun}",
|
||||
description: "Project #{noun} #{group} #{toBe} no longer on disk."
|
||||
|
||||
getStateKey: (paths) ->
|
||||
if paths?.length > 0
|
||||
sha1 = crypto.createHash('sha1').update(paths.slice().sort().join("\n")).digest('hex')
|
||||
|
||||
1003
src/pane.coffee
1003
src/pane.coffee
File diff suppressed because it is too large
Load Diff
1252
src/pane.js
Normal file
1252
src/pane.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,8 @@ class Project extends Model
|
||||
@repositoryProviders = [new GitRepositoryProvider(this, config)]
|
||||
@loadPromisesByPath = {}
|
||||
@watcherPromisesByPath = {}
|
||||
@retiredBufferIDs = new Set()
|
||||
@retiredBufferPaths = new Set()
|
||||
@consumeServices(packageManager)
|
||||
|
||||
destroyed: ->
|
||||
@@ -47,6 +49,8 @@ class Project extends Model
|
||||
@buffers = []
|
||||
@setPaths([])
|
||||
@loadPromisesByPath = {}
|
||||
@retiredBufferIDs = new Set()
|
||||
@retiredBufferPaths = new Set()
|
||||
@consumeServices(packageManager)
|
||||
|
||||
destroyUnretainedBuffers: ->
|
||||
@@ -58,21 +62,28 @@ class Project extends Model
|
||||
###
|
||||
|
||||
deserialize: (state) ->
|
||||
bufferPromises = []
|
||||
for bufferState in state.buffers
|
||||
continue if fs.isDirectorySync(bufferState.filePath)
|
||||
if bufferState.filePath
|
||||
try
|
||||
fs.closeSync(fs.openSync(bufferState.filePath, 'r'))
|
||||
catch error
|
||||
continue unless error.code is 'ENOENT'
|
||||
unless bufferState.shouldDestroyOnFileDelete?
|
||||
bufferState.shouldDestroyOnFileDelete = ->
|
||||
atom.config.get('core.closeDeletedFileTabs')
|
||||
bufferPromises.push(TextBuffer.deserialize(bufferState))
|
||||
Promise.all(bufferPromises).then (@buffers) =>
|
||||
@retiredBufferIDs = new Set()
|
||||
@retiredBufferPaths = new Set()
|
||||
|
||||
handleBufferState = (bufferState) =>
|
||||
bufferState.shouldDestroyOnFileDelete ?= -> atom.config.get('core.closeDeletedFileTabs')
|
||||
|
||||
# Use a little guilty knowledge of the way TextBuffers are serialized.
|
||||
# This allows TextBuffers that have never been saved (but have filePaths) to be deserialized, but prevents
|
||||
# TextBuffers backed by files that have been deleted from being saved.
|
||||
bufferState.mustExist = bufferState.digestWhenLastPersisted isnt false
|
||||
|
||||
TextBuffer.deserialize(bufferState).catch (err) =>
|
||||
@retiredBufferIDs.add(bufferState.id)
|
||||
@retiredBufferPaths.add(bufferState.filePath)
|
||||
null
|
||||
|
||||
bufferPromises = (handleBufferState(bufferState) for bufferState in state.buffers)
|
||||
|
||||
Promise.all(bufferPromises).then (buffers) =>
|
||||
@buffers = buffers.filter(Boolean)
|
||||
@subscribeToBuffer(buffer) for buffer in @buffers
|
||||
@setPaths(state.paths)
|
||||
@setPaths(state.paths or [], mustExist: true, exact: true)
|
||||
|
||||
serialize: (options={}) ->
|
||||
deserializer: 'Project'
|
||||
@@ -211,7 +222,12 @@ class Project extends Model
|
||||
# Public: Set the paths of the project's directories.
|
||||
#
|
||||
# * `projectPaths` {Array} of {String} paths.
|
||||
setPaths: (projectPaths) ->
|
||||
# * `options` An optional {Object} that may contain the following keys:
|
||||
# * `mustExist` If `true`, throw an Error if any `projectPaths` do not exist. Any remaining `projectPaths` that
|
||||
# do exist will still be added to the project. Default: `false`.
|
||||
# * `exact` If `true`, only add a `projectPath` if it names an existing directory. If `false` and any `projectPath`
|
||||
# is a file or does not exist, its parent directory will be added instead. Default: `false`.
|
||||
setPaths: (projectPaths, options = {}) ->
|
||||
repository?.destroy() for repository in @repositories
|
||||
@rootDirectories = []
|
||||
@repositories = []
|
||||
@@ -219,16 +235,46 @@ class Project extends Model
|
||||
watcher.then((w) -> w.dispose()) for _, watcher in @watcherPromisesByPath
|
||||
@watcherPromisesByPath = {}
|
||||
|
||||
@addPath(projectPath, emitEvent: false) for projectPath in projectPaths
|
||||
missingProjectPaths = []
|
||||
for projectPath in projectPaths
|
||||
try
|
||||
@addPath projectPath, emitEvent: false, mustExist: true, exact: options.exact is true
|
||||
catch e
|
||||
if e.missingProjectPaths?
|
||||
missingProjectPaths.push e.missingProjectPaths...
|
||||
else
|
||||
throw e
|
||||
|
||||
@emitter.emit 'did-change-paths', projectPaths
|
||||
|
||||
if options.mustExist is true and missingProjectPaths.length > 0
|
||||
err = new Error "One or more project directories do not exist"
|
||||
err.missingProjectPaths = missingProjectPaths
|
||||
throw err
|
||||
|
||||
# Public: Add a path to the project's list of root paths
|
||||
#
|
||||
# * `projectPath` {String} The path to the directory to add.
|
||||
addPath: (projectPath, options) ->
|
||||
# * `options` An optional {Object} that may contain the following keys:
|
||||
# * `mustExist` If `true`, throw an Error if the `projectPath` does not exist. If `false`, a `projectPath` that does
|
||||
# not exist is ignored. Default: `false`.
|
||||
# * `exact` If `true`, only add `projectPath` if it names an existing directory. If `false`, if `projectPath` is a
|
||||
# a file or does not exist, its parent directory will be added instead.
|
||||
addPath: (projectPath, options = {}) ->
|
||||
directory = @getDirectoryForProjectPath(projectPath)
|
||||
return unless directory.existsSync()
|
||||
|
||||
ok = true
|
||||
ok = ok and directory.getPath() is projectPath if options.exact is true
|
||||
ok = ok and directory.existsSync()
|
||||
|
||||
unless ok
|
||||
if options.mustExist is true
|
||||
err = new Error "Project directory #{directory} does not exist"
|
||||
err.missingProjectPaths = [projectPath]
|
||||
throw err
|
||||
else
|
||||
return
|
||||
|
||||
for existingDirectory in @getDirectories()
|
||||
return if existingDirectory.getPath() is directory.getPath()
|
||||
|
||||
@@ -248,7 +294,7 @@ class Project extends Model
|
||||
break if repo = provider.repositoryForDirectorySync?(directory)
|
||||
@repositories.push(repo ? null)
|
||||
|
||||
unless options?.emitEvent is false
|
||||
unless options.emitEvent is false
|
||||
@emitter.emit 'did-change-paths', @getPaths()
|
||||
|
||||
getDirectoryForProjectPath: (projectPath) ->
|
||||
@@ -412,11 +458,13 @@ class Project extends Model
|
||||
# Only to be used in specs
|
||||
bufferForPathSync: (filePath) ->
|
||||
absoluteFilePath = @resolvePath(filePath)
|
||||
return null if @retiredBufferPaths.has absoluteFilePath
|
||||
existingBuffer = @findBufferForPath(absoluteFilePath) if filePath
|
||||
existingBuffer ? @buildBufferSync(absoluteFilePath)
|
||||
|
||||
# Only to be used when deserializing
|
||||
bufferForIdSync: (id) ->
|
||||
return null if @retiredBufferIDs.has id
|
||||
existingBuffer = @findBufferForId(id) if id
|
||||
existingBuffer ? @buildBufferSync()
|
||||
|
||||
|
||||
@@ -128,7 +128,10 @@ class TextEditor extends Model
|
||||
state.tokenizedBuffer = state.displayBuffer.tokenizedBuffer
|
||||
|
||||
try
|
||||
state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment)
|
||||
tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment)
|
||||
return null unless tokenizedBuffer?
|
||||
|
||||
state.tokenizedBuffer = tokenizedBuffer
|
||||
state.tabLength = state.tokenizedBuffer.getTabLength()
|
||||
catch error
|
||||
if error.syscall is 'read'
|
||||
|
||||
@@ -23,11 +23,15 @@ class TokenizedBuffer extends Model
|
||||
changeCount: 0
|
||||
|
||||
@deserialize: (state, atomEnvironment) ->
|
||||
buffer = null
|
||||
if state.bufferId
|
||||
state.buffer = atomEnvironment.project.bufferForIdSync(state.bufferId)
|
||||
buffer = atomEnvironment.project.bufferForIdSync(state.bufferId)
|
||||
else
|
||||
# TODO: remove this fallback after everyone transitions to the latest version.
|
||||
state.buffer = atomEnvironment.project.bufferForPathSync(state.bufferPath)
|
||||
buffer = atomEnvironment.project.bufferForPathSync(state.bufferPath)
|
||||
return null unless buffer?
|
||||
|
||||
state.buffer = buffer
|
||||
state.assert = atomEnvironment.assert
|
||||
new this(state)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user