Merge pull request #6380 from atom/mb-reopen-windows

Reopen windows when starting app w/ no arguments
This commit is contained in:
Max Brunsfeld
2015-04-15 15:50:22 -07:00
7 changed files with 166 additions and 83 deletions

View File

@@ -139,7 +139,7 @@ describe "the `atom` global", ->
windowState: null
spyOn(Atom, 'getLoadSettings').andCallFake -> loadSettings
spyOn(Atom, 'getStorageDirPath').andReturn(temp.mkdirSync("storage-dir-"))
spyOn(Atom.getStorageFolder(), 'getPath').andReturn(temp.mkdirSync("storage-dir-"))
atom.mode = "editor"
atom.state.stuff = "cool"

View File

@@ -94,6 +94,15 @@ buildAtomClient = (args, env) ->
]), env: extend({}, process.env, env))
done()
.addCommand "dispatchCommand", (command, done) ->
@execute "atom.commands.dispatch(document.activeElement, '#{command}')"
.call(done)
.addCommand "simulateQuit", (done) ->
@execute -> atom.unloadEditorWindow()
.execute -> require("remote").require("app").emit("before-quit")
.call(done)
module.exports = (args, env, fn) ->
[chromedriver, chromedriverLogs, chromedriverExit] = []
@@ -119,6 +128,7 @@ module.exports = (args, env, fn) ->
waitsFor("webdriver to finish", (done) ->
finish = once ->
client
.simulateQuit()
.end()
.then(-> chromedriver.kill())
.then(chromedriverExit.then(
@@ -133,7 +143,7 @@ module.exports = (args, env, fn) ->
client = buildAtomClient(args, env)
client.on "error", (err) ->
jasmine.getEnv().currentSpec.fail(JSON.stringify(err))
jasmine.getEnv().currentSpec.fail(new Error(err.response?.body?.value?.message))
finish()
fn(client.init()).then(finish)

View File

@@ -6,22 +6,22 @@ return unless process.env.ATOM_INTEGRATION_TESTS_ENABLED
fs = require "fs"
path = require "path"
temp = require("temp").track()
AtomHome = temp.mkdirSync('atom-home')
fs.writeFileSync(path.join(AtomHome, 'config.cson'), fs.readFileSync(path.join(__dirname, 'fixtures', 'atom-home', 'config.cson')))
runAtom = require("./helpers/start-atom")
describe "Starting Atom", ->
[tempDirPath, otherTempDirPath] = []
[tempDirPath, otherTempDirPath, atomHome] = []
beforeEach ->
jasmine.useRealClock()
atomHome = temp.mkdirSync('atom-home')
fs.writeFileSync(path.join(atomHome, 'config.cson'), fs.readFileSync(path.join(__dirname, 'fixtures', 'atom-home', 'config.cson')))
tempDirPath = temp.mkdirSync("empty-dir")
otherTempDirPath = temp.mkdirSync("another-temp-dir")
describe "opening a new file", ->
it "opens the parent directory and creates an empty text editor", ->
runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: AtomHome}, (client) ->
runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: atomHome}, (client) ->
client
.waitForWindowCount(1, 1000)
.waitForExist("atom-workspace", 5000)
@@ -36,13 +36,14 @@ describe "Starting Atom", ->
.keys("Hello!")
.execute -> atom.workspace.getActiveTextEditor().getText()
.then ({value}) -> expect(value).toBe "Hello!"
.dispatchCommand("editor:delete-line")
describe "when there is already a window open", ->
it "reuses that window when opening files, but not when opening directories", ->
tempFilePath = path.join(temp.mkdirSync("a-third-dir"), "a-file")
fs.writeFileSync(tempFilePath, "This file was already here.")
runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: AtomHome}, (client) ->
runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: atomHome}, (client) ->
client
.waitForWindowCount(1, 1000)
.waitForExist("atom-workspace", 5000)
@@ -50,7 +51,7 @@ describe "Starting Atom", ->
# Opening another file reuses the same window and does not change the
# project paths.
.startAnotherAtom([tempFilePath], ATOM_HOME: AtomHome)
.startAnotherAtom([tempFilePath], ATOM_HOME: atomHome)
.waitForPaneItemCount(2, 5000)
.waitForWindowCount(1, 1000)
.treeViewRootDirectories()
@@ -60,7 +61,7 @@ describe "Starting Atom", ->
# Opening another directory creates a second window.
.waitForNewWindow(->
@startAnotherAtom([otherTempDirPath], ATOM_HOME: AtomHome)
@startAnotherAtom([otherTempDirPath], ATOM_HOME: atomHome)
, 5000)
.waitForExist("atom-workspace", 5000)
.waitForPaneItemCount(0, 1000)
@@ -69,15 +70,14 @@ describe "Starting Atom", ->
describe "reopening a directory that was previously opened", ->
it "remembers the state of the window", ->
runAtom [tempDirPath], {ATOM_HOME: AtomHome}, (client) ->
runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) ->
client
.waitForExist("atom-workspace", 5000)
.waitForPaneItemCount(0, 3000)
.execute -> atom.workspace.open()
.waitForPaneItemCount(1, 3000)
.execute -> atom.unloadEditorWindow()
runAtom [tempDirPath], {ATOM_HOME: AtomHome}, (client) ->
runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) ->
client
.waitForExist("atom-workspace", 5000)
.waitForPaneItemCount(1, 5000)
@@ -87,7 +87,7 @@ describe "Starting Atom", ->
nestedDir = path.join(otherTempDirPath, "nested-dir")
fs.mkdirSync(nestedDir)
runAtom [tempDirPath, otherTempDirPath], {ATOM_HOME: AtomHome}, (client) ->
runAtom [tempDirPath, otherTempDirPath], {ATOM_HOME: atomHome}, (client) ->
client
.waitForExist("atom-workspace", 5000)
.treeViewRootDirectories()
@@ -95,42 +95,67 @@ describe "Starting Atom", ->
# Opening one of those directories again reuses the same window and
# does not change the project paths.
.startAnotherAtom([nestedDir], ATOM_HOME: AtomHome)
.startAnotherAtom([nestedDir], ATOM_HOME: atomHome)
.waitForExist("atom-workspace", 5000)
.treeViewRootDirectories()
.then ({value}) -> expect(value).toEqual([tempDirPath, otherTempDirPath])
describe "when there is an existing window with no project path", ->
describe "opening a directory", ->
it "opens the directory in the existing window", ->
runAtom [], {ATOM_HOME: AtomHome}, (client) ->
client
.waitForExist("atom-workspace")
it "reuses that window to open a directory", ->
runAtom [], {ATOM_HOME: atomHome}, (client) ->
client
.waitForExist("atom-workspace")
.treeViewRootDirectories()
.then ({value}) -> expect(value).toEqual([])
.startAnotherAtom([tempDirPath], ATOM_HOME: atomHome)
.waitUntil(->
@treeViewRootDirectories()
.then ({value}) -> value[0] is tempDirPath
, 5000)
.then (result) -> expect(result).toBe(true)
.waitForWindowCount(1, 5000)
describe "launching with no path", ->
it "opens a new window with a single untitled buffer", ->
runAtom [], {ATOM_HOME: atomHome}, (client) ->
client
.waitForExist("atom-workspace")
.waitForPaneItemCount(1, 5000)
# Opening with no file paths always creates a new window, even if
# existing windows have no project paths.
.waitForNewWindow(->
@startAnotherAtom([], ATOM_HOME: atomHome)
, 5000)
.waitForExist("atom-workspace")
.waitForPaneItemCount(1, 5000)
it "reopens any previously opened windows", ->
runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) ->
client
.waitForExist("atom-workspace")
.waitForNewWindow(->
@startAnotherAtom([otherTempDirPath], ATOM_HOME: atomHome)
, 5000)
.waitForExist("atom-workspace")
runAtom [], {ATOM_HOME: atomHome}, (client) ->
windowProjectPaths = []
client
.waitForWindowCount(2, 10000)
.then ({value: windowHandles}) ->
@window(windowHandles[0])
.treeViewRootDirectories()
.then ({value}) -> expect(value).toEqual([])
.then ({value: directories}) -> windowProjectPaths.push(directories)
.startAnotherAtom([tempDirPath], ATOM_HOME: AtomHome)
.waitUntil(->
@treeViewRootDirectories()
.then ({value}) -> value[0] is tempDirPath
, 5000)
.then (result) -> expect(result).toBe(true)
.waitForWindowCount(1, 5000)
.window(windowHandles[1])
.treeViewRootDirectories()
.then ({value: directories}) -> windowProjectPaths.push(directories)
describe "launching with no path", ->
it "always opens a new window with a single untitled buffer", ->
runAtom [], {ATOM_HOME: AtomHome}, (client) ->
client
.waitForExist("atom-workspace")
.waitForPaneItemCount(1, 5000)
runAtom [], {ATOM_HOME: AtomHome}, (client) ->
client
.waitForExist("atom-workspace")
.waitForPaneItemCount(1, 5000)
# Opening with no file paths always creates a new window, even if
# existing windows have no project paths.
.waitForNewWindow(->
@startAnotherAtom([], ATOM_HOME: AtomHome)
, 5000)
.call ->
expect(windowProjectPaths.sort()).toEqual [
[tempDirPath]
[otherTempDirPath]
].sort()

