const crypto = require('crypto');
const path = require('path');
const util = require('util');
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');
const StartupTime = require('./startup-time');
const getReleaseChannel = require('./get-release-channel');

const stat = util.promisify(fs.stat);

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.unloading = 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);

    this.workspace.initialize();

    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.unloading) 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.initialize();
    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 full name of this Atom release (e.g. "Atom", "Atom Beta")
  //
  // Returns the app name {String}.
  getAppName() {
    if (this.appName == null) this.appName = this.getLoadSettings().appName;
    return this.appName;
  }

  // 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() {
    return getReleaseChannel(this.getVersion());
  }

  // 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 all the markers with the information about startup time.
  //
  // Returns an array of timing markers.
  // Each timing is an object with two keys:
  //  * `label`: string
  //  * `time`:  Time since the `startTime` (in milliseconds).
  getStartupMarkers() {
    const data = StartupTime.exportData();

    return data ? data.markers : [];
  }

  // 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() {
    StartupTime.addMarker('window:environment:start-editor-window:start');

    if (this.getLoadSettings().clearWindowState) {
      await this.stateStore.clear();
    }

    this.unloading = false;

    const updateProcessEnvPromise = this.updateProcessEnvAndTriggerHooks();

    const loadStatePromise = this.loadState().then(async state => {
      this.windowDimensions = state && state.windowDimensions;
      if (!this.getLoadSettings().headless) {
        StartupTime.addMarker(
          'window:environment:start-editor-window:display-window'
        );
        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(
          this.prepareToUnloadEditorWindow.bind(this)
        )
      );

      this.listenForUpdates();

      this.registerDefaultTargetForKeymaps();

      StartupTime.addMarker(
        'window:environment:start-editor-window:load-packages'
      );
      this.packages.loadPackages();

      const startTime = Date.now();
      StartupTime.addMarker(
        'window:environment:start-editor-window:deserialize-state'
      );
      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.setProjectRoots(newPaths);
        })
      );
      this.disposables.add(
        this.workspace.onDidDestroyPaneItem(({ item }) => {
          const path = item.getPath && item.getPath();
          if (this.pathsWithWaitSessions.has(path)) {
            this.applicationDelegate.didClosePathWithWaitSession(path);
          }
        })
      );

      StartupTime.addMarker(
        'window:environment:start-editor-window:activate-packages'
      );
      this.packages.activate();
      this.keymaps.loadUserKeymap();
      if (!this.getLoadSettings().safeMode) this.requireUserInitScript();

      this.menu.update();

      StartupTime.addMarker(
        'window:environment:start-editor-window:open-editor'
      );
      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();
    });

    const output = await Promise.all([
      loadStatePromise,
      loadHistoryPromise,
      updateProcessEnvPromise
    ]);

    StartupTime.addMarker('window:environment:start-editor-window:end');

    return output;
  }

  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
    };
  }

  async prepareToUnloadEditorWindow() {
    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) {
      this.unloading = true;
      await this.packages.deactivatePackages();
    }
    return closing;
  }

  unloadEditorWindow() {
    if (!this.project) return;

    this.storeWindowBackground();
    this.saveBlobStoreSync();
  }

  saveBlobStoreSync() {
    if (this.enablePersistence) {
      this.blobStore.save();
    }
  }

  openInitialEmptyEditorIfNecessary() {
    if (!this.config.get('core.openEmptyEditorOnStart')) return;
    const { hasOpenFiles } = this.getLoadSettings();
    if (!hasOpenFiles && this.workspace.getPaneItems().length === 0) {
      return this.workspace.open(null, { pending: true });
    }
  }

  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().initialProjectRoots);
      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) {
        // We handle the missingProjectPaths case in openLocations().
        if (!error.missingProjectPaths) {
          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 ? 'folder' : 'folders';
      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 = new Set();
    const fileLocationsToOpen = [];
    const missingFolders = [];

    // Asynchronously fetch stat information about each requested path to open.
    const locationStats = await Promise.all(
      locations.map(async location => {
        const stats = location.pathToOpen
          ? await stat(location.pathToOpen).catch(() => null)
          : null;
        return { location, stats };
      })
    );

    for (const { location, stats } of locationStats) {
      const { pathToOpen } = location;
      if (!pathToOpen) {
        // Untitled buffer
        fileLocationsToOpen.push(location);
        continue;
      }

      if (stats !== null) {
        // Path exists
        if (stats.isDirectory()) {
          // Directory: add as a project folder
          foldersToAddToProject.add(
            this.project.getDirectoryForProjectPath(pathToOpen).getPath()
          );
        } else if (stats.isFile()) {
          if (location.isDirectory) {
            // File: no longer a directory
            missingFolders.push(location);
          } else {
            // File: add as a file location
            fileLocationsToOpen.push(location);
          }
        }
      } else {
        // Path does not exist
        // Attempt to interpret as a URI from a non-default directory provider
        const directory = this.project.getProvidedDirectoryForProjectPath(
          pathToOpen
        );
        if (directory) {
          // Found: add as a project folder
          foldersToAddToProject.add(directory.getPath());
        } else if (location.isDirectory) {
          // Not found and must be a directory: add to missing list and use to derive state key
          missingFolders.push(location);
        } else {
          // Not found: open as a new file
          fileLocationsToOpen.push(location);
        }
      }

      if (location.hasWaitSession) this.pathsWithWaitSessions.add(pathToOpen);
    }

    let restoredState = false;
    if (foldersToAddToProject.size > 0 || missingFolders.length > 0) {
      // Include missing folders in the state key so that sessions restored with no-longer-present project root folders
      // don't lose data.
      const foldersForStateKey = Array.from(foldersToAddToProject).concat(
        missingFolders.map(location => location.pathToOpen)
      );
      const state = await this.loadState(
        this.getStateKey(Array.from(foldersForStateKey))
      );

      // 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,
          Array.from(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);
    }

    if (missingFolders.length > 0) {
      let message = 'Unable to open project folder';
      if (missingFolders.length > 1) {
        message += 's';
      }

      let description = 'The ';
      if (missingFolders.length === 1) {
        description += 'directory `';
        description += missingFolders[0].pathToOpen;
        description += '` does not exist.';
      } else if (missingFolders.length === 2) {
        description += `directories \`${missingFolders[0].pathToOpen}\` `;
        description += `and \`${missingFolders[1].pathToOpen}\` do not exist.`;
      } else {
        description += 'directories ';
        description += missingFolders
          .slice(0, -1)
          .map(location => location.pathToOpen)
          .map(pathToOpen => '`' + pathToOpen + '`, ')
          .join('');
        description +=
          'and `' +
          missingFolders[missingFolders.length - 1].pathToOpen +
          '` do not exist.';
      }

      this.notifications.addWarning(message, { description });
    }

    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 */
