Merge branch 'master' into wl-electron-35

This commit is contained in:
Max Brunsfeld
2016-02-16 13:16:13 -08:00
24 changed files with 426 additions and 157 deletions

View File

@@ -1,5 +1,6 @@
fs = require 'fs'
path = require 'path'
temp = require('temp').track()
_ = require 'underscore-plus'
async = require 'async'
@@ -86,7 +87,7 @@ module.exports = (grunt) ->
packageSpecQueue.concurrency = Math.max(1, concurrency - 1)
packageSpecQueue.drain = -> callback(null, failedPackages)
runCoreSpecs = (callback) ->
runCoreSpecs = (callback, logOutput = false) ->
appPath = getAppPath()
resourcePath = process.cwd()
coreSpecsPath = path.resolve('spec')
@@ -94,7 +95,7 @@ module.exports = (grunt) ->
if process.platform in ['darwin', 'linux']
options =
cmd: appPath
args: ['--test', "--resource-path=#{resourcePath}", coreSpecsPath]
args: ['--test', "--resource-path=#{resourcePath}", coreSpecsPath, "--user-data-dir=#{temp.mkdirSync('atom-user-data-dir')}"]
opts:
env: _.extend({}, process.env,
ATOM_INTEGRATION_TESTS_ENABLED: true
@@ -109,6 +110,9 @@ module.exports = (grunt) ->
ATOM_INTEGRATION_TESTS_ENABLED: true
)
if logOutput
options.opts.stdio = 'inherit'
grunt.log.ok "Launching core specs."
spawn options, (error, results, code) ->
if process.platform is 'win32'
@@ -130,11 +134,17 @@ module.exports = (grunt) ->
else
async.parallel
# If we're just running the core specs then we won't have any output to
# indicate the tests actually *are* running. This upsets Travis:
# https://github.com/atom/atom/issues/10837. So pass the test output
# through.
runCoreSpecsWithLogging = (callback) -> runCoreSpecs(callback, true)
specs =
if process.env.ATOM_SPECS_TASK is 'packages'
[runPackageSpecs]
else if process.env.ATOM_SPECS_TASK is 'core'
[runCoreSpecs]
[runCoreSpecsWithLogging]
else
[runCoreSpecs, runPackageSpecs]

View File

@@ -52,8 +52,10 @@ module.exports = (grunt) ->
stderr = []
error = null
proc = childProcess.spawn(options.cmd, options.args, options.opts)
proc.stdout.on 'data', (data) -> stdout.push(data.toString())
proc.stderr.on 'data', (data) -> stderr.push(data.toString())
if proc.stdout?
proc.stdout.on 'data', (data) -> stdout.push(data.toString())
if proc.stderr?
proc.stderr.on 'data', (data) -> stderr.push(data.toString())
proc.on 'error', (processError) -> error ?= processError
proc.on 'close', (exitCode, signal) ->
error ?= new Error(signal) if exitCode isnt 0

View File

@@ -54,7 +54,7 @@
"service-hub": "^0.7.0",
"source-map-support": "^0.3.2",
"temp": "0.8.1",
"text-buffer": "8.3.0",
"text-buffer": "8.3.1",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"yargs": "^3.23.0"
@@ -173,7 +173,8 @@
"runs",
"spyOn",
"waitsFor",
"waitsForPromise"
"waitsForPromise",
"indexedDB"
]
}
}

View File

@@ -152,6 +152,8 @@ describe "AtomEnvironment", ->
atom.enablePersistence = false
it "selects the state based on the current project paths", ->
jasmine.useRealClock()
[dir1, dir2] = [temp.mkdirSync("dir1-"), temp.mkdirSync("dir2-")]
loadSettings = _.extend atom.getLoadSettings(),
@@ -159,20 +161,40 @@ describe "AtomEnvironment", ->
windowState: null
spyOn(atom, 'getLoadSettings').andCallFake -> loadSettings
spyOn(atom.getStorageFolder(), 'getPath').andReturn(temp.mkdirSync("storage-dir-"))
spyOn(atom, 'serialize').andReturn({stuff: 'cool'})
spyOn(atom, 'deserialize')
atom.state.stuff = "cool"
atom.project.setPaths([dir1, dir2])
atom.saveStateSync()
# State persistence will fail if other Atom instances are running
waitsForPromise ->
atom.stateStore.connect().then (isConnected) ->
expect(isConnected).toBe true
atom.state = {}
atom.loadStateSync()
expect(atom.state.stuff).toBeUndefined()
waitsForPromise ->
atom.saveState().then ->
atom.loadState()
loadSettings.initialPaths = [dir2, dir1]
atom.state = {}
atom.loadStateSync()
expect(atom.state.stuff).toBe("cool")
runs ->
expect(atom.deserialize).not.toHaveBeenCalled()
waitsForPromise ->
loadSettings.initialPaths = [dir2, dir1]
atom.loadState()
runs ->
expect(atom.deserialize).toHaveBeenCalledWith({stuff: 'cool'})
it "saves state on keydown and mousedown events", ->
spyOn(atom, 'saveState')
keydown = new KeyboardEvent('keydown')
atom.document.dispatchEvent(keydown)
advanceClock atom.saveStateDebounceInterval
expect(atom.saveState).toHaveBeenCalled()
mousedown = new MouseEvent('mousedown')
atom.document.dispatchEvent(mousedown)
advanceClock atom.saveStateDebounceInterval
expect(atom.saveState).toHaveBeenCalled()
describe "openInitialEmptyEditorIfNecessary", ->
describe "when there are no paths set", ->
@@ -230,23 +252,6 @@ describe "AtomEnvironment", ->
atomEnvironment.destroy()
it "saves the serialized state of the window so it can be deserialized after reload", ->
atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, window, document})
spyOn(atomEnvironment, 'saveStateSync')
workspaceState = atomEnvironment.workspace.serialize()
grammarsState = {grammarOverridesByPath: atomEnvironment.grammars.grammarOverridesByPath}
projectState = atomEnvironment.project.serialize()
atomEnvironment.unloadEditorWindow()
expect(atomEnvironment.state.workspace).toEqual workspaceState
expect(atomEnvironment.state.grammars).toEqual grammarsState
expect(atomEnvironment.state.project).toEqual projectState
expect(atomEnvironment.saveStateSync).toHaveBeenCalled()
atomEnvironment.destroy()
describe "::destroy()", ->
it "does not throw exceptions when unsubscribing from ipc events (regression)", ->
configDirPath = temp.mkdirSync()