View File

@@ -14,6 +14,7 @@ Model = require './model'
{$} = require './space-pen-extensions'
WindowEventHandler = require './window-event-handler'
StylesElement = require './styles-element'
StorageFolder = require './storage-folder'
# Essential: Atom global for dealing with packages, themes, menus, and the window.
#
@@ -73,34 +74,24 @@ class Atom extends Model
# Loads and returns the serialized state corresponding to this window
# if it exists; otherwise returns undefined.
@loadState: (mode) ->
statePath = @getStatePath(@getLoadSettings().initialPaths, mode)
if stateKey = @getStateKey(@getLoadSettings().initialPaths, mode)
if state = @getStorageFolder().load(stateKey)
return state
if fs.existsSync(statePath)
if windowState = @getLoadSettings().windowState
try
stateString = fs.readFileSync(statePath, 'utf8')
JSON.parse(@getLoadSettings().windowState)
catch error
console.warn "Error reading window state: #{statePath}", error.stack, error
else
stateString = @getLoadSettings().windowState
try
JSON.parse(stateString) if stateString?
catch error
console.warn "Error parsing window state: #{statePath} #{error.stack}", error
console.warn "Error parsing window state: #{statePath} #{error.stack}", error
# Returns the path where the state for the current window will be
# located if it exists.
@getStatePath: (paths, mode) ->
switch mode
when 'spec'
filename = 'spec'
when 'editor'
if paths?.length > 0
sha1 = crypto.createHash('sha1').update(paths.slice().sort().join("\n")).digest('hex')
filename = "editor-#{sha1}"
if filename
path.join(@getStorageDirPath(), filename)
@getStateKey: (paths, mode) ->
if mode is 'spec'
'spec'
else if mode is 'editor' and paths?.length > 0
sha1 = crypto.createHash('sha1').update(paths.slice().sort().join("\n")).digest('hex')
"editor-#{sha1}"
else
null
@@ -110,11 +101,8 @@ class Atom extends Model
@getConfigDirPath: ->
@configDirPath ?= process.env.ATOM_HOME
# Get the path to Atom's storage directory.
#
# Returns the absolute path to ~/.atom/storage
@getStorageDirPath: ->
@storageDirPath ?= path.join(@getConfigDirPath(), 'storage')
@getStorageFolder: ->
@storageFolder ?= new StorageFolder(@getConfigDirPath())
# Returns the load settings hash associated with the current window.
@getLoadSettings: ->
@@ -791,11 +779,10 @@ class Atom extends Model
dialog.showSaveDialog currentWindow, {title: 'Save File', defaultPath}
saveSync: ->
stateString = JSON.stringify(@state)
if statePath = @constructor.getStatePath(@project?.getPaths(), @mode)
fs.writeFileSync(statePath, stateString, 'utf8')
if storageKey = @constructor.getStateKey(@project?.getPaths(), @mode)
@constructor.getStorageFolder().store(storageKey, @state)
else
@getCurrentWindow().loadSettings.windowState = stateString
@getCurrentWindow().loadSettings.windowState = JSON.stringify(@state)
crashMainProcess: ->
remote.process.crash()

