diff --git a/spec/atom-environment-spec.js b/spec/atom-environment-spec.js index d90001205..5e9617374 100644 --- a/spec/atom-environment-spec.js +++ b/spec/atom-environment-spec.js @@ -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]) diff --git a/src/atom-environment.js b/src/atom-environment.js index 03871ab74..98a89fe45 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -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') } diff --git a/src/main-process/atom-application.js b/src/main-process/atom-application.js index 04f7fba5c..f6ada6137 100644 --- a/src/main-process/atom-application.js +++ b/src/main-process/atom-application.js @@ -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