View File

@@ -338,10 +338,10 @@ describe('GitRepositoryAsync', () => {
})
describe('.refreshStatus()', () => {
let newPath, modifiedPath, cleanPath
let newPath, modifiedPath, cleanPath, workingDirectory
beforeEach(() => {
const workingDirectory = copyRepository()
workingDirectory = copyRepository()
repo = GitRepositoryAsync.open(workingDirectory)
modifiedPath = path.join(workingDirectory, 'file.txt')
newPath = path.join(workingDirectory, 'untracked.txt')
@@ -362,7 +362,7 @@ describe('GitRepositoryAsync', () => {
describe('in a repository with submodules', () => {
beforeEach(() => {
const workingDirectory = copySubmoduleRepository()
workingDirectory = copySubmoduleRepository()
repo = GitRepositoryAsync.open(workingDirectory)
modifiedPath = path.join(workingDirectory, 'jstips', 'README.md')
newPath = path.join(workingDirectory, 'You-Dont-Need-jQuery', 'untracked.txt')
@@ -380,6 +380,48 @@ describe('GitRepositoryAsync', () => {
expect(repo.isStatusModified(await repo.getCachedPathStatus(modifiedPath))).toBe(true)
})
})
it('caches the proper statuses when a subdir is open', async () => {
const subDir = path.join(workingDirectory, 'dir')
fs.mkdirSync(subDir)
const filePath = path.join(subDir, 'b.txt')
fs.writeFileSync(filePath, '')
atom.project.setPaths([subDir])
await atom.workspace.open('b.txt')
const repo = atom.project.getRepositories()[0].async
await repo.refreshStatus()
const status = await repo.getCachedPathStatus(filePath)
expect(repo.isStatusModified(status)).toBe(false)
expect(repo.isStatusNew(status)).toBe(false)
})
it('caches the proper statuses when multiple project are open', async () => {
const otherWorkingDirectory = copyRepository()
atom.project.setPaths([workingDirectory, otherWorkingDirectory])
await atom.workspace.open('b.txt')
const repo = atom.project.getRepositories()[0].async
await repo.refreshStatus()
const subDir = path.join(workingDirectory, 'dir')
fs.mkdirSync(subDir)
const filePath = path.join(subDir, 'b.txt')
fs.writeFileSync(filePath, 'some content!')
const status = await repo.getCachedPathStatus(filePath)
expect(repo.isStatusModified(status)).toBe(true)
expect(repo.isStatusNew(status)).toBe(false)
})
})
describe('.isProjectAtRoot()', () => {

View File

@@ -15,6 +15,8 @@ ChromedriverPort = 9515
ChromedriverURLBase = "/wd/hub"
ChromedriverStatusURL = "http://localhost:#{ChromedriverPort}#{ChromedriverURLBase}/status"
userDataDir = temp.mkdirSync('atom-user-data-dir')
chromeDriverUp = (done) ->
checkStatus = ->
http
@@ -48,7 +50,7 @@ buildAtomClient = (args, env) ->
"atom-env=#{map(env, (value, key) -> "#{key}=#{value}").join(" ")}"
"dev"
"safe"
"user-data-dir=#{temp.mkdirSync('atom-user-data-dir')}"
"user-data-dir=#{userDataDir}"
"socket-path=#{SocketPath}"
])

View File

@@ -28,13 +28,12 @@ describe "Starting Atom", ->
it "opens the parent directory and creates an empty text editor", ->
runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: atomHome}, (client) ->
client
.waitForPaneItemCount(1, 1000)
.treeViewRootDirectories()
.then ({value}) -> expect(value).toEqual([tempDirPath])
.waitForExist("atom-text-editor", 5000)
.then (exists) -> expect(exists).toBe true
.waitForPaneItemCount(1, 1000)
.click("atom-text-editor")
.keys("Hello!")
.execute -> atom.workspace.getActiveTextEditor().getText()
@@ -153,6 +152,8 @@ describe "Starting Atom", ->
.waitForPaneItemCount(0, 3000)
.execute -> atom.workspace.open()
.waitForPaneItemCount(1, 3000)
.keys("Hello!")
.waitUntil((-> Promise.resolve(false)), 1100)
runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) ->
client

View File

@@ -446,16 +446,15 @@ describe "PackageManager", ->
pack = null
waitsForPromise ->
atom.packages.activatePackage("package-with-serialization").then (p) -> pack = p
runs ->
expect(pack.mainModule.someNumber).not.toBe 77
pack.mainModule.someNumber = 77
atom.packages.deactivatePackage("package-with-serialization")
spyOn(pack.mainModule, 'activate').andCallThrough()
waitsForPromise ->
atom.packages.activatePackage("package-with-serialization")
runs ->
expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77})
waitsForPromise ->
atom.packages.activatePackage("package-with-serialization")
runs ->
expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77})
it "invokes ::onDidActivatePackage listeners with the activated package", ->
activatedPackage = null
@@ -819,6 +818,34 @@ describe "PackageManager", ->
expect(atom.packages.isPackageActive("package-with-missing-provided-services")).toBe true
expect(addErrorHandler.callCount).toBe 0
describe "::serialize", ->
it "does not serialize packages that threw an error during activation", ->
spyOn(console, 'warn')
badPack = null
waitsForPromise ->
atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p
runs ->
spyOn(badPack.mainModule, 'serialize').andCallThrough()
atom.packages.serialize()
expect(badPack.mainModule.serialize).not.toHaveBeenCalled()
it "absorbs exceptions that are thrown by the package module's serialize method", ->
spyOn(console, 'error')
waitsForPromise ->
atom.packages.activatePackage('package-with-serialize-error')
waitsForPromise ->
atom.packages.activatePackage('package-with-serialization')
runs ->
atom.packages.serialize()
expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined()
expect(atom.packages.packageStates['package-with-serialization']).toEqual someNumber: 1
expect(console.error).toHaveBeenCalled()
describe "::deactivatePackage(id)", ->
afterEach ->
atom.packages.unloadPackages()
@@ -850,33 +877,6 @@ describe "PackageManager", ->
expect(badPack.mainModule.deactivate).not.toHaveBeenCalled()
expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeFalsy()
it "does not serialize packages that have not been activated called on their main module", ->
spyOn(console, 'warn')
badPack = null
waitsForPromise ->
atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p
runs ->
spyOn(badPack.mainModule, 'serialize').andCallThrough()
atom.packages.deactivatePackage("package-that-throws-on-activate")
expect(badPack.mainModule.serialize).not.toHaveBeenCalled()
it "absorbs exceptions that are thrown by the package module's serialize method", ->
spyOn(console, 'error')
waitsForPromise ->
atom.packages.activatePackage('package-with-serialize-error')
waitsForPromise ->
atom.packages.activatePackage('package-with-serialization')
runs ->
atom.packages.deactivatePackages()
expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined()
expect(atom.packages.packageStates['package-with-serialization']).toEqual someNumber: 1
expect(console.error).toHaveBeenCalled()
it "absorbs exceptions that are thrown by the package module's deactivate method", ->
spyOn(console, 'error')