View File

@@ -3,6 +3,7 @@ ApplicationMenu = require './application-menu'
AtomProtocolHandler = require './atom-protocol-handler'
AutoUpdateManager = require './auto-update-manager'
BrowserWindow = require 'browser-window'
StorageFolder = require '../storage-folder'
Menu = require 'menu'
app = require 'app'
fs = require 'fs-plus'
@@ -43,7 +44,6 @@ class AtomApplication
createAtomApplication()
return
client = net.connect {path: options.socketPath}, ->
client.write JSON.stringify(options), ->
client.end()
@@ -78,10 +78,13 @@ class AtomApplication
@listenForArgumentsFromNewProcess()
@setupJavaScriptArguments()
@handleEvents()
@storageFolder = new StorageFolder(process.env.ATOM_HOME)
@openWithOptions(options)
if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0 or options.test
@openWithOptions(options)
else
@loadState() or @openPath(options)
# Opens a new window based on the options provided.
openWithOptions: ({pathsToOpen, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, apiPreviewMode, newWindow, specDirectory, logFile}) ->
if test
@runSpecs({exitWhenDone: true, @resourcePath, specDirectory, logFile})
@@ -198,6 +201,9 @@ class AtomApplication
app.on 'window-all-closed', ->
app.quit() if process.platform in ['win32', 'linux']
app.on 'before-quit', =>
@saveState()
app.on 'will-quit', =>
@killAllProcesses()
@deleteSocketFile()
@@ -328,7 +334,7 @@ class AtomApplication
focusedWindow: ->
_.find @windows, (atomWindow) -> atomWindow.isFocused()
# Public: Opens multiple paths, in existing windows if possible.
# Public: Opens a single path, in an existing window if possible.
#
# options -
# :pathToOpen - The file path to open
@@ -341,7 +347,7 @@ class AtomApplication
openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, window}) ->
@openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, window})
# Public: Opens a single path, in an existing window if possible.
# Public: Opens multiple paths, in existing windows if possible.
#
# options -
# :pathsToOpen - The array of file paths to open
@@ -411,6 +417,33 @@ class AtomApplication
console.log("Killing process #{pid} failed: #{error.code ? error.message}")
delete @pidsToOpenWindows[pid]
saveState: ->
states = []
for window in @windows
if loadSettings = window.getLoadSettings()
unless loadSettings.isSpec
states.push(_.pick(loadSettings,
'initialPaths'
'devMode'
'safeMode'
'apiPreviewMode'
))
@storageFolder.store('application.json', states)
loadState: ->
if (states = @storageFolder.load('application.json'))?.length > 0
for state in states
@openWithOptions({
pathsToOpen: state.initialPaths
urlsToOpen: []
devMode: state.devMode
safeMode: state.safeMode
apiPreviewMode: state.apiPreviewMode
})
true
else
false
# Open an atom:// url.
#
# The host of the URL being opened is assumed to be the package name

