Skip to content

Commit 9e80a72

Browse files
committed
Merge pull request atom#19207 from atom/aw/versioned-application-state
Only store directories in serialized application state
1 parent 8f75956 commit 9e80a72

File tree

6 files changed

+144
-72
lines changed

6 files changed

+144
-72
lines changed

spec/atom-environment-spec.js

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -336,57 +336,34 @@ describe('AtomEnvironment', () => {
336336
})
337337

338338
describe('deserialization failures', () => {
339-
it('propagates project state restoration failures', async () => {
339+
it('propagates unrecognized project state restoration failures', async () => {
340+
let err
340341
spyOn(atom.project, 'deserialize').andCallFake(() => {
341-
const err = new Error('deserialization failure')
342-
err.missingProjectPaths = ['/foo']
343-
return Promise.reject(err)
344-
})
345-
spyOn(atom.notifications, 'addError')
346-
347-
await atom.deserialize({ project: 'should work' })
348-
expect(atom.notifications.addError).toHaveBeenCalledWith(
349-
'Unable to open project directory',
350-
{
351-
description: 'Project directory `/foo` is no longer on disk.'
352-
}
353-
)
354-
})
355-
356-
it('accumulates and reports two errors with one notification', async () => {
357-
spyOn(atom.project, 'deserialize').andCallFake(() => {
358-
const err = new Error('deserialization failure')
359-
err.missingProjectPaths = ['/foo', '/wat']
342+
err = new Error('deserialization failure')
360343
return Promise.reject(err)
361344
})
362345
spyOn(atom.notifications, 'addError')
363346

364347
await atom.deserialize({ project: 'should work' })
365348
expect(atom.notifications.addError).toHaveBeenCalledWith(
366-
'Unable to open 2 project directories',
349+
'Unable to deserialize project',
367350
{
368-
description:
369-
'Project directories `/foo` and `/wat` are no longer on disk.'
351+
description: 'deserialization failure',
352+
stack: err.stack
370353
}
371354
)
372355
})
373356

374-
it('accumulates and reports three+ errors with one notification', async () => {
357+
it('disregards missing project folder errors', async () => {
375358
spyOn(atom.project, 'deserialize').andCallFake(() => {
376359
const err = new Error('deserialization failure')
377-
err.missingProjectPaths = ['/foo', '/wat', '/stuff', '/things']
360+
err.missingProjectPaths = ['nah']
378361
return Promise.reject(err)
379362
})
380363
spyOn(atom.notifications, 'addError')
381364

382365
await atom.deserialize({ project: 'should work' })
383-
expect(atom.notifications.addError).toHaveBeenCalledWith(
384-
'Unable to open 4 project directories',
385-
{
386-
description:
387-
'Project directories `/foo`, `/wat`, `/stuff`, and `/things` are no longer on disk.'
388-
}
389-
)
366+
expect(atom.notifications.addError).not.toHaveBeenCalled()
390367
})
391368
})
392369
})

spec/main-process/atom-application.test.js

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,13 @@ describe('AtomApplication', function () {
100100

101101
beforeEach(function () {
102102
app = scenario.addApplication({
103-
applicationJson: [
104-
{ initialPaths: ['b'] },
105-
{ initialPaths: ['c'] }
106-
]
103+
applicationJson: {
104+
version: '1',
105+
windows: [
106+
{ projectRoots: [scenario.convertRootPath('b')] },
107+
{ projectRoots: [scenario.convertRootPath('c')] }
108+
]
109+
}
107110
})
108111
})
109112

@@ -161,12 +164,12 @@ describe('AtomApplication', function () {
161164

162165
it('restores windows when launched with a project path to open', async function () {
163166
await scenario.launch({ app, pathsToOpen: ['a'] })
164-
await scenario.assert('[a _] [b _] [c _]')
167+
await scenario.assert('[b _] [c _] [a _]')
165168
})
166169

167170
it('restores windows when launched with a file path to open', async function () {
168171
await scenario.launch({ app, pathsToOpen: ['a/1.md'] })
169-
await scenario.assert('[b 1.md] [c _]')
172+
await scenario.assert('[b _] [c 1.md]')
170173
})
171174

172175
it('collapses new paths into restored windows when appropriate', async function () {
@@ -186,6 +189,33 @@ describe('AtomApplication', function () {
186189
})
187190
})
188191
})
192+
193+
describe('with unversioned application state', function () {
194+
it('reads "initialPaths" as project roots', async function () {
195+
const app = scenario.addApplication({
196+
applicationJson: [
197+
{initialPaths: [scenario.convertRootPath('a')]},
198+
{initialPaths: [scenario.convertRootPath('b'), scenario.convertRootPath('c')]}
199+
]
200+
})
201+
app.config.set('core.restorePreviousWindowsOnStart', 'always')
202+
203+
await scenario.launch({app})
204+
await scenario.assert('[a _] [b,c _]')
205+
})
206+
207+
it('filters file paths from project root lists', async function () {
208+
const app = scenario.addApplication({
209+
applicationJson: [
210+
{initialPaths: [scenario.convertRootPath('b'), scenario.convertEditorPath('a/1.md')]}
211+
]
212+
})
213+
app.config.set('core.restorePreviousWindowsOnStart', 'always')
214+
215+
await scenario.launch({app})
216+
await scenario.assert('[b _]')
217+
})
218+
})
189219
})
190220

191221
describe('with one empty window', function () {
@@ -855,10 +885,13 @@ describe('AtomApplication', function () {
855885

856886
assert.isTrue(scenario.getApplication(0).storageFolder.store.calledWith(
857887
'application.json',
858-
[
859-
{ initialPaths: [scenario.convertRootPath('a')] },
860-
{ initialPaths: [scenario.convertRootPath('b'), scenario.convertRootPath('c')] }
861-
]
888+
{
889+
version: '1',
890+
windows: [
891+
{ projectRoots: [scenario.convertRootPath('a')] },
892+
{ projectRoots: [scenario.convertRootPath('b'), scenario.convertRootPath('c')] }
893+
]
894+
}
862895
))
863896
})
864897

@@ -872,9 +905,12 @@ describe('AtomApplication', function () {
872905

873906
assert.isTrue(scenario.getApplication(0).storageFolder.store.calledWith(
874907
'application.json',
875-
[
876-
{ initialPaths: [scenario.convertRootPath('a')] }
877-
]
908+
{
909+
version: '1',
910+
windows: [
911+
{ projectRoots: [scenario.convertRootPath('a')] }
912+
]
913+
}
878914
))
879915
})
880916

@@ -1338,9 +1374,7 @@ class LaunchScenario {
13381374
return newWindow
13391375
})
13401376
this.sinon.stub(app.storageFolder, 'load', () => Promise.resolve(
1341-
(options.applicationJson || []).map(each => ({
1342-
initialPaths: this.convertPaths(each.initialPaths)
1343-
}))
1377+
options.applicationJson || {version: '1', windows: []}
13441378
))
13451379
this.sinon.stub(app.storageFolder, 'store', () => Promise.resolve())
13461380
this.applications.add(app)

spec/main-process/atom-window.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ describe('AtomWindow', function () {
190190
]
191191

192192
const w = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow, locationsToOpen })
193+
assert.deepEqual(w.projectRoots, ['/directory'])
193194

194195
const loadPromise = emitterEventPromise(w, 'window:loaded')
195196
w.browserWindow.emit('window:loaded')
@@ -258,6 +259,24 @@ describe('AtomWindow', function () {
258259
assert.isTrue(w.hasProjectPaths())
259260
})
260261

262+
it('is updated synchronously by openLocations', async function () {
263+
const locationsToOpen = [
264+
{ pathToOpen: 'file.txt', isFile: true },
265+
{ pathToOpen: 'directory1', isDirectory: true },
266+
{ pathToOpen: 'directory0', isDirectory: true },
267+
{ pathToOpen: 'directory0', isDirectory: true },
268+
{ pathToOpen: 'new-file.txt' }
269+
]
270+
271+
const w = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow })
272+
assert.deepEqual(w.projectRoots, [])
273+
274+
const promise = w.openLocations(locationsToOpen)
275+
assert.deepEqual(w.projectRoots, ['directory0', 'directory1'])
276+
w.resolveLoadedPromise()
277+
await promise
278+
})
279+
261280
it('is updated by setProjectRoots', function () {
262281
const locationsToOpen = [
263282
{ pathToOpen: 'directory0', exists: true, isDirectory: true }

src/atom-environment.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,9 +1247,8 @@ or use Pane::saveItemAs for programmatic saving.`)
12471247
try {
12481248
await this.project.deserialize(state.project, this.deserializers)
12491249
} catch (error) {
1250-
if (error.missingProjectPaths) {
1251-
missingProjectPaths.push(...error.missingProjectPaths)
1252-
} else {
1250+
// We handle the missingProjectPaths case in openLocations().
1251+
if (!error.missingProjectPaths) {
12531252
this.notifications.addError('Unable to deserialize project', {
12541253
description: error.message,
12551254
stack: error.stack
@@ -1268,7 +1267,7 @@ or use Pane::saveItemAs for programmatic saving.`)
12681267

12691268
if (missingProjectPaths.length > 0) {
12701269
const count = missingProjectPaths.length === 1 ? '' : missingProjectPaths.length + ' '
1271-
const noun = missingProjectPaths.length === 1 ? 'directory' : 'directories'
1270+
const noun = missingProjectPaths.length === 1 ? 'folder' : 'folders'
12721271
const toBe = missingProjectPaths.length === 1 ? 'is' : 'are'
12731272
const escaped = missingProjectPaths.map(projectPath => `\`${projectPath}\``)
12741273
let group

src/main-process/atom-application.js

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ const ConfigSchema = require('../config-schema')
2323

2424
const LocationSuffixRegExp = /(:\d+)(:\d+)?$/
2525

26+
// Increment this when changing the serialization format of `${ATOM_HOME}/storage/application.json` used by
27+
// AtomApplication::saveCurrentWindowOptions() and AtomApplication::loadPreviousWindowOptions() in a backward-
28+
// incompatible way.
29+
const APPLICATION_STATE_VERSION = '1'
30+
2631
const getDefaultPath = () => {
2732
const editor = atom.workspace.getActiveTextEditor()
2833
if (!editor || !editor.getPath()) {
@@ -247,8 +252,7 @@ class AtomApplication extends EventEmitter {
247252
this.config.onDidChange('core.colorProfile', () => this.promptForRestart())
248253
}
249254

250-
const optionsForWindowsToOpen = []
251-
255+
let optionsForWindowsToOpen = []
252256
let shouldReopenPreviousWindows = false
253257

254258
if (options.test || options.benchmark || options.benchmarkTest) {
@@ -264,9 +268,7 @@ class AtomApplication extends EventEmitter {
264268
}
265269

266270
if (shouldReopenPreviousWindows) {
267-
for (const previousOptions of await this.loadPreviousWindowOptions()) {
268-
optionsForWindowsToOpen.push(previousOptions)
269-
}
271+
optionsForWindowsToOpen = [...await this.loadPreviousWindowOptions(), ...optionsForWindowsToOpen]
270272
}
271273

272274
if (optionsForWindowsToOpen.length === 0) {
@@ -1139,27 +1141,58 @@ class AtomApplication extends EventEmitter {
11391141
async saveCurrentWindowOptions (allowEmpty = false) {
11401142
if (this.quitting) return
11411143

1142-
const states = []
1143-
for (let window of this.getAllWindows()) {
1144-
if (!window.isSpec) states.push({initialPaths: window.projectRoots})
1144+
const state = {
1145+
version: APPLICATION_STATE_VERSION,
1146+
windows: this.getAllWindows()
1147+
.filter(window => !window.isSpec)
1148+
.map(window => ({projectRoots: window.projectRoots}))
11451149
}
1146-
states.reverse()
1150+
state.windows.reverse()
11471151

1148-
if (states.length > 0 || allowEmpty) {
1149-
await this.storageFolder.store('application.json', states)
1152+
if (state.windows.length > 0 || allowEmpty) {
1153+
await this.storageFolder.store('application.json', state)
11501154
this.emit('application:did-save-state')
11511155
}
11521156
}
11531157

11541158
async loadPreviousWindowOptions () {
1155-
const states = await this.storageFolder.load('application.json')
1156-
if (states) {
1157-
return states.map(state => ({
1158-
foldersToOpen: state.initialPaths,
1159+
const state = await this.storageFolder.load('application.json')
1160+
if (!state) {
1161+
return []
1162+
}
1163+
1164+
if (state.version === APPLICATION_STATE_VERSION) {
1165+
// Atom >=1.36.1
1166+
// Schema: {version: '1', windows: [{projectRoots: ['<root-dir>', ...]}, ...]}
1167+
return state.windows.map(each => ({
1168+
foldersToOpen: each.projectRoots,
11591169
devMode: this.devMode,
1160-
safeMode: this.safeMode
1170+
safeMode: this.safeMode,
1171+
newWindow: true
11611172
}))
1173+
} else if (state.version === undefined) {
1174+
// Atom <= 1.36.0
1175+
// Schema: [{initialPaths: ['<root-dir>', ...]}, ...]
1176+
return await Promise.all(
1177+
state.map(async windowState => {
1178+
// Classify each window's initialPaths as directories or non-directories
1179+
const classifiedPaths = await Promise.all(
1180+
windowState.initialPaths.map(initialPath => new Promise(resolve => {
1181+
fs.isDirectory(initialPath, isDir => resolve({initialPath, isDir}))
1182+
}))
1183+
)
1184+
1185+
// Only accept initialPaths that are existing directories
1186+
return {
1187+
foldersToOpen: classifiedPaths.filter(({isDir}) => isDir).map(({initialPath}) => initialPath),
1188+
devMode: this.devMode,
1189+
safeMode: this.safeMode,
1190+
newWindow: true
1191+
}
1192+
})
1193+
)
11621194
} else {
1195+
// Unrecognized version (from a newer Atom?)
11631196
return []
11641197
}
11651198
}

src/main-process/atom-window.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,7 @@ class AtomWindow extends EventEmitter {
7272
if (this.loadSettings.safeMode == null) this.loadSettings.safeMode = false
7373
if (this.loadSettings.clearWindowState == null) this.loadSettings.clearWindowState = false
7474

75-
this.projectRoots = locationsToOpen
76-
.filter(location => location.pathToOpen && location.exists && location.isDirectory)
77-
.map(location => location.pathToOpen)
78-
this.projectRoots.sort()
75+
this.addLocationsToOpen(locationsToOpen)
7976

8077
this.loadSettings.hasOpenFiles = locationsToOpen
8178
.some(location => location.pathToOpen && !location.isDirectory)
@@ -240,6 +237,7 @@ class AtomWindow extends EventEmitter {
240237
}
241238

242239
async openLocations (locationsToOpen) {
240+
this.addLocationsToOpen(locationsToOpen)
243241
await this.loadedPromise
244242
this.sendMessage('open-locations', locationsToOpen)
245243
}
@@ -252,6 +250,18 @@ class AtomWindow extends EventEmitter {
252250
this.sendMessage('did-fail-to-read-user-settings', message)
253251
}
254252

253+
addLocationsToOpen (locationsToOpen) {
254+
const roots = new Set(this.projectRoots || [])
255+
for (const {pathToOpen, isDirectory} of locationsToOpen) {
256+
if (isDirectory) {
257+
roots.add(pathToOpen)
258+
}
259+
}
260+
261+
this.projectRoots = Array.from(roots)
262+
this.projectRoots.sort()
263+
}
264+
255265
replaceEnvironment (env) {
256266
this.browserWindow.webContents.send('environment', env)
257267
}

0 commit comments

Comments
 (0)