Merge remote-tracking branch 'origin/master' into mkt-url-based-command-dispatch

This commit is contained in:
Michelle Tilley
2017-09-20 20:33:44 -07:00
13 changed files with 2000 additions and 1465 deletions

View File

@@ -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",

View File

@@ -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 ->

View File

@@ -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
View 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)
})
)
})
})

View File

@@ -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', () => {

View File

@@ -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

View File

@@ -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

View File

@@ -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')

File diff suppressed because it is too large Load Diff

1252
src/pane.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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()

View File

@@ -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'

View File

@@ -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)