61
spec/state-store-spec.js Normal file
View File

@@ -0,0 +1,61 @@
/** @babel */
import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers'
const StateStore = require('../src/state-store.js')
describe("StateStore", () => {
let databaseName = `test-database-${Date.now()}`
let version = 1
it("can save and load states", () => {
const store = new StateStore(databaseName, version)
return store.save('key', {foo:'bar'})
.then(() => store.load('key'))
.then((state) => {
expect(state).toEqual({foo:'bar'})
})
})
it("resolves with null when a non-existent key is loaded", () => {
const store = new StateStore(databaseName, version)
return store.load('no-such-key').then((value) => {
expect(value).toBeNull()
})
})
it("can clear the state object store", () => {
const store = new StateStore(databaseName, version)
return store.save('key', {foo:'bar'})
.then(() => store.count())
.then((count) =>
expect(count).toBe(1)
)
.then(() => store.clear())
.then(() => store.count())
.then((count) => {
expect(count).toBe(0)
})
})
describe("when there is an error reading from the database", () => {
it("rejects the promise returned by load", () => {
const store = new StateStore(databaseName, version)
const fakeErrorEvent = {target: {errorCode: "Something bad happened"}}
spyOn(IDBObjectStore.prototype, 'get').andCallFake((key) => {
let request = {}
process.nextTick(() => request.onerror(fakeErrorEvent))
return request
})
return store.load('nonexistentKey')
.then(() => {
throw new Error("Promise should have been rejected")
})
.catch((event) => {
expect(event).toBe(fakeErrorEvent)
})
})
})
})

View File