View File

@@ -87,8 +87,9 @@ class AtomWindow
hash: encodeURIComponent(JSON.stringify(loadSettings))
getLoadSettings: ->
hash = url.parse(@browserWindow.webContents.getUrl()).hash.substr(1)
JSON.parse(decodeURIComponent(hash))
if @browserWindow.webContents.loaded
hash = url.parse(@browserWindow.webContents.getUrl()).hash.substr(1)
JSON.parse(decodeURIComponent(hash))
hasProjectPath: -> @getLoadSettings().initialPaths?.length > 0
@@ -105,7 +106,7 @@ class AtomWindow
true
containsPath: (pathToCheck) ->
@getLoadSettings().initialPaths?.some (projectPath) ->
@getLoadSettings()?.initialPaths?.some (projectPath) ->
if not projectPath
false
else if not pathToCheck

27
src/storage-folder.coffee Normal file
View File

@@ -0,0 +1,27 @@
path = require "path"
fs = require "fs-plus"
module.exports =
class StorageFolder
constructor: (containingPath) ->
@path = path.join(containingPath, "storage")
store: (name, object) ->
fs.writeFileSync(@pathForKey(name), JSON.stringify(object), 'utf8')
load: (name) ->
try
stateString = fs.readFileSync(@pathForKey(name), 'utf8')
catch error
if error.code is 'ENOENT'
return undefined
else
throw error
try
JSON.parse(stateString)
catch error
console.warn "Error reading state file: #{statePath}", error.stack, error
pathForKey: (name) -> path.join(@getPath(), name)
getPath: -> @path