diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee deleted file mode 100644 index e0d2d691f..000000000 --- a/src/main-process/atom-application.coffee +++ /dev/null @@ -1,926 +0,0 @@ -AtomWindow = require './atom-window' -ApplicationMenu = require './application-menu' -AtomProtocolHandler = require './atom-protocol-handler' -AutoUpdateManager = require './auto-update-manager' -StorageFolder = require '../storage-folder' -Config = require '../config' -FileRecoveryService = require './file-recovery-service' -ipcHelpers = require '../ipc-helpers' -{BrowserWindow, Menu, app, dialog, ipcMain, shell, screen} = require 'electron' -{CompositeDisposable, Disposable} = require 'event-kit' -crypto = require 'crypto' -fs = require 'fs-plus' -path = require 'path' -os = require 'os' -net = require 'net' -url = require 'url' -{EventEmitter} = require 'events' -_ = require 'underscore-plus' -FindParentDir = null -Resolve = null -ConfigSchema = require '../config-schema' - -LocationSuffixRegExp = /(:\d+)(:\d+)?$/ - -# 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 - Object.assign @prototype, EventEmitter.prototype - - # Public: The entry point into the Atom application. - @open: (options) -> - unless options.socketPath? - username = if process.platform is 'win32' then process.env.USERNAME else process.env.USER - # Lowercasing the ATOM_HOME to make sure that we don't get multiple sockets - # on case-insensitive filesystems due to arbitrary case differences in paths. - atomHomeUnique = path.resolve(process.env.ATOM_HOME).toLowerCase() - hash = crypto.createHash('sha1').update(options.version).update('|').update(process.arch).update('|').update(username).update('|').update(atomHomeUnique) - # We only keep the first 12 characters of the hash as not to have excessively long - # socket file. Note that macOS/BSD limit the length of socket file paths (see #15081). - # The replace calls convert the digest into "URL and Filename Safe" encoding (see RFC 4648). - atomInstanceDigest = hash.digest('base64').substring(0, 12).replace(/\+/g, '-').replace(/\//g, '_') - if process.platform is 'win32' - options.socketPath = "\\\\.\\pipe\\atom-#{atomInstanceDigest}-sock" - else - options.socketPath = path.join(os.tmpdir(), "atom-#{atomInstanceDigest}.sock") - - # 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 atom-shell, before it's fixed we check the existence of socketPath to - # speedup startup. - if (process.platform isnt 'win32' and not fs.existsSync options.socketPath) or options.test or options.benchmark or options.benchmarkTest - new AtomApplication(options).initialize(options) - return - - client = net.connect {path: options.socketPath}, -> - client.write JSON.stringify(options), -> - client.end() - app.quit() - - client.on 'error', -> new AtomApplication(options).initialize(options) - - windows: null - applicationMenu: null - atomProtocolHandler: null - resourcePath: null - version: null - quitting: false - - exit: (status) -> app.exit(status) - - constructor: (options) -> - {@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, @logFile, @userDataDir} = options - @socketPath = null if options.test or options.benchmark or options.benchmarkTest - @pidsToOpenWindows = {} - @windowStack = new WindowStack() - - @config = new Config({enablePersistence: true}) - @config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)} - ConfigSchema.projectHome = { - type: 'string', - default: path.join(fs.getHomeDirectory(), 'github'), - description: 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.' - } - @config.initialize({configDirPath: process.env.ATOM_HOME, @resourcePath, projectHomeSchema: ConfigSchema.projectHome}) - @config.load() - @fileRecoveryService = new FileRecoveryService(path.join(process.env.ATOM_HOME, "recovery")) - @storageFolder = new StorageFolder(process.env.ATOM_HOME) - @autoUpdateManager = new AutoUpdateManager( - @version, - options.test or options.benchmark or options.benchmarkTest, - @config - ) - - @disposable = new CompositeDisposable - @handleEvents() - - # 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. - initialize: (options) -> - 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 is 'darwin' and @config.get('core.useCustomTitleBar') - @config.unset('core.useCustomTitleBar') - @config.set('core.titleBar', 'custom') - - @config.onDidChange 'core.titleBar', @promptForRestart.bind(this) - - process.nextTick => @autoUpdateManager.initialize() - @applicationMenu = new ApplicationMenu(@version, @autoUpdateManager) - @atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode) - - @listenForArgumentsFromNewProcess() - @setupDockMenu() - - @launch(options) - - destroy: -> - windowsClosePromises = @getAllWindows().map (window) -> - window.close() - window.closedPromise - Promise.all(windowsClosePromises).then(=> @disposable.dispose()) - - launch: (options) -> - if options.test or options.benchmark or options.benchmarkTest - @openWithOptions(options) - else if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0 - if @config.get('core.restorePreviousWindowsOnStart') is 'always' - @loadState(_.deepClone(options)) - @openWithOptions(options) - else - @loadState(options) or @openPath(options) - - openWithOptions: (options) -> - { - initialPaths, pathsToOpen, executedFrom, urlsToOpen, benchmark, - benchmarkTest, test, pidToKillWhenClosed, devMode, safeMode, newWindow, - logFile, profileStartup, timeout, clearWindowState, addToLastWindow, env - } = options - - app.focus() - - if test - @runTests({ - headless: true, devMode, @resourcePath, executedFrom, pathsToOpen, - logFile, timeout, env - }) - else if benchmark or benchmarkTest - @runBenchmarks({headless: true, test: benchmarkTest, @resourcePath, executedFrom, pathsToOpen, timeout, env}) - else if pathsToOpen.length > 0 - @openPaths({ - initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, - devMode, safeMode, profileStartup, clearWindowState, addToLastWindow, env - }) - else if urlsToOpen.length > 0 - for urlToOpen in urlsToOpen - @openUrl({urlToOpen, devMode, safeMode, env}) - else - # Always open a editor window if this is the first instance of Atom. - @openPath({ - initialPaths, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, - clearWindowState, addToLastWindow, env - }) - - # Public: Removes the {AtomWindow} from the global window list. - removeWindow: (window) -> - @windowStack.removeWindow(window) - if @getAllWindows().length is 0 - @applicationMenu?.enableWindowSpecificItems(false) - if process.platform in ['win32', 'linux'] - app.quit() - return - @saveState(true) unless window.isSpec - - # Public: Adds the {AtomWindow} to the global window list. - addWindow: (window) -> - @windowStack.addWindow(window) - @applicationMenu?.addWindow(window.browserWindow) - window.once 'window:loaded', => - @autoUpdateManager?.emitUpdateAvailableEvent(window) - - unless window.isSpec - focusHandler = => @windowStack.touch(window) - blurHandler = => @saveState(false) - window.browserWindow.on 'focus', focusHandler - window.browserWindow.on 'blur', blurHandler - window.browserWindow.once 'closed', => - @windowStack.removeWindow(window) - window.browserWindow.removeListener 'focus', focusHandler - window.browserWindow.removeListener 'blur', blurHandler - window.browserWindow.webContents.once 'did-finish-load', => @saveState(false) - - getAllWindows: => - @windowStack.all().slice() - - getLastFocusedWindow: (predicate) => - @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. - listenForArgumentsFromNewProcess: -> - return unless @socketPath? - @deleteSocketFile() - server = net.createServer (connection) => - data = '' - connection.on 'data', (chunk) -> - data = data + chunk - - connection.on 'end', => - options = JSON.parse(data) - @openWithOptions(options) - - server.listen @socketPath - server.on 'error', (error) -> console.error 'Application server failed', error - - deleteSocketFile: -> - return if process.platform is 'win32' or not @socketPath? - - if fs.existsSync(@socketPath) - try - fs.unlinkSync(@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. - throw error unless error.code is 'ENOENT' - - # Registers basic application commands, non-idempotent. - handleEvents: -> - getLoadSettings = => - devMode: @focusedWindow()?.devMode - safeMode: @focusedWindow()?.safeMode - - @on 'application:quit', -> app.quit() - @on 'application:new-window', -> @openPath(getLoadSettings()) - @on 'application:new-file', -> (@focusedWindow() ? this).openPath() - @on 'application:open-dev', -> @promptForPathToOpen('all', devMode: true) - @on 'application:open-safe', -> @promptForPathToOpen('all', safeMode: true) - @on 'application:inspect', ({x, y, atomWindow}) -> - atomWindow ?= @focusedWindow() - atomWindow?.browserWindow.inspectElement(x, y) - - @on 'application:open-documentation', -> shell.openExternal('http://flight-manual.atom.io/') - @on 'application:open-discussions', -> shell.openExternal('https://discuss.atom.io') - @on 'application:open-faq', -> shell.openExternal('https://atom.io/faq') - @on 'application:open-terms-of-use', -> shell.openExternal('https://atom.io/terms') - @on 'application:report-issue', -> shell.openExternal('https://github.com/atom/atom/blob/master/CONTRIBUTING.md#reporting-bugs') - @on 'application:search-issues', -> shell.openExternal('https://github.com/search?q=+is%3Aissue+user%3Aatom') - - @on 'application:install-update', => - @quitting = true - @autoUpdateManager.install() - - @on 'application:check-for-update', => @autoUpdateManager.check() - - if process.platform is 'darwin' - @on 'application:bring-all-windows-to-front', -> Menu.sendActionToFirstResponder('arrangeInFront:') - @on 'application:hide', -> Menu.sendActionToFirstResponder('hide:') - @on 'application:hide-other-applications', -> Menu.sendActionToFirstResponder('hideOtherApplications:') - @on 'application:minimize', -> Menu.sendActionToFirstResponder('performMiniaturize:') - @on 'application:unhide-all-applications', -> Menu.sendActionToFirstResponder('unhideAllApplications:') - @on 'application:zoom', -> Menu.sendActionToFirstResponder('zoom:') - else - @on 'application:minimize', -> @focusedWindow()?.minimize() - @on 'application:zoom', -> @focusedWindow()?.maximize() - - @openPathOnEvent('application:about', 'atom://about') - @openPathOnEvent('application:show-settings', 'atom://config') - @openPathOnEvent('application:open-your-config', 'atom://.atom/config') - @openPathOnEvent('application:open-your-init-script', 'atom://.atom/init-script') - @openPathOnEvent('application:open-your-keymap', 'atom://.atom/keymap') - @openPathOnEvent('application:open-your-snippets', 'atom://.atom/snippets') - @openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet') - @openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md')) - - @disposable.add ipcHelpers.on app, 'before-quit', (event) => - resolveBeforeQuitPromise = null - @lastBeforeQuitPromise = new Promise((resolve) -> resolveBeforeQuitPromise = resolve) - if @quitting - resolveBeforeQuitPromise() - else - event.preventDefault() - @quitting = true - windowUnloadPromises = @getAllWindows().map((window) -> window.prepareToUnload()) - Promise.all(windowUnloadPromises).then((windowUnloadedResults) -> - didUnloadAllWindows = windowUnloadedResults.every((didUnloadWindow) -> didUnloadWindow) - app.quit() if didUnloadAllWindows - resolveBeforeQuitPromise() - ) - - @disposable.add ipcHelpers.on app, 'will-quit', => - @killAllProcesses() - @deleteSocketFile() - - @disposable.add ipcHelpers.on app, 'open-file', (event, pathToOpen) => - event.preventDefault() - @openPath({pathToOpen}) - - @disposable.add ipcHelpers.on app, 'open-url', (event, urlToOpen) => - event.preventDefault() - @openUrl({urlToOpen, @devMode, @safeMode}) - - @disposable.add ipcHelpers.on app, 'activate', (event, hasVisibleWindows) => - unless hasVisibleWindows - event?.preventDefault() - @emit('application:new-window') - - @disposable.add ipcHelpers.on ipcMain, 'restart-application', => - @restart() - - @disposable.add ipcHelpers.on ipcMain, 'resolve-proxy', (event, requestId, url) -> - event.sender.session.resolveProxy url, (proxy) -> - unless event.sender.isDestroyed() - event.sender.send('did-resolve-proxy', requestId, proxy) - - @disposable.add ipcHelpers.on ipcMain, 'did-change-history-manager', (event) => - for atomWindow in @getAllWindows() - webContents = atomWindow.browserWindow.webContents - if webContents isnt event.sender - webContents.send('did-change-history-manager') - - # A request from the associated render process to open a new render process. - @disposable.add ipcHelpers.on ipcMain, 'open', (event, options) => - window = @atomWindowForEvent(event) - if options? - if typeof options.pathsToOpen is 'string' - options.pathsToOpen = [options.pathsToOpen] - if options.pathsToOpen?.length > 0 - options.window = window - @openPaths(options) - else - new AtomWindow(this, @fileRecoveryService, options) - else - @promptForPathToOpen('all', {window}) - - @disposable.add ipcHelpers.on ipcMain, 'update-application-menu', (event, template, keystrokesByCommand) => - win = BrowserWindow.fromWebContents(event.sender) - @applicationMenu?.update(win, template, keystrokesByCommand) - - @disposable.add ipcHelpers.on ipcMain, 'run-package-specs', (event, packageSpecPath) => - @runTests({resourcePath: @devResourcePath, pathsToOpen: [packageSpecPath], headless: false}) - - @disposable.add ipcHelpers.on ipcMain, 'run-benchmarks', (event, benchmarksPath) => - @runBenchmarks({resourcePath: @devResourcePath, pathsToOpen: [benchmarksPath], headless: false, test: false}) - - @disposable.add ipcHelpers.on ipcMain, 'command', (event, command) => - @emit(command) - - @disposable.add ipcHelpers.on ipcMain, 'open-command', (event, command, args...) => - defaultPath = args[0] if args.length > 0 - switch command - when 'application:open' then @promptForPathToOpen('all', getLoadSettings(), defaultPath) - when 'application:open-file' then @promptForPathToOpen('file', getLoadSettings(), defaultPath) - when 'application:open-folder' then @promptForPathToOpen('folder', getLoadSettings(), defaultPath) - else console.log "Invalid open-command received: " + command - - @disposable.add ipcHelpers.on ipcMain, 'window-command', (event, command, args...) -> - win = BrowserWindow.fromWebContents(event.sender) - win.emit(command, args...) - - @disposable.add ipcHelpers.respondTo 'window-method', (browserWindow, method, args...) => - @atomWindowForBrowserWindow(browserWindow)?[method](args...) - - @disposable.add ipcHelpers.on ipcMain, 'pick-folder', (event, responseChannel) => - @promptForPath "folder", (selectedPaths) -> - event.sender.send(responseChannel, selectedPaths) - - @disposable.add ipcHelpers.respondTo 'set-window-size', (win, width, height) -> - win.setSize(width, height) - - @disposable.add ipcHelpers.respondTo 'set-window-position', (win, x, y) -> - win.setPosition(x, y) - - @disposable.add ipcHelpers.respondTo 'center-window', (win) -> - win.center() - - @disposable.add ipcHelpers.respondTo 'focus-window', (win) -> - win.focus() - - @disposable.add ipcHelpers.respondTo 'show-window', (win) -> - win.show() - - @disposable.add ipcHelpers.respondTo 'hide-window', (win) -> - win.hide() - - @disposable.add ipcHelpers.respondTo 'get-temporary-window-state', (win) -> - win.temporaryState - - @disposable.add ipcHelpers.respondTo 'set-temporary-window-state', (win, state) -> - win.temporaryState = state - - clipboard = require '../safe-clipboard' - @disposable.add ipcHelpers.on ipcMain, 'write-text-to-selection-clipboard', (event, selectedText) -> - clipboard.writeText(selectedText, 'selection') - - @disposable.add ipcHelpers.on ipcMain, 'write-to-stdout', (event, output) -> - process.stdout.write(output) - - @disposable.add ipcHelpers.on ipcMain, 'write-to-stderr', (event, output) -> - process.stderr.write(output) - - @disposable.add ipcHelpers.on ipcMain, 'add-recent-document', (event, filename) -> - app.addRecentDocument(filename) - - @disposable.add ipcHelpers.on ipcMain, 'execute-javascript-in-dev-tools', (event, code) -> - event.sender.devToolsWebContents?.executeJavaScript(code) - - @disposable.add ipcHelpers.on ipcMain, 'get-auto-update-manager-state', (event) => - event.returnValue = @autoUpdateManager.getState() - - @disposable.add ipcHelpers.on ipcMain, 'get-auto-update-manager-error', (event) => - event.returnValue = @autoUpdateManager.getErrorMessage() - - @disposable.add ipcHelpers.on ipcMain, 'will-save-path', (event, path) => - @fileRecoveryService.willSavePath(@atomWindowForEvent(event), path) - event.returnValue = true - - @disposable.add ipcHelpers.on ipcMain, 'did-save-path', (event, path) => - @fileRecoveryService.didSavePath(@atomWindowForEvent(event), path) - event.returnValue = true - - @disposable.add ipcHelpers.on ipcMain, 'did-change-paths', => - @saveState(false) - - @disposable.add(@disableZoomOnDisplayChange()) - - setupDockMenu: -> - if process.platform is 'darwin' - dockMenu = Menu.buildFromTemplate [ - {label: 'New Window', click: => @emit('application:new-window')} - ] - app.dock.setMenu dockMenu - - # 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...) -> - unless @emit(command, args...) - focusedWindow = @focusedWindow() - if focusedWindow? - focusedWindow.sendCommand(command, args...) - else - @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...) -> - unless @emit(command, args...) - if atomWindow? - atomWindow.sendCommand(command, args...) - else - @sendCommandToFirstResponder(command) - - # Translates the command into macOS action and sends it to application's first - # responder. - sendCommandToFirstResponder: (command) -> - return false unless process.platform is 'darwin' - - switch command - when 'core:undo' then Menu.sendActionToFirstResponder('undo:') - when 'core:redo' then Menu.sendActionToFirstResponder('redo:') - when 'core:copy' then Menu.sendActionToFirstResponder('copy:') - when 'core:cut' then Menu.sendActionToFirstResponder('cut:') - when 'core:paste' then Menu.sendActionToFirstResponder('paste:') - when 'core:select-all' then Menu.sendActionToFirstResponder('selectAll:') - else return false - 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) -> - @on eventName, -> - if window = @focusedWindow() - window.openPath(pathToOpen) - else - @openPath({pathToOpen}) - - # Returns the {AtomWindow} for the given paths. - windowForPaths: (pathsToOpen, devMode) -> - _.find @getAllWindows(), (atomWindow) -> - atomWindow.devMode is devMode and atomWindow.containsPaths(pathsToOpen) - - # Returns the {AtomWindow} for the given ipcMain event. - atomWindowForEvent: ({sender}) -> - @atomWindowForBrowserWindow(BrowserWindow.fromWebContents(sender)) - - atomWindowForBrowserWindow: (browserWindow) -> - @getAllWindows().find((atomWindow) -> atomWindow.browserWindow is browserWindow) - - # Public: Returns the currently focused {AtomWindow} or undefined if none. - focusedWindow: -> - _.find @getAllWindows(), (atomWindow) -> atomWindow.isFocused() - - # Get the platform-specific window offset for new windows. - getWindowOffsetForCurrentPlatform: -> - offsetByPlatform = - darwin: 22 - win32: 26 - offsetByPlatform[process.platform] ? 0 - - # Get the dimensions for opening a new window by cascading as appropriate to - # the platform. - getDimensionsForNewWindow: -> - return if (@focusedWindow() ? @getLastFocusedWindow())?.isMaximized() - dimensions = (@focusedWindow() ? @getLastFocusedWindow())?.getDimensions() - offset = @getWindowOffsetForCurrentPlatform() - if dimensions? and offset? - dimensions.x += offset - dimensions.y += offset - 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: ({initialPaths, pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow, env} = {}) -> - @openPaths({initialPaths, 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 - # :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. - openPaths: ({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window, clearWindowState, addToLastWindow, env}={}) -> - if not pathsToOpen? or pathsToOpen.length is 0 - return - env = process.env unless env? - devMode = Boolean(devMode) - safeMode = Boolean(safeMode) - clearWindowState = Boolean(clearWindowState) - locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom, addToLastWindow) for pathToOpen in pathsToOpen) - pathsToOpen = (locationToOpen.pathToOpen for locationToOpen in locationsToOpen) - - unless pidToKillWhenClosed or newWindow - existingWindow = @windowForPaths(pathsToOpen, devMode) - stats = (fs.statSyncNoException(pathToOpen) for pathToOpen in pathsToOpen) - unless existingWindow? - if currentWindow = window ? @getLastFocusedWindow() - existingWindow = currentWindow if ( - addToLastWindow or - currentWindow.devMode is devMode and - ( - stats.every((stat) -> stat.isFile?()) or - stats.some((stat) -> stat.isDirectory?() and not currentWindow.hasProjectPath()) - ) - ) - - if existingWindow? - openedWindow = existingWindow - openedWindow.openLocations(locationsToOpen) - if openedWindow.isMinimized() - openedWindow.restore() - else - openedWindow.focus() - openedWindow.replaceEnvironment(env) - else - if devMode - try - windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window')) - resourcePath = @devResourcePath - - windowInitializationScript ?= require.resolve('../initialize-application-window') - resourcePath ?= @resourcePath - windowDimensions ?= @getDimensionsForNewWindow() - openedWindow = new AtomWindow(this, @fileRecoveryService, {initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env}) - openedWindow.focus() - @windowStack.addWindow(openedWindow) - - if pidToKillWhenClosed? - @pidsToOpenWindows[pidToKillWhenClosed] = openedWindow - - openedWindow.browserWindow.once 'closed', => - @killProcessForWindow(openedWindow) - - openedWindow - - # Kill all processes associated with opened windows. - killAllProcesses: -> - @killProcess(pid) for pid of @pidsToOpenWindows - return - - # Kill process associated with the given opened window. - killProcessForWindow: (openedWindow) -> - for pid, trackedWindow of @pidsToOpenWindows - @killProcess(pid) if trackedWindow is openedWindow - return - - # Kill the process with the given pid. - killProcess: (pid) -> - try - parsedPid = parseInt(pid) - process.kill(parsedPid) if isFinite(parsedPid) - catch error - if error.code isnt 'ESRCH' - console.log("Killing process #{pid} failed: #{error.code ? error.message}") - delete @pidsToOpenWindows[pid] - - saveState: (allowEmpty=false) -> - return if @quitting - states = [] - for window in @getAllWindows() - unless window.isSpec - states.push({initialPaths: window.representedDirectoryPaths}) - states.reverse() - if states.length > 0 or allowEmpty - @storageFolder.storeSync('application.json', states) - @emit('application:did-save-state') - - loadState: (options) -> - if (@config.get('core.restorePreviousWindowsOnStart') in ['yes', 'always']) and (states = @storageFolder.load('application.json'))?.length > 0 - for state in states - @openWithOptions(Object.assign(options, { - initialPaths: state.initialPaths - pathsToOpen: state.initialPaths.filter (directoryPath) -> fs.isDirectorySync(directoryPath) - urlsToOpen: [] - devMode: @devMode - safeMode: @safeMode - })) - else - null - - # 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}) -> - parsedUrl = url.parse(urlToOpen, true) - return unless parsedUrl.protocol is "atom:" - - pack = @findPackageWithName(parsedUrl.host, devMode) - if pack?.urlMain - @openPackageUrlMain(parsedUrl.host, pack.urlMain, urlToOpen, devMode, safeMode, env) - else - @openPackageUriHandler(urlToOpen, parsedUrl, devMode, safeMode, env) - - openPackageUriHandler: (url, parsedUrl, devMode, safeMode, env) -> - bestWindow = null - if parsedUrl.host is 'core' - predicate = require('../core-uri-handlers').windowPredicate(parsedUrl) - bestWindow = @getLastFocusedWindow (win) -> - not win.isSpecWindow() and predicate(win) - - bestWindow ?= @getLastFocusedWindow (win) -> not win.isSpecWindow() - if bestWindow? - bestWindow.sendURIMessage url - bestWindow.focus() - else - resourcePath = @resourcePath - if devMode - try - windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window')) - resourcePath = @devResourcePath - - windowInitializationScript ?= require.resolve('../initialize-application-window') - windowDimensions = @getDimensionsForNewWindow() - win = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env}) - @windowStack.addWindow(win) - win.on 'window:loaded', -> - win.sendURIMessage url - - findPackageWithName: (packageName, devMode) -> - _.find @getPackageManager(devMode).getAvailablePackageMetadata(), ({name}) -> name is packageName - - openPackageUrlMain: (packageName, packageUrlMain, urlToOpen, devMode, safeMode, env) -> - packagePath = @getPackageManager(devMode).resolvePackagePath(packageName) - windowInitializationScript = path.resolve(packagePath, packageUrlMain) - windowDimensions = @getDimensionsForNewWindow() - new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env}) - - getPackageManager: (devMode) -> - unless @packages? - PackageManager = require '../package-manager' - @packages = new PackageManager({}) - @packages.initialize - configDirPath: process.env.ATOM_HOME - devMode: devMode - resourcePath: @resourcePath - - @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}) -> - if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath) - resourcePath = @resourcePath - - timeoutInSeconds = Number.parseFloat(timeout) - unless Number.isNaN(timeoutInSeconds) - timeoutHandler = -> - console.log "The test suite has timed out because it has been running for more than #{timeoutInSeconds} seconds." - process.exit(124) # Use the same exit code as the UNIX timeout util. - setTimeout(timeoutHandler, timeoutInSeconds * 1000) - - try - windowInitializationScript = require.resolve(path.resolve(@devResourcePath, 'src', 'initialize-test-window')) - catch error - windowInitializationScript = require.resolve(path.resolve(__dirname, '..', '..', 'src', 'initialize-test-window')) - - testPaths = [] - if pathsToOpen? - for pathToOpen in pathsToOpen - testPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen))) - - if testPaths.length is 0 - process.stderr.write 'Error: Specify at least one test path\n\n' - process.exit(1) - - legacyTestRunnerPath = @resolveLegacyTestRunnerPath() - testRunnerPath = @resolveTestRunnerPath(testPaths[0]) - devMode = true - isSpec = true - safeMode ?= false - new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode, env}) - - runBenchmarks: ({headless, test, resourcePath, executedFrom, pathsToOpen, env}) -> - if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath) - resourcePath = @resourcePath - - try - windowInitializationScript = require.resolve(path.resolve(@devResourcePath, 'src', 'initialize-benchmark-window')) - catch error - windowInitializationScript = require.resolve(path.resolve(__dirname, '..', '..', 'src', 'initialize-benchmark-window')) - - benchmarkPaths = [] - if pathsToOpen? - for pathToOpen in pathsToOpen - benchmarkPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen))) - - if benchmarkPaths.length is 0 - process.stderr.write 'Error: Specify at least one benchmark path.\n\n' - process.exit(1) - - devMode = true - isSpec = true - safeMode = false - new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, resourcePath, headless, test, isSpec, devMode, benchmarkPaths, safeMode, env}) - - resolveTestRunnerPath: (testPath) -> - FindParentDir ?= require 'find-parent-dir' - - if packageRoot = FindParentDir.sync(testPath, 'package.json') - packageMetadata = require(path.join(packageRoot, 'package.json')) - if packageMetadata.atomTestRunner - 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) - - @resolveLegacyTestRunnerPath() - - resolveLegacyTestRunnerPath: -> - try - require.resolve(path.resolve(@devResourcePath, 'spec', 'jasmine-test-runner')) - catch error - require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner')) - - locationForPathToOpen: (pathToOpen, executedFrom='', forceAddToWindow) -> - return {pathToOpen} unless pathToOpen - - pathToOpen = pathToOpen.replace(/[:\s]+$/, '') - match = pathToOpen.match(LocationSuffixRegExp) - - if match? - pathToOpen = pathToOpen.slice(0, -match[0].length) - initialLine = Math.max(0, parseInt(match[1].slice(1)) - 1) if match[1] - initialColumn = Math.max(0, parseInt(match[2].slice(1)) - 1) if match[2] - else - initialLine = initialColumn = null - - unless url.parse(pathToOpen).protocol? - pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen)) - - {pathToOpen, initialLine, initialColumn, forceAddToWindow} - - # 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 a selected file path. - # :path - An optional String which controls the default path to which the - # file dialog opens. - promptForPathToOpen: (type, {devMode, safeMode, window}, path=null) -> - @promptForPath type, ((pathsToOpen) => - @openPaths({pathsToOpen, devMode, safeMode, window})), path - - promptForPath: (type, callback, path) -> - properties = - switch type - when 'file' then ['openFile'] - when 'folder' then ['openDirectory'] - when 'all' then ['openFile', 'openDirectory'] - else throw new Error("#{type} is an invalid type for promptForPath") - - # Show the open dialog as child window on Windows and Linux, and as - # independent dialog on macOS. This matches most native apps. - parentWindow = - if process.platform is 'darwin' - null - else - BrowserWindow.getFocusedWindow() - - openOptions = - properties: properties.concat(['multiSelections', 'createDirectory']) - title: switch type - when 'file' then 'Open File' - when 'folder' then 'Open Folder' - else 'Open' - - # File dialog defaults to project directory of currently active editor - if path? - openOptions.defaultPath = path - - dialog.showOpenDialog(parentWindow, openOptions, callback) - - promptForRestart: -> - chosen = 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 chosen is 0 - @restart() - - restart: -> - args = [] - args.push("--safe") if @safeMode - args.push("--log-file=#{@logFile}") if @logFile? - args.push("--socket-path=#{@socketPath}") if @socketPath? - args.push("--user-data-dir=#{@userDataDir}") if @userDataDir? - if @devMode - args.push('--dev') - args.push("--resource-path=#{@resourcePath}") - app.relaunch({args}) - app.quit() - - disableZoomOnDisplayChange: -> - outerCallback = => - for window in @getAllWindows() - 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', outerCallback) - screen.on('display-removed', outerCallback) - new Disposable -> - screen.removeListener('display-added', outerCallback) - screen.removeListener('display-removed', outerCallback) - -class WindowStack - constructor: (@windows = []) -> - - addWindow: (window) => - @removeWindow(window) - @windows.unshift(window) - - touch: (window) => - @addWindow(window) - - removeWindow: (window) => - currentIndex = @windows.indexOf(window) - @windows.splice(currentIndex, 1) if currentIndex > -1 - - getLastFocusedWindow: (predicate) => - predicate ?= (win) -> true - @windows.find(predicate) - - all: => - @windows diff --git a/src/main-process/atom-application.js b/src/main-process/atom-application.js new file mode 100644 index 000000000..1797d5090 --- /dev/null +++ b/src/main-process/atom-application.js @@ -0,0 +1,1350 @@ +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 FileRecoveryService = require('./file-recovery-service') +const ipcHelpers = require('../ipc-helpers') +const {BrowserWindow, Menu, app, 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 {EventEmitter} = require('events') +const _ = require('underscore-plus') +let FindParentDir = null +let Resolve = null +const ConfigSchema = require('../config-schema') + +const LocationSuffixRegExp = /(:\d+)(:\d+)?$/ + +// 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) { + if (!options.socketPath) { + const username = process.platform === 'win32' ? process.env.USERNAME : process.env.USER + + // Lowercasing the ATOM_HOME to make sure that we don't get multiple sockets + // on case-insensitive filesystems due to arbitrary case differences in paths. + const atomHomeUnique = path.resolve(process.env.ATOM_HOME).toLowerCase() + const hash = crypto + .createHash('sha1') + .update(options.version) + .update('|') + .update(process.arch) + .update('|') + .update(username) + .update('|') + .update(atomHomeUnique) + + // We only keep the first 12 characters of the hash as not to have excessively long + // socket file. Note that macOS/BSD limit the length of socket file paths (see #15081). + // The replace calls convert the digest into "URL and Filename Safe" encoding (see RFC 4648). + const atomInstanceDigest = hash + .digest('base64') + .substring(0, 12) + .replace(/\+/g, '-') + .replace(/\//g, '_') + + if (process.platform === 'win32') { + options.socketPath = `\\\\.\\pipe\\atom-${atomInstanceDigest}-sock` + } else { + options.socketPath = path.join(os.tmpdir(), `atom-${atomInstanceDigest}.sock`) + } + } + + // 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 ((process.platform !== 'win32' && !fs.existsSync(options.socketPath)) || + options.test || options.benchmark || options.benchmarkTest) { + new AtomApplication(options).initialize(options) + return + } + + const client = net.connect({path: options.socketPath}, () => { + client.write(JSON.stringify(options), () => { + client.end() + app.quit() + }) + }) + + client.on('error', () => new AtomApplication(options).initialize(options)) + } + + exit (status) { + app.exit(status) + } + + constructor (options) { + super() + this.quitting = 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.socketPath = options.socketPath + this.logFile = options.logFile + this.userDataDir = options.userDataDir + if (options.test || options.benchmark || options.benchmarkTest) this.socketPath = null + + this.pidsToOpenWindows = {} + this.windowStack = new WindowStack() + + this.config = new Config({enablePersistence: true}) + this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)}) + ConfigSchema.projectHome = { + type: 'string', + default: path.join(fs.getHomeDirectory(), 'github'), + description: + 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.' + } + this.config.initialize({ + configDirPath: process.env.ATOM_HOME, + resourcePath: this.resourcePath, + projectHomeSchema: ConfigSchema.projectHome + }) + this.config.load() + + 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() + } + + // 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. + initialize (options) { + 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.config.onDidChange('core.titleBar', this.promptForRestart.bind(this)) + + process.nextTick(() => this.autoUpdateManager.initialize()) + this.applicationMenu = new ApplicationMenu(this.version, this.autoUpdateManager) + this.atomProtocolHandler = new AtomProtocolHandler(this.resourcePath, this.safeMode) + + this.listenForArgumentsFromNewProcess() + this.setupDockMenu() + + return this.launch(options) + } + + async destroy () { + const windowsClosePromises = this.getAllWindows().map(window => { + window.close() + return window.closedPromise + }) + await Promise.all(windowsClosePromises) + this.disposable.dispose() + } + + launch (options) { + if (options.test || options.benchmark || options.benchmarkTest) { + return this.openWithOptions(options) + } else if ((options.pathsToOpen && options.pathsToOpen.length > 0) || + (options.urlsToOpen && options.urlsToOpen.length > 0)) { + if (this.config.get('core.restorePreviousWindowsOnStart') === 'always') { + this.loadState(_.deepClone(options)) + } + return this.openWithOptions(options) + } else { + return this.loadState(options) || this.openPath(options) + } + } + + openWithOptions (options) { + const { + initialPaths, + pathsToOpen, + executedFrom, + urlsToOpen, + benchmark, + benchmarkTest, + test, + pidToKillWhenClosed, + devMode, + safeMode, + newWindow, + logFile, + profileStartup, + timeout, + clearWindowState, + addToLastWindow, + env + } = options + + 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.length > 0) { + return this.openPaths({ + initialPaths, + pathsToOpen, + executedFrom, + pidToKillWhenClosed, + newWindow, + devMode, + safeMode, + profileStartup, + clearWindowState, + addToLastWindow, + env + }) + } else if (urlsToOpen.length > 0) { + return urlsToOpen.map(urlToOpen => this.openUrl({urlToOpen, devMode, safeMode, env})) + } else { + // Always open a editor window if this is the first instance of Atom. + return this.openPath({ + initialPaths, + pidToKillWhenClosed, + newWindow, + devMode, + safeMode, + profileStartup, + clearWindowState, + addToLastWindow, + env + }) + } + } + + // 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.saveState(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.saveState(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) + } + } + + 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. + listenForArgumentsFromNewProcess () { + if (!this.socketPath) return + + this.deleteSocketFile() + const server = net.createServer(connection => { + let data = '' + connection.on('data', chunk => { data += chunk }) + connection.on('end', () => this.openWithOptions(JSON.parse(data))) + }) + + server.listen(this.socketPath) + server.on('error', error => console.error('Application server failed', error)) + } + + deleteSocketFile () { + if (process.platform === 'win32' || !this.socketPath) return + + 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 + } + } + } + + // Registers basic application commands, non-idempotent. + handleEvents () { + const getLoadSettings = () => { + const window = this.focusedWindow() + return {devMode: window && window.devMode, safeMode: window && window.safeMode} + } + + this.on('application:quit', () => app.quit()) + this.on('application:new-window', () => this.openPath(getLoadSettings())) + 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.autoUpdateManager.install() + }) + + this.on('application:check-for-update', () => this.autoUpdateManager.check()) + + if (process.platform === 'darwin') { + 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.disposable.add(ipcHelpers.on(app, 'before-quit', event => { + let resolveBeforeQuitPromise + this.lastBeforeQuitPromise = new Promise(resolve => { + resolveBeforeQuitPromise = resolve + }) + + if (this.quitting) return resolveBeforeQuitPromise() + + this.quitting = true + event.preventDefault() + const windowUnloadPromises = this.getAllWindows().map(window => window.prepareToUnload()) + return Promise.all(windowUnloadPromises).then(windowUnloadedResults => { + const didUnloadAllWindows = windowUnloadedResults.every(Boolean) + if (didUnloadAllWindows) app.quit() + resolveBeforeQuitPromise() + }) + })) + + this.disposable.add(ipcHelpers.on(app, 'will-quit', () => { + this.killAllProcesses() + this.deleteSocketFile() + })) + + 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 new render process. + this.disposable.add(ipcHelpers.on(ipcMain, 'open', (event, options) => { + const window = this.atomWindowForEvent(event) + if (options) { + if (typeof options.pathsToOpen === 'string') { + options.pathsToOpen = [options.pathsToOpen] + } + + if (options.pathsToOpen && options.pathsToOpen.length > 0) { + options.window = window + this.openPaths(options) + } else { + new AtomWindow(this, this.fileRecoveryService, options) + } + } else { + this.promptForPathToOpen('all', {window}) + } + })) + + 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) => { + this.runTests({ + resourcePath: this.devResourcePath, + pathsToOpen: [packageSpecPath], + headless: false + }) + })) + + 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, 'open-command', (event, command, defaultPath) => { + switch (command) { + case 'application:open': + return this.promptForPathToOpen('all', getLoadSettings(), defaultPath) + case 'application:open-file': + return this.promptForPathToOpen('file', getLoadSettings(), defaultPath) + case 'application:open-folder': + return this.promptForPathToOpen('folder', getLoadSettings(), defaultPath) + default: + return console.log(`Invalid open-command received: ${command}`) + } + })) + + this.disposable.add(ipcHelpers.on(ipcMain, 'window-command', (event, command, ...args) => { + const window = BrowserWindow.fromWebContents(event.sender) + return 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('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 + })) + + const clipboard = require('../safe-clipboard') + 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.on(ipcMain, 'will-save-path', (event, path) => { + this.fileRecoveryService.willSavePath(this.atomWindowForEvent(event), path) + event.returnValue = true + })) + + this.disposable.add(ipcHelpers.on(ipcMain, 'did-save-path', (event, path) => { + this.fileRecoveryService.didSavePath(this.atomWindowForEvent(event), path) + event.returnValue = true + })) + + this.disposable.add(ipcHelpers.on(ipcMain, 'did-change-paths', () => + this.saveState(false) + )) + + 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')} + ])) + } + } + + // 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 paths. + windowForPaths (pathsToOpen, devMode) { + return this.getAllWindows().find(window => + window.devMode === devMode && window.containsPaths(pathsToOpen) + ) + } + + // 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 ({ + initialPaths, + pathToOpen, + pidToKillWhenClosed, + newWindow, + devMode, + safeMode, + profileStartup, + window, + clearWindowState, + addToLastWindow, + env + } = {}) { + return this.openPaths({ + initialPaths, + 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 + // :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. + openPaths ({ + initialPaths, + pathsToOpen, + executedFrom, + pidToKillWhenClosed, + newWindow, + devMode, + safeMode, + windowDimensions, + profileStartup, + window, + clearWindowState, + addToLastWindow, + env + } = {}) { + if (!pathsToOpen || pathsToOpen.length === 0) return + if (!env) env = process.env + devMode = Boolean(devMode) + safeMode = Boolean(safeMode) + clearWindowState = Boolean(clearWindowState) + + const locationsToOpen = pathsToOpen.map(pathToOpen => + this.locationForPathToOpen(pathToOpen, executedFrom, addToLastWindow) + ) + pathsToOpen = locationsToOpen.map(locationToOpen => locationToOpen.pathToOpen) + + let existingWindow + if (!pidToKillWhenClosed && !newWindow) { + existingWindow = this.windowForPaths(pathsToOpen, devMode) + const stats = pathsToOpen.map(pathToOpen => fs.statSyncNoException(pathToOpen)) + if (!existingWindow) { + let lastWindow = window || this.getLastFocusedWindow() + if (lastWindow && lastWindow.devMode === devMode) { + if (addToLastWindow || ( + stats.every(s => s.isFile && s.isFile()) || + (stats.some(s => s.isDirectory && s.isDirectory()) && !lastWindow.hasProjectPath()))) { + existingWindow = lastWindow + } + } + } + } + + let openedWindow + if (existingWindow) { + openedWindow = existingWindow + 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() + openedWindow = new AtomWindow(this, this.fileRecoveryService, { + initialPaths, + locationsToOpen, + windowInitializationScript, + resourcePath, + devMode, + safeMode, + windowDimensions, + profileStartup, + clearWindowState, + env + }) + openedWindow.focus() + this.windowStack.addWindow(openedWindow) + } + + if (pidToKillWhenClosed != null) { + this.pidsToOpenWindows[pidToKillWhenClosed] = openedWindow + } + + openedWindow.browserWindow.once('closed', () => this.killProcessForWindow(openedWindow)) + return openedWindow + } + + // Kill all processes associated with opened windows. + killAllProcesses () { + for (let pid in this.pidsToOpenWindows) { + this.killProcess(pid) + } + } + + // Kill process associated with the given opened window. + killProcessForWindow (openedWindow) { + for (let pid in this.pidsToOpenWindows) { + const trackedWindow = this.pidsToOpenWindows[pid] + if (trackedWindow === openedWindow) { + this.killProcess(pid) + } + } + } + + // Kill the process with the given pid. + killProcess (pid) { + try { + const parsedPid = parseInt(pid) + if (isFinite(parsedPid)) process.kill(parsedPid) + } catch (error) { + if (error.code !== 'ESRCH') { + console.log(`Killing process ${pid} failed: ${error.code != null ? error.code : error.message}`) + } + } + delete this.pidsToOpenWindows[pid] + } + + saveState (allowEmpty = false) { + if (this.quitting) return + + const states = [] + for (let window of this.getAllWindows()) { + if (!window.isSpec) states.push({initialPaths: window.representedDirectoryPaths}) + } + states.reverse() + + if (states.length > 0 || allowEmpty) { + this.storageFolder.storeSync('application.json', states) + this.emit('application:did-save-state') + } + } + + loadState (options) { + const states = this.storageFolder.load('application.json') + if ( + ['yes', 'always'].includes(this.config.get('core.restorePreviousWindowsOnStart')) && + states && states.length > 0 + ) { + return states.map(state => + this.openWithOptions(Object.assign(options, { + initialPaths: state.initialPaths, + pathsToOpen: state.initialPaths.filter(p => fs.isDirectorySync(p)), + urlsToOpen: [], + devMode: this.devMode, + safeMode: this.safeMode + })) + ) + } else { + return null + } + } + + // 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() + } 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 = new AtomWindow(this, this.fileRecoveryService, { + resourcePath, + windowInitializationScript, + devMode, + safeMode, + windowDimensions, + env + }) + this.windowStack.addWindow(window) + window.on('window:loaded', () => window.sendURIMessage(url)) + } + } + + 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() + return new AtomWindow(this, this.fileRecoveryService, { + windowInitializationScript, + resourcePath: this.resourcePath, + devMode, + safeMode, + urlToOpen, + windowDimensions, + env + }) + } + + 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 + } + return new AtomWindow(this, this.fileRecoveryService, { + windowInitializationScript, + resourcePath, + headless, + isSpec, + devMode, + testRunnerPath, + legacyTestRunnerPath, + testPaths, + logFile, + safeMode, + env + }) + } + + 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 + return new AtomWindow(this, this.fileRecoveryService, { + windowInitializationScript, + resourcePath, + headless, + test, + isSpec, + devMode, + benchmarkPaths, + safeMode, + env + }) + } + + 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')) + } + } + + locationForPathToOpen (pathToOpen, executedFrom = '', forceAddToWindow) { + let initialColumn, initialLine + if (!pathToOpen) { + return {pathToOpen} + } + + pathToOpen = pathToOpen.replace(/[:\s]+$/, '') + const match = pathToOpen.match(LocationSuffixRegExp) + + if (match != null) { + pathToOpen = pathToOpen.slice(0, -match[0].length) + if (match[1]) { + initialLine = Math.max(0, parseInt(match[1].slice(1)) - 1) + } + if (match[2]) { + initialColumn = Math.max(0, parseInt(match[2].slice(1)) - 1) + } + } else { + initialLine = initialColumn = null + } + + if (url.parse(pathToOpen).protocol == null) { + pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen)) + } + + return {pathToOpen, initialLine, initialColumn, forceAddToWindow} + } + + // 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 a selected file path. + // :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, + pathsToOpen => { + return this.openPaths({pathsToOpen, devMode, safeMode, window}) + }, + 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 + // 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 + return dialog.showOpenDialog(parentWindow, openOptions, callback) + } + + promptForRestart () { + const chosen = 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 (chosen === 0) return this.restart() + } + + restart () { + const args = [] + if (this.safeMode) args.push('--safe') + if (this.logFile != null) args.push(`--log-file=${this.logFile}`) + if (this.socketPath != null) args.push(`--socket-path=${this.socketPath}`) + 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 + } +}