@@ -92,10 +92,10 @@ describe "TextEditorPresenter", ->
waitsForStateToUpdate = (presenter, fn) ->
waitsFor "presenter state to update", 1000, (done) ->
fn?()
disposable = presenter.onDidUpdateState ->
disposable.dispose()
process.nextTick(done)
fn?()
tiledContentContract = (stateFn) ->
it "contains states for tiles that are visible on screen", ->
@@ -1336,9 +1336,9 @@ describe "TextEditorPresenter", ->
presenter = buildPresenter()
blockDecoration2 = addBlockDecorationBeforeScreenRow(3)
blockDecoration3 = addBlockDecorationBeforeScreenRow(7)
blockDecoration4 = addBlockDecorationAfterScreenRow(7)
blockDecoration4 = null
waitsForStateToUpdate presenter
waitsForStateToUpdate presenter, blockDecoration4 = addBlockDecorationAfterScreenRow(7)
runs ->
expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual([blockDecoration1])
expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual([])
@@ -1472,9 +1472,9 @@ describe "TextEditorPresenter", ->
decoration1 = editor.decorateMarker(marker1, type: 'line', class: 'a')
presenter = buildPresenter()
marker2 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch')
decoration2 = editor.decorateMarker(marker2, type: 'line', class: 'b')
decoration2 = null
waitsForStateToUpdate presenter
waitsForStateToUpdate presenter, -> decoration2 = editor.decorateMarker(marker2, type: 'line', class: 'b')
runs ->
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
@@ -2150,31 +2150,40 @@ describe "TextEditorPresenter", ->
}
# becoming empty
waitsForStateToUpdate presenter, -> editor.getSelections()[1].clear(autoscroll: false)
runs ->
editor.getSelections()[1].clear(autoscroll: false)
waitsForStateToUpdate presenter
runs ->
expectUndefinedStateForSelection(presenter, 1)
# becoming non-empty
waitsForStateToUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false)
runs ->
editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false)
waitsForStateToUpdate presenter
runs ->
expectValues stateForSelectionInTile(presenter, 1, 2), {
regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}]
}
# moving out of view
waitsForStateToUpdate presenter, -> editor.getSelections()[1].setBufferRange([[3, 4], [3, 6]], autoscroll: false)
runs ->
editor.getSelections()[1].setBufferRange([[3, 4], [3, 6]], autoscroll: false)
waitsForStateToUpdate presenter
runs ->
expectUndefinedStateForSelection(presenter, 1)
# adding
waitsForStateToUpdate presenter, -> editor.addSelectionForBufferRange([[1, 4], [1, 6]], autoscroll: false)
runs -> editor.addSelectionForBufferRange([[1, 4], [1, 6]], autoscroll: false)
waitsForStateToUpdate presenter
runs ->
expectValues stateForSelectionInTile(presenter, 2, 0), {
regions: [{top: 10, left: 4 * 10, width: 2 * 10, height: 10}]
}
# moving added selection
waitsForStateToUpdate presenter, -> editor.getSelections()[2].setBufferRange([[1, 4], [1, 8]], autoscroll: false)
runs ->
editor.getSelections()[2].setBufferRange([[1, 4], [1, 8]], autoscroll: false)
waitsForStateToUpdate presenter
destroyedSelection = null
runs ->
@@ -2208,8 +2217,9 @@ describe "TextEditorPresenter", ->
presenter = buildPresenter(explicitHeight: 30, scrollTop: 20, tileSize: 2)
marker = editor.markBufferPosition([2, 2])
highlight = editor.decorateMarker(marker, type: 'highlight', class: 'a')
highlight = null
waitsForStateToUpdate presenter, ->
highlight = editor.decorateMarker(marker, type: 'highlight', class: 'a')
marker.setBufferRange([[2, 2], [5, 2]])
highlight.flash('b', 500)
runs ->
@@ -2969,9 +2979,8 @@ describe "TextEditorPresenter", ->
presenter.setBlockDecorationDimensions(blockDecoration4, 0, 35)
presenter.setBlockDecorationDimensions(blockDecoration4, 0, 40)
presenter.setBlockDecorationDimensions(blockDecoration5, 0, 50)
presenter.setBlockDecorationDimensions(blockDecoration6, 0, 60)
waitsForStateToUpdate presenter
waitsForStateToUpdate presenter, -> presenter.setBlockDecorationDimensions(blockDecoration6, 0, 60)
runs ->
expect(lineNumberStateForScreenRow(presenter, 0).blockDecorationsHeight).toBe(10)
expect(lineNumberStateForScreenRow(presenter, 1).blockDecorationsHeight).toBe(0)
@@ -3460,9 +3469,9 @@ describe "TextEditorPresenter", ->
gutterName: 'test-gutter-2'
class: 'test-class'
marker4 = editor.markBufferRange([[0, 0], [1, 0]])
decoration4 = editor.decorateMarker(marker4, decorationParams)
decoration4 = null
waitsForStateToUpdate presenter
waitsForStateToUpdate presenter, -> decoration4 = editor.decorateMarker(marker4, decorationParams)
runs ->
expectStateUpdate presenter, -> editor.addGutter({name: 'test-gutter-2'})

View File

