text: const AtomWindow = require('./atom-window');
  const ApplicationMenu = require('./application-menu');
  const AtomProtocolHandler = require('./atom-protocol-handler');
  const AutoUpdateManager = require('./auto-update-manager');
  const StorageFolder = require('../storage-folder');
  const Config = require('../config');
  const ConfigFile = require('../config-file');
  const FileRecoveryService = require('./file-recovery-service');
  const StartupTime = require('../startup-time');
  const ipcHelpers = require('../ipc-helpers');
  const {
    BrowserWindow,
    Menu,
    app,
    clipboard,
    dialog,
    ipcMain,
    shell,
    screen
  } = require('electron');
  const { CompositeDisposable, Disposable } = require('event-kit');
  const crypto = require('crypto');
  const fs = require('fs-plus');
  const path = require('path');
  const os = require('os');
  const net = require('net');
  const url = require('url');
  const { promisify } = require('util');
  const { EventEmitter } = require('events');
  const _ = require('underscore-plus');
  let FindParentDir = null;
  let Resolve = null;
  const ConfigSchema = require('../config-schema');
  
  const LocationSuffixRegExp = /(:\d+)(:\d+)?$/;
  
  // Increment this when changing the serialization format of `${ATOM_HOME}/storage/application.json` used by
  // AtomApplication::saveCurrentWindowOptions() and AtomApplication::loadPreviousWindowOptions() in a backward-
  // incompatible way.
  const APPLICATION_STATE_VERSION = '1';
  
  const getDefaultPath = () => {
    const editor = atom.workspace.getActiveTextEditor();
    if (!editor || !editor.getPath()) {
      return;
    }
    const paths = atom.project.getPaths();
    if (paths) {
      return paths[0];
    }
  };
  
  const getSocketSecretPath = atomVersion => {
    const { username } = os.userInfo();
    const atomHome = path.resolve(process.env.ATOM_HOME);
  
    return path.join(atomHome, `.atom-socket-secret-${username}-${atomVersion}`);
  };
  
  const getSocketPath = socketSecret => {
    if (!socketSecret) {
      return null;
    }
  
    // Hash the secret to create the socket name to not expose it.
    const socketName = crypto
      .createHmac('sha256', socketSecret)
      .update('socketName')
      .digest('hex')
      .substr(0, 12);
  
    if (process.platform === 'win32') {
      return `\\\\.\\pipe\\atom-${socketName}-sock`;
    } else {
      return path.join(os.tmpdir(), `atom-${socketName}.sock`);
    }
  };
  
  const getExistingSocketSecret = atomVersion => {
    const socketSecretPath = getSocketSecretPath(atomVersion);
  
    if (!fs.existsSync(socketSecretPath)) {
      return null;
    }
  
    return fs.readFileSync(socketSecretPath, 'utf8');
  };
  
  const getRandomBytes = promisify(crypto.randomBytes);
  const writeFile = promisify(fs.writeFile);
  
  const createSocketSecret = async atomVersion => {
    const socketSecret = (await getRandomBytes(16)).toString('hex');
  
    await writeFile(getSocketSecretPath(atomVersion), socketSecret, {
      encoding: 'utf8',
      mode: 0o600
    });
  
    return socketSecret;
  };
  
  const encryptOptions = (options, secret) => {
    const message = JSON.stringify(options);
  
    // Even if the following IV is not cryptographically secure, there's a really good chance
    // it's going to be unique between executions which is the requirement for GCM.
    // We're not using `crypto.randomBytes()` because in electron v2, that API is really slow
    // on Windows machines, which affects the startup time of Atom.
    // TodoElectronIssue: Once we upgrade to electron v3 we can use `crypto.randomBytes()`
    const initVectorHash = crypto.createHash('sha1');
    initVectorHash.update(Date.now() + '');
    initVectorHash.update(Math.random() + '');
    const initVector = initVectorHash.digest();
  
    const cipher = crypto.createCipheriv('aes-256-gcm', secret, initVector);
  
    let content = cipher.update(message, 'utf8', 'hex');
    content += cipher.final('hex');
  
    const authTag = cipher.getAuthTag().toString('hex');
  
    return JSON.stringify({
      authTag,
      content,
      initVector: initVector.toString('hex')
    });
  };
  
  const decryptOptions = (optionsMessage, secret) => {
    const { authTag, content, initVector } = JSON.parse(optionsMessage);
  
    const decipher = crypto.createDecipheriv(
      'aes-256-gcm',
      secret,
      Buffer.from(initVector, 'hex')
    );
    decipher.setAuthTag(Buffer.from(authTag, 'hex'));
  
    let message = decipher.update(content, 'hex', 'utf8');
    message += decipher.final('utf8');
  
    return JSON.parse(message);
  };
  
  // The application's singleton class.
  //
  // It's the entry point into the Atom application and maintains the global state
  // of the application.
  //
  module.exports = class AtomApplication extends EventEmitter {
    // Public: The entry point into the Atom application.
    static open(options) {
      StartupTime.addMarker('main-process:atom-application:open');
  
      const socketSecret = getExistingSocketSecret(options.version);
      const socketPath = getSocketPath(socketSecret);
      const createApplication =
        options.createApplication ||
        (async () => {
          const app = new AtomApplication(options);
          await app.initialize(options);
          return app;
        });
  
      // FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely
      // take a few seconds to trigger 'error' event, it could be a bug of node
      // or electron, before it's fixed we check the existence of socketPath to
      // speedup startup.
      if (
        !socketPath ||
        options.test ||
        options.benchmark ||
        options.benchmarkTest ||
        (process.platform !== 'win32' && !fs.existsSync(socketPath))
      ) {
        return createApplication(options);
      }
  
      return new Promise(resolve => {
        const client = net.connect({ path: socketPath }, () => {
          client.write(encryptOptions(options, socketSecret), () => {
            client.end();
            app.quit();
            resolve(null);
          });
        });
  
        client.on('error', () => resolve(createApplication(options)));
      });
    }
  
    exit(status) {
      app.exit(status);
    }
  
    constructor(options) {
      StartupTime.addMarker('main-process:atom-application:constructor:start');
  
      super();
      this.quitting = false;
      this.quittingForUpdate = false;
      this.getAllWindows = this.getAllWindows.bind(this);
      this.getLastFocusedWindow = this.getLastFocusedWindow.bind(this);
      this.resourcePath = options.resourcePath;
      this.devResourcePath = options.devResourcePath;
      this.version = options.version;
      this.devMode = options.devMode;
      this.safeMode = options.safeMode;
      this.logFile = options.logFile;
      this.userDataDir = options.userDataDir;
      this._killProcess = options.killProcess || process.kill.bind(process);
      this.waitSessionsByWindow = new Map();
      this.windowStack = new WindowStack();
  
      this.initializeAtomHome(process.env.ATOM_HOME);
  
      const configFilePath = fs.existsSync(
        path.join(process.env.ATOM_HOME, 'config.json')
      )
        ? path.join(process.env.ATOM_HOME, 'config.json')
        : path.join(process.env.ATOM_HOME, 'config.cson');
  
      this.configFile = ConfigFile.at(configFilePath);
      this.config = new Config({
        saveCallback: settings => {
          if (!this.quitting) {
            return this.configFile.update(settings);
          }
        }
      });
      this.config.setSchema(null, {
        type: 'object',
        properties: _.clone(ConfigSchema)
      });
  
      this.fileRecoveryService = new FileRecoveryService(
        path.join(process.env.ATOM_HOME, 'recovery')
      );
      this.storageFolder = new StorageFolder(process.env.ATOM_HOME);
      this.autoUpdateManager = new AutoUpdateManager(
        this.version,
        options.test || options.benchmark || options.benchmarkTest,
        this.config
      );
  
      this.disposable = new CompositeDisposable();
      this.handleEvents();
  
      StartupTime.addMarker('main-process:atom-application:constructor:end');
    }
  
    // This stuff was previously done in the constructor, but we want to be able to construct this object
    // for testing purposes without booting up the world. As you add tests, feel free to move instantiation
    // of these various sub-objects into the constructor, but you'll need to remove the side-effects they
    // perform during their construction, adding an initialize method that you call here.
    async initialize(options) {
      StartupTime.addMarker('main-process:atom-application:initialize:start');
  
      global.atomApplication = this;
  
      // DEPRECATED: This can be removed at some point (added in 1.13)
      // It converts `useCustomTitleBar: true` to `titleBar: "custom"`
      if (
        process.platform === 'darwin' &&
        this.config.get('core.useCustomTitleBar')
      ) {
        this.config.unset('core.useCustomTitleBar');
        this.config.set('core.titleBar', 'custom');
      }
  
      this.applicationMenu = new ApplicationMenu(
        this.version,
        this.autoUpdateManager
      );
      this.atomProtocolHandler = new AtomProtocolHandler(
        this.resourcePath,
        this.safeMode
      );
  
      // Don't await for the following method to avoid delaying the opening of a new window.
      // (we await it just after opening it).
      // We need to do this because `listenForArgumentsFromNewProcess()` calls `crypto.randomBytes`,
      // which is really slow on Windows machines.
      // (TodoElectronIssue: This got fixed in electron v3: https://github.com/electron/electron/issues/2073).
      let socketServerPromise;
      if (options.test || options.benchmark || options.benchmarkTest) {
        socketServerPromise = Promise.resolve();
      } else {
        socketServerPromise = this.listenForArgumentsFromNewProcess();
      }
  
      this.setupDockMenu();
  
      const result = await this.launch(options);
      this.autoUpdateManager.initialize();
      await socketServerPromise;
  
      StartupTime.addMarker('main-process:atom-application:initialize:end');
  
      return result;
    }
  
    async destroy() {
      const windowsClosePromises = this.getAllWindows().map(window => {
        window.close();
        return window.closedPromise;
      });
      await Promise.all(windowsClosePromises);
      this.disposable.dispose();
    }
  
    async launch(options) {
      if (!this.configFilePromise) {
        this.configFilePromise = this.configFile.watch().then(disposable => {
          this.disposable.add(disposable);
          this.config.onDidChange('core.titleBar', () => this.promptForRestart());
          this.config.onDidChange('core.colorProfile', () =>
            this.promptForRestart()
          );
        });
        await this.configFilePromise;
      }
  
      let optionsForWindowsToOpen = [];
      let shouldReopenPreviousWindows = false;
  
      if (options.test || options.benchmark || options.benchmarkTest) {
        optionsForWindowsToOpen.push(options);
      } else if (options.newWindow) {
        shouldReopenPreviousWindows = false;
      } else if (
        (options.pathsToOpen && options.pathsToOpen.length > 0) ||
        (options.urlsToOpen && options.urlsToOpen.length > 0)
      ) {
        optionsForWindowsToOpen.push(options);
        shouldReopenPreviousWindows =
          this.config.get('core.restorePreviousWindowsOnStart') === 'always';
      } else {
        shouldReopenPreviousWindows =
          this.config.get('core.restorePreviousWindowsOnStart') !== 'no';
      }
  
      if (shouldReopenPreviousWindows) {
        optionsForWindowsToOpen = [
          ...(await this.loadPreviousWindowOptions()),
          ...optionsForWindowsToOpen
        ];
      }
  
      if (optionsForWindowsToOpen.length === 0) {
        optionsForWindowsToOpen.push(options);
      }
  
      // Preserve window opening order
      const windows = [];
      for (const options of optionsForWindowsToOpen) {
        windows.push(await this.openWithOptions(options));
      }
      return windows;
    }
  
    openWithOptions(options) {
      const {
        pathsToOpen,
        executedFrom,
        foldersToOpen,
        urlsToOpen,
        benchmark,
        benchmarkTest,
        test,
        pidToKillWhenClosed,
        devMode,
        safeMode,
        newWindow,
        logFile,
        profileStartup,
        timeout,
        clearWindowState,
        addToLastWindow,
        preserveFocus,
        env
      } = options;
  
      if (!preserveFocus) {
        app.focus();
      }
  
      if (test) {
        return this.runTests({
          headless: true,
          devMode,
          resourcePath: this.resourcePath,
          executedFrom,
          pathsToOpen,
          logFile,
          timeout,
          env
        });
      } else if (benchmark || benchmarkTest) {
        return this.runBenchmarks({
          headless: true,
          test: benchmarkTest,
          resourcePath: this.resourcePath,
          executedFrom,
          pathsToOpen,
          timeout,
          env
        });
      } else if (
        (pathsToOpen && pathsToOpen.length > 0) ||
        (foldersToOpen && foldersToOpen.length > 0)
      ) {
        return this.openPaths({
          pathsToOpen,
          foldersToOpen,
          executedFrom,
          pidToKillWhenClosed,
          newWindow,
          devMode,
          safeMode,
          profileStartup,
          clearWindowState,
          addToLastWindow,
          env
        });
      } else if (urlsToOpen && urlsToOpen.length > 0) {
        return Promise.all(
          urlsToOpen.map(urlToOpen =>
            this.openUrl({ urlToOpen, devMode, safeMode, env })
          )
        );
      } else {
        // Always open an editor window if this is the first instance of Atom.
        return this.openPath({
          pathToOpen: null,
          pidToKillWhenClosed,
          newWindow,
          devMode,
          safeMode,
          profileStartup,
          clearWindowState,
          addToLastWindow,
          env
        });
      }
    }
  
    // Public: Create a new {AtomWindow} bound to this application.
    createWindow(settings) {
      return new AtomWindow(this, this.fileRecoveryService, settings);
    }
  
    // Public: Removes the {AtomWindow} from the global window list.
    removeWindow(window) {
      this.windowStack.removeWindow(window);
      if (this.getAllWindows().length === 0) {
        if (this.applicationMenu != null) {
          this.applicationMenu.enableWindowSpecificItems(false);
        }
        if (['win32', 'linux'].includes(process.platform)) {
          app.quit();
          return;
        }
      }
      if (!window.isSpec) this.saveCurrentWindowOptions(true);
    }
  
    // Public: Adds the {AtomWindow} to the global window list.
    addWindow(window) {
      this.windowStack.addWindow(window);
      if (this.applicationMenu)
        this.applicationMenu.addWindow(window.browserWindow);
  
      window.once('window:loaded', () => {
        this.autoUpdateManager &&
          this.autoUpdateManager.emitUpdateAvailableEvent(window);
      });
  
      if (!window.isSpec) {
        const focusHandler = () => this.windowStack.touch(window);
        const blurHandler = () => this.saveCurrentWindowOptions(false);
        window.browserWindow.on('focus', focusHandler);
        window.browserWindow.on('blur', blurHandler);
        window.browserWindow.once('closed', () => {
          this.windowStack.removeWindow(window);
          window.browserWindow.removeListener('focus', focusHandler);
          window.browserWindow.removeListener('blur', blurHandler);
        });
        window.browserWindow.webContents.once('did-finish-load', blurHandler);
        this.saveCurrentWindowOptions(false);
      }
    }
  
    getAllWindows() {
      return this.windowStack.all().slice();
    }
  
    getLastFocusedWindow(predicate) {
      return this.windowStack.getLastFocusedWindow(predicate);
    }
  
    // Creates server to listen for additional atom application launches.
    //
    // You can run the atom command multiple times, but after the first launch
    // the other launches will just pass their information to this server and then
    // close immediately.
    async listenForArgumentsFromNewProcess() {
      this.socketSecretPromise = createSocketSecret(this.version);
      this.socketSecret = await this.socketSecretPromise;
      this.socketPath = getSocketPath(this.socketSecret);
  
      await this.deleteSocketFile();
  
      const server = net.createServer(connection => {
        let data = '';
        connection.on('data', chunk => {
          data += chunk;
        });
        connection.on('end', () => {
          try {
            const options = decryptOptions(data, this.socketSecret);
            this.openWithOptions(options);
          } catch (e) {
            // Error while parsing/decrypting the options passed by the client.
            // We cannot trust the client, aborting.
          }
        });
      });
  
      return new Promise(resolve => {
        server.listen(this.socketPath, resolve);
        server.on('error', error =>
          console.error('Application server failed', error)
        );
      });
    }
  
    async deleteSocketFile() {
      if (process.platform === 'win32') return;
  
      if (!this.socketSecretPromise) {
        return;
      }
      await this.socketSecretPromise;
  
      if (fs.existsSync(this.socketPath)) {
        try {
          fs.unlinkSync(this.socketPath);
        } catch (error) {
          // Ignore ENOENT errors in case the file was deleted between the exists
          // check and the call to unlink sync. This occurred occasionally on CI
          // which is why this check is here.
          if (error.code !== 'ENOENT') throw error;
        }
      }
    }
  
    async deleteSocketSecretFile() {
      if (!this.socketSecretPromise) {
        return;
      }
      await this.socketSecretPromise;
  
      const socketSecretPath = getSocketSecretPath(this.version);
  
      if (fs.existsSync(socketSecretPath)) {
        try {
          fs.unlinkSync(socketSecretPath);
        } catch (error) {
          // Ignore ENOENT errors in case the file was deleted between the exists
          // check and the call to unlink sync.
          if (error.code !== 'ENOENT') throw error;
        }
      }
    }
  
    // Registers basic application commands, non-idempotent.
    handleEvents() {
      const createOpenSettings = ({ event, sameWindow }) => {
        const targetWindow = event
          ? this.atomWindowForEvent(event)
          : this.focusedWindow();
        return {
          devMode: targetWindow ? targetWindow.devMode : false,
          safeMode: targetWindow ? targetWindow.safeMode : false,
          window: sameWindow && targetWindow ? targetWindow : null
        };
      };
  
      this.on('application:quit', () => app.quit());
      this.on('application:new-window', () =>
        this.openPath(createOpenSettings({}))
      );
      this.on('application:new-file', () =>
        (this.focusedWindow() || this).openPath()
      );
      this.on('application:open-dev', () =>
        this.promptForPathToOpen('all', { devMode: true })
      );
      this.on('application:open-safe', () =>
        this.promptForPathToOpen('all', { safeMode: true })
      );
      this.on('application:inspect', ({ x, y, atomWindow }) => {
        if (!atomWindow) atomWindow = this.focusedWindow();
        if (atomWindow) atomWindow.browserWindow.inspectElement(x, y);
      });
  
      this.on('application:open-documentation', () =>
        shell.openExternal('http://flight-manual.atom.io')
      );
      this.on('application:open-discussions', () =>
        shell.openExternal('https://discuss.atom.io')
      );
      this.on('application:open-faq', () =>
        shell.openExternal('https://atom.io/faq')
      );
      this.on('application:open-terms-of-use', () =>
        shell.openExternal('https://atom.io/terms')
      );
      this.on('application:report-issue', () =>
        shell.openExternal(
          'https://github.com/atom/atom/blob/master/CONTRIBUTING.md#reporting-bugs'
        )
      );
      this.on('application:search-issues', () =>
        shell.openExternal('https://github.com/search?q=+is%3Aissue+user%3Aatom')
      );
  
      this.on('application:install-update', () => {
        this.quitting = true;
        this.quittingForUpdate = true;
        this.autoUpdateManager.install();
      });
  
      this.on('application:check-for-update', () =>
        this.autoUpdateManager.check()
      );
  
      if (process.platform === 'darwin') {
        this.on('application:reopen-project', ({ paths }) => {
          this.openPaths({ pathsToOpen: paths });
        });
  
        this.on('application:open', () => {
          this.promptForPathToOpen(
            'all',
            createOpenSettings({ sameWindow: true }),
            getDefaultPath()
          );
        });
        this.on('application:open-file', () => {
          this.promptForPathToOpen(
            'file',
            createOpenSettings({ sameWindow: true }),
            getDefaultPath()
          );
        });
        this.on('application:open-folder', () => {
          this.promptForPathToOpen(
            'folder',
            createOpenSettings({ sameWindow: true }),
            getDefaultPath()
          );
        });
  
        this.on('application:bring-all-windows-to-front', () =>
          Menu.sendActionToFirstResponder('arrangeInFront:')
        );
        this.on('application:hide', () =>
          Menu.sendActionToFirstResponder('hide:')
        );
        this.on('application:hide-other-applications', () =>
          Menu.sendActionToFirstResponder('hideOtherApplications:')
        );
        this.on('application:minimize', () =>
          Menu.sendActionToFirstResponder('performMiniaturize:')
        );
        this.on('application:unhide-all-applications', () =>
          Menu.sendActionToFirstResponder('unhideAllApplications:')
        );
        this.on('application:zoom', () =>
          Menu.sendActionToFirstResponder('zoom:')
        );
      } else {
        this.on('application:minimize', () => {
          const window = this.focusedWindow();
          if (window) window.minimize();
        });
        this.on('application:zoom', function() {
          const window = this.focusedWindow();
          if (window) window.maximize();
        });
      }
  
      this.openPathOnEvent('application:about', 'atom://about');
      this.openPathOnEvent('application:show-settings', 'atom://config');
      this.openPathOnEvent('application:open-your-config', 'atom://.atom/config');
      this.openPathOnEvent(
        'application:open-your-init-script',
        'atom://.atom/init-script'
      );
      this.openPathOnEvent('application:open-your-keymap', 'atom://.atom/keymap');
      this.openPathOnEvent(
        'application:open-your-snippets',
        'atom://.atom/snippets'
      );
      this.openPathOnEvent(
        'application:open-your-stylesheet',
        'atom://.atom/stylesheet'
      );
      this.openPathOnEvent(
        'application:open-license',
        path.join(process.resourcesPath, 'LICENSE.md')
      );
  
      this.configFile.onDidChange(settings => {
        for (let window of this.getAllWindows()) {
          window.didChangeUserSettings(settings);
        }
        this.config.resetUserSettings(settings);
      });
  
      this.configFile.onDidError(message => {
        const window = this.focusedWindow() || this.getLastFocusedWindow();
        if (window) {
          window.didFailToReadUserSettings(message);
        } else {
          console.error(message);
        }
      });
  
      this.disposable.add(
        ipcHelpers.on(app, 'before-quit', async event => {
          let resolveBeforeQuitPromise;
          this.lastBeforeQuitPromise = new Promise(resolve => {
            resolveBeforeQuitPromise = resolve;
          });
  
          if (!this.quitting) {
            this.quitting = true;
            event.preventDefault();
            const windowUnloadPromises = this.getAllWindows().map(
              async window => {
                const unloaded = await window.prepareToUnload();
                if (unloaded) {
                  window.close();
                  await window.closedPromise;
                }
                return unloaded;
              }
            );
            const windowUnloadedResults = await Promise.all(windowUnloadPromises);
            if (windowUnloadedResults.every(Boolean)) {
              app.quit();
            } else {
              this.quitting = false;
            }
          }
  
          resolveBeforeQuitPromise();
        })
      );
  
      this.disposable.add(
        ipcHelpers.on(app, 'will-quit', () => {
          this.killAllProcesses();
  
          return Promise.all([
            this.deleteSocketFile(),
            this.deleteSocketSecretFile()
          ]);
        })
      );
  
      // Triggered by the 'open-file' event from Electron:
      // https://electronjs.org/docs/api/app#event-open-file-macos
      // For example, this is fired when a file is dragged and dropped onto the Atom application icon in the dock.
      this.disposable.add(
        ipcHelpers.on(app, 'open-file', (event, pathToOpen) => {
          event.preventDefault();
          this.openPath({ pathToOpen });
        })
      );
  
      this.disposable.add(
        ipcHelpers.on(app, 'open-url', (event, urlToOpen) => {
          event.preventDefault();
          this.openUrl({
            urlToOpen,
            devMode: this.devMode,
            safeMode: this.safeMode
          });
        })
      );
  
      this.disposable.add(
        ipcHelpers.on(app, 'activate', (event, hasVisibleWindows) => {
          if (hasVisibleWindows) return;
          if (event) event.preventDefault();
          this.emit('application:new-window');
        })
      );
  
      this.disposable.add(
        ipcHelpers.on(ipcMain, 'restart-application', () => {
          this.restart();
        })
      );
  
      this.disposable.add(
        ipcHelpers.on(ipcMain, 'resolve-proxy', (event, requestId, url) => {
          event.sender.session.resolveProxy(url, proxy => {
            if (!event.sender.isDestroyed())
              event.sender.send('did-resolve-proxy', requestId, proxy);
          });
        })
      );
  
      this.disposable.add(
        ipcHelpers.on(ipcMain, 'did-change-history-manager', event => {
          for (let atomWindow of this.getAllWindows()) {
            const { webContents } = atomWindow.browserWindow;
            if (webContents !== event.sender)
              webContents.send('did-change-history-manager');
          }
        })
      );
  
      // A request from the associated render process to open a set of paths using the standard window location logic.
      // Used for application:reopen-project.
      this.disposable.add(
        ipcHelpers.on(ipcMain, 'open', (event, options) => {
          if (options) {
            if (typeof options.pathsToOpen === 'string') {
              options.pathsToOpen = [options.pathsToOpen];
            }
  
            if (options.here) {
              options.window = this.atomWindowForEvent(event);
            }
  
            if (options.pathsToOpen && options.pathsToOpen.length > 0) {
              this.openPaths(options);
            } else {
              this.addWindow(this.createWindow(options));
            }
          } else {
            this.promptForPathToOpen('all', {});
          }
        })
      );
  
      // Prompt for a file, folder, or either, then open the chosen paths. Files will be opened in the originating
      // window; folders will be opened in a new window unless an existing window exactly contains all of them.
      this.disposable.add(
        ipcHelpers.on(ipcMain, 'open-chosen-any', (event, defaultPath) => {
          this.promptForPathToOpen(
            'all',
            createOpenSettings({ event, sameWindow: true }),
            defaultPath
          );
        })
      );
      this.disposable.add(
        ipcHelpers.on(ipcMain, 'open-chosen-file', (event, defaultPath) => {
          this.promptForPathToOpen(
            'file',
            createOpenSettings({ event, sameWindow: true }),
            defaultPath
          );
        })
      );
      this.disposable.add(
        ipcHelpers.on(ipcMain, 'open-chosen-folder', (event, defaultPath) => {
          this.promptForPathToOpen(
            'folder',
            createOpenSettings({ event }),
            defaultPath
          );
        })
      );
  
      this.disposable.add(
        ipcHelpers.on(
          ipcMain,
          'update-application-menu',
          (event, template, menu) => {
            const window = BrowserWindow.fromWebContents(event.sender);
            if (this.applicationMenu)
              this.applicationMenu.update(window, template, menu);
          }
        )
      );
  
      this.disposable.add(
        ipcHelpers.on(
          ipcMain,
          'run-package-specs',
          (event, packageSpecPath, options = {}) => {
            this.runTests(
              Object.assign(
                {
                  resourcePath: this.devResourcePath,
                  pathsToOpen: [packageSpecPath],
                  headless: false
                },
                options
              )
            );
          }
        )
      );
  
      this.disposable.add(
        ipcHelpers.on(ipcMain, 'run-benchmarks', (event, benchmarksPath) => {
          this.runBenchmarks({
            resourcePath: this.devResourcePath,
            pathsToOpen: [benchmarksPath],
            headless: false,
            test: false
          });
        })
      );
  
      this.disposable.add(
        ipcHelpers.on(ipcMain, 'command', (event, command) => {
          this.emit(command);
        })
      );
  
      this.disposable.add(
        ipcHelpers.on(ipcMain, 'window-command', (event, command, ...args) => {
          const window = BrowserWindow.fromWebContents(event.sender);
          return window && window.emit(command, ...args);
        })
      );
  
      this.disposable.add(
        ipcHelpers.respondTo(
          'window-method',
          (browserWindow, method, ...args) => {
            const window = this.atomWindowForBrowserWindow(browserWindow);
            if (window) window[method](...args);
          }
        )
      );
  
      this.disposable.add(
        ipcHelpers.on(ipcMain, 'pick-folder', (event, responseChannel) => {
          this.promptForPath('folder', paths =>
            event.sender.send(responseChannel, paths)
          );
        })
      );
  
      this.disposable.add(
        ipcHelpers.respondTo('set-window-size', (window, width, height) => {
          window.setSize(width, height);
        })
      );
  
      this.disposable.add(
        ipcHelpers.respondTo('set-window-position', (window, x, y) => {
          window.setPosition(x, y);
        })
      );
  
      this.disposable.add(
        ipcHelpers.respondTo(
          'set-user-settings',
          (window, settings, filePath) => {
            if (!this.quitting) {
              return ConfigFile.at(filePath || this.configFilePath).update(
                JSON.parse(settings)
              );
            }
          }
        )
      );
  
      this.disposable.add(
        ipcHelpers.respondTo('center-window', window => window.center())
      );
      this.disposable.add(
        ipcHelpers.respondTo('focus-window', window => window.focus())
      );
      this.disposable.add(
        ipcHelpers.respondTo('show-window', window => window.show())
      );
      this.disposable.add(
        ipcHelpers.respondTo('hide-window', window => window.hide())
      );
      this.disposable.add(
        ipcHelpers.respondTo(
          'get-temporary-window-state',
          window => window.temporaryState
        )
      );
  
      this.disposable.add(
        ipcHelpers.respondTo('set-temporary-window-state', (win, state) => {
          win.temporaryState = state;
        })
      );
  
      this.disposable.add(
        ipcHelpers.on(
          ipcMain,
          'write-text-to-selection-clipboard',
          (event, text) => clipboard.writeText(text, 'selection')
        )
      );
  
      this.disposable.add(
        ipcHelpers.on(ipcMain, 'write-to-stdout', (event, output) =>
          process.stdout.write(output)
        )
      );
  
      this.disposable.add(
        ipcHelpers.on(ipcMain, 'write-to-stderr', (event, output) =>
          process.stderr.write(output)
        )
      );
  
      this.disposable.add(
        ipcHelpers.on(ipcMain, 'add-recent-document', (event, filename) =>
          app.addRecentDocument(filename)
        )
      );
  
      this.disposable.add(
        ipcHelpers.on(
          ipcMain,
          'execute-javascript-in-dev-tools',
          (event, code) =>
            event.sender.devToolsWebContents &&
            event.sender.devToolsWebContents.executeJavaScript(code)
        )
      );
  
      this.disposable.add(
        ipcHelpers.on(ipcMain, 'get-auto-update-manager-state', event => {
          event.returnValue = this.autoUpdateManager.getState();
        })
      );
  
      this.disposable.add(
        ipcHelpers.on(ipcMain, 'get-auto-update-manager-error', event => {
          event.returnValue = this.autoUpdateManager.getErrorMessage();
        })
      );
  
      this.disposable.add(
        ipcHelpers.respondTo('will-save-path', (window, path) =>
          this.fileRecoveryService.willSavePath(window, path)
        )
      );
  
      this.disposable.add(
        ipcHelpers.respondTo('did-save-path', (window, path) =>
          this.fileRecoveryService.didSavePath(window, path)
        )
      );
  
      this.disposable.add(this.disableZoomOnDisplayChange());
    }
  
    setupDockMenu() {
      if (process.platform === 'darwin') {
        return app.dock.setMenu(
          Menu.buildFromTemplate([
            {
              label: 'New Window',
              click: () => this.emit('application:new-window')
            }
          ])
        );
      }
    }
  
    initializeAtomHome(configDirPath) {
      if (!fs.existsSync(configDirPath)) {
        const templateConfigDirPath = fs.resolve(this.resourcePath, 'dot-atom');
        fs.copySync(templateConfigDirPath, configDirPath);
      }
    }
  
    // Public: Executes the given command.
    //
    // If it isn't handled globally, delegate to the currently focused window.
    //
    // command - The string representing the command.
    // args - The optional arguments to pass along.
    sendCommand(command, ...args) {
      if (!this.emit(command, ...args)) {
        const focusedWindow = this.focusedWindow();
        if (focusedWindow) {
          return focusedWindow.sendCommand(command, ...args);
        } else {
          return this.sendCommandToFirstResponder(command);
        }
      }
    }
  
    // Public: Executes the given command on the given window.
    //
    // command - The string representing the command.
    // atomWindow - The {AtomWindow} to send the command to.
    // args - The optional arguments to pass along.
    sendCommandToWindow(command, atomWindow, ...args) {
      if (!this.emit(command, ...args)) {
        if (atomWindow) {
          return atomWindow.sendCommand(command, ...args);
        } else {
          return this.sendCommandToFirstResponder(command);
        }
      }
    }
  
    // Translates the command into macOS action and sends it to application's first
    // responder.
    sendCommandToFirstResponder(command) {
      if (process.platform !== 'darwin') return false;
  
      switch (command) {
        case 'core:undo':
          Menu.sendActionToFirstResponder('undo:');
          break;
        case 'core:redo':
          Menu.sendActionToFirstResponder('redo:');
          break;
        case 'core:copy':
          Menu.sendActionToFirstResponder('copy:');
          break;
        case 'core:cut':
          Menu.sendActionToFirstResponder('cut:');
          break;
        case 'core:paste':
          Menu.sendActionToFirstResponder('paste:');
          break;
        case 'core:select-all':
          Menu.sendActionToFirstResponder('selectAll:');
          break;
        default:
          return false;
      }
      return true;
    }
  
    // Public: Open the given path in the focused window when the event is
    // triggered.
    //
    // A new window will be created if there is no currently focused window.
    //
    // eventName - The event to listen for.
    // pathToOpen - The path to open when the event is triggered.
    openPathOnEvent(eventName, pathToOpen) {
      this.on(eventName, () => {
        const window = this.focusedWindow();
        if (window) {
          return window.openPath(pathToOpen);
        } else {
          return this.openPath({ pathToOpen });
        }
      });
    }
  
    // Returns the {AtomWindow} for the given locations.
    windowForLocations(locationsToOpen, devMode, safeMode) {
      return this.getLastFocusedWindow(
        window =>
          !window.isSpec &&
          window.devMode === devMode &&
          window.safeMode === safeMode &&
          window.containsLocations(locationsToOpen)
      );
    }
  
    // Returns the {AtomWindow} for the given ipcMain event.
    atomWindowForEvent({ sender }) {
      return this.atomWindowForBrowserWindow(
        BrowserWindow.fromWebContents(sender)
      );
    }
  
    atomWindowForBrowserWindow(browserWindow) {
      return this.getAllWindows().find(
        atomWindow => atomWindow.browserWindow === browserWindow
      );
    }
  
    // Public: Returns the currently focused {AtomWindow} or undefined if none.
    focusedWindow() {
      return this.getAllWindows().find(window => window.isFocused());
    }
  
    // Get the platform-specific window offset for new windows.
    getWindowOffsetForCurrentPlatform() {
      const offsetByPlatform = {
        darwin: 22,
        win32: 26
      };
      return offsetByPlatform[process.platform] || 0;
    }
  
    // Get the dimensions for opening a new window by cascading as appropriate to
    // the platform.
    getDimensionsForNewWindow() {
      const window = this.focusedWindow() || this.getLastFocusedWindow();
      if (!window || window.isMaximized()) return;
      const dimensions = window.getDimensions();
      if (dimensions) {
        const offset = this.getWindowOffsetForCurrentPlatform();
        dimensions.x += offset;
        dimensions.y += offset;
        return dimensions;
      }
    }
  
    // Public: Opens a single path, in an existing window if possible.
    //
    // options -
    //   :pathToOpen - The file path to open
    //   :pidToKillWhenClosed - The integer of the pid to kill
    //   :newWindow - Boolean of whether this should be opened in a new window.
    //   :devMode - Boolean to control the opened window's dev mode.
    //   :safeMode - Boolean to control the opened window's safe mode.
    //   :profileStartup - Boolean to control creating a profile of the startup time.
    //   :window - {AtomWindow} to open file paths in.
    //   :addToLastWindow - Boolean of whether this should be opened in last focused window.
    openPath({
      pathToOpen,
      pidToKillWhenClosed,
      newWindow,
      devMode,
      safeMode,
      profileStartup,
      window,
      clearWindowState,
      addToLastWindow,
      env
    } = {}) {
      return this.openPaths({
        pathsToOpen: [pathToOpen],
        pidToKillWhenClosed,
        newWindow,
        devMode,
        safeMode,
        profileStartup,
        window,
        clearWindowState,
        addToLastWindow,
        env
      });
    }
  
    // Public: Opens multiple paths, in existing windows if possible.
    //
    // options -
    //   :pathsToOpen - The array of file paths to open
    //   :foldersToOpen - An array of additional paths to open that must be existing directories
    //   :pidToKillWhenClosed - The integer of the pid to kill
    //   :newWindow - Boolean of whether this should be opened in a new window.
    //   :devMode - Boolean to control the opened window's dev mode.
    //   :safeMode - Boolean to control the opened window's safe mode.
    //   :windowDimensions - Object with height and width keys.
    //   :window - {AtomWindow} to open file paths in.
    //   :addToLastWindow - Boolean of whether this should be opened in last focused window.
    async openPaths({
      pathsToOpen,
      foldersToOpen,
      executedFrom,
      pidToKillWhenClosed,
      newWindow,
      devMode,
      safeMode,
      windowDimensions,
      profileStartup,
      window,
      clearWindowState,
      addToLastWindow,
      env
    } = {}) {
      if (!env) env = process.env;
      if (!pathsToOpen) pathsToOpen = [];
      if (!foldersToOpen) foldersToOpen = [];
  
      devMode = Boolean(devMode);
      safeMode = Boolean(safeMode);
      clearWindowState = Boolean(clearWindowState);
  
      const locationsToOpen = await Promise.all(
        pathsToOpen.map(pathToOpen =>
          this.parsePathToOpen(pathToOpen, executedFrom, {
            hasWaitSession: pidToKillWhenClosed != null
          })
        )
      );
  
      for (const folderToOpen of foldersToOpen) {
        locationsToOpen.push({
          pathToOpen: folderToOpen,
          initialLine: null,
          initialColumn: null,
          exists: true,
          isDirectory: true,
          hasWaitSession: pidToKillWhenClosed != null
        });
      }
  
      if (locationsToOpen.length === 0) {
        return;
      }
  
      const hasNonEmptyPath = locationsToOpen.some(
        location => location.pathToOpen
      );
      const createNewWindow = newWindow || !hasNonEmptyPath;
  
      let existingWindow;
  
      if (!createNewWindow) {
        // An explicitly provided AtomWindow has precedence.
        existingWindow = window;
  
        // If no window is specified and at least one path is provided, locate an existing window that contains all
        // provided paths.
        if (!existingWindow && hasNonEmptyPath) {
          existingWindow = this.windowForLocations(
            locationsToOpen,
            devMode,
            safeMode
          );
        }
  
        // No window specified, no existing window found, and addition to the last window requested. Find the last
        // focused window that matches the requested dev and safe modes.
        if (!existingWindow && addToLastWindow) {
          existingWindow = this.getLastFocusedWindow(win => {
            return (
              !win.isSpec && win.devMode === devMode && win.safeMode === safeMode
            );
          });
        }
  
        // Fall back to the last focused window that has no project roots.
        if (!existingWindow) {
          existingWindow = this.getLastFocusedWindow(
            win => !win.isSpec && !win.hasProjectPaths()
          );
        }
  
        // One last case: if *no* paths are directories, add to the last focused window.
        if (!existingWindow) {
          const noDirectories = locationsToOpen.every(
            location => !location.isDirectory
          );
          if (noDirectories) {
            existingWindow = this.getLastFocusedWindow(win => {
              return (
                !win.isSpec &&
                win.devMode === devMode &&
                win.safeMode === safeMode
              );
            });
          }
        }
      }
  
      let openedWindow;
      if (existingWindow) {
        openedWindow = existingWindow;
        StartupTime.addMarker('main-process:atom-application:open-in-existing');
        openedWindow.openLocations(locationsToOpen);
        if (openedWindow.isMinimized()) {
          openedWindow.restore();
        } else {
          openedWindow.focus();
        }
        openedWindow.replaceEnvironment(env);
      } else {
        let resourcePath, windowInitializationScript;
        if (devMode) {
          try {
            windowInitializationScript = require.resolve(
              path.join(
                this.devResourcePath,
                'src',
                'initialize-application-window'
              )
            );
            resourcePath = this.devResourcePath;
          } catch (error) {}
        }
  
        if (!windowInitializationScript) {
          windowInitializationScript = require.resolve(
            '../initialize-application-window'
          );
        }
        if (!resourcePath) resourcePath = this.resourcePath;
        if (!windowDimensions)
          windowDimensions = this.getDimensionsForNewWindow();
  
        StartupTime.addMarker('main-process:atom-application:create-window');
        openedWindow = this.createWindow({
          locationsToOpen,
          windowInitializationScript,
          resourcePath,
          devMode,
          safeMode,
          windowDimensions,
          profileStartup,
          clearWindowState,
          env
        });
        this.addWindow(openedWindow);
        openedWindow.focus();
      }
  
      if (pidToKillWhenClosed != null) {
        if (!this.waitSessionsByWindow.has(openedWindow)) {
          this.waitSessionsByWindow.set(openedWindow, []);
        }
        this.waitSessionsByWindow.get(openedWindow).push({
          pid: pidToKillWhenClosed,
          remainingPaths: new Set(
            locationsToOpen.map(location => location.pathToOpen).filter(Boolean)
          )
        });
      }
  
      openedWindow.browserWindow.once('closed', () =>
        this.killProcessesForWindow(openedWindow)
      );
      return openedWindow;
    }
  
    // Kill all processes associated with opened windows.
    killAllProcesses() {
      for (let window of this.waitSessionsByWindow.keys()) {
        this.killProcessesForWindow(window);
      }
    }
  
    killProcessesForWindow(window) {
      const sessions = this.waitSessionsByWindow.get(window);
      if (!sessions) return;
      for (const session of sessions) {
        this.killProcess(session.pid);
      }
      this.waitSessionsByWindow.delete(window);
    }
  
    windowDidClosePathWithWaitSession(window, initialPath) {
      const waitSessions = this.waitSessionsByWindow.get(window);
      if (!waitSessions) return;
      for (let i = waitSessions.length - 1; i >= 0; i--) {
        const session = waitSessions[i];
        session.remainingPaths.delete(initialPath);
        if (session.remainingPaths.size === 0) {
          this.killProcess(session.pid);
          waitSessions.splice(i, 1);
        }
      }
    }
  
    // Kill the process with the given pid.
    killProcess(pid) {
      try {
        const parsedPid = parseInt(pid);
        if (isFinite(parsedPid)) this._killProcess(parsedPid);
      } catch (error) {
        if (error.code !== 'ESRCH') {
          console.log(
            `Killing process ${pid} failed: ${
              error.code != null ? error.code : error.message
            }`
          );
        }
      }
    }
  
    async saveCurrentWindowOptions(allowEmpty = false) {
      if (this.quitting) return;
  
      const state = {
        version: APPLICATION_STATE_VERSION,
        windows: this.getAllWindows()
          .filter(window => !window.isSpec)
          .map(window => ({ projectRoots: window.projectRoots }))
      };
      state.windows.reverse();
  
      if (state.windows.length > 0 || allowEmpty) {
        await this.storageFolder.store('application.json', state);
        this.emit('application:did-save-state');
      }
    }
  
    async loadPreviousWindowOptions() {
      const state = await this.storageFolder.load('application.json');
      if (!state) {
        return [];
      }
  
      if (state.version === APPLICATION_STATE_VERSION) {
        // Atom >=1.36.1
        // Schema: {version: '1', windows: [{projectRoots: ['<root-dir>', ...]}, ...]}
        return state.windows.map(each => ({
          foldersToOpen: each.projectRoots,
          devMode: this.devMode,
          safeMode: this.safeMode,
          newWindow: true
        }));
      } else if (state.version === undefined) {
        // Atom <= 1.36.0
        // Schema: [{initialPaths: ['<root-dir>', ...]}, ...]
        return Promise.all(
          state.map(async windowState => {
            // Classify each window's initialPaths as directories or non-directories
            const classifiedPaths = await Promise.all(
              windowState.initialPaths.map(
                initialPath =>
                  new Promise(resolve => {
                    fs.isDirectory(initialPath, isDir =>
                      resolve({ initialPath, isDir })
                    );
                  })
              )
            );
  
            // Only accept initialPaths that are existing directories
            return {
              foldersToOpen: classifiedPaths
                .filter(({ isDir }) => isDir)
                .map(({ initialPath }) => initialPath),
              devMode: this.devMode,
              safeMode: this.safeMode,
              newWindow: true
            };
          })
        );
      } else {
        // Unrecognized version (from a newer Atom?)
        return [];
      }
    }
  
    // Open an atom:// url.
    //
    // The host of the URL being opened is assumed to be the package name
    // responsible for opening the URL.  A new window will be created with
    // that package's `urlMain` as the bootstrap script.
    //
    // options -
    //   :urlToOpen - The atom:// url to open.
    //   :devMode - Boolean to control the opened window's dev mode.
    //   :safeMode - Boolean to control the opened window's safe mode.
    openUrl({ urlToOpen, devMode, safeMode, env }) {
      const parsedUrl = url.parse(urlToOpen, true);
      if (parsedUrl.protocol !== 'atom:') return;
  
      const pack = this.findPackageWithName(parsedUrl.host, devMode);
      if (pack && pack.urlMain) {
        return this.openPackageUrlMain(
          parsedUrl.host,
          pack.urlMain,
          urlToOpen,
          devMode,
          safeMode,
          env
        );
      } else {
        return this.openPackageUriHandler(
          urlToOpen,
          parsedUrl,
          devMode,
          safeMode,
          env
        );
      }
    }
  
    openPackageUriHandler(url, parsedUrl, devMode, safeMode, env) {
      let bestWindow;
  
      if (parsedUrl.host === 'core') {
        const predicate = require('../core-uri-handlers').windowPredicate(
          parsedUrl
        );
        bestWindow = this.getLastFocusedWindow(
          win => !win.isSpecWindow() && predicate(win)
        );
      }
  
      if (!bestWindow)
        bestWindow = this.getLastFocusedWindow(win => !win.isSpecWindow());
  
      if (bestWindow) {
        bestWindow.sendURIMessage(url);
        bestWindow.focus();
        return bestWindow;
      } else {
        let windowInitializationScript;
        let { resourcePath } = this;
        if (devMode) {
          try {
            windowInitializationScript = require.resolve(
              path.join(
                this.devResourcePath,
                'src',
                'initialize-application-window'
              )
            );
            resourcePath = this.devResourcePath;
          } catch (error) {}
        }
  
        if (!windowInitializationScript) {
          windowInitializationScript = require.resolve(
            '../initialize-application-window'
          );
        }
  
        const windowDimensions = this.getDimensionsForNewWindow();
        const window = this.createWindow({
          resourcePath,
          windowInitializationScript,
          devMode,
          safeMode,
          windowDimensions,
          env
        });
        this.addWindow(window);
        window.on('window:loaded', () => window.sendURIMessage(url));
        return window;
      }
    }
  
    findPackageWithName(packageName, devMode) {
      return this.getPackageManager(devMode)
        .getAvailablePackageMetadata()
        .find(({ name }) => name === packageName);
    }
  
    openPackageUrlMain(
      packageName,
      packageUrlMain,
      urlToOpen,
      devMode,
      safeMode,
      env
    ) {
      const packagePath = this.getPackageManager(devMode).resolvePackagePath(
        packageName
      );
      const windowInitializationScript = path.resolve(
        packagePath,
        packageUrlMain
      );
      const windowDimensions = this.getDimensionsForNewWindow();
      const window = this.createWindow({
        windowInitializationScript,
        resourcePath: this.resourcePath,
        devMode,
        safeMode,
        urlToOpen,
        windowDimensions,
        env
      });
      this.addWindow(window);
      return window;
    }
  
    getPackageManager(devMode) {
      if (this.packages == null) {
        const PackageManager = require('../package-manager');
        this.packages = new PackageManager({});
        this.packages.initialize({
          configDirPath: process.env.ATOM_HOME,
          devMode,
          resourcePath: this.resourcePath
        });
      }
  
      return this.packages;
    }
  
    // Opens up a new {AtomWindow} to run specs within.
    //
    // options -
    //   :headless - A Boolean that, if true, will close the window upon
    //                   completion.
    //   :resourcePath - The path to include specs from.
    //   :specPath - The directory to load specs from.
    //   :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages
    //               and ~/.atom/dev/packages, defaults to false.
    runTests({
      headless,
      resourcePath,
      executedFrom,
      pathsToOpen,
      logFile,
      safeMode,
      timeout,
      env
    }) {
      let windowInitializationScript;
      if (resourcePath !== this.resourcePath && !fs.existsSync(resourcePath)) {
        ({ resourcePath } = this);
      }
  
      const timeoutInSeconds = Number.parseFloat(timeout);
      if (!Number.isNaN(timeoutInSeconds)) {
        const timeoutHandler = function() {
          console.log(
            `The test suite has timed out because it has been running for more than ${timeoutInSeconds} seconds.`
          );
          return process.exit(124); // Use the same exit code as the UNIX timeout util.
        };
        setTimeout(timeoutHandler, timeoutInSeconds * 1000);
      }
  
      try {
        windowInitializationScript = require.resolve(
          path.resolve(this.devResourcePath, 'src', 'initialize-test-window')
        );
      } catch (error) {
        windowInitializationScript = require.resolve(
          path.resolve(__dirname, '..', '..', 'src', 'initialize-test-window')
        );
      }
  
      const testPaths = [];
      if (pathsToOpen != null) {
        for (let pathToOpen of pathsToOpen) {
          testPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen)));
        }
      }
  
      if (testPaths.length === 0) {
        process.stderr.write('Error: Specify at least one test path\n\n');
        process.exit(1);
      }
  
      const legacyTestRunnerPath = this.resolveLegacyTestRunnerPath();
      const testRunnerPath = this.resolveTestRunnerPath(testPaths[0]);
      const devMode = true;
      const isSpec = true;
      if (safeMode == null) {
        safeMode = false;
      }
      const window = this.createWindow({
        windowInitializationScript,
        resourcePath,
        headless,
        isSpec,
        devMode,
        testRunnerPath,
        legacyTestRunnerPath,
        testPaths,
        logFile,
        safeMode,
        env
      });
      this.addWindow(window);
      if (env) window.replaceEnvironment(env);
      return window;
    }
  
    runBenchmarks({
      headless,
      test,
      resourcePath,
      executedFrom,
      pathsToOpen,
      env
    }) {
      let windowInitializationScript;
      if (resourcePath !== this.resourcePath && !fs.existsSync(resourcePath)) {
        ({ resourcePath } = this);
      }
  
      try {
        windowInitializationScript = require.resolve(
          path.resolve(this.devResourcePath, 'src', 'initialize-benchmark-window')
        );
      } catch (error) {
        windowInitializationScript = require.resolve(
          path.resolve(
            __dirname,
            '..',
            '..',
            'src',
            'initialize-benchmark-window'
          )
        );
      }
  
      const benchmarkPaths = [];
      if (pathsToOpen != null) {
        for (let pathToOpen of pathsToOpen) {
          benchmarkPaths.push(
            path.resolve(executedFrom, fs.normalize(pathToOpen))
          );
        }
      }
  
      if (benchmarkPaths.length === 0) {
        process.stderr.write('Error: Specify at least one benchmark path.\n\n');
        process.exit(1);
      }
  
      const devMode = true;
      const isSpec = true;
      const safeMode = false;
      const window = this.createWindow({
        windowInitializationScript,
        resourcePath,
        headless,
        test,
        isSpec,
        devMode,
        benchmarkPaths,
        safeMode,
        env
      });
      this.addWindow(window);
      return window;
    }
  
    resolveTestRunnerPath(testPath) {
      let packageRoot;
      if (FindParentDir == null) {
        FindParentDir = require('find-parent-dir');
      }
  
      if ((packageRoot = FindParentDir.sync(testPath, 'package.json'))) {
        const packageMetadata = require(path.join(packageRoot, 'package.json'));
        if (packageMetadata.atomTestRunner) {
          let testRunnerPath;
          if (Resolve == null) {
            Resolve = require('resolve');
          }
          if (
            (testRunnerPath = Resolve.sync(packageMetadata.atomTestRunner, {
              basedir: packageRoot,
              extensions: Object.keys(require.extensions)
            }))
          ) {
            return testRunnerPath;
          } else {
            process.stderr.write(
              `Error: Could not resolve test runner path '${
                packageMetadata.atomTestRunner
              }'`
            );
            process.exit(1);
          }
        }
      }
  
      return this.resolveLegacyTestRunnerPath();
    }
  
    resolveLegacyTestRunnerPath() {
      try {
        return require.resolve(
          path.resolve(this.devResourcePath, 'spec', 'jasmine-test-runner')
        );
      } catch (error) {
        return require.resolve(
          path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner')
        );
      }
    }
  
    async parsePathToOpen(pathToOpen, executedFrom, extra) {
      const result = Object.assign(
        {
          pathToOpen,
          initialColumn: null,
          initialLine: null,
          exists: false,
          isDirectory: false,
          isFile: false
        },
        extra
      );
  
      if (!pathToOpen) {
        return result;
      }
  
      result.pathToOpen = result.pathToOpen.replace(/[:\s]+$/, '');
      const match = result.pathToOpen.match(LocationSuffixRegExp);
  
      if (match != null) {
        result.pathToOpen = result.pathToOpen.slice(0, -match[0].length);
        if (match[1]) {
          result.initialLine = Math.max(0, parseInt(match[1].slice(1), 10) - 1);
        }
        if (match[2]) {
          result.initialColumn = Math.max(0, parseInt(match[2].slice(1), 10) - 1);
        }
      }
  
      const normalizedPath = path.normalize(
        path.resolve(executedFrom, fs.normalize(result.pathToOpen))
      );
      if (!url.parse(pathToOpen).protocol) {
        result.pathToOpen = normalizedPath;
      }
  
      await new Promise((resolve, reject) => {
        fs.stat(result.pathToOpen, (err, st) => {
          if (err) {
            if (err.code === 'ENOENT' || err.code === 'EACCES') {
              result.exists = false;
              resolve();
            } else {
              reject(err);
            }
            return;
          }
  
          result.exists = true;
          result.isFile = st.isFile();
          result.isDirectory = st.isDirectory();
          resolve();
        });
      });
  
      return result;
    }
  
    // Opens a native dialog to prompt the user for a path.
    //
    // Once paths are selected, they're opened in a new or existing {AtomWindow}s.
    //
    // options -
    //   :type - A String which specifies the type of the dialog, could be 'file',
    //           'folder' or 'all'. The 'all' is only available on macOS.
    //   :devMode - A Boolean which controls whether any newly opened windows
    //              should be in dev mode or not.
    //   :safeMode - A Boolean which controls whether any newly opened windows
    //               should be in safe mode or not.
    //   :window - An {AtomWindow} to use for opening selected file paths as long as
    //             all are files.
    //   :path - An optional String which controls the default path to which the
    //           file dialog opens.
    promptForPathToOpen(type, { devMode, safeMode, window }, path = null) {
      return this.promptForPath(
        type,
        async pathsToOpen => {
          let targetWindow;
  
          // Open in :window as long as no chosen paths are folders. If any chosen path is a folder, open in a
          // new window instead.
          if (type === 'folder') {
            targetWindow = null;
          } else if (type === 'file') {
            targetWindow = window;
          } else if (type === 'all') {
            const areDirectories = await Promise.all(
              pathsToOpen.map(
                pathToOpen =>
                  new Promise(resolve => fs.isDirectory(pathToOpen, resolve))
              )
            );
            if (!areDirectories.some(Boolean)) {
              targetWindow = window;
            }
          }
  
          return this.openPaths({
            pathsToOpen,
            devMode,
            safeMode,
            window: targetWindow
          });
        },
        path
      );
    }
  
    promptForPath(type, callback, path) {
      const properties = (() => {
        switch (type) {
          case 'file':
            return ['openFile'];
          case 'folder':
            return ['openDirectory'];
          case 'all':
            return ['openFile', 'openDirectory'];
          default:
            throw new Error(`${type} is an invalid type for promptForPath`);
        }
      })();
  
      // Show the open dialog as child window on Windows and Linux, and as an independent dialog on macOS. This matches
      // most native apps.
      const parentWindow =
        process.platform === 'darwin' ? null : BrowserWindow.getFocusedWindow();
  
      const openOptions = {
        properties: properties.concat(['multiSelections', 'createDirectory']),
        title: (() => {
          switch (type) {
            case 'file':
              return 'Open File';
            case 'folder':
              return 'Open Folder';
            default:
              return 'Open';
          }
        })()
      };
  
      // File dialog defaults to project directory of currently active editor
      if (path) openOptions.defaultPath = path;
      dialog.showOpenDialog(parentWindow, openOptions, callback);
    }
  
    async promptForRestart() {
      const result = await dialog.showMessageBox(
        BrowserWindow.getFocusedWindow(),
        {
          type: 'warning',
          title: 'Restart required',
          message:
            'You will need to restart Atom for this change to take effect.',
          buttons: ['Restart Atom', 'Cancel']
        }
      );
      if (result.response === 0) this.restart();
    }
  
    restart() {
      const args = [];
      if (this.safeMode) args.push('--safe');
      if (this.logFile != null) args.push(`--log-file=${this.logFile}`);
      if (this.userDataDir != null)
        args.push(`--user-data-dir=${this.userDataDir}`);
      if (this.devMode) {
        args.push('--dev');
        args.push(`--resource-path=${this.resourcePath}`);
      }
      app.relaunch({ args });
      app.quit();
    }
  
    disableZoomOnDisplayChange() {
      const callback = () => {
        this.getAllWindows().map(window => window.disableZoom());
      };
  
      // Set the limits every time a display is added or removed, otherwise the
      // configuration gets reset to the default, which allows zooming the
      // webframe.
      screen.on('display-added', callback);
      screen.on('display-removed', callback);
      return new Disposable(() => {
        screen.removeListener('display-added', callback);
        screen.removeListener('display-removed', callback);
      });
    }
  };
  
  class WindowStack {
    constructor(windows = []) {
      this.addWindow = this.addWindow.bind(this);
      this.touch = this.touch.bind(this);
      this.removeWindow = this.removeWindow.bind(this);
      this.getLastFocusedWindow = this.getLastFocusedWindow.bind(this);
      this.all = this.all.bind(this);
      this.windows = windows;
    }
  
    addWindow(window) {
      this.removeWindow(window);
      return this.windows.unshift(window);
    }
  
    touch(window) {
      return this.addWindow(window);
    }
  
    removeWindow(window) {
      const currentIndex = this.windows.indexOf(window);
      if (currentIndex > -1) {
        return this.windows.splice(currentIndex, 1);
      }
    }
  
    getLastFocusedWindow(predicate) {
      if (predicate == null) {
        predicate = win => true;
      }
      return this.windows.find(predicate);
    }
  
    all() {
      return this.windows;
    }
  }
  