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 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: Properties */ constructor (params = {}) { this.id = (params.id != null) ? params.id : nextId++ // Public: A {Clipboard} instance 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.pathsWithWaitSessions = new Set() // Public: A {DeserializerManager} instance this.deserializers = new DeserializerManager(this) this.deserializeTimings = {} // Public: A {ViewRegistry} instance this.views = new ViewRegistry(this) // Public: A {NotificationManager} instance this.notifications = new NotificationManager() this.stateStore = new StateStore('AtomEnvironments', 1) // Public: A {Config} instance this.config = new Config({ saveCallback: settings => { if (this.enablePersistence) { this.applicationDelegate.setUserSettings(settings, this.config.getUserConfigPath()) } } }) this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)}) // Public: A {KeymapManager} instance this.keymaps = new KeymapManager({notificationManager: this.notifications}) // Public: A {TooltipManager} instance this.tooltips = new TooltipManager({keymapManager: this.keymaps, viewRegistry: this.views}) // Public: A {CommandRegistry} instance this.commands = new CommandRegistry() this.uriHandlerRegistry = new URIHandlerRegistry() // Public: A {GrammarRegistry} instance this.grammars = new GrammarRegistry({config: this.config}) // Public: A {StyleManager} instance this.styles = new StyleManager() // Public: A {PackageManager} instance 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 }) // Public: A {ThemeManager} instance this.themes = new ThemeManager({ packageManager: this.packages, config: this.config, styleManager: this.styles, notificationManager: this.notifications, viewRegistry: this.views }) // Public: A {MenuManager} instance this.menu = new MenuManager({keymapManager: this.keymaps, packageManager: this.packages}) // Public: A {ContextMenuManager} instance this.contextMenu = new ContextMenuManager({keymapManager: this.keymaps}) this.packages.setMenuManager(this.menu) this.packages.setContextMenuManager(this.contextMenu) this.packages.setThemeManager(this.themes) // Public: A {Project} instance this.project = new Project({ notificationManager: this.notifications, packageManager: this.packages, grammarRegistry: this.grammars, config: this.config, applicationDelegate: this.applicationDelegate }) this.commandInstaller = new CommandInstaller(this.applicationDelegate) this.protocolHandlerInstaller = new ProtocolHandlerInstaller() // Public: A {TextEditorRegistry} instance this.textEditors = new TextEditorRegistry({ config: this.config, grammarRegistry: this.grammars, assert: this.assert.bind(this), packageManager: this.packages }) // Public: A {Workspace} instance 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}) // Public: A {HistoryManager} instance 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, userSettings, projectSpecification} = this.getLoadSettings() 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({ mainSource: this.enablePersistence && path.join(this.configDirPath, 'config.cson'), projectHomeSchema: ConfigSchema.projectHome }) this.config.resetUserSettings(userSettings) if (projectSpecification != null && projectSpecification.config != null) { this.project.replace(projectSpecification) } 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.uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(this)) this.autoUpdater.initialize() this.protocolHandlerInstaller.initialize(this.config, this.notifications) 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) } async 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() 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() this.pathsWithWaitSessions.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() if (this.stylesElement) this.stylesElement.remove() 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 a specific release channel // name like 'beta' or 'nightly' if one is found in the Atom version or 'stable' // otherwise. getReleaseChannel () { // This matches stable, dev (with or without commit hash) and any other // release channel following the pattern '1.00.0-channel0' const match = this.getVersion().match(/\d+\.\d+\.\d+(-([a-z]+)(\d+|-\w{4,})?)?$/) if (!match) { return 'unrecognized' } else if (match[2]) { return match[2] } return 'stable' } // Public: Returns a {Boolean} that is `true` if the current version is an official release. isReleasedVersion () { return this.getReleaseChannel().match(/stable|beta|nightly/) != null } // 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. 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. // // 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} } } async restoreWindowDimensions () { if (!this.windowDimensions || !this.isValidDimensions(this.windowDimensions)) { this.windowDimensions = this.getDefaultWindowDimensions() } await this.setWindowDimensions(this.windowDimensions) return 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. async startEditorWindow () { if (this.getLoadSettings().clearWindowState) { await this.stateStore.clear() } this.unloaded = false const updateProcessEnvPromise = this.updateProcessEnvAndTriggerHooks() const loadStatePromise = this.loadState().then(async state => { this.windowDimensions = state && state.windowDimensions 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.onDidChangeUserSettings(settings => this.config.resetUserSettings(settings) )) this.disposables.add(this.applicationDelegate.onDidFailToReadUserSettings(message => this.notifications.addError(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() let previousProjectPaths = this.project.getPaths() this.disposables.add(this.project.onDidChangePaths(newPaths => { for (let path of previousProjectPaths) { if (this.pathsWithWaitSessions.has(path) && !newPaths.includes(path)) { this.applicationDelegate.didClosePathWithWaitSession(path) } } previousProjectPaths = newPaths this.applicationDelegate.setRepresentedDirectoryPaths(newPaths) })) this.disposables.add(this.workspace.onDidDestroyPaneItem(({item}) => { const path = item.getPath && item.getPath() if (this.pathsWithWaitSessions.has(path)) { this.applicationDelegate.didClosePathWithWaitSession(path) } })) this.packages.activate() this.keymaps.loadUserKeymap() if (!this.getLoadSettings().safeMode) this.requireUserInitScript() this.menu.update() await 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}) }) 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: this.grammars.serialize(), fullScreen: this.isFullScreen(), windowDimensions: this.windowDimensions } } 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 = (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} 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() } } async updateProcessEnvAndTriggerHooks () { await this.updateProcessEnv(this.getLoadSettings().env) 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. // // While both async and sync versions are provided, it is recommended to use the async version // such that the renderer process is not blocked while the dialog box is open. // // The async version accepts the same options as Electron's `dialog.showMessageBox`. // For convenience, it sets `type` to `'info'` and `normalizeAccessKeys` to `true` by default. // // 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 // // ```js // // Async version (recommended) // atom.confirm({ // message: 'How you feeling?', // detail: 'Be honest.', // buttons: ['Good', 'Bad'] // }, response => { // if (response === 0) { // window.alert('good to hear') // } else { // window.alert('bummer') // } // }) // // ```js // // Legacy sync version // const chosen = atom.confirm({ // message: 'How you feeling?', // detailedMessage: 'Be honest.', // buttons: { // Good: () => window.alert('good to hear'), // Bad: () => window.alert('bummer') // } // }) // ``` // // * `options` An options {Object}. If the callback argument is also supplied, see the documentation at // https://electronjs.org/docs/api/dialog#dialogshowmessageboxbrowserwindow-options-callback for the list of // available options. Otherwise, only the following keys are accepted: // * `message` The {String} message to display. // * `detailedMessage` (optional) The {String} detailed message to display. // * `buttons` (optional) Either an {Array} of {String}s or an {Object} where keys are // button names and the values are callback {Function}s to invoke when clicked. // * `callback` (optional) A {Function} that will be called with the index of the chosen option. // If a callback is supplied, the dialog will be non-blocking. This argument is recommended. // // 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. // If a callback function is supplied, returns `undefined`. confirm (options = {}, callback) { if (callback) { // Async: no return value this.applicationDelegate.confirm(options, callback) } else { return this.applicationDelegate.confirm(options) } } /* 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() } 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 () { return new Promise((resolve) => { this.pickFolder((selectedPaths) => { this.addToProject(selectedPaths || []).then(resolve) }) }) } 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)) } } async 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()) { await this.restoreStateIntoThisEnvironment(state) return Promise.all(filesToOpen.map(file => this.workspace.open(file))) } else { let resolveDiscardStatePromise = null const discardStatePromise = new Promise((resolve) => { resolveDiscardStatePromise = resolve }) const nouns = projectPaths.length === 1 ? 'folder' : 'folders' this.confirm({ message: 'Previous automatically-saved project state detected', detail: `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' ] }, response => { if (response === 0) { this.open({ pathsToOpen: projectPaths.concat(filesToOpen), newWindow: true, devMode: this.inDevMode(), safeMode: this.inSafeMode() }) resolveDiscardStatePromise(Promise.resolve(null)) } else if (response === 1) { for (let selectedPath of projectPaths) { this.project.addPath(selectedPath) } resolveDiscardStatePromise(Promise.all(filesToOpen.map(file => this.workspace.open(file)))) } }) return discardStatePromise } } restoreStateIntoThisEnvironment (state) { state.fullScreen = this.isFullScreen() for (let pane of this.workspace.getPanes()) { pane.destroy() } return this.deserialize(state) } showSaveDialogSync (options = {}) { deprecate(`atom.showSaveDialogSync is deprecated and will be removed soon. Please, implement ::saveAs and ::getSaveDialogOptions instead for pane items or use Pane::saveItemAs for programmatic saving.`) return this.applicationDelegate.showSaveDialog(options) } 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 { await this.applicationDelegate.setTemporaryWindowState(state) } } } 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) } } async deserialize (state) { if (!state) return Promise.resolve() this.setFullScreen(state.fullScreen) const missingProjectPaths = [] this.packages.packageStates = state.packageStates || {} let startTime = Date.now() if (state.project) { 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 }) } } } this.deserializeTimings.project = Date.now() - startTime if (state.grammars) this.grammars.deserialize(state.grammars) 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 } } 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) }) } } async 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 (const location of locations) { const {pathToOpen} = location if (pathToOpen && (needsProjectPaths || location.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(location) } if (location.hasWaitSession) this.pathsWithWaitSessions.add(pathToOpen) } let restoredState = false if (foldersToAddToProject.length > 0) { 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) } } } if (!restoredState) { const fileOpenPromises = [] for (const {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) { 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 /* 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 */