diff --git a/spec/async-spec-helpers.js b/spec/async-spec-helpers.js index 56550cd9f..73002c049 100644 --- a/spec/async-spec-helpers.js +++ b/spec/async-spec-helpers.js @@ -34,7 +34,7 @@ export function afterEach (fn) { } }) -export async function conditionPromise (condition) { +export async function conditionPromise (condition, description = 'anonymous condition') { const startTime = Date.now() while (true) { @@ -45,7 +45,7 @@ export async function conditionPromise (condition) { } if (Date.now() - startTime > 5000) { - throw new Error('Timed out waiting on condition') + throw new Error('Timed out waiting on ' + description) } } } diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 62fae82b3..01d052b96 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -5,6 +5,7 @@ import dedent from 'dedent' import electron from 'electron' import fs from 'fs-plus' import path from 'path' +import sinon from 'sinon' import AtomApplication from '../../src/main-process/atom-application' import parseCommandLine from '../../src/main-process/parse-command-line' import {timeoutPromise, conditionPromise, emitterEventPromise} from '../async-spec-helpers' @@ -137,7 +138,7 @@ describe('AtomApplication', function () { // Does not change the project paths when doing so. const reusedWindow = atomApplication.launch(parseCommandLine([existingDirCFilePath])) assert.equal(reusedWindow, window1) - assert.deepEqual(atomApplication.windows, [window1]) + assert.deepEqual(atomApplication.getAllWindows(), [window1]) activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { const subscription = atom.workspace.onDidChangeActivePaneItem(function (textEditor) { sendBackToMainProcess(textEditor.getPath()) @@ -177,7 +178,7 @@ describe('AtomApplication', function () { // parent directory to the project let reusedWindow = atomApplication.launch(parseCommandLine([existingDirCFilePath, '--add'])) assert.equal(reusedWindow, window1) - assert.deepEqual(atomApplication.windows, [window1]) + assert.deepEqual(atomApplication.getAllWindows(), [window1]) activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { const subscription = atom.workspace.onDidChangeActivePaneItem(function (textEditor) { sendBackToMainProcess(textEditor.getPath()) @@ -191,7 +192,7 @@ describe('AtomApplication', function () { // the directory to the project reusedWindow = atomApplication.launch(parseCommandLine([dirBPath, '-a'])) assert.equal(reusedWindow, window1) - assert.deepEqual(atomApplication.windows, [window1]) + assert.deepEqual(atomApplication.getAllWindows(), [window1]) await conditionPromise(async () => (await getTreeViewRootDirectories(reusedWindow)).length === 3) assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath, dirCPath, dirBPath]) @@ -276,7 +277,7 @@ describe('AtomApplication', function () { }) assert.equal(window2EditorTitle, 'untitled') - assert.deepEqual(atomApplication.windows, [window1, window2]) + assert.deepEqual(atomApplication.getAllWindows(), [window2, window1]) }) it('does not open an empty editor when opened with no path if the core.openEmptyEditorOnStart config setting is false', async function () { @@ -461,6 +462,31 @@ describe('AtomApplication', function () { assert.equal(reached, true); windows[0].close(); }) + + it('triggers /core/open/file in the correct window', async function() { + const dirAPath = makeTempDir('a') + const dirBPath = makeTempDir('b') + + const atomApplication = buildAtomApplication() + const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath)])) + await focusWindow(window1) + const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath)])) + await focusWindow(window2) + + const fileA = path.join(dirAPath, 'file-a') + const uriA = `atom://core/open/file?filename=${fileA}` + const fileB = path.join(dirBPath, 'file-b') + const uriB = `atom://core/open/file?filename=${fileB}` + + sinon.spy(window1, 'sendURIMessage') + sinon.spy(window2, 'sendURIMessage') + + atomApplication.launch(parseCommandLine(['--uri-handler', uriA])) + await conditionPromise(() => window1.sendURIMessage.calledWith(uriA), `window1 to be focused from ${fileA}`) + + atomApplication.launch(parseCommandLine(['--uri-handler', uriB])) + await conditionPromise(() => window2.sendURIMessage.calledWith(uriB), `window2 to be focused from ${fileB}`) + }) }) }) @@ -514,7 +540,7 @@ describe('AtomApplication', function () { async function focusWindow (window) { window.focus() await window.loadedPromise - await conditionPromise(() => window.atomApplication.lastFocusedWindow === window) + await conditionPromise(() => window.atomApplication.getLastFocusedWindow() === window) } function mockElectronAppQuit () { diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index af61ffb36..50b5d541e 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -32,6 +32,7 @@ ThemeManager = require './theme-manager' MenuManager = require './menu-manager' ContextMenuManager = require './context-menu-manager' CommandInstaller = require './command-installer' +CoreURIHandlers = require './core-uri-handlers' ProtocolHandlerInstaller = require './protocol-handler-installer' Project = require './project' TitleBar = require './title-bar' @@ -237,6 +238,7 @@ class AtomEnvironment extends Model @commandInstaller.initialize(@getVersion()) @protocolHandlerInstaller.initialize(@config, @notifications) + @uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(this)) @autoUpdater.initialize() @config.load() diff --git a/src/core-uri-handlers.js b/src/core-uri-handlers.js new file mode 100644 index 000000000..2af00f610 --- /dev/null +++ b/src/core-uri-handlers.js @@ -0,0 +1,38 @@ +function openFile (atom, {query}) { + const {filename, line, column} = query + + atom.workspace.open(filename, { + initialLine: parseInt(line || 0, 10), + initialColumn: parseInt(column || 0, 10), + searchAllPanes: true + }) +} + +function windowShouldOpenFile ({query}) { + const {filename} = query + return (win) => win.containsPath(filename) +} + +const ROUTER = { + '/open/file': { handler: openFile, getWindowPredicate: windowShouldOpenFile } +} + +module.exports = { + create (atomEnv) { + return function coreURIHandler (parsed) { + const config = ROUTER[parsed.pathname] + if (config) { + config.handler(atomEnv, parsed) + } + } + }, + + windowPredicate (parsed) { + const config = ROUTER[parsed.pathname] + if (config && config.getWindowPredicate) { + return config.getWindowPredicate(parsed) + } else { + return (win) => true + } + } +} diff --git a/src/main-process/application-menu.coffee b/src/main-process/application-menu.coffee index 681677603..35bc7d66c 100644 --- a/src/main-process/application-menu.coffee +++ b/src/main-process/application-menu.coffee @@ -128,7 +128,7 @@ class ApplicationMenu ] focusedWindow: -> - _.find global.atomApplication.windows, (atomWindow) -> atomWindow.isFocused() + _.find global.atomApplication.getAllWindows(), (atomWindow) -> atomWindow.isFocused() # Combines a menu template with the appropriate keystroke. # diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 0c587020e..f6802705e 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -67,7 +67,7 @@ class AtomApplication {@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, @logFile, @userDataDir} = options @socketPath = null if options.test or options.benchmark or options.benchmarkTest @pidsToOpenWindows = {} - @windows = [] + @windowStack = new WindowStack() @config = new Config({enablePersistence: true}) @config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)} @@ -114,7 +114,7 @@ class AtomApplication @launch(options) destroy: -> - windowsClosePromises = @windows.map (window) -> + windowsClosePromises = @getAllWindows().map (window) -> window.close() window.closedPromise Promise.all(windowsClosePromises).then(=> @disposable.dispose()) @@ -162,8 +162,8 @@ class AtomApplication # Public: Removes the {AtomWindow} from the global window list. removeWindow: (window) -> - @windows.splice(@windows.indexOf(window), 1) - if @windows.length is 0 + @windowStack.removeWindow(window) + if @getAllWindows().length is 0 @applicationMenu?.enableWindowSpecificItems(false) if process.platform in ['win32', 'linux'] app.quit() @@ -172,22 +172,28 @@ class AtomApplication # Public: Adds the {AtomWindow} to the global window list. addWindow: (window) -> - @windows.push window + @windowStack.addWindow(window) @applicationMenu?.addWindow(window.browserWindow) window.once 'window:loaded', => @autoUpdateManager?.emitUpdateAvailableEvent(window) unless window.isSpec - focusHandler = => @lastFocusedWindow = window + focusHandler = => @windowStack.touch(window) blurHandler = => @saveState(false) window.browserWindow.on 'focus', focusHandler window.browserWindow.on 'blur', blurHandler window.browserWindow.once 'closed', => - @lastFocusedWindow = null if window is @lastFocusedWindow + @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 @@ -276,7 +282,7 @@ class AtomApplication else event.preventDefault() @quitting = true - windowUnloadPromises = @windows.map((window) -> window.prepareToUnload()) + windowUnloadPromises = @getAllWindows().map((window) -> window.prepareToUnload()) Promise.all(windowUnloadPromises).then((windowUnloadedResults) -> didUnloadAllWindows = windowUnloadedResults.every((didUnloadWindow) -> didUnloadWindow) app.quit() if didUnloadAllWindows @@ -309,7 +315,7 @@ class AtomApplication event.sender.send('did-resolve-proxy', requestId, proxy) @disposable.add ipcHelpers.on ipcMain, 'did-change-history-manager', (event) => - for atomWindow in @windows + for atomWindow in @getAllWindows() webContents = atomWindow.browserWindow.webContents if webContents isnt event.sender webContents.send('did-change-history-manager') @@ -483,7 +489,7 @@ class AtomApplication # Returns the {AtomWindow} for the given paths. windowForPaths: (pathsToOpen, devMode) -> - _.find @windows, (atomWindow) -> + _.find @getAllWindows(), (atomWindow) -> atomWindow.devMode is devMode and atomWindow.containsPaths(pathsToOpen) # Returns the {AtomWindow} for the given ipcMain event. @@ -491,11 +497,11 @@ class AtomApplication @atomWindowForBrowserWindow(BrowserWindow.fromWebContents(sender)) atomWindowForBrowserWindow: (browserWindow) -> - @windows.find((atomWindow) -> atomWindow.browserWindow is browserWindow) + @getAllWindows().find((atomWindow) -> atomWindow.browserWindow is browserWindow) # Public: Returns the currently focused {AtomWindow} or undefined if none. focusedWindow: -> - _.find @windows, (atomWindow) -> atomWindow.isFocused() + _.find @getAllWindows(), (atomWindow) -> atomWindow.isFocused() # Get the platform-specific window offset for new windows. getWindowOffsetForCurrentPlatform: -> @@ -507,8 +513,8 @@ class AtomApplication # Get the dimensions for opening a new window by cascading as appropriate to # the platform. getDimensionsForNewWindow: -> - return if (@focusedWindow() ? @lastFocusedWindow)?.isMaximized() - dimensions = (@focusedWindow() ? @lastFocusedWindow)?.getDimensions() + return if (@focusedWindow() ? @getLastFocusedWindow())?.isMaximized() + dimensions = (@focusedWindow() ? @getLastFocusedWindow())?.getDimensions() offset = @getWindowOffsetForCurrentPlatform() if dimensions? and offset? dimensions.x += offset @@ -554,7 +560,7 @@ class AtomApplication existingWindow = @windowForPaths(pathsToOpen, devMode) stats = (fs.statSyncNoException(pathToOpen) for pathToOpen in pathsToOpen) unless existingWindow? - if currentWindow = window ? @lastFocusedWindow + if currentWindow = window ? @getLastFocusedWindow() existingWindow = currentWindow if ( addToLastWindow or currentWindow.devMode is devMode and @@ -583,7 +589,7 @@ class AtomApplication windowDimensions ?= @getDimensionsForNewWindow() openedWindow = new AtomWindow(this, @fileRecoveryService, {initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env}) openedWindow.focus() - @lastFocusedWindow = openedWindow + @windowStack.addWindow(openedWindow) if pidToKillWhenClosed? @pidsToOpenWindows[pidToKillWhenClosed] = openedWindow @@ -617,9 +623,10 @@ class AtomApplication saveState: (allowEmpty=false) -> return if @quitting states = [] - for window in @windows + 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') @@ -648,30 +655,39 @@ class AtomApplication # :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) + 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, devMode, safeMode, env) + @openPackageUriHandler(urlToOpen, parsedUrl, devMode, safeMode, env) - openPackageUriHandler: (url, devMode, safeMode, env) -> - resourcePath = @resourcePath - if devMode - try - windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window')) - resourcePath = @devResourcePath + 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) - windowInitializationScript ?= require.resolve('../initialize-application-window') - if @lastFocusedWindow? - @lastFocusedWindow.sendURIMessage url + 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() - @lastFocusedWindow = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env}) - @lastFocusedWindow.on 'window:loaded', => - @lastFocusedWindow.sendURIMessage url + 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 @@ -867,7 +883,7 @@ class AtomApplication disableZoomOnDisplayChange: -> outerCallback = => - for window in @windows + for window in @getAllWindows() window.disableZoom() # Set the limits every time a display is added or removed, otherwise the @@ -878,3 +894,24 @@ class AtomApplication 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/auto-update-manager.coffee b/src/main-process/auto-update-manager.coffee index 2ff2852cb..0e4144c1a 100644 --- a/src/main-process/auto-update-manager.coffee +++ b/src/main-process/auto-update-manager.coffee @@ -138,4 +138,4 @@ class AutoUpdateManager detail: message getWindows: -> - global.atomApplication.windows + global.atomApplication.getAllWindows()