Merge pull request #18742 from atom/aw/deleted-project-folder

Improve behavior when restoring session that references a missing project folder
This commit is contained in:
Ash Wilson 2019-01-24 18:11:29 -05:00 committed by GitHub
commit 6055f289ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 112 additions and 7 deletions

View File

@ -669,6 +669,28 @@ describe('AtomEnvironment', () => {
expect(atom.workspace.getTextEditors().map(e => e.getPath())).toEqual([pathToOpen])
expect(atom.project.getPaths()).toEqual([])
})
it('may be required to be an existing directory', async () => {
spyOn(atom.notifications, 'addWarning')
const nonExistent = path.join(__dirname, 'no')
const existingFile = __filename
const existingDir = path.join(__dirname, 'fixtures')
await atom.openLocations([
{pathToOpen: nonExistent, mustBeDirectory: true},
{pathToOpen: existingFile, mustBeDirectory: true},
{pathToOpen: existingDir, mustBeDirectory: true}
])
expect(atom.workspace.getTextEditors()).toEqual([])
expect(atom.project.getPaths()).toEqual([existingDir])
expect(atom.notifications.addWarning).toHaveBeenCalledWith(
'Unable to open project folders',
{description: `The directories \`${nonExistent}\` and \`${existingFile}\` do not exist.`}
)
})
})
describe('when the opened path is handled by a registered directory provider', () => {
@ -720,6 +742,27 @@ describe('AtomEnvironment', () => {
expect(atom.project.getPaths()).toEqual([])
})
it('includes missing mandatory project folders in computation of initial state key', async () => {
const existingDir = path.join(__dirname, 'fixtures')
const missingDir = path.join(__dirname, 'no')
atom.loadState.andCallFake(function (key) {
if (key === `${existingDir}:${missingDir}`) {
return Promise.resolve(state)
} else {
return Promise.resolve(null)
}
})
await atom.openLocations([
{pathToOpen: existingDir},
{pathToOpen: missingDir, mustBeDirectory: true}
])
expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [existingDir], [])
expect(atom.project.getPaths(), [existingDir])
})
it('opens the specified files', async () => {
await atom.openLocations([{pathToOpen: __dirname}, {pathToOpen: __filename}])
expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname], [__filename])

View File

@ -1364,6 +1364,7 @@ or use Pane::saveItemAs for programmatic saving.`)
const needsProjectPaths = this.project && this.project.getPaths().length === 0
const foldersToAddToProject = new Set()
const fileLocationsToOpen = []
const missingFolders = []
// Asynchronously fetch stat information about each requested path to open.
const locationStats = await Promise.all(
@ -1387,8 +1388,13 @@ or use Pane::saveItemAs for programmatic saving.`)
// Directory: add as a project folder
foldersToAddToProject.add(this.project.getDirectoryForProjectPath(pathToOpen).getPath())
} else if (stats.isFile()) {
// File: add as a file location
fileLocationsToOpen.push(location)
if (location.mustBeDirectory) {
// File: no longer a directory
missingFolders.push(location)
} else {
// File: add as a file location
fileLocationsToOpen.push(location)
}
}
} else {
// Path does not exist
@ -1397,6 +1403,9 @@ or use Pane::saveItemAs for programmatic saving.`)
if (directory) {
// Found: add as a project folder
foldersToAddToProject.add(directory.getPath())
} else if (location.mustBeDirectory) {
// Not found and must be a directory: add to missing list and use to derive state key
missingFolders.push(location)
} else {
// Not found: open as a new file
fileLocationsToOpen.push(location)
@ -1407,8 +1416,12 @@ or use Pane::saveItemAs for programmatic saving.`)
}
let restoredState = false
if (foldersToAddToProject.size > 0) {
const state = await this.loadState(this.getStateKey(Array.from(foldersToAddToProject)))
if (foldersToAddToProject.size > 0 || missingFolders.length > 0) {
// Include missing folders in the state key so that sessions restored with no-longer-present project root folders
// don't lose data.
const foldersForStateKey = Array.from(foldersToAddToProject)
.concat(missingFolders.map(location => location.pathToOpen))
const state = await this.loadState(this.getStateKey(Array.from(foldersForStateKey)))
// only restore state if this is the first path added to the project
if (state && needsProjectPaths) {
@ -1430,6 +1443,33 @@ or use Pane::saveItemAs for programmatic saving.`)
await Promise.all(fileOpenPromises)
}
if (missingFolders.length > 0) {
let message = 'Unable to open project folder'
if (missingFolders.length > 1) {
message += 's'
}
let description = 'The '
if (missingFolders.length === 1) {
description += 'directory `'
description += missingFolders[0].pathToOpen
description += '` does not exist.'
} else if (missingFolders.length === 2) {
description += `directories \`${missingFolders[0].pathToOpen}\` `
description += `and \`${missingFolders[1].pathToOpen}\` do not exist.`
} else {
description += 'directories '
description += (missingFolders
.slice(0, -1)
.map(location => location.pathToOpen)
.map(pathToOpen => '`' + pathToOpen + '`, ')
.join(''))
description += 'and `' + missingFolders[missingFolders.length - 1].pathToOpen + '` do not exist.'
}
this.notifications.addWarning(message, {description})
}
ipcRenderer.send('window-command', 'window:locations-opened')
}