@@ -4,13 +4,13 @@ path = require 'path'
_ = require 'underscore-plus'
{deprecate} = require 'grim'
{CompositeDisposable, Emitter} = require 'event-kit'
{CompositeDisposable, Disposable, Emitter} = require 'event-kit'
fs = require 'fs-plus'
{mapSourcePosition} = require 'source-map-support'
Model = require './model'
WindowEventHandler = require './window-event-handler'
StylesElement = require './styles-element'
StorageFolder = require './storage-folder'
StateStore = require './state-store'
{getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers'
registerDefaultCommands = require './register-default-commands'
@@ -111,6 +111,8 @@ class AtomEnvironment extends Model
# Public: A {Workspace} instance
workspace: null
saveStateDebounceInterval: 1000
###
Section: Construction and Destruction
###
@@ -119,14 +121,16 @@ class AtomEnvironment extends Model
constructor: (params={}) ->
{@blobStore, @applicationDelegate, @window, @document, configDirPath, @enablePersistence, onlyLoadBaseStyleSheets} = params
@state = {version: @constructor.version}
@loadTime = null
{devMode, safeMode, resourcePath} = @getLoadSettings()
{devMode, safeMode, resourcePath, clearWindowState} = @getLoadSettings()
@emitter = new Emitter
@disposables = new CompositeDisposable
@stateStore = new StateStore('AtomEnvironments', 1)
@stateStore.clear() if clearWindowState
@deserializers = new DeserializerManager(this)
@deserializeTimings = {}
@@ -200,6 +204,7 @@ class AtomEnvironment extends Model
@registerDefaultViewProviders()
@installUncaughtErrorHandler()
@attachSaveStateListeners()
@installWindowEventHandler()
@observeAutoHideMenuBar()
@@ -213,6 +218,14 @@ class AtomEnvironment extends Model
checkPortableHomeWritable()
attachSaveStateListeners: ->
debouncedSaveState = _.debounce((=> @saveState()), @saveStateDebounceInterval)
@document.addEventListener('mousedown', debouncedSaveState, true)
@document.addEventListener('keydown', debouncedSaveState, true)
@disposables.add new Disposable =>
@document.removeEventListener('mousedown', debouncedSaveState, true)
@document.removeEventListener('keydown', debouncedSaveState, true)
setConfigSchema: ->
@config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))}
@@ -302,9 +315,6 @@ class AtomEnvironment extends Model
@views.clear()
@registerDefaultViewProviders()
@state.packageStates = {}
delete @state.workspace
destroy: ->
return if not @project
@@ -611,7 +621,7 @@ class AtomEnvironment extends Model
# But after that, e.g., when the window's been reloaded, we want to use the
# dimensions we've saved for it.
if not @isFirstLoad()
dimensions = @state.windowDimensions
dimensions = @windowDimensions
unless @isValidDimensions(dimensions)
dimensions = @getDefaultWindowDimensions()
@@ -619,7 +629,7 @@ class AtomEnvironment extends Model
storeWindowDimensions: ->
dimensions = @getWindowDimensions()
@state.windowDimensions = dimensions if @isValidDimensions(dimensions)
@windowDimensions = dimensions if @isValidDimensions(dimensions)
restoreWindowBackground: ->
if backgroundColor = window.localStorage.getItem('atom:window-background-color')
@@ -664,17 +674,20 @@ class AtomEnvironment extends Model
@openInitialEmptyEditorIfNecessary()
serialize: ->
version: @constructor.version
project: @project.serialize()
workspace: @workspace.serialize()
packageStates: @packages.serialize()
grammars: {grammarOverridesByPath: @grammars.grammarOverridesByPath}
fullScreen: @isFullScreen()
windowDimensions: @windowDimensions
unloadEditorWindow: ->
return if not @project
@storeWindowBackground()
@state.grammars = {grammarOverridesByPath: @grammars.grammarOverridesByPath}
@state.project = @project.serialize()
@state.workspace = @workspace.serialize()
@packages.deactivatePackages()
@state.packageStates = @packages.packageStates
@state.fullScreen = @isFullScreen()
@saveStateSync()
@saveBlobStoreSync()
openInitialEmptyEditorIfNecessary: ->
@@ -808,45 +821,54 @@ class AtomEnvironment extends Model
@blobStore.save()
saveStateSync: ->
return unless @enablePersistence
saveState: ->
return Promise.resolve() unless @enablePersistence
state = @serialize()
if storageKey = @getStateKey(@project?.getPaths())
@getStorageFolder().store(storageKey, @state)
@stateStore.save(storageKey, state)
else
@getCurrentWindow().loadSettings.windowState = JSON.stringify(@state)
@getCurrentWindow().loadSettings.windowState = JSON.stringify(state)
Promise.resolve()
loadStateSync: ->
return unless @enablePersistence
loadState: ->
return Promise.resolve() unless @enablePersistence
startTime = Date.now()
statePromise = null
if stateKey = @getStateKey(@getLoadSettings().initialPaths)
if state = @getStorageFolder().load(stateKey)
@state = state
statePromise = @stateStore.load(stateKey)
if not @state? and windowState = @getLoadSettings().windowState
if not statePromise? and windowState = @getLoadSettings().windowState
try
if state = JSON.parse(@getLoadSettings().windowState)
@state = state
statePromise = Promise.resolve(JSON.parse(@getLoadSettings().windowState))
catch error
console.warn "Error parsing window state: #{statePath} #{error.stack}", error
@deserializeTimings.atom = Date.now() - startTime
if statePromise?
statePromise.then (state) =>
@deserializeTimings.atom = Date.now() - startTime
@deserialize(state) if state?
else
Promise.resolve()
if grammarOverridesByPath = @state.grammars?.grammarOverridesByPath
deserialize: (state) ->
if grammarOverridesByPath = state.grammars?.grammarOverridesByPath
@grammars.grammarOverridesByPath = grammarOverridesByPath
@setFullScreen(@state.fullScreen)
@setFullScreen(state.fullScreen)
@packages.packageStates = @state.packageStates ? {}
@windowDimensions = state.windowDimensions if state.windowDimensions
@packages.packageStates = state.packageStates ? {}
startTime = Date.now()
@project.deserialize(@state.project, @deserializers) if @state.project?
@project.deserialize(state.project, @deserializers) if state.project?
@deserializeTimings.project = Date.now() - startTime
startTime = Date.now()
@workspace.deserialize(@state.workspace, @deserializers) if @state.workspace?
@workspace.deserialize(state.workspace, @deserializers) if state.workspace?
@deserializeTimings.workspace = Date.now() - startTime
getStateKey: (paths) ->
@@ -859,9 +881,6 @@ class AtomEnvironment extends Model
getConfigDirPath: ->
@configDirPath ?= process.env.ATOM_HOME
getStorageFolder: ->
@storageFolder ?= new StorageFolder(@getConfigDirPath())
getUserInitScriptPath: ->
initScriptPath = fs.resolve(@getConfigDirPath(), 'init', ['js', 'coffee'])
initScriptPath ? path.join(@getConfigDirPath(), 'init.coffee')

