From bc774773f74557f50fc9b039ec90cdacf9412341 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Nov 2017 15:28:21 -0800 Subject: [PATCH 1/3] Convert AtomEnvironment to JS --- spec/atom-environment-spec.js | 23 +- src/atom-environment.coffee | 1168 ---------------------------- src/atom-environment.js | 1351 +++++++++++++++++++++++++++++++++ 3 files changed, 1353 insertions(+), 1189 deletions(-) delete mode 100644 src/atom-environment.coffee create mode 100644 src/atom-environment.js diff --git a/spec/atom-environment-spec.js b/spec/atom-environment-spec.js index b8d7e309a..84b415eab 100644 --- a/spec/atom-environment-spec.js +++ b/spec/atom-environment-spec.js @@ -224,25 +224,6 @@ describe('AtomEnvironment', () => { expect(await atom.loadState()).toEqual({stuff: 'cool'}) }) - it("loads state from the storage folder when it can't be found in atom.stateStore", async () => { - jasmine.useRealClock() - - const storageFolderState = {foo: 1, bar: 2} - const serializedState = {someState: 42} - const loadSettings = _.extend(atom.getLoadSettings(), {initialPaths: [temp.mkdirSync('project-directory')]}) - spyOn(atom, 'getLoadSettings').andReturn(loadSettings) - spyOn(atom, 'serialize').andReturn(serializedState) - spyOn(atom, 'getStorageFolder').andReturn(new StorageFolder(temp.mkdirSync('config-directory'))) - atom.project.setPaths(atom.getLoadSettings().initialPaths) - - await atom.stateStore.connect() - atom.getStorageFolder().storeSync(atom.getStateKey(loadSettings.initialPaths), storageFolderState) - expect(await atom.loadState()).toEqual(storageFolderState) - - await atom.saveState() - expect(await atom.loadState()).toEqual(serializedState) - }) - it('saves state when the CPU is idle after a keydown or mousedown event', () => { const atomEnv = new AtomEnvironment({ applicationDelegate: global.atom.applicationDelegate @@ -488,7 +469,7 @@ describe('AtomEnvironment', () => { }) it('automatically restores the saved state into the current environment', () => { - const state = Symbol() + const state = {} spyOn(atom.workspace, 'open') spyOn(atom, 'restoreStateIntoThisEnvironment') @@ -505,7 +486,7 @@ describe('AtomEnvironment', () => { getTitle () { return 'title' }, element: document.createElement('div') }) - const state = Symbol() + const state = {} spyOn(atom, 'confirm') atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename]) expect(atom.confirm).not.toHaveBeenCalled() diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee deleted file mode 100644 index 50b5d541e..000000000 --- a/src/atom-environment.coffee +++ /dev/null @@ -1,1168 +0,0 @@ -crypto = require 'crypto' -path = require 'path' -{ipcRenderer} = require 'electron' - -_ = require 'underscore-plus' -{deprecate} = require 'grim' -{CompositeDisposable, Disposable, Emitter} = require 'event-kit' -fs = require 'fs-plus' -{mapSourcePosition} = require '@atom/source-map-support' -Model = require './model' -WindowEventHandler = require './window-event-handler' -StateStore = require './state-store' -StorageFolder = require './storage-folder' -registerDefaultCommands = require './register-default-commands' -{updateProcessEnv} = require './update-process-env' -ConfigSchema = require './config-schema' - -DeserializerManager = require './deserializer-manager' -ViewRegistry = require './view-registry' -NotificationManager = require './notification-manager' -Config = require './config' -KeymapManager = require './keymap-extensions' -TooltipManager = require './tooltip-manager' -CommandRegistry = require './command-registry' -URIHandlerRegistry = require './uri-handler-registry' -GrammarRegistry = require './grammar-registry' -{HistoryManager, HistoryProject} = require './history-manager' -ReopenProjectMenuManager = require './reopen-project-menu-manager' -StyleManager = require './style-manager' -PackageManager = require './package-manager' -ThemeManager = require './theme-manager' -MenuManager = require './menu-manager' -ContextMenuManager = require './context-menu-manager' -CommandInstaller = require './command-installer' -CoreURIHandlers = require './core-uri-handlers' -ProtocolHandlerInstaller = require './protocol-handler-installer' -Project = require './project' -TitleBar = require './title-bar' -Workspace = require './workspace' -PanelContainer = require './panel-container' -Panel = require './panel' -PaneContainer = require './pane-container' -PaneAxis = require './pane-axis' -Pane = require './pane' -Dock = require './dock' -TextEditor = require './text-editor' -TextBuffer = require 'text-buffer' -Gutter = require './gutter' -TextEditorRegistry = require './text-editor-registry' -AutoUpdateManager = require './auto-update-manager' - -# Essential: Atom global for dealing with packages, themes, menus, and the window. -# -# An instance of this class is always available as the `atom` global. -module.exports = -class AtomEnvironment extends Model - @version: 1 # Increment this when the serialization format changes - - lastUncaughtError: null - - ### - Section: Properties - ### - - # Public: A {CommandRegistry} instance - commands: null - - # Public: A {Config} instance - config: null - - # Public: A {Clipboard} instance - clipboard: null - - # Public: A {ContextMenuManager} instance - contextMenu: null - - # Public: A {MenuManager} instance - menu: null - - # Public: A {KeymapManager} instance - keymaps: null - - # Public: A {TooltipManager} instance - tooltips: null - - # Public: A {NotificationManager} instance - notifications: null - - # Public: A {Project} instance - project: null - - # Public: A {GrammarRegistry} instance - grammars: null - - # Public: A {HistoryManager} instance - history: null - - # Public: A {PackageManager} instance - packages: null - - # Public: A {ThemeManager} instance - themes: null - - # Public: A {StyleManager} instance - styles: null - - # Public: A {DeserializerManager} instance - deserializers: null - - # Public: A {ViewRegistry} instance - views: null - - # Public: A {Workspace} instance - workspace: null - - # Public: A {TextEditorRegistry} instance - textEditors: null - - # Private: An {AutoUpdateManager} instance - autoUpdater: null - - saveStateDebounceInterval: 1000 - - ### - Section: Construction and Destruction - ### - - # Call .loadOrCreate instead - constructor: (params={}) -> - {@applicationDelegate, @clipboard, @enablePersistence, onlyLoadBaseStyleSheets, @updateProcessEnv} = params - - @nextProxyRequestId = 0 - @unloaded = false - @loadTime = null - @emitter = new Emitter - @disposables = new CompositeDisposable - @deserializers = new DeserializerManager(this) - @deserializeTimings = {} - @views = new ViewRegistry(this) - TextEditor.setScheduler(@views) - @notifications = new NotificationManager - @updateProcessEnv ?= updateProcessEnv # For testing - - @stateStore = new StateStore('AtomEnvironments', 1) - - @config = new Config({notificationManager: @notifications, @enablePersistence}) - @config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)} - - @keymaps = new KeymapManager({notificationManager: @notifications}) - @tooltips = new TooltipManager(keymapManager: @keymaps, viewRegistry: @views) - @commands = new CommandRegistry - @uriHandlerRegistry = new URIHandlerRegistry - @grammars = new GrammarRegistry({@config}) - @styles = new StyleManager() - @packages = new PackageManager({ - @config, styleManager: @styles, - commandRegistry: @commands, keymapManager: @keymaps, notificationManager: @notifications, - grammarRegistry: @grammars, deserializerManager: @deserializers, viewRegistry: @views, - uriHandlerRegistry: @uriHandlerRegistry - }) - @themes = new ThemeManager({ - packageManager: @packages, @config, styleManager: @styles, - notificationManager: @notifications, viewRegistry: @views - }) - @menu = new MenuManager({keymapManager: @keymaps, packageManager: @packages}) - @contextMenu = new ContextMenuManager({keymapManager: @keymaps}) - @packages.setMenuManager(@menu) - @packages.setContextMenuManager(@contextMenu) - @packages.setThemeManager(@themes) - - @project = new Project({notificationManager: @notifications, packageManager: @packages, @config, @applicationDelegate}) - @commandInstaller = new CommandInstaller(@applicationDelegate) - @protocolHandlerInstaller = new ProtocolHandlerInstaller() - - @textEditors = new TextEditorRegistry({ - @config, grammarRegistry: @grammars, assert: @assert.bind(this), - packageManager: @packages - }) - - @workspace = new Workspace({ - @config, @project, packageManager: @packages, grammarRegistry: @grammars, deserializerManager: @deserializers, - notificationManager: @notifications, @applicationDelegate, viewRegistry: @views, assert: @assert.bind(this), - textEditorRegistry: @textEditors, styleManager: @styles, @enablePersistence - }) - - @themes.workspace = @workspace - - @autoUpdater = new AutoUpdateManager({@applicationDelegate}) - - if @keymaps.canLoadBundledKeymapsFromMemory() - @keymaps.loadBundledKeymaps() - - @registerDefaultCommands() - @registerDefaultOpeners() - @registerDefaultDeserializers() - - @windowEventHandler = new WindowEventHandler({atomEnvironment: this, @applicationDelegate}) - - @history = new HistoryManager({@project, @commands, @stateStore}) - # Keep instances of HistoryManager in sync - @disposables.add @history.onDidChangeProjects (e) => - @applicationDelegate.didChangeHistoryManager() unless e.reloaded - - initialize: (params={}) -> - # This will force TextEditorElement to register the custom element, so that - # using `document.createElement('atom-text-editor')` works if it's called - # before opening a buffer. - require './text-editor-element' - - {@window, @document, @blobStore, @configDirPath, onlyLoadBaseStyleSheets} = params - {devMode, safeMode, resourcePath, clearWindowState} = @getLoadSettings() - - if clearWindowState - @getStorageFolder().clear() - @stateStore.clear() - - ConfigSchema.projectHome = { - type: 'string', - default: path.join(fs.getHomeDirectory(), 'github'), - description: 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.' - } - @config.initialize({@configDirPath, resourcePath, projectHomeSchema: ConfigSchema.projectHome}) - - @menu.initialize({resourcePath}) - @contextMenu.initialize({resourcePath, devMode}) - - @keymaps.configDirPath = @configDirPath - @keymaps.resourcePath = resourcePath - @keymaps.devMode = devMode - unless @keymaps.canLoadBundledKeymapsFromMemory() - @keymaps.loadBundledKeymaps() - - @commands.attach(@window) - - @styles.initialize({@configDirPath}) - @packages.initialize({devMode, @configDirPath, resourcePath, safeMode}) - @themes.initialize({@configDirPath, resourcePath, safeMode, devMode}) - - @commandInstaller.initialize(@getVersion()) - @protocolHandlerInstaller.initialize(@config, @notifications) - @uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(this)) - @autoUpdater.initialize() - - @config.load() - - @themes.loadBaseStylesheets() - @initialStyleElements = @styles.getSnapshot() - @themes.initialLoadComplete = true if onlyLoadBaseStyleSheets - @setBodyPlatformClass() - - @stylesElement = @styles.buildStylesElement() - @document.head.appendChild(@stylesElement) - - @keymaps.subscribeToFileReadFailure() - - @installUncaughtErrorHandler() - @attachSaveStateListeners() - @windowEventHandler.initialize(@window, @document) - - didChangeStyles = @didChangeStyles.bind(this) - @disposables.add(@styles.onDidAddStyleElement(didChangeStyles)) - @disposables.add(@styles.onDidUpdateStyleElement(didChangeStyles)) - @disposables.add(@styles.onDidRemoveStyleElement(didChangeStyles)) - - @observeAutoHideMenuBar() - - @disposables.add @applicationDelegate.onDidChangeHistoryManager(=> @history.loadState()) - - preloadPackages: -> - @packages.preloadPackages() - - attachSaveStateListeners: -> - saveState = _.debounce((=> - @window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded - ), @saveStateDebounceInterval) - @document.addEventListener('mousedown', saveState, true) - @document.addEventListener('keydown', saveState, true) - @disposables.add new Disposable => - @document.removeEventListener('mousedown', saveState, true) - @document.removeEventListener('keydown', saveState, true) - - registerDefaultDeserializers: -> - @deserializers.add(Workspace) - @deserializers.add(PaneContainer) - @deserializers.add(PaneAxis) - @deserializers.add(Pane) - @deserializers.add(Dock) - @deserializers.add(Project) - @deserializers.add(TextEditor) - @deserializers.add(TextBuffer) - - registerDefaultCommands: -> - registerDefaultCommands({commandRegistry: @commands, @config, @commandInstaller, notificationManager: @notifications, @project, @clipboard}) - - registerDefaultOpeners: -> - @workspace.addOpener (uri) => - switch uri - when 'atom://.atom/stylesheet' - @workspace.openTextFile(@styles.getUserStyleSheetPath()) - when 'atom://.atom/keymap' - @workspace.openTextFile(@keymaps.getUserKeymapPath()) - when 'atom://.atom/config' - @workspace.openTextFile(@config.getUserConfigPath()) - when 'atom://.atom/init-script' - @workspace.openTextFile(@getUserInitScriptPath()) - - registerDefaultTargetForKeymaps: -> - @keymaps.defaultTarget = @workspace.getElement() - - observeAutoHideMenuBar: -> - @disposables.add @config.onDidChange 'core.autoHideMenuBar', ({newValue}) => - @setAutoHideMenuBar(newValue) - @setAutoHideMenuBar(true) if @config.get('core.autoHideMenuBar') - - reset: -> - @deserializers.clear() - @registerDefaultDeserializers() - - @config.clear() - @config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)} - - @keymaps.clear() - @keymaps.loadBundledKeymaps() - - @commands.clear() - @registerDefaultCommands() - - @styles.restoreSnapshot(@initialStyleElements) - - @menu.clear() - - @clipboard.reset() - - @notifications.clear() - - @contextMenu.clear() - - @packages.reset().then => - @workspace.reset(@packages) - @registerDefaultOpeners() - @project.reset(@packages) - @workspace.subscribeToEvents() - @grammars.clear() - @textEditors.clear() - @views.clear() - - destroy: -> - return if not @project - - @disposables.dispose() - @workspace?.destroy() - @workspace = null - @themes.workspace = null - @project?.destroy() - @project = null - @commands.clear() - @stylesElement.remove() - @config.unobserveUserConfig() - @autoUpdater.destroy() - @uriHandlerRegistry.destroy() - - @uninstallWindowEventHandler() - - ### - Section: Event Subscription - ### - - # Extended: Invoke the given callback whenever {::beep} is called. - # - # * `callback` {Function} to be called whenever {::beep} is called. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidBeep: (callback) -> - @emitter.on 'did-beep', callback - - # Extended: Invoke the given callback when there is an unhandled error, but - # before the devtools pop open - # - # * `callback` {Function} to be called whenever there is an unhandled error - # * `event` {Object} - # * `originalError` {Object} the original error object - # * `message` {String} the original error object - # * `url` {String} Url to the file where the error originated. - # * `line` {Number} - # * `column` {Number} - # * `preventDefault` {Function} call this to avoid popping up the dev tools. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onWillThrowError: (callback) -> - @emitter.on 'will-throw-error', callback - - # Extended: Invoke the given callback whenever there is an unhandled error. - # - # * `callback` {Function} to be called whenever there is an unhandled error - # * `event` {Object} - # * `originalError` {Object} the original error object - # * `message` {String} the original error object - # * `url` {String} Url to the file where the error originated. - # * `line` {Number} - # * `column` {Number} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidThrowError: (callback) -> - @emitter.on 'did-throw-error', callback - - # TODO: Make this part of the public API. We should make onDidThrowError - # match the interface by only yielding an exception object to the handler - # and deprecating the old behavior. - onDidFailAssertion: (callback) -> - @emitter.on 'did-fail-assertion', callback - - # Extended: Invoke the given callback as soon as the shell environment is - # loaded (or immediately if it was already loaded). - # - # * `callback` {Function} to be called whenever there is an unhandled error - whenShellEnvironmentLoaded: (callback) -> - if @shellEnvironmentLoaded - callback() - new Disposable() - else - @emitter.once 'loaded-shell-environment', callback - - ### - Section: Atom Details - ### - - # Public: Returns a {Boolean} that is `true` if the current window is in development mode. - inDevMode: -> - @devMode ?= @getLoadSettings().devMode - - # Public: Returns a {Boolean} that is `true` if the current window is in safe mode. - inSafeMode: -> - @safeMode ?= @getLoadSettings().safeMode - - # Public: Returns a {Boolean} that is `true` if the current window is running specs. - inSpecMode: -> - @specMode ?= @getLoadSettings().isSpec - - # Returns a {Boolean} indicating whether this the first time the window's been - # loaded. - isFirstLoad: -> - @firstLoad ?= @getLoadSettings().firstLoad - - # Public: Get the version of the Atom application. - # - # Returns the version text {String}. - getVersion: -> - @appVersion ?= @getLoadSettings().appVersion - - # Public: Gets the release channel of the Atom application. - # - # Returns the release channel as a {String}. Will return one of `dev`, `beta`, or `stable`. - getReleaseChannel: -> - version = @getVersion() - if version.indexOf('beta') > -1 - 'beta' - else if version.indexOf('dev') > -1 - 'dev' - else - 'stable' - - # Public: Returns a {Boolean} that is `true` if the current version is an official release. - isReleasedVersion: -> - not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix - - # Public: Get the time taken to completely load the current window. - # - # This time include things like loading and activating packages, creating - # DOM elements for the editor, and reading the config. - # - # Returns the {Number} of milliseconds taken to load the window or null - # if the window hasn't finished loading yet. - getWindowLoadTime: -> - @loadTime - - # Public: Get the load settings for the current window. - # - # Returns an {Object} containing all the load setting key/value pairs. - getLoadSettings: -> - @applicationDelegate.getWindowLoadSettings() - - ### - Section: Managing The Atom Window - ### - - # Essential: Open a new Atom window using the given options. - # - # Calling this method without an options parameter will open a prompt to pick - # a file/folder to open in the new window. - # - # * `params` An {Object} with the following keys: - # * `pathsToOpen` An {Array} of {String} paths to open. - # * `newWindow` A {Boolean}, true to always open a new window instead of - # reusing existing windows depending on the paths to open. - # * `devMode` A {Boolean}, true to open the window in development mode. - # Development mode loads the Atom source from the locally cloned - # repository and also loads all the packages in ~/.atom/dev/packages - # * `safeMode` A {Boolean}, true to open the window in safe mode. Safe - # mode prevents all packages installed to ~/.atom/packages from loading. - open: (params) -> - @applicationDelegate.open(params) - - # Extended: Prompt the user to select one or more folders. - # - # * `callback` A {Function} to call once the user has confirmed the selection. - # * `paths` An {Array} of {String} paths that the user selected, or `null` - # if the user dismissed the dialog. - pickFolder: (callback) -> - @applicationDelegate.pickFolder(callback) - - # Essential: Close the current window. - close: -> - @applicationDelegate.closeWindow() - - # Essential: Get the size of current window. - # - # Returns an {Object} in the format `{width: 1000, height: 700}` - getSize: -> - @applicationDelegate.getWindowSize() - - # Essential: Set the size of current window. - # - # * `width` The {Number} of pixels. - # * `height` The {Number} of pixels. - setSize: (width, height) -> - @applicationDelegate.setWindowSize(width, height) - - # Essential: Get the position of current window. - # - # Returns an {Object} in the format `{x: 10, y: 20}` - getPosition: -> - @applicationDelegate.getWindowPosition() - - # Essential: Set the position of current window. - # - # * `x` The {Number} of pixels. - # * `y` The {Number} of pixels. - setPosition: (x, y) -> - @applicationDelegate.setWindowPosition(x, y) - - # Extended: Get the current window - getCurrentWindow: -> - @applicationDelegate.getCurrentWindow() - - # Extended: Move current window to the center of the screen. - center: -> - @applicationDelegate.centerWindow() - - # Extended: Focus the current window. - focus: -> - @applicationDelegate.focusWindow() - @window.focus() - - # Extended: Show the current window. - show: -> - @applicationDelegate.showWindow() - - # Extended: Hide the current window. - hide: -> - @applicationDelegate.hideWindow() - - # Extended: Reload the current window. - reload: -> - @applicationDelegate.reloadWindow() - - # Extended: Relaunch the entire application. - restartApplication: -> - @applicationDelegate.restartApplication() - - # Extended: Returns a {Boolean} that is `true` if the current window is maximized. - isMaximized: -> - @applicationDelegate.isWindowMaximized() - - maximize: -> - @applicationDelegate.maximizeWindow() - - # Extended: Returns a {Boolean} that is `true` if the current window is in full screen mode. - isFullScreen: -> - @applicationDelegate.isWindowFullScreen() - - # Extended: Set the full screen state of the current window. - setFullScreen: (fullScreen=false) -> - @applicationDelegate.setWindowFullScreen(fullScreen) - - # Extended: Toggle the full screen state of the current window. - toggleFullScreen: -> - @setFullScreen(not @isFullScreen()) - - # Restore the window to its previous dimensions and show it. - # - # Restores the full screen and maximized state after the window has resized to - # prevent resize glitches. - displayWindow: -> - @restoreWindowDimensions().then => - steps = [ - @restoreWindowBackground(), - @show(), - @focus() - ] - steps.push(@setFullScreen(true)) if @windowDimensions?.fullScreen - steps.push(@maximize()) if @windowDimensions?.maximized and process.platform isnt 'darwin' - Promise.all(steps) - - # Get the dimensions of this window. - # - # Returns an {Object} with the following keys: - # * `x` The window's x-position {Number}. - # * `y` The window's y-position {Number}. - # * `width` The window's width {Number}. - # * `height` The window's height {Number}. - getWindowDimensions: -> - browserWindow = @getCurrentWindow() - [x, y] = browserWindow.getPosition() - [width, height] = browserWindow.getSize() - maximized = browserWindow.isMaximized() - {x, y, width, height, maximized} - - # Set the dimensions of the window. - # - # The window will be centered if either the x or y coordinate is not set - # in the dimensions parameter. If x or y are omitted the window will be - # centered. If height or width are omitted only the position will be changed. - # - # * `dimensions` An {Object} with the following keys: - # * `x` The new x coordinate. - # * `y` The new y coordinate. - # * `width` The new width. - # * `height` The new height. - setWindowDimensions: ({x, y, width, height}) -> - steps = [] - if width? and height? - steps.push(@setSize(width, height)) - if x? and y? - steps.push(@setPosition(x, y)) - else - steps.push(@center()) - Promise.all(steps) - - # Returns true if the dimensions are useable, false if they should be ignored. - # Work around for https://github.com/atom/atom-shell/issues/473 - isValidDimensions: ({x, y, width, height}={}) -> - width > 0 and height > 0 and x + width > 0 and y + height > 0 - - storeWindowDimensions: -> - @windowDimensions = @getWindowDimensions() - if @isValidDimensions(@windowDimensions) - localStorage.setItem("defaultWindowDimensions", JSON.stringify(@windowDimensions)) - - getDefaultWindowDimensions: -> - {windowDimensions} = @getLoadSettings() - return windowDimensions if windowDimensions? - - dimensions = null - try - dimensions = JSON.parse(localStorage.getItem("defaultWindowDimensions")) - catch error - console.warn "Error parsing default window dimensions", error - localStorage.removeItem("defaultWindowDimensions") - - if @isValidDimensions(dimensions) - dimensions - else - {width, height} = @applicationDelegate.getPrimaryDisplayWorkAreaSize() - {x: 0, y: 0, width: Math.min(1024, width), height} - - restoreWindowDimensions: -> - unless @windowDimensions? and @isValidDimensions(@windowDimensions) - @windowDimensions = @getDefaultWindowDimensions() - @setWindowDimensions(@windowDimensions).then => @windowDimensions - - restoreWindowBackground: -> - if backgroundColor = window.localStorage.getItem('atom:window-background-color') - @backgroundStylesheet = document.createElement('style') - @backgroundStylesheet.type = 'text/css' - @backgroundStylesheet.innerText = 'html, body { background: ' + backgroundColor + ' !important; }' - document.head.appendChild(@backgroundStylesheet) - - storeWindowBackground: -> - return if @inSpecMode() - - backgroundColor = @window.getComputedStyle(@workspace.getElement())['background-color'] - @window.localStorage.setItem('atom:window-background-color', backgroundColor) - - # Call this method when establishing a real application window. - startEditorWindow: -> - @unloaded = false - - updateProcessEnvPromise = @updateProcessEnvAndTriggerHooks() - - loadStatePromise = @loadState().then (state) => - @windowDimensions = state?.windowDimensions - @displayWindow().then => - @commandInstaller.installAtomCommand false, (error) -> - console.warn error.message if error? - @commandInstaller.installApmCommand false, (error) -> - console.warn error.message if error? - - @disposables.add(@applicationDelegate.onDidOpenLocations(@openLocations.bind(this))) - @disposables.add(@applicationDelegate.onApplicationMenuCommand(@dispatchApplicationMenuCommand.bind(this))) - @disposables.add(@applicationDelegate.onContextMenuCommand(@dispatchContextMenuCommand.bind(this))) - @disposables.add(@applicationDelegate.onURIMessage(@dispatchURIMessage.bind(this))) - @disposables.add @applicationDelegate.onDidRequestUnload => - @saveState({isUnloading: true}) - .catch(console.error) - .then => - @workspace?.confirmClose({ - windowCloseRequested: true, - projectHasPaths: @project.getPaths().length > 0 - }) - .then (closing) => - if closing - @packages.deactivatePackages().then -> closing - else - closing - - @listenForUpdates() - - @registerDefaultTargetForKeymaps() - - @packages.loadPackages() - - startTime = Date.now() - @deserialize(state).then => - @deserializeTimings.atom = Date.now() - startTime - - if process.platform is 'darwin' and @config.get('core.titleBar') is 'custom' - @workspace.addHeaderPanel({item: new TitleBar({@workspace, @themes, @applicationDelegate})}) - @document.body.classList.add('custom-title-bar') - if process.platform is 'darwin' and @config.get('core.titleBar') is 'custom-inset' - @workspace.addHeaderPanel({item: new TitleBar({@workspace, @themes, @applicationDelegate})}) - @document.body.classList.add('custom-inset-title-bar') - if process.platform is 'darwin' and @config.get('core.titleBar') is 'hidden' - @document.body.classList.add('hidden-title-bar') - - @document.body.appendChild(@workspace.getElement()) - @backgroundStylesheet?.remove() - - @watchProjectPaths() - - @packages.activate() - @keymaps.loadUserKeymap() - @requireUserInitScript() unless @getLoadSettings().safeMode - - @menu.update() - - @openInitialEmptyEditorIfNecessary() - - loadHistoryPromise = @history.loadState().then => - @reopenProjectMenuManager = new ReopenProjectMenuManager({ - @menu, @commands, @history, @config, - open: (paths) => @open(pathsToOpen: paths) - }) - @reopenProjectMenuManager.update() - - Promise.all([loadStatePromise, loadHistoryPromise, updateProcessEnvPromise]) - - serialize: (options) -> - version: @constructor.version - project: @project.serialize(options) - workspace: @workspace.serialize() - packageStates: @packages.serialize() - grammars: {grammarOverridesByPath: @grammars.grammarOverridesByPath} - fullScreen: @isFullScreen() - windowDimensions: @windowDimensions - textEditors: @textEditors.serialize() - - unloadEditorWindow: -> - return if not @project - - @storeWindowBackground() - @saveBlobStoreSync() - @unloaded = true - - saveBlobStoreSync: -> - if @enablePersistence - @blobStore.save() - - openInitialEmptyEditorIfNecessary: -> - return unless @config.get('core.openEmptyEditorOnStart') - if @getLoadSettings().initialPaths?.length is 0 and @workspace.getPaneItems().length is 0 - @workspace.open(null) - - installUncaughtErrorHandler: -> - @previousWindowErrorHandler = @window.onerror - @window.onerror = => - @lastUncaughtError = Array::slice.call(arguments) - [message, url, line, column, originalError] = @lastUncaughtError - - {line, column, source} = mapSourcePosition({source: url, line, column}) - - if url is '' - url = source - - eventObject = {message, url, line, column, originalError} - - openDevTools = true - eventObject.preventDefault = -> openDevTools = false - - @emitter.emit 'will-throw-error', eventObject - - if openDevTools - @openDevTools().then => @executeJavaScriptInDevTools('DevToolsAPI.showPanel("console")') - - @emitter.emit 'did-throw-error', {message, url, line, column, originalError} - - uninstallUncaughtErrorHandler: -> - @window.onerror = @previousWindowErrorHandler - - installWindowEventHandler: -> - @windowEventHandler = new WindowEventHandler({atomEnvironment: this, @applicationDelegate}) - @windowEventHandler.initialize(@window, @document) - - uninstallWindowEventHandler: -> - @windowEventHandler?.unsubscribe() - @windowEventHandler = null - - didChangeStyles: (styleElement) -> - TextEditor.didUpdateStyles() - if styleElement.textContent.indexOf('scrollbar') >= 0 - TextEditor.didUpdateScrollbarStyles() - - updateProcessEnvAndTriggerHooks: -> - @updateProcessEnv(@getLoadSettings().env).then => - @shellEnvironmentLoaded = true - @emitter.emit('loaded-shell-environment') - @packages.triggerActivationHook('core:loaded-shell-environment') - - ### - Section: Messaging the User - ### - - # Essential: Visually and audibly trigger a beep. - beep: -> - @applicationDelegate.playBeepSound() if @config.get('core.audioBeep') - @emitter.emit 'did-beep' - - # Essential: A flexible way to open a dialog akin to an alert dialog. - # - # If the dialog is closed (via `Esc` key or `X` in the top corner) without selecting a button - # the first button will be clicked unless a "Cancel" or "No" button is provided. - # - # ## Examples - # - # ```coffee - # atom.confirm - # message: 'How you feeling?' - # detailedMessage: 'Be honest.' - # buttons: - # Good: -> window.alert('good to hear') - # Bad: -> window.alert('bummer') - # ``` - # - # * `options` An {Object} with the following keys: - # * `message` The {String} message to display. - # * `detailedMessage` (optional) The {String} detailed message to display. - # * `buttons` (optional) Either an array of strings or an object where keys are - # button names and the values are callbacks to invoke when clicked. - # - # Returns the chosen button index {Number} if the buttons option is an array or the return value of the callback if the buttons option is an object. - confirm: (params={}) -> - @applicationDelegate.confirm(params) - - ### - Section: Managing the Dev Tools - ### - - # Extended: Open the dev tools for the current window. - # - # Returns a {Promise} that resolves when the DevTools have been opened. - openDevTools: -> - @applicationDelegate.openWindowDevTools() - - # Extended: Toggle the visibility of the dev tools for the current window. - # - # Returns a {Promise} that resolves when the DevTools have been opened or - # closed. - toggleDevTools: -> - @applicationDelegate.toggleWindowDevTools() - - # Extended: Execute code in dev tools. - executeJavaScriptInDevTools: (code) -> - @applicationDelegate.executeJavaScriptInWindowDevTools(code) - - ### - Section: Private - ### - - assert: (condition, message, callbackOrMetadata) -> - return true if condition - - error = new Error("Assertion failed: #{message}") - Error.captureStackTrace(error, @assert) - - if callbackOrMetadata? - if typeof callbackOrMetadata is 'function' - callbackOrMetadata?(error) - else - error.metadata = callbackOrMetadata - - @emitter.emit 'did-fail-assertion', error - unless @isReleasedVersion() - throw error - - false - - loadThemes: -> - @themes.load() - - # Notify the browser project of the window's current project path - watchProjectPaths: -> - @disposables.add @project.onDidChangePaths => - @applicationDelegate.setRepresentedDirectoryPaths(@project.getPaths()) - - setDocumentEdited: (edited) -> - @applicationDelegate.setWindowDocumentEdited?(edited) - - setRepresentedFilename: (filename) -> - @applicationDelegate.setWindowRepresentedFilename?(filename) - - addProjectFolder: -> - @pickFolder (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 = []) -> - center = @workspace.getCenter() - windowIsUnused = => - for container in @workspace.getPaneContainers() - for item in container.getPaneItems() - if item instanceof TextEditor - return false if item.getPath() or item.isModified() - else - return false if container is center - true - - 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()) - - showSaveDialogSync: (options={}) -> - @applicationDelegate.showSaveDialog(options) - - saveState: (options, storageKey) -> - new Promise (resolve, reject) => - if @enablePersistence and @project - state = @serialize(options) - savePromise = - if storageKey ?= @getStateKey(@project?.getPaths()) - @stateStore.save(storageKey, state) - else - @applicationDelegate.setTemporaryWindowState(state) - savePromise.catch(reject).then(resolve) - else - resolve() - - loadState: (stateKey) -> - if @enablePersistence - if stateKey ?= @getStateKey(@getLoadSettings().initialPaths) - @stateStore.load(stateKey).then (state) => - if state - state - else - # TODO: remove this when every user has migrated to the IndexedDb state store. - @getStorageFolder().load(stateKey) - else - @applicationDelegate.getTemporaryWindowState() - else - Promise.resolve(null) - - deserialize: (state) -> - return Promise.resolve() unless state? - - if grammarOverridesByPath = state.grammars?.grammarOverridesByPath - @grammars.grammarOverridesByPath = grammarOverridesByPath - - @setFullScreen(state.fullScreen) - - missingProjectPaths = [] - - @packages.packageStates = state.packageStates ? {} - - startTime = Date.now() - if state.project? - projectPromise = @project.deserialize(state.project, @deserializers) - .catch (err) => - if err.missingProjectPaths? - missingProjectPaths.push(err.missingProjectPaths...) - else - @notifications.addError "Unable to deserialize project", description: err.message, stack: err.stack - else - projectPromise = Promise.resolve() - - projectPromise.then => - @deserializeTimings.project = Date.now() - startTime - - @textEditors.deserialize(state.textEditors) if state.textEditors - - startTime = Date.now() - @workspace.deserialize(state.workspace, @deserializers) if state.workspace? - @deserializeTimings.workspace = Date.now() - startTime - - if missingProjectPaths.length > 0 - count = if missingProjectPaths.length is 1 then '' else missingProjectPaths.length + ' ' - noun = if missingProjectPaths.length is 1 then 'directory' else 'directories' - toBe = if missingProjectPaths.length is 1 then 'is' else 'are' - escaped = missingProjectPaths.map (projectPath) -> "`#{projectPath}`" - group = switch escaped.length - when 1 then escaped[0] - when 2 then "#{escaped[0]} and #{escaped[1]}" - else escaped[..-2].join(", ") + ", and #{escaped[escaped.length - 1]}" - - @notifications.addError "Unable to open #{count}project #{noun}", - description: "Project #{noun} #{group} #{toBe} no longer on disk." - - getStateKey: (paths) -> - if paths?.length > 0 - sha1 = crypto.createHash('sha1').update(paths.slice().sort().join("\n")).digest('hex') - "editor-#{sha1}" - else - null - - getStorageFolder: -> - @storageFolder ?= new StorageFolder(@getConfigDirPath()) - - getConfigDirPath: -> - @configDirPath ?= process.env.ATOM_HOME - - getUserInitScriptPath: -> - initScriptPath = fs.resolve(@getConfigDirPath(), 'init', ['js', 'coffee']) - initScriptPath ? path.join(@getConfigDirPath(), 'init.coffee') - - requireUserInitScript: -> - if userInitScriptPath = @getUserInitScriptPath() - try - require(userInitScriptPath) if fs.isFileSync(userInitScriptPath) - catch error - @notifications.addError "Failed to load `#{userInitScriptPath}`", - detail: error.message - dismissable: true - - # TODO: We should deprecate the update events here, and use `atom.autoUpdater` instead - onUpdateAvailable: (callback) -> - @emitter.on 'update-available', callback - - updateAvailable: (details) -> - @emitter.emit 'update-available', details - - listenForUpdates: -> - # listen for updates available locally (that have been successfully downloaded) - @disposables.add(@autoUpdater.onDidCompleteDownloadingUpdate(@updateAvailable.bind(this))) - - setBodyPlatformClass: -> - @document.body.classList.add("platform-#{process.platform}") - - setAutoHideMenuBar: (autoHide) -> - @applicationDelegate.setAutoHideWindowMenuBar(autoHide) - @applicationDelegate.setWindowMenuBarVisibility(not autoHide) - - dispatchApplicationMenuCommand: (command, arg) -> - activeElement = @document.activeElement - # Use the workspace element if body has focus - if activeElement is @document.body - activeElement = @workspace.getElement() - @commands.dispatch(activeElement, command, arg) - - dispatchContextMenuCommand: (command, args...) -> - @commands.dispatch(@contextMenu.activeElement, command, args) - - dispatchURIMessage: (uri) -> - if @packages.hasLoadedInitialPackages() - @uriHandlerRegistry.handleURI(uri) - else - sub = @packages.onDidLoadInitialPackages -> - sub.dispose() - @uriHandlerRegistry.handleURI(uri) - - 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) - pushFolderToOpen @project.getDirectoryForProjectPath(pathToOpen).getPath() - else if fs.existsSync(path.dirname(pathToOpen)) - pushFolderToOpen @project.getDirectoryForProjectPath(path.dirname(pathToOpen)).getPath() - else - pushFolderToOpen @project.getDirectoryForProjectPath(pathToOpen).getPath() - - unless fs.isDirectorySync(pathToOpen) - fileLocationsToOpen.push({pathToOpen, initialLine, initialColumn}) - - 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) => - requestId = @nextProxyRequestId++ - disposable = @applicationDelegate.onDidResolveProxy (id, proxy) -> - if id is requestId - disposable.dispose() - resolve(proxy) - - @applicationDelegate.resolveProxy(requestId, url) - -# Preserve this deprecation until 2.0. Sorry. Should have removed Q sooner. -Promise.prototype.done = (callback) -> - deprecate("Atom now uses ES6 Promises instead of Q. Call promise.then instead of promise.done") - @then(callback) diff --git a/src/atom-environment.js b/src/atom-environment.js new file mode 100644 index 000000000..1c2f1ebcf --- /dev/null +++ b/src/atom-environment.js @@ -0,0 +1,1351 @@ +const crypto = require('crypto') +const path = require('path') +const {ipcRenderer} = require('electron') + +const _ = require('underscore-plus') +const {deprecate} = require('grim') +const {CompositeDisposable, Disposable, Emitter} = require('event-kit') +const fs = require('fs-plus') +const {mapSourcePosition} = require('@atom/source-map-support') +const WindowEventHandler = require('./window-event-handler') +const StateStore = require('./state-store') +const StorageFolder = require('./storage-folder') +const registerDefaultCommands = require('./register-default-commands') +const {updateProcessEnv} = require('./update-process-env') +const ConfigSchema = require('./config-schema') + +const DeserializerManager = require('./deserializer-manager') +const ViewRegistry = require('./view-registry') +const NotificationManager = require('./notification-manager') +const Config = require('./config') +const KeymapManager = require('./keymap-extensions') +const TooltipManager = require('./tooltip-manager') +const CommandRegistry = require('./command-registry') +const URIHandlerRegistry = require('./uri-handler-registry') +const GrammarRegistry = require('./grammar-registry') +const {HistoryManager} = require('./history-manager') +const ReopenProjectMenuManager = require('./reopen-project-menu-manager') +const StyleManager = require('./style-manager') +const PackageManager = require('./package-manager') +const ThemeManager = require('./theme-manager') +const MenuManager = require('./menu-manager') +const ContextMenuManager = require('./context-menu-manager') +const CommandInstaller = require('./command-installer') +const CoreURIHandlers = require('./core-uri-handlers') +const ProtocolHandlerInstaller = require('./protocol-handler-installer') +const Project = require('./project') +const TitleBar = require('./title-bar') +const Workspace = require('./workspace') +const PaneContainer = require('./pane-container') +const PaneAxis = require('./pane-axis') +const Pane = require('./pane') +const Dock = require('./dock') +const TextEditor = require('./text-editor') +const TextBuffer = require('text-buffer') +const TextEditorRegistry = require('./text-editor-registry') +const AutoUpdateManager = require('./auto-update-manager') + +let nextId = 0 + +// Essential: Atom global for dealing with packages, themes, menus, and the window. +// +// An instance of this class is always available as the `atom` global. +class AtomEnvironment { + /* + Section: Construction and Destruction + */ + + // Call .loadOrCreate instead + constructor (params = {}) { + this.id = (params.id != null) ? params.id : nextId++ + this.clipboard = params.clipboard + this.updateProcessEnv = params.updateProcessEnv || updateProcessEnv + this.enablePersistence = params.enablePersistence + this.applicationDelegate = params.applicationDelegate + + this.nextProxyRequestId = 0 + this.unloaded = false + this.loadTime = null + this.emitter = new Emitter() + this.disposables = new CompositeDisposable() + this.deserializers = new DeserializerManager(this) + this.deserializeTimings = {} + this.views = new ViewRegistry(this) + TextEditor.setScheduler(this.views) + this.notifications = new NotificationManager() + + this.stateStore = new StateStore('AtomEnvironments', 1) + + this.config = new Config({ + notificationManager: this.notifications, + enablePersistence: this.enablePersistence + }) + this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)}) + + this.keymaps = new KeymapManager({notificationManager: this.notifications}) + this.tooltips = new TooltipManager({keymapManager: this.keymaps, viewRegistry: this.views}) + this.commands = new CommandRegistry() + this.uriHandlerRegistry = new URIHandlerRegistry() + this.grammars = new GrammarRegistry({config: this.config}) + this.styles = new StyleManager() + this.packages = new PackageManager({ + config: this.config, + styleManager: this.styles, + commandRegistry: this.commands, + keymapManager: this.keymaps, + notificationManager: this.notifications, + grammarRegistry: this.grammars, + deserializerManager: this.deserializers, + viewRegistry: this.views, + uriHandlerRegistry: this.uriHandlerRegistry + }) + this.themes = new ThemeManager({ + packageManager: this.packages, + config: this.config, + styleManager: this.styles, + notificationManager: this.notifications, + viewRegistry: this.views + }) + this.menu = new MenuManager({keymapManager: this.keymaps, packageManager: this.packages}) + this.contextMenu = new ContextMenuManager({keymapManager: this.keymaps}) + this.packages.setMenuManager(this.menu) + this.packages.setContextMenuManager(this.contextMenu) + this.packages.setThemeManager(this.themes) + + this.project = new Project({notificationManager: this.notifications, packageManager: this.packages, config: this.config, applicationDelegate: this.applicationDelegate}) + this.commandInstaller = new CommandInstaller(this.applicationDelegate) + this.protocolHandlerInstaller = new ProtocolHandlerInstaller() + + this.textEditors = new TextEditorRegistry({ + config: this.config, + grammarRegistry: this.grammars, + assert: this.assert.bind(this), + packageManager: this.packages + }) + + this.workspace = new Workspace({ + config: this.config, + project: this.project, + packageManager: this.packages, + grammarRegistry: this.grammars, + deserializerManager: this.deserializers, + notificationManager: this.notifications, + applicationDelegate: this.applicationDelegate, + viewRegistry: this.views, + assert: this.assert.bind(this), + textEditorRegistry: this.textEditors, + styleManager: this.styles, + enablePersistence: this.enablePersistence + }) + + this.themes.workspace = this.workspace + + this.autoUpdater = new AutoUpdateManager({applicationDelegate: this.applicationDelegate}) + + if (this.keymaps.canLoadBundledKeymapsFromMemory()) { + this.keymaps.loadBundledKeymaps() + } + + this.registerDefaultCommands() + this.registerDefaultOpeners() + this.registerDefaultDeserializers() + + this.windowEventHandler = new WindowEventHandler({atomEnvironment: this, applicationDelegate: this.applicationDelegate}) + + this.history = new HistoryManager({project: this.project, commands: this.commands, stateStore: this.stateStore}) + // Keep instances of HistoryManager in sync + this.disposables.add(this.history.onDidChangeProjects(event => { + if (!event.reloaded) this.applicationDelegate.didChangeHistoryManager() + })) + } + + initialize (params = {}) { + // This will force TextEditorElement to register the custom element, so that + // using `document.createElement('atom-text-editor')` works if it's called + // before opening a buffer. + require('./text-editor-element') + + this.window = params.window + this.document = params.document + this.blobStore = params.blobStore + this.configDirPath = params.configDirPath + + const {devMode, safeMode, resourcePath, clearWindowState} = this.getLoadSettings() + + if (clearWindowState) { + this.getStorageFolder().clear() + this.stateStore.clear() + } + + ConfigSchema.projectHome = { + type: 'string', + default: path.join(fs.getHomeDirectory(), 'github'), + description: 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.' + } + this.config.initialize({configDirPath: this.configDirPath, resourcePath, projectHomeSchema: ConfigSchema.projectHome}) + + this.menu.initialize({resourcePath}) + this.contextMenu.initialize({resourcePath, devMode}) + + this.keymaps.configDirPath = this.configDirPath + this.keymaps.resourcePath = resourcePath + this.keymaps.devMode = devMode + if (!this.keymaps.canLoadBundledKeymapsFromMemory()) { + this.keymaps.loadBundledKeymaps() + } + + this.commands.attach(this.window) + + this.styles.initialize({configDirPath: this.configDirPath}) + this.packages.initialize({devMode, configDirPath: this.configDirPath, resourcePath, safeMode}) + this.themes.initialize({configDirPath: this.configDirPath, resourcePath, safeMode, devMode}) + + this.commandInstaller.initialize(this.getVersion()) + this.protocolHandlerInstaller.initialize(this.config, this.notifications) + this.uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(this)) + this.autoUpdater.initialize() + + this.config.load() + + this.themes.loadBaseStylesheets() + this.initialStyleElements = this.styles.getSnapshot() + if (params.onlyLoadBaseStyleSheets) this.themes.initialLoadComplete = true + this.setBodyPlatformClass() + + this.stylesElement = this.styles.buildStylesElement() + this.document.head.appendChild(this.stylesElement) + + this.keymaps.subscribeToFileReadFailure() + + this.installUncaughtErrorHandler() + this.attachSaveStateListeners() + this.windowEventHandler.initialize(this.window, this.document) + + const didChangeStyles = this.didChangeStyles.bind(this) + this.disposables.add(this.styles.onDidAddStyleElement(didChangeStyles)) + this.disposables.add(this.styles.onDidUpdateStyleElement(didChangeStyles)) + this.disposables.add(this.styles.onDidRemoveStyleElement(didChangeStyles)) + + this.observeAutoHideMenuBar() + + this.disposables.add(this.applicationDelegate.onDidChangeHistoryManager(() => this.history.loadState())) + } + + preloadPackages () { + return this.packages.preloadPackages() + } + + attachSaveStateListeners () { + const saveState = _.debounce(() => { + this.window.requestIdleCallback(() => { + if (!this.unloaded) this.saveState({isUnloading: false}) + }) + }, this.saveStateDebounceInterval) + this.document.addEventListener('mousedown', saveState, true) + this.document.addEventListener('keydown', saveState, true) + this.disposables.add(new Disposable(() => { + this.document.removeEventListener('mousedown', saveState, true) + this.document.removeEventListener('keydown', saveState, true) + })) + } + + registerDefaultDeserializers () { + this.deserializers.add(Workspace) + this.deserializers.add(PaneContainer) + this.deserializers.add(PaneAxis) + this.deserializers.add(Pane) + this.deserializers.add(Dock) + this.deserializers.add(Project) + this.deserializers.add(TextEditor) + this.deserializers.add(TextBuffer) + } + + registerDefaultCommands () { + registerDefaultCommands({commandRegistry: this.commands, config: this.config, commandInstaller: this.commandInstaller, notificationManager: this.notifications, project: this.project, clipboard: this.clipboard}) + } + + registerDefaultOpeners () { + this.workspace.addOpener(uri => { + switch (uri) { + case 'atom://.atom/stylesheet': + return this.workspace.openTextFile(this.styles.getUserStyleSheetPath()) + case 'atom://.atom/keymap': + return this.workspace.openTextFile(this.keymaps.getUserKeymapPath()) + case 'atom://.atom/config': + return this.workspace.openTextFile(this.config.getUserConfigPath()) + case 'atom://.atom/init-script': + return this.workspace.openTextFile(this.getUserInitScriptPath()) + } + }) + } + + registerDefaultTargetForKeymaps () { + this.keymaps.defaultTarget = this.workspace.getElement() + } + + observeAutoHideMenuBar () { + this.disposables.add(this.config.onDidChange('core.autoHideMenuBar', ({newValue}) => { + this.setAutoHideMenuBar(newValue) + })) + if (this.config.get('core.autoHideMenuBar')) this.setAutoHideMenuBar(true) + } + + reset () { + this.deserializers.clear() + this.registerDefaultDeserializers() + + this.config.clear() + this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)}) + + this.keymaps.clear() + this.keymaps.loadBundledKeymaps() + + this.commands.clear() + this.registerDefaultCommands() + + this.styles.restoreSnapshot(this.initialStyleElements) + + this.menu.clear() + + this.clipboard.reset() + + this.notifications.clear() + + this.contextMenu.clear() + + return this.packages.reset().then(() => { + this.workspace.reset(this.packages) + this.registerDefaultOpeners() + this.project.reset(this.packages) + this.workspace.subscribeToEvents() + this.grammars.clear() + this.textEditors.clear() + this.views.clear() + }) + } + + destroy () { + if (!this.project) return + + this.disposables.dispose() + if (this.workspace) this.workspace.destroy() + this.workspace = null + this.themes.workspace = null + if (this.project) this.project.destroy() + this.project = null + this.commands.clear() + this.stylesElement.remove() + this.config.unobserveUserConfig() + this.autoUpdater.destroy() + this.uriHandlerRegistry.destroy() + + this.uninstallWindowEventHandler() + } + + /* + Section: Event Subscription + */ + + // Extended: Invoke the given callback whenever {::beep} is called. + // + // * `callback` {Function} to be called whenever {::beep} is called. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidBeep (callback) { + return this.emitter.on('did-beep', callback) + } + + // Extended: Invoke the given callback when there is an unhandled error, but + // before the devtools pop open + // + // * `callback` {Function} to be called whenever there is an unhandled error + // * `event` {Object} + // * `originalError` {Object} the original error object + // * `message` {String} the original error object + // * `url` {String} Url to the file where the error originated. + // * `line` {Number} + // * `column` {Number} + // * `preventDefault` {Function} call this to avoid popping up the dev tools. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onWillThrowError (callback) { + return this.emitter.on('will-throw-error', callback) + } + + // Extended: Invoke the given callback whenever there is an unhandled error. + // + // * `callback` {Function} to be called whenever there is an unhandled error + // * `event` {Object} + // * `originalError` {Object} the original error object + // * `message` {String} the original error object + // * `url` {String} Url to the file where the error originated. + // * `line` {Number} + // * `column` {Number} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidThrowError (callback) { + return this.emitter.on('did-throw-error', callback) + } + + // TODO: Make this part of the public API. We should make onDidThrowError + // match the interface by only yielding an exception object to the handler + // and deprecating the old behavior. + onDidFailAssertion (callback) { + return this.emitter.on('did-fail-assertion', callback) + } + + // Extended: Invoke the given callback as soon as the shell environment is + // loaded (or immediately if it was already loaded). + // + // * `callback` {Function} to be called whenever there is an unhandled error + whenShellEnvironmentLoaded (callback) { + if (this.shellEnvironmentLoaded) { + callback() + return new Disposable() + } else { + return this.emitter.once('loaded-shell-environment', callback) + } + } + + /* + Section: Atom Details + */ + + // Public: Returns a {Boolean} that is `true` if the current window is in development mode. + inDevMode () { + if (this.devMode == null) this.devMode = this.getLoadSettings().devMode + return this.devMode + } + + // Public: Returns a {Boolean} that is `true` if the current window is in safe mode. + inSafeMode () { + if (this.safeMode == null) this.safeMode = this.getLoadSettings().safeMode + return this.safeMode + } + + // Public: Returns a {Boolean} that is `true` if the current window is running specs. + inSpecMode () { + if (this.specMode == null) this.specMode = this.getLoadSettings().isSpec + return this.specMode + } + + // Returns a {Boolean} indicating whether this the first time the window's been + // loaded. + isFirstLoad () { + if (this.firstLoad == null) this.firstLoad = this.getLoadSettings().firstLoad + return this.firstLoad + } + + // Public: Get the version of the Atom application. + // + // Returns the version text {String}. + getVersion () { + if (this.appVersion == null) this.appVersion = this.getLoadSettings().appVersion + return this.appVersion + } + + // Public: Gets the release channel of the Atom application. + // + // Returns the release channel as a {String}. Will return one of `dev`, `beta`, or `stable`. + getReleaseChannel () { + const version = this.getVersion() + if (version.includes('beta')) { + return 'beta' + } else if (version.includes('dev')) { + return 'dev' + } else { + return 'stable' + } + } + + // Public: Returns a {Boolean} that is `true` if the current version is an official release. + isReleasedVersion () { + return !/\w{7}/.test(this.getVersion()) // Check if the release is a 7-character SHA prefix + } + + // Public: Get the time taken to completely load the current window. + // + // This time include things like loading and activating packages, creating + // DOM elements for the editor, and reading the config. + // + // Returns the {Number} of milliseconds taken to load the window or null + // if the window hasn't finished loading yet. + getWindowLoadTime () { + return this.loadTime + } + + // Public: Get the load settings for the current window. + // + // Returns an {Object} containing all the load setting key/value pairs. + getLoadSettings () { + return this.applicationDelegate.getWindowLoadSettings() + } + + /* + Section: Managing The Atom Window + */ + + // Essential: Open a new Atom window using the given options. + // + // Calling this method without an options parameter will open a prompt to pick + // a file/folder to open in the new window. + // + // * `params` An {Object} with the following keys: + // * `pathsToOpen` An {Array} of {String} paths to open. + // * `newWindow` A {Boolean}, true to always open a new window instead of + // reusing existing windows depending on the paths to open. + // * `devMode` A {Boolean}, true to open the window in development mode. + // Development mode loads the Atom source from the locally cloned + // repository and also loads all the packages in ~/.atom/dev/packages + // * `safeMode` A {Boolean}, true to open the window in safe mode. Safe + // mode prevents all packages installed to ~/.atom/packages from loading. + open (params) { + return this.applicationDelegate.open(params) + } + + // Extended: Prompt the user to select one or more folders. + // + // * `callback` A {Function} to call once the user has confirmed the selection. + // * `paths` An {Array} of {String} paths that the user selected, or `null` + // if the user dismissed the dialog. + pickFolder (callback) { + return this.applicationDelegate.pickFolder(callback) + } + + // Essential: Close the current window. + close () { + return this.applicationDelegate.closeWindow() + } + + // Essential: Get the size of current window. + // + // Returns an {Object} in the format `{width: 1000, height: 700}` + getSize () { + return this.applicationDelegate.getWindowSize() + } + + // Essential: Set the size of current window. + // + // * `width` The {Number} of pixels. + // * `height` The {Number} of pixels. + setSize (width, height) { + return this.applicationDelegate.setWindowSize(width, height) + } + + // Essential: Get the position of current window. + // + // Returns an {Object} in the format `{x: 10, y: 20}` + getPosition () { + return this.applicationDelegate.getWindowPosition() + } + + // Essential: Set the position of current window. + // + // * `x` The {Number} of pixels. + // * `y` The {Number} of pixels. + setPosition (x, y) { + return this.applicationDelegate.setWindowPosition(x, y) + } + + // Extended: Get the current window + getCurrentWindow () { + return this.applicationDelegate.getCurrentWindow() + } + + // Extended: Move current window to the center of the screen. + center () { + return this.applicationDelegate.centerWindow() + } + + // Extended: Focus the current window. + focus () { + this.applicationDelegate.focusWindow() + return this.window.focus() + } + + // Extended: Show the current window. + show () { + return this.applicationDelegate.showWindow() + } + + // Extended: Hide the current window. + hide () { + return this.applicationDelegate.hideWindow() + } + + // Extended: Reload the current window. + reload () { + return this.applicationDelegate.reloadWindow() + } + + // Extended: Relaunch the entire application. + restartApplication () { + return this.applicationDelegate.restartApplication() + } + + // Extended: Returns a {Boolean} that is `true` if the current window is maximized. + isMaximized () { + return this.applicationDelegate.isWindowMaximized() + } + + maximize () { + return this.applicationDelegate.maximizeWindow() + } + + // Extended: Returns a {Boolean} that is `true` if the current window is in full screen mode. + isFullScreen () { + return this.applicationDelegate.isWindowFullScreen() + } + + // Extended: Set the full screen state of the current window. + setFullScreen (fullScreen = false) { + return this.applicationDelegate.setWindowFullScreen(fullScreen) + } + + // Extended: Toggle the full screen state of the current window. + toggleFullScreen () { + return this.setFullScreen(!this.isFullScreen()) + } + + // Restore the window to its previous dimensions and show it. + // + // Restores the full screen and maximized state after the window has resized to + // prevent resize glitches. + displayWindow () { + return this.restoreWindowDimensions().then(() => { + const steps = [ + this.restoreWindowBackground(), + this.show(), + this.focus() + ] + if (this.windowDimensions && this.windowDimensions.fullScreen) { + steps.push(this.setFullScreen(true)) + } + if (this.windowDimensions && this.windowDimensions.maximized && process.platform !== 'darwin') { + steps.push(this.maximize()) + } + return Promise.all(steps) + }) + } + + // Get the dimensions of this window. + // + // Returns an {Object} with the following keys: + // * `x` The window's x-position {Number}. + // * `y` The window's y-position {Number}. + // * `width` The window's width {Number}. + // * `height` The window's height {Number}. + getWindowDimensions () { + const browserWindow = this.getCurrentWindow() + const [x, y] = browserWindow.getPosition() + const [width, height] = browserWindow.getSize() + const maximized = browserWindow.isMaximized() + return {x, y, width, height, maximized} + } + + // Set the dimensions of the window. + // + // The window will be centered if either the x or y coordinate is not set + // in the dimensions parameter. If x or y are omitted the window will be + // centered. If height or width are omitted only the position will be changed. + // + // * `dimensions` An {Object} with the following keys: + // * `x` The new x coordinate. + // * `y` The new y coordinate. + // * `width` The new width. + // * `height` The new height. + setWindowDimensions ({x, y, width, height}) { + const steps = [] + if (width != null && height != null) { + steps.push(this.setSize(width, height)) + } + if (x != null && y != null) { + steps.push(this.setPosition(x, y)) + } else { + steps.push(this.center()) + } + return Promise.all(steps) + } + + // Returns true if the dimensions are useable, false if they should be ignored. + // Work around for https://github.com/atom/atom-shell/issues/473 + isValidDimensions ({x, y, width, height} = {}) { + return (width > 0) && (height > 0) && ((x + width) > 0) && ((y + height) > 0) + } + + storeWindowDimensions () { + this.windowDimensions = this.getWindowDimensions() + if (this.isValidDimensions(this.windowDimensions)) { + localStorage.setItem('defaultWindowDimensions', JSON.stringify(this.windowDimensions)) + } + } + + getDefaultWindowDimensions () { + const {windowDimensions} = this.getLoadSettings() + if (windowDimensions) return windowDimensions + + let dimensions + try { + dimensions = JSON.parse(localStorage.getItem('defaultWindowDimensions')) + } catch (error) { + console.warn('Error parsing default window dimensions', error) + localStorage.removeItem('defaultWindowDimensions') + } + + if (dimensions && this.isValidDimensions(dimensions)) { + return dimensions + } else { + const {width, height} = this.applicationDelegate.getPrimaryDisplayWorkAreaSize() + return {x: 0, y: 0, width: Math.min(1024, width), height} + } + } + + restoreWindowDimensions () { + if (!this.windowDimensions || !this.isValidDimensions(this.windowDimensions)) { + this.windowDimensions = this.getDefaultWindowDimensions() + } + return this.setWindowDimensions(this.windowDimensions).then(() => this.windowDimensions) + } + + restoreWindowBackground () { + const backgroundColor = window.localStorage.getItem('atom:window-background-color') + if (backgroundColor) { + this.backgroundStylesheet = document.createElement('style') + this.backgroundStylesheet.type = 'text/css' + this.backgroundStylesheet.innerText = `html, body { background: ${backgroundColor} !important; }` + document.head.appendChild(this.backgroundStylesheet) + } + } + + storeWindowBackground () { + if (this.inSpecMode()) return + + const backgroundColor = this.window.getComputedStyle(this.workspace.getElement())['background-color'] + this.window.localStorage.setItem('atom:window-background-color', backgroundColor) + } + + // Call this method when establishing a real application window. + startEditorWindow () { + this.unloaded = false + + const updateProcessEnvPromise = this.updateProcessEnvAndTriggerHooks() + + const loadStatePromise = this.loadState().then(state => { + this.windowDimensions = state && state.windowDimensions + return this.displayWindow().then(() => { + this.commandInstaller.installAtomCommand(false, (error) => { + if (error) console.warn(error.message) + }) + this.commandInstaller.installApmCommand(false, (error) => { + if (error) console.warn(error.message) + }) + + this.disposables.add(this.applicationDelegate.onDidOpenLocations(this.openLocations.bind(this))) + this.disposables.add(this.applicationDelegate.onApplicationMenuCommand(this.dispatchApplicationMenuCommand.bind(this))) + this.disposables.add(this.applicationDelegate.onContextMenuCommand(this.dispatchContextMenuCommand.bind(this))) + this.disposables.add(this.applicationDelegate.onURIMessage(this.dispatchURIMessage.bind(this))) + this.disposables.add(this.applicationDelegate.onDidRequestUnload(() => { + return this.saveState({isUnloading: true}) + .catch(console.error) + .then(() => { + if (this.workspace) { + return this.workspace.confirmClose({ + windowCloseRequested: true, + projectHasPaths: this.project.getPaths().length > 0 + }) + } + }).then(closing => { + if (closing) { + return this.packages.deactivatePackages().then(() => closing) + } else { + return closing + } + }) + })) + + this.listenForUpdates() + + this.registerDefaultTargetForKeymaps() + + this.packages.loadPackages() + + const startTime = Date.now() + return this.deserialize(state).then(() => { + this.deserializeTimings.atom = Date.now() - startTime + + if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'custom') { + this.workspace.addHeaderPanel({item: new TitleBar({workspace: this.workspace, themes: this.themes, applicationDelegate: this.applicationDelegate})}) + this.document.body.classList.add('custom-title-bar') + } + if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'custom-inset') { + this.workspace.addHeaderPanel({item: new TitleBar({workspace: this.workspace, themes: this.themes, applicationDelegate: this.applicationDelegate})}) + this.document.body.classList.add('custom-inset-title-bar') + } + if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'hidden') { + this.document.body.classList.add('hidden-title-bar') + } + + this.document.body.appendChild(this.workspace.getElement()) + if (this.backgroundStylesheet) this.backgroundStylesheet.remove() + + this.watchProjectPaths() + + this.packages.activate() + this.keymaps.loadUserKeymap() + if (!this.getLoadSettings().safeMode) this.requireUserInitScript() + + this.menu.update() + + return this.openInitialEmptyEditorIfNecessary() + }) + }) + }) + + const loadHistoryPromise = this.history.loadState().then(() => { + this.reopenProjectMenuManager = new ReopenProjectMenuManager({ + menu: this.menu, + commands: this.commands, + history: this.history, + config: this.config, + open: paths => this.open({pathsToOpen: paths}) + }) + return this.reopenProjectMenuManager.update() + }) + + return Promise.all([loadStatePromise, loadHistoryPromise, updateProcessEnvPromise]) + } + + serialize (options) { + return { + version: this.constructor.version, + project: this.project.serialize(options), + workspace: this.workspace.serialize(), + packageStates: this.packages.serialize(), + grammars: {grammarOverridesByPath: this.grammars.grammarOverridesByPath}, + fullScreen: this.isFullScreen(), + windowDimensions: this.windowDimensions, + textEditors: this.textEditors.serialize() + } + } + + unloadEditorWindow () { + if (!this.project) return + + this.storeWindowBackground() + this.saveBlobStoreSync() + this.unloaded = true + } + + saveBlobStoreSync () { + if (this.enablePersistence) { + this.blobStore.save() + } + } + + openInitialEmptyEditorIfNecessary () { + if (!this.config.get('core.openEmptyEditorOnStart')) return + const {initialPaths} = this.getLoadSettings() + if (initialPaths && initialPaths.length === 0 && this.workspace.getPaneItems().length === 0) { + return this.workspace.open(null) + } + } + + installUncaughtErrorHandler () { + this.previousWindowErrorHandler = this.window.onerror + this.window.onerror = (...args) => { + this.lastUncaughtError = args + let [message, url, line, column, originalError] = this.lastUncaughtError + + let source + ;({line, column, source} = mapSourcePosition({source: url, line, column})) + if (url === '') url = source + + const eventObject = {message, url, line, column, originalError} + + let openDevTools = true + eventObject.preventDefault = () => { openDevTools = false } + + this.emitter.emit('will-throw-error', eventObject) + + if (openDevTools) { + this.openDevTools().then(() => this.executeJavaScriptInDevTools('DevToolsAPI.showPanel("console")')) + } + + this.emitter.emit('did-throw-error', {message, url, line, column, originalError}) + } + } + + uninstallUncaughtErrorHandler () { + this.window.onerror = this.previousWindowErrorHandler + } + + installWindowEventHandler () { + this.windowEventHandler = new WindowEventHandler({atomEnvironment: this, applicationDelegate: this.applicationDelegate}) + this.windowEventHandler.initialize(this.window, this.document) + } + + uninstallWindowEventHandler () { + if (this.windowEventHandler) { + this.windowEventHandler.unsubscribe() + } + this.windowEventHandler = null + } + + didChangeStyles (styleElement) { + TextEditor.didUpdateStyles() + if (styleElement.textContent.indexOf('scrollbar') >= 0) { + TextEditor.didUpdateScrollbarStyles() + } + } + + updateProcessEnvAndTriggerHooks () { + return this.updateProcessEnv(this.getLoadSettings().env).then(() => { + this.shellEnvironmentLoaded = true + this.emitter.emit('loaded-shell-environment') + this.packages.triggerActivationHook('core:loaded-shell-environment') + }) + } + + /* + Section: Messaging the User + */ + + // Essential: Visually and audibly trigger a beep. + beep () { + if (this.config.get('core.audioBeep')) this.applicationDelegate.playBeepSound() + this.emitter.emit('did-beep') + } + + // Essential: A flexible way to open a dialog akin to an alert dialog. + // + // If the dialog is closed (via `Esc` key or `X` in the top corner) without selecting a button + // the first button will be clicked unless a "Cancel" or "No" button is provided. + // + // ## Examples + // + // ```coffee + // atom.confirm + // message: 'How you feeling?' + // detailedMessage: 'Be honest.' + // buttons: + // Good: -> window.alert('good to hear') + // Bad: -> window.alert('bummer') + // ``` + // + // * `options` An {Object} with the following keys: + // * `message` The {String} message to display. + // * `detailedMessage` (optional) The {String} detailed message to display. + // * `buttons` (optional) Either an array of strings or an object where keys are + // button names and the values are callbacks to invoke when clicked. + // + // Returns the chosen button index {Number} if the buttons option is an array or the return value of the callback if the buttons option is an object. + confirm (params = {}) { + return this.applicationDelegate.confirm(params) + } + + /* + Section: Managing the Dev Tools + */ + + // Extended: Open the dev tools for the current window. + // + // Returns a {Promise} that resolves when the DevTools have been opened. + openDevTools () { + return this.applicationDelegate.openWindowDevTools() + } + + // Extended: Toggle the visibility of the dev tools for the current window. + // + // Returns a {Promise} that resolves when the DevTools have been opened or + // closed. + toggleDevTools () { + return this.applicationDelegate.toggleWindowDevTools() + } + + // Extended: Execute code in dev tools. + executeJavaScriptInDevTools (code) { + return this.applicationDelegate.executeJavaScriptInWindowDevTools(code) + } + + /* + Section: Private + */ + + assert (condition, message, callbackOrMetadata) { + if (condition) return true + + const error = new Error(`Assertion failed: ${message}`) + Error.captureStackTrace(error, this.assert) + + if (callbackOrMetadata) { + if (typeof callbackOrMetadata === 'function') { + callbackOrMetadata(error) + } else { + error.metadata = callbackOrMetadata + } + } + + this.emitter.emit('did-fail-assertion', error) + if (!this.isReleasedVersion()) throw error + + return false + } + + loadThemes () { + return this.themes.load() + } + + // Notify the browser project of the window's current project path + watchProjectPaths () { + this.disposables.add(this.project.onDidChangePaths(() => { + this.applicationDelegate.setRepresentedDirectoryPaths(this.project.getPaths()) + })) + } + + setDocumentEdited (edited) { + if (typeof this.applicationDelegate.setWindowDocumentEdited === 'function') { + this.applicationDelegate.setWindowDocumentEdited(edited) + } + } + + setRepresentedFilename (filename) { + if (typeof this.applicationDelegate.setWindowRepresentedFilename === 'function') { + this.applicationDelegate.setWindowRepresentedFilename(filename) + } + } + + addProjectFolder () { + this.pickFolder((selectedPaths = []) => { + this.addToProject(selectedPaths) + }) + } + + addToProject (projectPaths) { + this.loadState(this.getStateKey(projectPaths)).then(state => { + if (state && (this.project.getPaths().length === 0)) { + this.attemptRestoreProjectStateForPaths(state, projectPaths) + } else { + projectPaths.map((folder) => this.project.addPath(folder)) + } + }) + } + + attemptRestoreProjectStateForPaths (state, projectPaths, filesToOpen = []) { + const center = this.workspace.getCenter() + const windowIsUnused = () => { + for (let container of this.workspace.getPaneContainers()) { + for (let item of container.getPaneItems()) { + if (item instanceof TextEditor) { + if (item.getPath() || item.isModified()) return false + } else { + if (container === center) return false + } + } + } + return true + } + + if (windowIsUnused()) { + this.restoreStateIntoThisEnvironment(state) + return Promise.all(filesToOpen.map(file => this.workspace.open(file))) + } else { + const nouns = projectPaths.length === 1 ? 'folder' : 'folders' + const choice = this.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 (choice === 0) { + this.open({ + pathsToOpen: projectPaths.concat(filesToOpen), + newWindow: true, + devMode: this.inDevMode(), + safeMode: this.inSafeMode() + }) + return Promise.resolve(null) + } else if (choice === 1) { + for (let selectedPath of projectPaths) { + this.project.addPath(selectedPath) + } + return Promise.all(filesToOpen.map(file => this.workspace.open(file))) + } + } + } + + restoreStateIntoThisEnvironment (state) { + state.fullScreen = this.isFullScreen() + for (let pane of this.workspace.getPanes()) { + pane.destroy() + } + return this.deserialize(state) + } + + showSaveDialog (callback) { + callback(this.showSaveDialogSync()) + } + + showSaveDialogSync (options = {}) { + this.applicationDelegate.showSaveDialog(options) + } + + saveState (options, storageKey) { + return new Promise((resolve, reject) => { + if (this.enablePersistence && this.project) { + const state = this.serialize(options) + if (!storageKey) storageKey = this.getStateKey(this.project && this.project.getPaths()) + const savePromise = storageKey + ? this.stateStore.save(storageKey, state) + : this.applicationDelegate.setTemporaryWindowState(state) + return savePromise.catch(reject).then(resolve) + } else { + return resolve() + } + }) + } + + loadState (stateKey) { + if (this.enablePersistence) { + if (!stateKey) stateKey = this.getStateKey(this.getLoadSettings().initialPaths) + if (stateKey) { + return this.stateStore.load(stateKey) + } else { + return this.applicationDelegate.getTemporaryWindowState() + } + } else { + return Promise.resolve(null) + } + } + + deserialize (state) { + if (!state) return Promise.resolve() + + const grammarOverridesByPath = state.grammars && state.grammars.grammarOverridesByPath + if (grammarOverridesByPath) { + this.grammars.grammarOverridesByPath = grammarOverridesByPath + } + + this.setFullScreen(state.fullScreen) + + const missingProjectPaths = [] + + this.packages.packageStates = state.packageStates || {} + + let projectPromise + let startTime = Date.now() + if (state.project) { + projectPromise = this.project.deserialize(state.project, this.deserializers) + .catch(err => { + if (err.missingProjectPaths) { + missingProjectPaths.push(...err.missingProjectPaths) + } else { + this.notifications.addError('Unable to deserialize project', { + description: err.message, + stack: err.stack + }) + } + }) + } else { + projectPromise = Promise.resolve() + } + + return projectPromise.then(() => { + this.deserializeTimings.project = Date.now() - startTime + + if (state.textEditors) this.textEditors.deserialize(state.textEditors) + + startTime = Date.now() + if (state.workspace) this.workspace.deserialize(state.workspace, this.deserializers) + this.deserializeTimings.workspace = Date.now() - startTime + + if (missingProjectPaths.length > 0) { + const count = missingProjectPaths.length === 1 ? '' : missingProjectPaths.length + ' ' + const noun = missingProjectPaths.length === 1 ? 'directory' : 'directories' + const toBe = missingProjectPaths.length === 1 ? 'is' : 'are' + const escaped = missingProjectPaths.map(projectPath => `\`${projectPath}\``) + let group + switch (escaped.length) { + case 1: + group = escaped[0] + break + case 2: + group = `${escaped[0]} and ${escaped[1]}` + break + default: + group = escaped.slice(0, -1).join(', ') + `, and ${escaped[escaped.length - 1]}` + } + + this.notifications.addError(`Unable to open ${count}project ${noun}`, { + description: `Project ${noun} ${group} ${toBe} no longer on disk.` + }) + } + }) + } + + getStateKey (paths) { + if (paths && paths.length > 0) { + const sha1 = crypto.createHash('sha1').update(paths.slice().sort().join('\n')).digest('hex') + return `editor-${sha1}` + } else { + return null + } + } + + getStorageFolder () { + if (!this.storageFolder) this.storageFolder = new StorageFolder(this.getConfigDirPath()) + return this.storageFolder + } + + getConfigDirPath () { + if (!this.configDirPath) this.configDirPath = process.env.ATOM_HOME + return this.configDirPath + } + + getUserInitScriptPath () { + const initScriptPath = fs.resolve(this.getConfigDirPath(), 'init', ['js', 'coffee']) + return initScriptPath || path.join(this.getConfigDirPath(), 'init.coffee') + } + + requireUserInitScript () { + const userInitScriptPath = this.getUserInitScriptPath() + if (userInitScriptPath) { + try { + if (fs.isFileSync(userInitScriptPath)) require(userInitScriptPath) + } catch (error) { + this.notifications.addError(`Failed to load \`${userInitScriptPath}\``, { + detail: error.message, + dismissable: true + }) + } + } + } + + // TODO: We should deprecate the update events here, and use `atom.autoUpdater` instead + onUpdateAvailable (callback) { + return this.emitter.on('update-available', callback) + } + + updateAvailable (details) { + return this.emitter.emit('update-available', details) + } + + listenForUpdates () { + // listen for updates available locally (that have been successfully downloaded) + this.disposables.add(this.autoUpdater.onDidCompleteDownloadingUpdate(this.updateAvailable.bind(this))) + } + + setBodyPlatformClass () { + this.document.body.classList.add(`platform-${process.platform}`) + } + + setAutoHideMenuBar (autoHide) { + this.applicationDelegate.setAutoHideWindowMenuBar(autoHide) + this.applicationDelegate.setWindowMenuBarVisibility(!autoHide) + } + + dispatchApplicationMenuCommand (command, arg) { + let {activeElement} = this.document + // Use the workspace element if body has focus + if (activeElement === this.document.body) { + activeElement = this.workspace.getElement() + } + this.commands.dispatch(activeElement, command, arg) + } + + dispatchContextMenuCommand (command, ...args) { + this.commands.dispatch(this.contextMenu.activeElement, command, args) + } + + dispatchURIMessage (uri) { + if (this.packages.hasLoadedInitialPackages()) { + this.uriHandlerRegistry.handleURI(uri) + } else { + let subscription = this.packages.onDidLoadInitialPackages(() => { + subscription.dispose() + this.uriHandlerRegistry.handleURI(uri) + }) + } + } + + openLocations (locations) { + const needsProjectPaths = this.project && this.project.getPaths().length === 0 + const foldersToAddToProject = [] + const fileLocationsToOpen = [] + + function pushFolderToOpen (folder) { + if (!foldersToAddToProject.includes(folder)) { + foldersToAddToProject.push(folder) + } + } + + for (var {pathToOpen, initialLine, initialColumn, forceAddToWindow} of locations) { + if (pathToOpen && (needsProjectPaths || forceAddToWindow)) { + if (fs.existsSync(pathToOpen)) { + pushFolderToOpen(this.project.getDirectoryForProjectPath(pathToOpen).getPath()) + } else if (fs.existsSync(path.dirname(pathToOpen))) { + pushFolderToOpen(this.project.getDirectoryForProjectPath(path.dirname(pathToOpen)).getPath()) + } else { + pushFolderToOpen(this.project.getDirectoryForProjectPath(pathToOpen).getPath()) + } + } + + if (!fs.isDirectorySync(pathToOpen)) { + fileLocationsToOpen.push({pathToOpen, initialLine, initialColumn}) + } + } + + let promise = Promise.resolve(null) + if (foldersToAddToProject.length > 0) { + promise = this.loadState(this.getStateKey(foldersToAddToProject)).then(state => { + if (state && needsProjectPaths) { // only load state if this is the first path added to the project + const files = (fileLocationsToOpen.map((location) => location.pathToOpen)) + return this.attemptRestoreProjectStateForPaths(state, foldersToAddToProject, files) + } else { + const promises = [] + for (let folder of foldersToAddToProject) { + this.project.addPath(folder) + } + for ({pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) { + promises.push(this.workspace && this.workspace.open(pathToOpen, {initialLine, initialColumn})) + } + return Promise.all(promises) + } + }) + } else { + const promises = [] + for ({pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) { + promises.push(this.workspace && this.workspace.open(pathToOpen, {initialLine, initialColumn})) + } + promise = Promise.all(promises) + } + + return promise.then(() => ipcRenderer.send('window-command', 'window:locations-opened')) + } + + resolveProxy (url) { + return new Promise((resolve, reject) => { + const requestId = this.nextProxyRequestId++ + const disposable = this.applicationDelegate.onDidResolveProxy((id, proxy) => { + if (id === requestId) { + disposable.dispose() + resolve(proxy) + } + }) + + return this.applicationDelegate.resolveProxy(requestId, url) + }) + } +} + +AtomEnvironment.version = 1 +AtomEnvironment.prototype.saveStateDebounceInterval = 1000 +module.exports = AtomEnvironment + +// Preserve this deprecation until 2.0. Sorry. Should have removed Q sooner. +Promise.prototype.done = function (callback) { + deprecate('Atom now uses ES6 Promises instead of Q. Call promise.then instead of promise.done') + return this.then(callback) +} From 188142bac30e7dc34414b75922fbdffc316b64f8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Nov 2017 16:58:30 -0800 Subject: [PATCH 2/3] Suppress lint warning for Promise.prototype monkey patch --- src/atom-environment.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/atom-environment.js b/src/atom-environment.js index 1c2f1ebcf..c23d804c0 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -1344,8 +1344,12 @@ AtomEnvironment.version = 1 AtomEnvironment.prototype.saveStateDebounceInterval = 1000 module.exports = AtomEnvironment +/* eslint-disable */ + // Preserve this deprecation until 2.0. Sorry. Should have removed Q sooner. Promise.prototype.done = function (callback) { deprecate('Atom now uses ES6 Promises instead of Q. Call promise.then instead of promise.done') return this.then(callback) } + +/* eslint-enable */ From fed595b49f07aaa259370f95e2080b3a7dfacd73 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Nov 2017 17:31:45 -0800 Subject: [PATCH 3/3] Use async/await in AtomEnvironment --- src/atom-environment.js | 368 +++++++++++++++++++--------------------- 1 file changed, 176 insertions(+), 192 deletions(-) diff --git a/src/atom-environment.js b/src/atom-environment.js index c23d804c0..663bb6c00 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -290,7 +290,7 @@ class AtomEnvironment { if (this.config.get('core.autoHideMenuBar')) this.setAutoHideMenuBar(true) } - reset () { + async reset () { this.deserializers.clear() this.registerDefaultDeserializers() @@ -313,15 +313,14 @@ class AtomEnvironment { this.contextMenu.clear() - return this.packages.reset().then(() => { - this.workspace.reset(this.packages) - this.registerDefaultOpeners() - this.project.reset(this.packages) - this.workspace.subscribeToEvents() - this.grammars.clear() - this.textEditors.clear() - this.views.clear() - }) + await this.packages.reset() + this.workspace.reset(this.packages) + this.registerDefaultOpeners() + this.project.reset(this.packages) + this.workspace.subscribeToEvents() + this.grammars.clear() + this.textEditors.clear() + this.views.clear() } destroy () { @@ -611,21 +610,20 @@ class AtomEnvironment { // // Restores the full screen and maximized state after the window has resized to // prevent resize glitches. - displayWindow () { - return this.restoreWindowDimensions().then(() => { - const steps = [ - this.restoreWindowBackground(), - this.show(), - this.focus() - ] - if (this.windowDimensions && this.windowDimensions.fullScreen) { - steps.push(this.setFullScreen(true)) - } - if (this.windowDimensions && this.windowDimensions.maximized && process.platform !== 'darwin') { - steps.push(this.maximize()) - } - return Promise.all(steps) - }) + async displayWindow () { + await this.restoreWindowDimensions() + const steps = [ + this.restoreWindowBackground(), + this.show(), + this.focus() + ] + if (this.windowDimensions && this.windowDimensions.fullScreen) { + steps.push(this.setFullScreen(true)) + } + if (this.windowDimensions && this.windowDimensions.maximized && process.platform !== 'darwin') { + steps.push(this.maximize()) + } + await Promise.all(steps) } // Get the dimensions of this window. @@ -700,11 +698,12 @@ class AtomEnvironment { } } - restoreWindowDimensions () { + async restoreWindowDimensions () { if (!this.windowDimensions || !this.isValidDimensions(this.windowDimensions)) { this.windowDimensions = this.getDefaultWindowDimensions() } - return this.setWindowDimensions(this.windowDimensions).then(() => this.windowDimensions) + await this.setWindowDimensions(this.windowDimensions) + return this.windowDimensions } restoreWindowBackground () { @@ -730,75 +729,70 @@ class AtomEnvironment { const updateProcessEnvPromise = this.updateProcessEnvAndTriggerHooks() - const loadStatePromise = this.loadState().then(state => { + const loadStatePromise = this.loadState().then(async state => { this.windowDimensions = state && state.windowDimensions - return this.displayWindow().then(() => { - this.commandInstaller.installAtomCommand(false, (error) => { - if (error) console.warn(error.message) - }) - this.commandInstaller.installApmCommand(false, (error) => { - if (error) console.warn(error.message) - }) - - this.disposables.add(this.applicationDelegate.onDidOpenLocations(this.openLocations.bind(this))) - this.disposables.add(this.applicationDelegate.onApplicationMenuCommand(this.dispatchApplicationMenuCommand.bind(this))) - this.disposables.add(this.applicationDelegate.onContextMenuCommand(this.dispatchContextMenuCommand.bind(this))) - this.disposables.add(this.applicationDelegate.onURIMessage(this.dispatchURIMessage.bind(this))) - this.disposables.add(this.applicationDelegate.onDidRequestUnload(() => { - return this.saveState({isUnloading: true}) - .catch(console.error) - .then(() => { - if (this.workspace) { - return this.workspace.confirmClose({ - windowCloseRequested: true, - projectHasPaths: this.project.getPaths().length > 0 - }) - } - }).then(closing => { - if (closing) { - return this.packages.deactivatePackages().then(() => closing) - } else { - return closing - } - }) - })) - - this.listenForUpdates() - - this.registerDefaultTargetForKeymaps() - - this.packages.loadPackages() - - const startTime = Date.now() - return this.deserialize(state).then(() => { - this.deserializeTimings.atom = Date.now() - startTime - - if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'custom') { - this.workspace.addHeaderPanel({item: new TitleBar({workspace: this.workspace, themes: this.themes, applicationDelegate: this.applicationDelegate})}) - this.document.body.classList.add('custom-title-bar') - } - if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'custom-inset') { - this.workspace.addHeaderPanel({item: new TitleBar({workspace: this.workspace, themes: this.themes, applicationDelegate: this.applicationDelegate})}) - this.document.body.classList.add('custom-inset-title-bar') - } - if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'hidden') { - this.document.body.classList.add('hidden-title-bar') - } - - this.document.body.appendChild(this.workspace.getElement()) - if (this.backgroundStylesheet) this.backgroundStylesheet.remove() - - this.watchProjectPaths() - - this.packages.activate() - this.keymaps.loadUserKeymap() - if (!this.getLoadSettings().safeMode) this.requireUserInitScript() - - this.menu.update() - - return this.openInitialEmptyEditorIfNecessary() - }) + await this.displayWindow() + this.commandInstaller.installAtomCommand(false, (error) => { + if (error) console.warn(error.message) }) + this.commandInstaller.installApmCommand(false, (error) => { + if (error) console.warn(error.message) + }) + + this.disposables.add(this.applicationDelegate.onDidOpenLocations(this.openLocations.bind(this))) + this.disposables.add(this.applicationDelegate.onApplicationMenuCommand(this.dispatchApplicationMenuCommand.bind(this))) + this.disposables.add(this.applicationDelegate.onContextMenuCommand(this.dispatchContextMenuCommand.bind(this))) + this.disposables.add(this.applicationDelegate.onURIMessage(this.dispatchURIMessage.bind(this))) + this.disposables.add(this.applicationDelegate.onDidRequestUnload(async () => { + try { + await this.saveState({isUnloading: true}) + } catch (error) { + console.error(error) + } + + const closing = !this.workspace || await this.workspace.confirmClose({ + windowCloseRequested: true, + projectHasPaths: this.project.getPaths().length > 0 + }) + + if (closing) await this.packages.deactivatePackages() + return closing + })) + + this.listenForUpdates() + + this.registerDefaultTargetForKeymaps() + + this.packages.loadPackages() + + const startTime = Date.now() + await this.deserialize(state) + this.deserializeTimings.atom = Date.now() - startTime + + if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'custom') { + this.workspace.addHeaderPanel({item: new TitleBar({workspace: this.workspace, themes: this.themes, applicationDelegate: this.applicationDelegate})}) + this.document.body.classList.add('custom-title-bar') + } + if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'custom-inset') { + this.workspace.addHeaderPanel({item: new TitleBar({workspace: this.workspace, themes: this.themes, applicationDelegate: this.applicationDelegate})}) + this.document.body.classList.add('custom-inset-title-bar') + } + if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'hidden') { + this.document.body.classList.add('hidden-title-bar') + } + + this.document.body.appendChild(this.workspace.getElement()) + if (this.backgroundStylesheet) this.backgroundStylesheet.remove() + + this.watchProjectPaths() + + this.packages.activate() + this.keymaps.loadUserKeymap() + if (!this.getLoadSettings().safeMode) this.requireUserInitScript() + + this.menu.update() + + await this.openInitialEmptyEditorIfNecessary() }) const loadHistoryPromise = this.history.loadState().then(() => { @@ -809,7 +803,7 @@ class AtomEnvironment { config: this.config, open: paths => this.open({pathsToOpen: paths}) }) - return this.reopenProjectMenuManager.update() + this.reopenProjectMenuManager.update() }) return Promise.all([loadStatePromise, loadHistoryPromise, updateProcessEnvPromise]) @@ -852,13 +846,11 @@ class AtomEnvironment { installUncaughtErrorHandler () { this.previousWindowErrorHandler = this.window.onerror - this.window.onerror = (...args) => { - this.lastUncaughtError = args - let [message, url, line, column, originalError] = this.lastUncaughtError - - let source - ;({line, column, source} = mapSourcePosition({source: url, line, column})) - if (url === '') url = source + this.window.onerror = (message, url, line, column, originalError) => { + const mapping = mapSourcePosition({source: url, line, column}) + line = mapping.line + column = mapping.column + if (url === '') url = mapping.source const eventObject = {message, url, line, column, originalError} @@ -868,7 +860,9 @@ class AtomEnvironment { this.emitter.emit('will-throw-error', eventObject) if (openDevTools) { - this.openDevTools().then(() => this.executeJavaScriptInDevTools('DevToolsAPI.showPanel("console")')) + this.openDevTools().then(() => + this.executeJavaScriptInDevTools('DevToolsAPI.showPanel("console")') + ) } this.emitter.emit('did-throw-error', {message, url, line, column, originalError}) @@ -898,12 +892,11 @@ class AtomEnvironment { } } - updateProcessEnvAndTriggerHooks () { - return this.updateProcessEnv(this.getLoadSettings().env).then(() => { - this.shellEnvironmentLoaded = true - this.emitter.emit('loaded-shell-environment') - this.packages.triggerActivationHook('core:loaded-shell-environment') - }) + async updateProcessEnvAndTriggerHooks () { + await this.updateProcessEnv(this.getLoadSettings().env) + this.shellEnvironmentLoaded = true + this.emitter.emit('loaded-shell-environment') + this.packages.triggerActivationHook('core:loaded-shell-environment') } /* @@ -1020,14 +1013,13 @@ class AtomEnvironment { }) } - addToProject (projectPaths) { - this.loadState(this.getStateKey(projectPaths)).then(state => { - if (state && (this.project.getPaths().length === 0)) { - this.attemptRestoreProjectStateForPaths(state, projectPaths) - } else { - projectPaths.map((folder) => this.project.addPath(folder)) - } - }) + async addToProject (projectPaths) { + const state = await this.loadState(this.getStateKey(projectPaths)) + if (state && (this.project.getPaths().length === 0)) { + this.attemptRestoreProjectStateForPaths(state, projectPaths) + } else { + projectPaths.map((folder) => this.project.addPath(folder)) + } } attemptRestoreProjectStateForPaths (state, projectPaths, filesToOpen = []) { @@ -1092,19 +1084,16 @@ class AtomEnvironment { this.applicationDelegate.showSaveDialog(options) } - saveState (options, storageKey) { - return new Promise((resolve, reject) => { - if (this.enablePersistence && this.project) { - const state = this.serialize(options) - if (!storageKey) storageKey = this.getStateKey(this.project && this.project.getPaths()) - const savePromise = storageKey - ? this.stateStore.save(storageKey, state) - : this.applicationDelegate.setTemporaryWindowState(state) - return savePromise.catch(reject).then(resolve) + async saveState (options, storageKey) { + if (this.enablePersistence && this.project) { + const state = this.serialize(options) + if (!storageKey) storageKey = this.getStateKey(this.project && this.project.getPaths()) + if (storageKey) { + await this.stateStore.save(storageKey, state) } else { - return resolve() + await this.applicationDelegate.setTemporaryWindowState(state) } - }) + } } loadState (stateKey) { @@ -1120,7 +1109,7 @@ class AtomEnvironment { } } - deserialize (state) { + async deserialize (state) { if (!state) return Promise.resolve() const grammarOverridesByPath = state.grammars && state.grammars.grammarOverridesByPath @@ -1134,55 +1123,51 @@ class AtomEnvironment { this.packages.packageStates = state.packageStates || {} - let projectPromise let startTime = Date.now() if (state.project) { - projectPromise = this.project.deserialize(state.project, this.deserializers) - .catch(err => { - if (err.missingProjectPaths) { - missingProjectPaths.push(...err.missingProjectPaths) - } else { - this.notifications.addError('Unable to deserialize project', { - description: err.message, - stack: err.stack - }) - } - }) - } else { - projectPromise = Promise.resolve() + try { + await this.project.deserialize(state.project, this.deserializers) + } catch (error) { + if (error.missingProjectPaths) { + missingProjectPaths.push(...error.missingProjectPaths) + } else { + this.notifications.addError('Unable to deserialize project', { + description: error.message, + stack: error.stack + }) + } + } } - return projectPromise.then(() => { - this.deserializeTimings.project = Date.now() - startTime + this.deserializeTimings.project = Date.now() - startTime - if (state.textEditors) this.textEditors.deserialize(state.textEditors) + if (state.textEditors) this.textEditors.deserialize(state.textEditors) - startTime = Date.now() - if (state.workspace) this.workspace.deserialize(state.workspace, this.deserializers) - this.deserializeTimings.workspace = Date.now() - startTime + startTime = Date.now() + if (state.workspace) this.workspace.deserialize(state.workspace, this.deserializers) + this.deserializeTimings.workspace = Date.now() - startTime - if (missingProjectPaths.length > 0) { - const count = missingProjectPaths.length === 1 ? '' : missingProjectPaths.length + ' ' - const noun = missingProjectPaths.length === 1 ? 'directory' : 'directories' - const toBe = missingProjectPaths.length === 1 ? 'is' : 'are' - const escaped = missingProjectPaths.map(projectPath => `\`${projectPath}\``) - let group - switch (escaped.length) { - case 1: - group = escaped[0] - break - case 2: - group = `${escaped[0]} and ${escaped[1]}` - break - default: - group = escaped.slice(0, -1).join(', ') + `, and ${escaped[escaped.length - 1]}` - } - - this.notifications.addError(`Unable to open ${count}project ${noun}`, { - description: `Project ${noun} ${group} ${toBe} no longer on disk.` - }) + if (missingProjectPaths.length > 0) { + const count = missingProjectPaths.length === 1 ? '' : missingProjectPaths.length + ' ' + const noun = missingProjectPaths.length === 1 ? 'directory' : 'directories' + const toBe = missingProjectPaths.length === 1 ? 'is' : 'are' + const escaped = missingProjectPaths.map(projectPath => `\`${projectPath}\``) + let group + switch (escaped.length) { + case 1: + group = escaped[0] + break + case 2: + group = `${escaped[0]} and ${escaped[1]}` + break + default: + group = escaped.slice(0, -1).join(', ') + `, and ${escaped[escaped.length - 1]}` } - }) + + this.notifications.addError(`Unable to open ${count}project ${noun}`, { + description: `Project ${noun} ${group} ${toBe} no longer on disk.` + }) + } } getStateKey (paths) { @@ -1270,7 +1255,7 @@ class AtomEnvironment { } } - openLocations (locations) { + async openLocations (locations) { const needsProjectPaths = this.project && this.project.getPaths().length === 0 const foldersToAddToProject = [] const fileLocationsToOpen = [] @@ -1297,32 +1282,31 @@ class AtomEnvironment { } } - let promise = Promise.resolve(null) + let restoredState = false if (foldersToAddToProject.length > 0) { - promise = this.loadState(this.getStateKey(foldersToAddToProject)).then(state => { - if (state && needsProjectPaths) { // only load state if this is the first path added to the project - const files = (fileLocationsToOpen.map((location) => location.pathToOpen)) - return this.attemptRestoreProjectStateForPaths(state, foldersToAddToProject, files) - } else { - const promises = [] - for (let folder of foldersToAddToProject) { - this.project.addPath(folder) - } - for ({pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) { - promises.push(this.workspace && this.workspace.open(pathToOpen, {initialLine, initialColumn})) - } - return Promise.all(promises) + const state = await this.loadState(this.getStateKey(foldersToAddToProject)) + + // only restore state if this is the first path added to the project + if (state && needsProjectPaths) { + const files = fileLocationsToOpen.map((location) => location.pathToOpen) + await this.attemptRestoreProjectStateForPaths(state, foldersToAddToProject, files) + restoredState = true + } else { + for (let folder of foldersToAddToProject) { + this.project.addPath(folder) } - }) - } else { - const promises = [] - for ({pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) { - promises.push(this.workspace && this.workspace.open(pathToOpen, {initialLine, initialColumn})) } - promise = Promise.all(promises) } - return promise.then(() => ipcRenderer.send('window-command', 'window:locations-opened')) + if (!restoredState) { + const fileOpenPromises = [] + for ({pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) { + fileOpenPromises.push(this.workspace && this.workspace.open(pathToOpen, {initialLine, initialColumn})) + } + await Promise.all(fileOpenPromises) + } + + ipcRenderer.send('window-command', 'window:locations-opened') } resolveProxy (url) {