Merge pull request #13963 from atom/ku-mkt-restore-atom-env-when-adding-folder-to-fresh-window

Restore atom environment when adding project folders to a fresh window
This commit is contained in:
Michelle Tilley 2017-03-27 10:56:37 -07:00 committed by GitHub
commit 1ff5c9e684
7 changed files with 287 additions and 57 deletions

View File

@ -65,6 +65,7 @@
"sinon": "1.17.4",
"source-map-support": "^0.3.2",
"temp": "0.8.1",
"test-until": "^1.1.1",
"text-buffer": "11.4.0",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",

View File

@ -1,5 +1,7 @@
/** @babel */
import until from 'test-until'
export function beforeEach (fn) {
global.beforeEach(function () {
const result = fn()
@ -60,3 +62,9 @@ function waitsForPromise (fn) {
})
})
}
export function emitterEventPromise (emitter, event, timeout = 5000) {
let emitted = false
emitter.once(event, () => { emitted = true })
return until(`${event} is emitted`, () => emitted, timeout)
}

View File

@ -321,14 +321,6 @@ describe "AtomEnvironment", ->
expect(atom.workspace.open).not.toHaveBeenCalled()
describe "adding a project folder", ->
it "adds a second path to the project", ->
initialPaths = atom.project.getPaths()
tempDirectory = temp.mkdirSync("a-new-directory")
spyOn(atom, "pickFolder").andCallFake (callback) ->
callback([tempDirectory])
atom.addProjectFolder()
expect(atom.project.getPaths()).toEqual(initialPaths.concat([tempDirectory]))
it "does nothing if the user dismisses the file picker", ->
initialPaths = atom.project.getPaths()
tempDirectory = temp.mkdirSync("a-new-directory")
@ -336,6 +328,106 @@ describe "AtomEnvironment", ->
atom.addProjectFolder()
expect(atom.project.getPaths()).toEqual(initialPaths)
describe "when there is no saved state for the added folders", ->
beforeEach ->
spyOn(atom, 'loadState').andReturn(Promise.resolve(null))
spyOn(atom, 'attemptRestoreProjectStateForPaths')
it "adds the selected folder to the project", ->
initialPaths = atom.project.setPaths([])
tempDirectory = temp.mkdirSync("a-new-directory")
spyOn(atom, "pickFolder").andCallFake (callback) ->
callback([tempDirectory])
waitsForPromise ->
atom.addProjectFolder()
runs ->
expect(atom.project.getPaths()).toEqual([tempDirectory])
expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled()
describe "when there is saved state for the relevant directories", ->
state = Symbol('savedState')
beforeEach ->
spyOn(atom, "getStateKey").andCallFake (dirs) -> dirs.join(':')
spyOn(atom, "loadState").andCallFake (key) ->
if key is __dirname then Promise.resolve(state) else Promise.resolve(null)
spyOn(atom, "attemptRestoreProjectStateForPaths")
spyOn(atom, "pickFolder").andCallFake (callback) ->
callback([__dirname])
atom.project.setPaths([])
describe "when there are no project folders", ->
it "attempts to restore the project state", ->
waitsForPromise ->
atom.addProjectFolder()
runs ->
expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname])
expect(atom.project.getPaths()).toEqual([])
describe "when there are already project folders", ->
openedPath = path.join(__dirname, 'fixtures')
beforeEach ->
atom.project.setPaths([openedPath])
it "does not attempt to restore the project state, instead adding the project paths", ->
waitsForPromise ->
atom.addProjectFolder()
runs ->
expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled()
expect(atom.project.getPaths()).toEqual([openedPath, __dirname])
describe "attemptRestoreProjectStateForPaths(state, projectPaths, filesToOpen)", ->
describe "when the window is clean (empty or has only unnamed, unmodified buffers)", ->
beforeEach ->
# Unnamed, unmodified buffer doesn't count toward "clean"-ness
waitsForPromise -> atom.workspace.open()
it "automatically restores the saved state into the current environment", ->
state = Symbol()
spyOn(atom.workspace, 'open')
spyOn(atom, 'restoreStateIntoThisEnvironment')
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.restoreStateIntoThisEnvironment).toHaveBeenCalledWith(state)
expect(atom.workspace.open.callCount).toBe(1)
expect(atom.workspace.open).toHaveBeenCalledWith(__filename)
describe "when the window is dirty", ->
editor = null
beforeEach ->
waitsForPromise -> atom.workspace.open().then (e) ->
editor = e
editor.setText('new editor')
it "prompts the user to restore the state in a new window, discarding it and adding folder to current window", ->
spyOn(atom, "confirm").andReturn(1)
spyOn(atom.project, 'addPath')
spyOn(atom.workspace, 'open')
state = Symbol()
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.confirm).toHaveBeenCalled()
expect(atom.project.addPath.callCount).toBe(1)
expect(atom.project.addPath).toHaveBeenCalledWith(__dirname)
expect(atom.workspace.open.callCount).toBe(1)
expect(atom.workspace.open).toHaveBeenCalledWith(__filename)
it "prompts the user to restore the state in a new window, opening a new window", ->
spyOn(atom, "confirm").andReturn(0)
spyOn(atom, "open")
state = Symbol()
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.confirm).toHaveBeenCalled()
expect(atom.open).toHaveBeenCalledWith
pathsToOpen: [__dirname, __filename]
newWindow: true
devMode: atom.inDevMode()
safeMode: atom.inSafeMode()
describe "::unloadEditorWindow()", ->
it "saves the BlobStore so it can be loaded after reload", ->
configDirPath = temp.mkdirSync('atom-spec-environment')
@ -371,44 +463,91 @@ describe "AtomEnvironment", ->
spyOn(atom.workspace, 'open')
atom.project.setPaths([])
describe "when the opened path exists", ->
it "adds it to the project's paths", ->
pathToOpen = __filename
atom.openLocations([{pathToOpen}])
expect(atom.project.getPaths()[0]).toBe __dirname
describe "when there is no saved state", ->
beforeEach ->
spyOn(atom, "loadState").andReturn(Promise.resolve(null))
describe "then a second path is opened with forceAddToWindow", ->
it "adds the second path to the project's paths", ->
firstPathToOpen = __dirname
secondPathToOpen = path.resolve(__dirname, './fixtures')
atom.openLocations([{pathToOpen: firstPathToOpen}])
atom.openLocations([{pathToOpen: secondPathToOpen, forceAddToWindow: true}])
expect(atom.project.getPaths()).toEqual([firstPathToOpen, secondPathToOpen])
describe "when the opened path exists", ->
it "adds it to the project's paths", ->
pathToOpen = __filename
waitsForPromise -> atom.openLocations([{pathToOpen}])
runs -> expect(atom.project.getPaths()[0]).toBe __dirname
describe "when the opened path does not exist but its parent directory does", ->
it "adds the parent directory to the project paths", ->
pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt')
atom.openLocations([{pathToOpen}])
expect(atom.project.getPaths()[0]).toBe __dirname
describe "then a second path is opened with forceAddToWindow", ->
it "adds the second path to the project's paths", ->
firstPathToOpen = __dirname
secondPathToOpen = path.resolve(__dirname, './fixtures')
waitsForPromise -> atom.openLocations([{pathToOpen: firstPathToOpen}])
waitsForPromise -> atom.openLocations([{pathToOpen: secondPathToOpen, forceAddToWindow: true}])
runs -> expect(atom.project.getPaths()).toEqual([firstPathToOpen, secondPathToOpen])
describe "when the opened path is a file", ->
it "opens it in the workspace", ->
pathToOpen = __filename
atom.openLocations([{pathToOpen}])
expect(atom.workspace.open.mostRecentCall.args[0]).toBe __filename
describe "when the opened path does not exist but its parent directory does", ->
it "adds the parent directory to the project paths", ->
pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt')
waitsForPromise -> atom.openLocations([{pathToOpen}])
runs -> expect(atom.project.getPaths()[0]).toBe __dirname
describe "when the opened path is a directory", ->
it "does not open it in the workspace", ->
pathToOpen = __dirname
atom.openLocations([{pathToOpen}])
expect(atom.workspace.open.callCount).toBe 0
describe "when the opened path is a file", ->
it "opens it in the workspace", ->
pathToOpen = __filename
waitsForPromise -> atom.openLocations([{pathToOpen}])
runs -> expect(atom.workspace.open.mostRecentCall.args[0]).toBe __filename
describe "when the opened path is a uri", ->
it "adds it to the project's paths as is", ->
pathToOpen = 'remote://server:7644/some/dir/path'
spyOn(atom.project, 'addPath')
atom.openLocations([{pathToOpen}])
expect(atom.project.addPath).toHaveBeenCalledWith(pathToOpen)
describe "when the opened path is a directory", ->
it "does not open it in the workspace", ->
pathToOpen = __dirname
waitsForPromise -> atom.openLocations([{pathToOpen}])
runs -> expect(atom.workspace.open.callCount).toBe 0
describe "when the opened path is a uri", ->
it "adds it to the project's paths as is", ->
pathToOpen = 'remote://server:7644/some/dir/path'
spyOn(atom.project, 'addPath')
waitsForPromise -> atom.openLocations([{pathToOpen}])
runs -> expect(atom.project.addPath).toHaveBeenCalledWith(pathToOpen)
describe "when there is saved state for the relevant directories", ->
state = Symbol('savedState')
beforeEach ->
spyOn(atom, "getStateKey").andCallFake (dirs) -> dirs.join(':')
spyOn(atom, "loadState").andCallFake (key) ->
if key is __dirname then Promise.resolve(state) else Promise.resolve(null)
spyOn(atom, "attemptRestoreProjectStateForPaths")
describe "when there are no project folders", ->
it "attempts to restore the project state", ->
pathToOpen = __dirname
waitsForPromise -> atom.openLocations([{pathToOpen}])
runs ->
expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [pathToOpen], [])
expect(atom.project.getPaths()).toEqual([])
it "opens the specified files", ->
waitsForPromise -> atom.openLocations([{pathToOpen: __dirname}, {pathToOpen: __filename}])
runs ->
expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname], [__filename])
expect(atom.project.getPaths()).toEqual([])
describe "when there are already project folders", ->
beforeEach ->
atom.project.setPaths([__dirname])
it "does not attempt to restore the project state, instead adding the project paths", ->
pathToOpen = path.join(__dirname, 'fixtures')
waitsForPromise -> atom.openLocations([{pathToOpen, forceAddToWindow: true}])
runs ->
expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled()
expect(atom.project.getPaths()).toEqual([__dirname, pathToOpen])
it "opens the specified files", ->
pathToOpen = path.join(__dirname, 'fixtures')
fileToOpen = path.join(pathToOpen, 'michelle-is-awesome.txt')
waitsForPromise -> atom.openLocations([{pathToOpen}, {pathToOpen: fileToOpen}])
runs ->
expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalledWith(state, [pathToOpen], [fileToOpen])
expect(atom.project.getPaths()).toEqual([__dirname])
describe "::updateAvailable(info) (called via IPC from browser process)", ->
subscription = null