View File

@@ -61,7 +61,7 @@ class AtomApplication
exit: (status) -> app.exit(status)
constructor: (options) ->
{@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, timeout} = options
{@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, timeout, clearWindowState} = options
@socketPath = null if options.test
@@ -85,16 +85,16 @@ class AtomApplication
else
@loadState(options) or @openPath(options)
openWithOptions: ({pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, timeout}) ->
openWithOptions: ({pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, timeout, clearWindowState}) ->
if test
@runTests({headless: true, devMode, @resourcePath, executedFrom, pathsToOpen, logFile, timeout})
else if pathsToOpen.length > 0
@openPaths({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup})
@openPaths({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState})
else if urlsToOpen.length > 0
@openUrl({urlToOpen, devMode, safeMode}) for urlToOpen in urlsToOpen
else
# Always open a editor window if this is the first instance of Atom.
@openPath({pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup})
@openPath({pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState})
# Public: Removes the {AtomWindow} from the global window list.
removeWindow: (window) ->
@@ -403,8 +403,8 @@ class AtomApplication
# :safeMode - Boolean to control the opened window's safe mode.
# :profileStartup - Boolean to control creating a profile of the startup time.
# :window - {AtomWindow} to open file paths in.
openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window} = {}) ->
@openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window})
openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState} = {}) ->
@openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState})
# Public: Opens multiple paths, in existing windows if possible.
#
@@ -416,9 +416,10 @@ class AtomApplication
# :safeMode - Boolean to control the opened window's safe mode.
# :windowDimensions - Object with height and width keys.
# :window - {AtomWindow} to open file paths in.
openPaths: ({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window}={}) ->
openPaths: ({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window, clearWindowState}={}) ->
devMode = Boolean(devMode)
safeMode = Boolean(safeMode)
clearWindowState = Boolean(clearWindowState)
locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom) for pathToOpen in pathsToOpen)
pathsToOpen = (locationToOpen.pathToOpen for locationToOpen in locationsToOpen)
@@ -451,7 +452,7 @@ class AtomApplication
windowInitializationScript ?= require.resolve('../initialize-application-window')
resourcePath ?= @resourcePath
windowDimensions ?= @getDimensionsForNewWindow()
openedWindow = new AtomWindow({locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup})
openedWindow = new AtomWindow({locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState})
if pidToKillWhenClosed?
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
@@ -488,7 +489,7 @@ class AtomApplication
if loadSettings = window.getLoadSettings()
states.push(initialPaths: loadSettings.initialPaths)
if states.length > 0 or allowEmpty
@storageFolder.store('application.json', states)
@storageFolder.storeSync('application.json', states)
loadState: (options) ->
if (states = @storageFolder.load('application.json'))?.length > 0

View File

@@ -48,6 +48,7 @@ class AtomWindow
loadSettings.safeMode ?= false
loadSettings.atomHome = process.env.ATOM_HOME
loadSettings.firstLoad = true
loadSettings.clearWindowState ?= false
# Only send to the first non-spec window created
if @constructor.includeShellLoadTime and not @isSpec

View File