View File

@ -210,6 +210,7 @@ class AtomApplication extends EventEmitter {
const {
pathsToOpen,
executedFrom,
foldersToOpen,
urlsToOpen,
benchmark,
benchmarkTest,
@ -248,9 +249,10 @@ class AtomApplication extends EventEmitter {
timeout,
env
})
} else if (pathsToOpen.length > 0) {
} else if ((pathsToOpen && pathsToOpen.length > 0) || (foldersToOpen && foldersToOpen.length > 0)) {
return this.openPaths({
pathsToOpen,
foldersToOpen,
executedFrom,
pidToKillWhenClosed,
devMode,
@ -806,6 +808,7 @@ class AtomApplication extends EventEmitter {
//
// options -
// :pathsToOpen - The array of file paths to open
// :foldersToOpen - An array of additional paths to open that must be existing directories
// :pidToKillWhenClosed - The integer of the pid to kill
// :devMode - Boolean to control the opened window's dev mode.
// :safeMode - Boolean to control the opened window's safe mode.
@ -814,6 +817,7 @@ class AtomApplication extends EventEmitter {
// :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPaths ({
pathsToOpen,
foldersToOpen,
executedFrom,
pidToKillWhenClosed,
devMode,
@ -825,8 +829,10 @@ class AtomApplication extends EventEmitter {
addToLastWindow,
env
} = {}) {
if (!pathsToOpen || pathsToOpen.length === 0) return
if (!env) env = process.env
if (!pathsToOpen) pathsToOpen = []
if (!foldersToOpen) foldersToOpen = []
devMode = Boolean(devMode)
safeMode = Boolean(safeMode)
clearWindowState = Boolean(clearWindowState)
@ -837,6 +843,22 @@ class AtomApplication extends EventEmitter {
hasWaitSession: pidToKillWhenClosed != null
})
})
for (const folderToOpen of foldersToOpen) {
locationsToOpen.push({
pathToOpen: folderToOpen,
initialLine: null,
initialColumn: null,
mustBeDirectory: true,
forceAddToWindow: addToLastWindow,
hasWaitSession: pidToKillWhenClosed != null
})
}
if (locationsToOpen.length === 0) {
return
}
const normalizedPathsToOpen = locationsToOpen.map(location => location.pathToOpen).filter(Boolean)
let existingWindow
@ -966,7 +988,7 @@ class AtomApplication extends EventEmitter {
const states = await this.storageFolder.load('application.json')
if (states) {
return states.map(state => ({
pathsToOpen: state.initialPaths,
foldersToOpen: state.initialPaths,
urlsToOpen: [],
devMode: this.devMode,
safeMode: this.safeMode