View File

@ -7,7 +7,7 @@ import fs from 'fs-plus'
import path from 'path'
import AtomApplication from '../../src/main-process/atom-application'
import parseCommandLine from '../../src/main-process/parse-command-line'
import {timeoutPromise, conditionPromise} from '../async-spec-helpers'
import {timeoutPromise, conditionPromise, emitterEventPromise} from '../async-spec-helpers'
const ATOM_RESOURCE_PATH = path.resolve(__dirname, '..', '..')
@ -121,6 +121,7 @@ describe('AtomApplication', function () {
const atomApplication = buildAtomApplication()
const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath, 'new-file')]))
await emitterEventPromise(window1, 'window:locations-opened')
await focusWindow(window1)
let activeEditorPath
@ -146,6 +147,7 @@ describe('AtomApplication', function () {
// Opens new windows when opening directories
const window2 = atomApplication.launch(parseCommandLine([dirCPath]))
await emitterEventPromise(window2, 'window:locations-opened')
assert.notEqual(window2, window1)
await focusWindow(window2)
assert.deepEqual(await getTreeViewRootDirectories(window2), [dirCPath])
@ -365,6 +367,9 @@ describe('AtomApplication', function () {
const atomApplication2 = buildAtomApplication()
const [app2Window1, app2Window2] = atomApplication2.launch(parseCommandLine([]))
const p1 = emitterEventPromise(app2Window1, 'window:locations-opened', 15000)
const p2 = emitterEventPromise(app2Window2, 'window:locations-opened', 15000)
await Promise.all([p1, p2])
await app2Window1.loadedPromise
await app2Window2.loadedPromise
@ -420,6 +425,7 @@ describe('AtomApplication', function () {
const atomApplication = buildAtomApplication()
const window = atomApplication.launch(parseCommandLine([dirA, dirB]))
await emitterEventPromise(window, 'window:locations-opened', 15000)
await focusWindow(window)
assert.deepEqual(await getTreeViewRootDirectories(window), [dirA, dirB])

View File

@ -894,7 +894,51 @@ class AtomEnvironment extends Model
addProjectFolder: ->
@pickFolder (selectedPaths = []) =>
@project.addPath(selectedPath) for selectedPath in selectedPaths
@addToProject(selectedPaths)
addToProject: (projectPaths) ->
@loadState(@getStateKey(projectPaths)).then (state) =>
if state and @project.getPaths().length is 0
@attemptRestoreProjectStateForPaths(state, projectPaths)
else
@project.addPath(folder) for folder in projectPaths
attemptRestoreProjectStateForPaths: (state, projectPaths, filesToOpen = []) ->
paneItemIsEmptyUnnamedTextEditor = (item) ->
return false unless item instanceof TextEditor
return false if item.getPath() or item.isModified()
true
windowIsUnused = @workspace.getPaneItems().every(paneItemIsEmptyUnnamedTextEditor)
if windowIsUnused
@restoreStateIntoThisEnvironment(state)
Promise.all (@workspace.open(file) for file in filesToOpen)
else
nouns = if projectPaths.length is 1 then 'folder' else 'folders'
btn = @confirm
message: 'Previous automatically-saved project state detected'
detailedMessage: "There is previously saved state for the selected #{nouns}. " +
"Would you like to add the #{nouns} to this window, permanently discarding the saved state, " +
"or open the #{nouns} in a new window, restoring the saved state?"
buttons: [
'Open in new window and recover state'
'Add to this window and discard state'
]
if btn is 0
@open
pathsToOpen: projectPaths.concat(filesToOpen)
newWindow: true
devMode: @inDevMode()
safeMode: @inSafeMode()
Promise.resolve(null)
else if btn is 1
@project.addPath(selectedPath) for selectedPath in projectPaths
Promise.all (@workspace.open(file) for file in filesToOpen)
restoreStateIntoThisEnvironment: (state) ->
state.fullScreen = @isFullScreen()
pane.destroy() for pane in @workspace.getPanes()
@deserialize(state)
showSaveDialog: (callback) ->
callback(@showSaveDialogSync())
@ -902,12 +946,12 @@ class AtomEnvironment extends Model
showSaveDialogSync: (options={}) ->
@applicationDelegate.showSaveDialog(options)
saveState: (options) ->
saveState: (options, storageKey) ->
new Promise (resolve, reject) =>
if @enablePersistence and @project
state = @serialize(options)
savePromise =
if storageKey = @getStateKey(@project?.getPaths())
if storageKey ?= @getStateKey(@project?.getPaths())
@stateStore.save(storageKey, state)
else
@applicationDelegate.setTemporaryWindowState(state)
@ -915,9 +959,9 @@ class AtomEnvironment extends Model
else
resolve()
loadState: ->
loadState: (stateKey) ->
if @enablePersistence
if stateKey = @getStateKey(@getLoadSettings().initialPaths)
if stateKey ?= @getStateKey(@getLoadSettings().initialPaths)
@stateStore.load(stateKey).then (state) =>
if state
state
@ -1004,19 +1048,45 @@ class AtomEnvironment extends Model
openLocations: (locations) ->
needsProjectPaths = @project?.getPaths().length is 0
foldersToAddToProject = []
fileLocationsToOpen = []
pushFolderToOpen = (folder) ->
if folder not in foldersToAddToProject
foldersToAddToProject.push(folder)
for {pathToOpen, initialLine, initialColumn, forceAddToWindow} in locations
if pathToOpen? and (needsProjectPaths or forceAddToWindow)
if fs.existsSync(pathToOpen)
@project.addPath(pathToOpen)
pushFolderToOpen @project.getDirectoryForProjectPath(pathToOpen).getPath()
else if fs.existsSync(path.dirname(pathToOpen))
@project.addPath(path.dirname(pathToOpen))
pushFolderToOpen @project.getDirectoryForProjectPath(path.dirname(pathToOpen)).getPath()
else
@project.addPath(pathToOpen)
pushFolderToOpen @project.getDirectoryForProjectPath(pathToOpen).getPath()
unless fs.isDirectorySync(pathToOpen)
@workspace?.open(pathToOpen, {initialLine, initialColumn})
fileLocationsToOpen.push({pathToOpen, initialLine, initialColumn})
return
promise = Promise.resolve(null)
if foldersToAddToProject.length > 0
promise = @loadState(@getStateKey(foldersToAddToProject)).then (state) =>
if state and needsProjectPaths # only load state if this is the first path added to the project
files = (location.pathToOpen for location in fileLocationsToOpen)
@attemptRestoreProjectStateForPaths(state, foldersToAddToProject, files)
else
promises = []
@project.addPath(folder) for folder in foldersToAddToProject
for {pathToOpen, initialLine, initialColumn} in fileLocationsToOpen
promises.push @workspace?.open(pathToOpen, {initialLine, initialColumn})
Promise.all(promises)
else
promises = []
for {pathToOpen, initialLine, initialColumn} in fileLocationsToOpen
promises.push @workspace?.open(pathToOpen, {initialLine, initialColumn})
promise = Promise.all(promises)
promise.then ->
ipcRenderer.send 'window-command', 'window:locations-opened'
resolveProxy: (url) ->
return new Promise (resolve, reject) =>

View File

@ -89,6 +89,9 @@ class AtomWindow
@emit 'window:loaded'
@resolveLoadedPromise()
@browserWindow.on 'window:locations-opened', =>
@emit 'window:locations-opened'
@browserWindow.on 'enter-full-screen', =>
@browserWindow.webContents.send('did-enter-full-screen')

View File

@ -184,11 +184,7 @@ class Project extends Model
#
# * `projectPath` {String} The path to the directory to add.
addPath: (projectPath, options) ->
directory = null
for provider in @directoryProviders
break if directory = provider.directoryForURISync?(projectPath)
directory ?= @defaultDirectoryProvider.directoryForURISync(projectPath)
directory = @getDirectoryForProjectPath(projectPath)
return unless directory.existsSync()
for existingDirectory in @getDirectories()
return if existingDirectory.getPath() is directory.getPath()
@ -203,6 +199,13 @@ class Project extends Model
unless options?.emitEvent is false
@emitter.emit 'did-change-paths', @getPaths()
getDirectoryForProjectPath: (projectPath) ->
directory = null
for provider in @directoryProviders
break if directory = provider.directoryForURISync?(projectPath)
directory ?= @defaultDirectoryProvider.directoryForURISync(projectPath)
directory
# Public: remove a path from the project's list of root paths.
#
# * `projectPath` {String} The path to remove.