@@ -31,6 +31,9 @@ start = ->
app.on 'open-url', addUrlToOpen
app.on 'will-finish-launching', setupCrashReporter
if args.userDataDir?
app.setPath('userData', args.userDataDir)
app.on 'ready', ->
app.removeListener 'open-file', addPathToOpen
app.removeListener 'open-url', addUrlToOpen
@@ -118,6 +121,8 @@ parseCommandLine = ->
options.alias('v', 'version').boolean('v').describe('v', 'Print the version.')
options.alias('w', 'wait').boolean('w').describe('w', 'Wait for window to be closed before returning.')
options.string('socket-path')
options.string('user-data-dir')
options.boolean('clear-window-state').describe('clear-window-state', 'Delete all Atom environment state.')
args = options.argv
@@ -139,7 +144,9 @@ parseCommandLine = ->
pidToKillWhenClosed = args['pid'] if args['wait']
logFile = args['log-file']
socketPath = args['socket-path']
userDataDir = args['user-data-dir']
profileStartup = args['profile-startup']
clearWindowState = args['clear-window-state']
urlsToOpen = []
devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH ? path.join(app.getPath('home'), 'github', 'atom')
setPortable = args.portable
@@ -163,6 +170,7 @@ parseCommandLine = ->
{resourcePath, devResourcePath, pathsToOpen, urlsToOpen, executedFrom, test,
version, pidToKillWhenClosed, devMode, safeMode, newWindow,
logFile, socketPath, profileStartup, timeout, setPortable}
logFile, socketPath, userDataDir, profileStartup, timeout, setPortable,
clearWindowState}
start()

View File

@@ -20,6 +20,7 @@ else
fileKeyPath = 'HKCU\\Software\\Classes\\*\\shell\\Atom'
directoryKeyPath = 'HKCU\\Software\\Classes\\directory\\shell\\Atom'
backgroundKeyPath = 'HKCU\\Software\\Classes\\directory\\background\\shell\\Atom'
applicationsKeyPath = 'HKCU\\Software\\Classes\\Applications\\atom.exe'
environmentKeyPath = 'HKCU\\Environment'
# Spawn a command and invoke the callback when it completes with an error
@@ -64,6 +65,10 @@ installContextMenu = (callback) ->
args.push('/f')
spawnReg(args, callback)
installFileHandler = (callback) ->
args = ["#{applicationsKeyPath}\\shell\\open\\command", '/ve', '/d', "\"#{process.execPath}\" \"%1\""]
addToRegistry(args, callback)
installMenu = (keyPath, arg, callback) ->
args = [keyPath, '/ve', '/d', 'Open with Atom']
addToRegistry args, ->
@@ -74,7 +79,8 @@ installContextMenu = (callback) ->
installMenu fileKeyPath, '%1', ->
installMenu directoryKeyPath, '%1', ->
installMenu(backgroundKeyPath, '%V', callback)
installMenu backgroundKeyPath, '%V', ->
installFileHandler(callback)
isAscii = (text) ->
index = 0
@@ -124,7 +130,8 @@ uninstallContextMenu = (callback) ->
deleteFromRegistry fileKeyPath, ->
deleteFromRegistry directoryKeyPath, ->
deleteFromRegistry(backgroundKeyPath, callback)
deleteFromRegistry backgroundKeyPath, ->
deleteFromRegistry(applicationsKeyPath, callback)
# Add atom and apm to the PATH
#

View File

