Files
atom/src/atom-environment.js
2018-06-28 14:29:09 -07:00

1440 lines
48 KiB
JavaScript

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 === '<embedded>') 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 */