@@ -456,7 +456,10 @@ export default class GitRepositoryAsync {
// information.
getDirectoryStatus (directoryPath) {
return this.relativizeToWorkingDirectory(directoryPath)
.then(relativePath => this._getStatus([relativePath]))
.then(relativePath => {
const pathspec = relativePath + '/**'
return this._getStatus([pathspec])
})
.then(statuses => {
return Promise.all(statuses.map(s => s.statusBit())).then(bits => {
return bits
@@ -800,7 +803,7 @@ export default class GitRepositoryAsync {
}
return Promise.all(projectPathsPromises)
.then(paths => paths.filter(p => p.length > 0))
.then(paths => paths.map(p => p.length > 0 ? p + '/**' : '*'))
.then(projectPaths => {
return this._getStatus(projectPaths.length > 0 ? projectPaths : null)
})
@@ -1032,7 +1035,7 @@ export default class GitRepositoryAsync {
return this.getRepo()
.then(repo => {
const opts = {
flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS | Git.Status.OPT.DISABLE_PATHSPEC_MATCH
flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS
}
if (paths) {

View File

@@ -23,12 +23,12 @@ module.exports = ({blobStore}) ->
enablePersistence: true
})
atom.loadStateSync()
atom.displayWindow().then ->
atom.startEditorWindow()
atom.loadState().then ->
atom.displayWindow().then ->
atom.startEditorWindow()
# Workaround for focus getting cleared upon window creation
windowFocused = ->
window.removeEventListener('focus', windowFocused)
setTimeout (-> document.querySelector('atom-workspace').focus()), 0
window.addEventListener('focus', windowFocused)
# Workaround for focus getting cleared upon window creation
windowFocused = ->
window.removeEventListener('focus', windowFocused)
setTimeout (-> document.querySelector('atom-workspace').focus()), 0
window.addEventListener('focus', windowFocused)

View File

@@ -68,8 +68,8 @@ module.exports = ({blobStore}) ->
logFile, headless, testPaths, buildAtomEnvironment, buildDefaultApplicationDelegate, legacyTestRunner
})
promise.then (code) ->
exitWithStatusCode(code) if getWindowLoadSettings().headless
promise.then (statusCode) ->
exitWithStatusCode(statusCode) if getWindowLoadSettings().headless
catch error
if getWindowLoadSettings().headless
console.error(error.stack ? error)

View File

@@ -467,6 +467,14 @@ class PackageManager
return unless hook? and _.isString(hook) and hook.length > 0
@activationHookEmitter.on(hook, callback)
serialize: ->
for pack in @getActivePackages()
@serializePackage(pack)
@packageStates
serializePackage: (pack) ->
@setPackageState(pack.name, state) if state = pack.serialize?()
# Deactivate all packages
deactivatePackages: ->
@config.transact =>
@@ -478,8 +486,7 @@ class PackageManager
# Deactivate the package with the given name
deactivatePackage: (name) ->
pack = @getLoadedPackage(name)
if @isPackageActive(name)
@setPackageState(pack.name, state) if state = pack.serialize?()
@serializePackage(pack) if @isPackageActive(pack.name)
pack.deactivate()
delete @activePackages[pack.name]
delete @activatingPackages[pack.name]

View File

@@ -713,7 +713,7 @@ class Pane extends Model
if @parent.orientation is 'vertical'
bottommostSibling = last(@parent.children)
if bottommostSibling instanceof PaneAxis
@splitRight()
@splitDown()
else
bottommostSibling
else

91
src/state-store.js Normal file
View File

@@ -0,0 +1,91 @@
'use strict'
module.exports =
class StateStore {
constructor (databaseName, version) {
this.dbPromise = new Promise((resolve) => {
let dbOpenRequest = indexedDB.open(databaseName, version)
dbOpenRequest.onupgradeneeded = (event) => {
let db = event.target.result
db.createObjectStore('states')
}
dbOpenRequest.onsuccess = () => {
resolve(dbOpenRequest.result)
}
dbOpenRequest.onerror = (error) => {
console.error('Could not connect to indexedDB', error)
resolve(null)
}
})
}
connect () {
return this.dbPromise.then(db => !!db)
}
save (key, value) {
return this.dbPromise.then(db => {
if (!db) return
return new Promise((resolve, reject) => {
var request = db.transaction(['states'], 'readwrite')
.objectStore('states')
.put({value: value, storedAt: new Date().toString()}, key)
request.onsuccess = resolve
request.onerror = reject
})
})
}
load (key) {
return this.dbPromise.then(db => {
if (!db) return
return new Promise((resolve, reject) => {
var request = db.transaction(['states'])
.objectStore('states')
.get(key)
request.onsuccess = (event) => {
let result = event.target.result
resolve(result ? result.value : null)
}
request.onerror = (event) => reject(event)
})
})
}
clear () {
return this.dbPromise.then(db => {
if (!db) return
return new Promise((resolve, reject) => {
var request = db.transaction(['states'], 'readwrite')
.objectStore('states')
.clear()
request.onsuccess = resolve
request.onerror = reject
})
})
}
count () {
return this.dbPromise.then(db => {
if (!db) return
return new Promise((resolve, reject) => {
var request = db.transaction(['states'])
.objectStore('states')
.count()
request.onsuccess = () => {
resolve(request.result)
}
request.onerror = reject
})
})
}
}

View File

@@ -6,7 +6,7 @@ class StorageFolder
constructor: (containingPath) ->
@path = path.join(containingPath, "storage") if containingPath?
store: (name, object) ->
storeSync: (name, object) ->
return unless @path?
fs.writeFileSync(@pathForKey(name), JSON.stringify(object), 'utf8')

View File

@@ -394,11 +394,11 @@ class Workspace extends Model
# initially. Defaults to `0`.
# * `initialColumn` A {Number} indicating which column to move the cursor to
# initially. Defaults to `0`.
# * `split` Either 'left', 'right', 'top' or 'bottom'.
# * `split` Either 'left', 'right', 'up' or 'down'.
# If 'left', the item will be opened in leftmost pane of the current active pane's row.
# If 'right', the item will be opened in the rightmost pane of the current active pane's row.
# If 'up', the item will be opened in topmost pane of the current active pane's row.
# If 'down', the item will be opened in the bottommost pane of the current active pane's row.
# If 'right', the item will be opened in the rightmost pane of the current active pane's row. If only one pane exists in the row, a new pane will be created.
# If 'up', the item will be opened in topmost pane of the current active pane's column.
# If 'down', the item will be opened in the bottommost pane of the current active pane's column. If only one pane exists in the column, a new pane will be created.
# * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on
# containing pane. Defaults to `true`.
# * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem}

View File

@@ -84,7 +84,6 @@
setupCsonCache(CompileCache.getCacheDirectory())
var initialize = require(loadSettings.windowInitializationScript)
return initialize({blobStore: blobStore}).then(function () {
require('electron').ipcRenderer.send('window-command', 'window:loaded')
})