From a11efa7376a564245c1cf51b955ebfa245d8c71d Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 18 Oct 2017 16:00:37 -0700 Subject: [PATCH 01/36] Add core URI handlers --- src/atom-environment.coffee | 2 ++ src/core-uri-handlers.js | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 src/core-uri-handlers.js diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index a32c4424b..7bbc513d6 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' @@ -238,6 +239,7 @@ class AtomEnvironment extends Model @commandInstaller.initialize(@getVersion()) @protocolHandlerInstaller.initialize(@config, @notifications) + @uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(@)) @autoUpdater.initialize() @config.load() diff --git a/src/core-uri-handlers.js b/src/core-uri-handlers.js new file mode 100644 index 000000000..c575b3f40 --- /dev/null +++ b/src/core-uri-handlers.js @@ -0,0 +1,24 @@ +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 + }) +} + +const ROUTER = { + '/open/file': openFile +} + +module.exports = { + create (atomEnv) { + return function coreURIHandler (parsed) { + const handler = ROUTER[parsed.pathname] + if (handler) { + handler(atomEnv, parsed) + } + } + } +} From 2901a484c2c796664abf10f75186b87ad5eeef4b Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 18 Oct 2017 17:34:24 -0700 Subject: [PATCH 02/36] :shirt: --- src/atom-environment.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 7bbc513d6..b9c6306ab 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -239,7 +239,7 @@ class AtomEnvironment extends Model @commandInstaller.initialize(@getVersion()) @protocolHandlerInstaller.initialize(@config, @notifications) - @uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(@)) + @uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(this)) @autoUpdater.initialize() @config.load() From 158622ce48f02fbace7a7617acef619ebe9a0c24 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 19 Oct 2017 14:19:24 -0700 Subject: [PATCH 03/36] Convert array of windows in AtomApplication to a WindowStack --- spec/main-process/atom-application.test.js | 10 +-- src/main-process/atom-application.coffee | 75 +++++++++++++++------- 2 files changed, 57 insertions(+), 28 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 62fae82b3..6434710ce 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -137,7 +137,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 +177,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 +191,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 +276,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 () { @@ -514,7 +514,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/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 0c587020e..f17aef902 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 = [] + @windows = 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 + @windows.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 + @windows.addWindow(window) @applicationMenu?.addWindow(window.browserWindow) window.once 'window:loaded', => @autoUpdateManager?.emitUpdateAvailableEvent(window) unless window.isSpec - focusHandler = => @lastFocusedWindow = window + focusHandler = => @windows.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 + @windows.removeWindow(window) window.browserWindow.removeListener 'focus', focusHandler window.browserWindow.removeListener 'blur', blurHandler window.browserWindow.webContents.once 'did-finish-load', => @saveState(false) + getAllWindows: () => + @windows.all().slice() + + getLastFocusedWindow: () => + @windows.getLastFocusedWindow() + # 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 + @windows.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') @@ -665,13 +672,14 @@ class AtomApplication resourcePath = @devResourcePath windowInitializationScript ?= require.resolve('../initialize-application-window') - if @lastFocusedWindow? - @lastFocusedWindow.sendURIMessage url + if @getLastFocusedWindow()? + @getLastFocusedWindow().sendURIMessage url else 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}) + @windows.addWindow(win) + win.on 'window:loaded', => + win.sendURIMessage url findPackageWithName: (packageName, devMode) -> _.find @getPackageManager(devMode).getAvailablePackageMetadata(), ({name}) -> name is packageName @@ -867,7 +875,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 +886,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 From e1bc9b593b01e062e800d71a945ab5898f255d4e Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 19 Oct 2017 14:37:08 -0700 Subject: [PATCH 04/36] Run URI handlers in last non-spec window --- src/main-process/atom-application.coffee | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index f17aef902..76b0d2bed 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -191,8 +191,8 @@ class AtomApplication getAllWindows: () => @windows.all().slice() - getLastFocusedWindow: () => - @windows.getLastFocusedWindow() + getLastFocusedWindow: (predicate) => + @windows.getLastFocusedWindow(predicate) # Creates server to listen for additional atom application launches. # @@ -672,8 +672,10 @@ class AtomApplication resourcePath = @devResourcePath windowInitializationScript ?= require.resolve('../initialize-application-window') - if @getLastFocusedWindow()? - @getLastFocusedWindow().sendURIMessage url + lastNonSpecWindow = @getLastFocusedWindow (win) -> !win.isSpecWindow() + if lastNonSpecWindow? + lastNonSpecWindow.sendURIMessage url + lastNonSpecWindow.focus() else windowDimensions = @getDimensionsForNewWindow() win = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env}) From 8111ba6c1ede4674ce0f52603e7f321531eca911 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 19 Oct 2017 15:47:57 -0700 Subject: [PATCH 05/36] Allow core URI handlers to determine which window to trigger the URI on --- src/core-uri-handlers.js | 22 +++++++++++++--- src/main-process/atom-application.coffee | 32 ++++++++++++++---------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/core-uri-handlers.js b/src/core-uri-handlers.js index c575b3f40..2af00f610 100644 --- a/src/core-uri-handlers.js +++ b/src/core-uri-handlers.js @@ -8,17 +8,31 @@ function openFile (atom, {query}) { }) } +function windowShouldOpenFile ({query}) { + const {filename} = query + return (win) => win.containsPath(filename) +} + const ROUTER = { - '/open/file': openFile + '/open/file': { handler: openFile, getWindowPredicate: windowShouldOpenFile } } module.exports = { create (atomEnv) { return function coreURIHandler (parsed) { - const handler = ROUTER[parsed.pathname] - if (handler) { - handler(atomEnv, 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/atom-application.coffee b/src/main-process/atom-application.coffee index 76b0d2bed..1f4d7214f 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -655,28 +655,34 @@ 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) -> + !win.isSpecWindow() && predicate(win) - windowInitializationScript ?= require.resolve('../initialize-application-window') - lastNonSpecWindow = @getLastFocusedWindow (win) -> !win.isSpecWindow() - if lastNonSpecWindow? - lastNonSpecWindow.sendURIMessage url - lastNonSpecWindow.focus() + bestWindow ?= @getLastFocusedWindow (win) -> !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}) @windows.addWindow(win) From 662a3978604ebcac0f7693a6422ec87a250a36c0 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 19 Oct 2017 16:19:44 -0700 Subject: [PATCH 06/36] :shirt: --- src/main-process/atom-application.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 1f4d7214f..fc2058dd4 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -188,7 +188,7 @@ class AtomApplication window.browserWindow.removeListener 'blur', blurHandler window.browserWindow.webContents.once 'did-finish-load', => @saveState(false) - getAllWindows: () => + getAllWindows: => @windows.all().slice() getLastFocusedWindow: (predicate) => @@ -669,9 +669,9 @@ class AtomApplication if parsedUrl.host is 'core' predicate = require('../core-uri-handlers').windowPredicate(parsedUrl) bestWindow = @getLastFocusedWindow (win) -> - !win.isSpecWindow() && predicate(win) + not win.isSpecWindow() and predicate(win) - bestWindow ?= @getLastFocusedWindow (win) -> !win.isSpecWindow() + bestWindow ?= @getLastFocusedWindow (win) -> not win.isSpecWindow() if bestWindow? bestWindow.sendURIMessage url bestWindow.focus() @@ -686,7 +686,7 @@ class AtomApplication windowDimensions = @getDimensionsForNewWindow() win = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env}) @windows.addWindow(win) - win.on 'window:loaded', => + win.on 'window:loaded', -> win.sendURIMessage url findPackageWithName: (packageName, devMode) -> @@ -910,8 +910,8 @@ class WindowStack @windows.splice(currentIndex, 1) if currentIndex > -1 getLastFocusedWindow: (predicate) => - predicate ?= (win) => true + predicate ?= (win) -> true @windows.find(predicate) - all: () => + all: => @windows From 2d20886cfa991a1db2431d1883ddd5db65914472 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 1 Nov 2017 14:36:23 -0700 Subject: [PATCH 07/36] Rename AtomApplication#windows -> #windowStack Update references across codebase --- src/main-process/application-menu.coffee | 2 +- src/main-process/atom-application.coffee | 18 +++++++++--------- src/main-process/auto-update-manager.coffee | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) 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 fc2058dd4..a5e3e6b0b 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 = new WindowStack() + @windowStack = new WindowStack() @config = new Config({enablePersistence: true}) @config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)} @@ -162,7 +162,7 @@ class AtomApplication # Public: Removes the {AtomWindow} from the global window list. removeWindow: (window) -> - @windows.removeWindow(window) + @windowStacktack.removeWindow(window) if @getAllWindows().length is 0 @applicationMenu?.enableWindowSpecificItems(false) if process.platform in ['win32', 'linux'] @@ -172,27 +172,27 @@ class AtomApplication # Public: Adds the {AtomWindow} to the global window list. addWindow: (window) -> - @windows.addWindow(window) + @windowStack.addWindow(window) @applicationMenu?.addWindow(window.browserWindow) window.once 'window:loaded', => @autoUpdateManager?.emitUpdateAvailableEvent(window) unless window.isSpec - focusHandler = => @windows.touch(window) + focusHandler = => @windowStack.touch(window) blurHandler = => @saveState(false) window.browserWindow.on 'focus', focusHandler window.browserWindow.on 'blur', blurHandler window.browserWindow.once 'closed', => - @windows.removeWindow(window) + @windowStack.removeWindow(window) window.browserWindow.removeListener 'focus', focusHandler window.browserWindow.removeListener 'blur', blurHandler window.browserWindow.webContents.once 'did-finish-load', => @saveState(false) getAllWindows: => - @windows.all().slice() + @windowStack.all().slice() getLastFocusedWindow: (predicate) => - @windows.getLastFocusedWindow(predicate) + @windowStack.getLastFocusedWindow(predicate) # Creates server to listen for additional atom application launches. # @@ -589,7 +589,7 @@ class AtomApplication windowDimensions ?= @getDimensionsForNewWindow() openedWindow = new AtomWindow(this, @fileRecoveryService, {initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env}) openedWindow.focus() - @windows.addWindow(openedWindow) + @windowStack.addWindow(openedWindow) if pidToKillWhenClosed? @pidsToOpenWindows[pidToKillWhenClosed] = openedWindow @@ -685,7 +685,7 @@ class AtomApplication windowInitializationScript ?= require.resolve('../initialize-application-window') windowDimensions = @getDimensionsForNewWindow() win = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env}) - @windows.addWindow(win) + @windowStack.addWindow(win) win.on 'window:loaded', -> win.sendURIMessage url 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() From 7054eefe1d7f249e951103c7640c61ee84e1c3b4 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Wed, 1 Nov 2017 09:21:17 -0400 Subject: [PATCH 08/36] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20spec/?= =?UTF-8?q?theme-manager-spec.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/theme-manager-spec.coffee | 437 ---------------------------- spec/theme-manager-spec.js | 503 +++++++++++++++++++++++++++++++++ 2 files changed, 503 insertions(+), 437 deletions(-) delete mode 100644 spec/theme-manager-spec.coffee create mode 100644 spec/theme-manager-spec.js diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee deleted file mode 100644 index 86237b71d..000000000 --- a/spec/theme-manager-spec.coffee +++ /dev/null @@ -1,437 +0,0 @@ -path = require 'path' -fs = require 'fs-plus' -temp = require('temp').track() - -describe "atom.themes", -> - beforeEach -> - spyOn(atom, 'inSpecMode').andReturn(false) - spyOn(console, 'warn') - - afterEach -> - waitsForPromise -> - atom.themes.deactivateThemes() - runs -> - try - temp.cleanupSync() - - describe "theme getters and setters", -> - beforeEach -> - jasmine.snapshotDeprecations() - atom.packages.loadPackages() - - afterEach -> - jasmine.restoreDeprecationsSnapshot() - - describe 'getLoadedThemes', -> - it 'gets all the loaded themes', -> - themes = atom.themes.getLoadedThemes() - expect(themes.length).toBeGreaterThan(2) - - describe "getActiveThemes", -> - it 'gets all the active themes', -> - waitsForPromise -> atom.themes.activateThemes() - - runs -> - names = atom.config.get('core.themes') - expect(names.length).toBeGreaterThan(0) - themes = atom.themes.getActiveThemes() - expect(themes).toHaveLength(names.length) - - describe "when the core.themes config value contains invalid entry", -> - it "ignores theme", -> - atom.config.set 'core.themes', [ - 'atom-light-ui' - null - undefined - '' - false - 4 - {} - [] - 'atom-dark-ui' - ] - - expect(atom.themes.getEnabledThemeNames()).toEqual ['atom-dark-ui', 'atom-light-ui'] - - describe "::getImportPaths()", -> - it "returns the theme directories before the themes are loaded", -> - atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui', 'atom-light-ui']) - - paths = atom.themes.getImportPaths() - - # syntax theme is not a dir at this time, so only two. - expect(paths.length).toBe 2 - expect(paths[0]).toContain 'atom-light-ui' - expect(paths[1]).toContain 'atom-dark-ui' - - it "ignores themes that cannot be resolved to a directory", -> - atom.config.set('core.themes', ['definitely-not-a-theme']) - expect(-> atom.themes.getImportPaths()).not.toThrow() - - describe "when the core.themes config value changes", -> - it "add/removes stylesheets to reflect the new config value", -> - atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy() - spyOn(atom.styles, 'getUserStyleSheetPath').andCallFake -> null - - waitsForPromise -> - atom.themes.activateThemes() - - runs -> - didChangeActiveThemesHandler.reset() - atom.config.set('core.themes', []) - - waitsFor 'a', -> - didChangeActiveThemesHandler.callCount is 1 - - runs -> - didChangeActiveThemesHandler.reset() - expect(document.querySelectorAll('style.theme')).toHaveLength 0 - atom.config.set('core.themes', ['atom-dark-ui']) - - waitsFor 'b', -> - didChangeActiveThemesHandler.callCount is 1 - - runs -> - didChangeActiveThemesHandler.reset() - expect(document.querySelectorAll('style[priority="1"]')).toHaveLength 2 - expect(document.querySelector('style[priority="1"]').getAttribute('source-path')).toMatch /atom-dark-ui/ - atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-ui']) - - waitsFor 'c', -> - didChangeActiveThemesHandler.callCount is 1 - - runs -> - didChangeActiveThemesHandler.reset() - expect(document.querySelectorAll('style[priority="1"]')).toHaveLength 2 - expect(document.querySelectorAll('style[priority="1"]')[0].getAttribute('source-path')).toMatch /atom-dark-ui/ - expect(document.querySelectorAll('style[priority="1"]')[1].getAttribute('source-path')).toMatch /atom-light-ui/ - atom.config.set('core.themes', []) - - waitsFor -> - didChangeActiveThemesHandler.callCount is 1 - - runs -> - didChangeActiveThemesHandler.reset() - expect(document.querySelectorAll('style[priority="1"]')).toHaveLength 2 - # atom-dark-ui has an directory path, the syntax one doesn't - atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui']) - - waitsFor -> - didChangeActiveThemesHandler.callCount is 1 - - runs -> - expect(document.querySelectorAll('style[priority="1"]')).toHaveLength 2 - importPaths = atom.themes.getImportPaths() - expect(importPaths.length).toBe 1 - expect(importPaths[0]).toContain 'atom-dark-ui' - - it 'adds theme-* classes to the workspace for each active theme', -> - atom.config.set('core.themes', ['atom-dark-ui', 'atom-dark-syntax']) - workspaceElement = atom.workspace.getElement() - atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy() - - waitsForPromise -> - atom.themes.activateThemes() - - runs -> - expect(workspaceElement).toHaveClass 'theme-atom-dark-ui' - - atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy() - atom.config.set('core.themes', ['theme-with-ui-variables', 'theme-with-syntax-variables']) - - waitsFor -> - didChangeActiveThemesHandler.callCount > 0 - - runs -> - # `theme-` twice as it prefixes the name with `theme-` - expect(workspaceElement).toHaveClass 'theme-theme-with-ui-variables' - expect(workspaceElement).toHaveClass 'theme-theme-with-syntax-variables' - expect(workspaceElement).not.toHaveClass 'theme-atom-dark-ui' - expect(workspaceElement).not.toHaveClass 'theme-atom-dark-syntax' - - describe "when a theme fails to load", -> - it "logs a warning", -> - console.warn.reset() - atom.packages.activatePackage('a-theme-that-will-not-be-found').then((->), (->)) - expect(console.warn.callCount).toBe 1 - expect(console.warn.argsForCall[0][0]).toContain "Could not resolve 'a-theme-that-will-not-be-found'" - - describe "::requireStylesheet(path)", -> - beforeEach -> - jasmine.snapshotDeprecations() - - afterEach -> - jasmine.restoreDeprecationsSnapshot() - - it "synchronously loads css at the given path and installs a style tag for it in the head", -> - atom.styles.onDidAddStyleElement styleElementAddedHandler = jasmine.createSpy("styleElementAddedHandler") - - cssPath = atom.project.getDirectories()[0]?.resolve('css.css') - lengthBefore = document.querySelectorAll('head style').length - - atom.themes.requireStylesheet(cssPath) - expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1 - - expect(styleElementAddedHandler).toHaveBeenCalled() - - element = document.querySelector('head style[source-path*="css.css"]') - expect(element.getAttribute('source-path')).toEqualPath cssPath - expect(element.textContent).toBe fs.readFileSync(cssPath, 'utf8') - - # doesn't append twice - styleElementAddedHandler.reset() - atom.themes.requireStylesheet(cssPath) - expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1 - expect(styleElementAddedHandler).not.toHaveBeenCalled() - - for styleElement in document.querySelectorAll('head style[id*="css.css"]') - styleElement.remove() - - it "synchronously loads and parses less files at the given path and installs a style tag for it in the head", -> - lessPath = atom.project.getDirectories()[0]?.resolve('sample.less') - lengthBefore = document.querySelectorAll('head style').length - atom.themes.requireStylesheet(lessPath) - expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1 - - element = document.querySelector('head style[source-path*="sample.less"]') - expect(element.getAttribute('source-path')).toEqualPath lessPath - expect(element.textContent.toLowerCase()).toBe """ - #header { - color: #4d926f; - } - h2 { - color: #4d926f; - } - - """ - - # doesn't append twice - atom.themes.requireStylesheet(lessPath) - expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1 - for styleElement in document.querySelectorAll('head style[id*="sample.less"]') - styleElement.remove() - - it "supports requiring css and less stylesheets without an explicit extension", -> - atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'css') - expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')).toEqualPath atom.project.getDirectories()[0]?.resolve('css.css') - atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'sample') - expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')).toEqualPath atom.project.getDirectories()[0]?.resolve('sample.less') - - document.querySelector('head style[source-path*="css.css"]').remove() - document.querySelector('head style[source-path*="sample.less"]').remove() - - it "returns a disposable allowing styles applied by the given path to be removed", -> - cssPath = require.resolve('./fixtures/css.css') - - expect(getComputedStyle(document.body).fontWeight).not.toBe("bold") - disposable = atom.themes.requireStylesheet(cssPath) - expect(getComputedStyle(document.body).fontWeight).toBe("bold") - - atom.styles.onDidRemoveStyleElement styleElementRemovedHandler = jasmine.createSpy("styleElementRemovedHandler") - - disposable.dispose() - - expect(getComputedStyle(document.body).fontWeight).not.toBe("bold") - - expect(styleElementRemovedHandler).toHaveBeenCalled() - - - describe "base style sheet loading", -> - beforeEach -> - workspaceElement = atom.workspace.getElement() - jasmine.attachToDOM(atom.workspace.getElement()) - workspaceElement.appendChild document.createElement('atom-text-editor') - - waitsForPromise -> - atom.themes.activateThemes() - - it "loads the correct values from the theme's ui-variables file", -> - atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy() - atom.config.set('core.themes', ['theme-with-ui-variables', 'theme-with-syntax-variables']) - - waitsFor -> - didChangeActiveThemesHandler.callCount > 0 - - runs -> - # an override loaded in the base css - expect(getComputedStyle(atom.workspace.getElement())["background-color"]).toBe "rgb(0, 0, 255)" - - # from within the theme itself - expect(getComputedStyle(document.querySelector("atom-text-editor")).paddingTop).toBe "150px" - expect(getComputedStyle(document.querySelector("atom-text-editor")).paddingRight).toBe "150px" - expect(getComputedStyle(document.querySelector("atom-text-editor")).paddingBottom).toBe "150px" - - describe "when there is a theme with incomplete variables", -> - it "loads the correct values from the fallback ui-variables", -> - atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy() - atom.config.set('core.themes', ['theme-with-incomplete-ui-variables', 'theme-with-syntax-variables']) - - waitsFor -> - didChangeActiveThemesHandler.callCount > 0 - - runs -> - # an override loaded in the base css - expect(getComputedStyle(atom.workspace.getElement())["background-color"]).toBe "rgb(0, 0, 255)" - - # from within the theme itself - expect(getComputedStyle(document.querySelector("atom-text-editor")).backgroundColor).toBe "rgb(0, 152, 255)" - - describe "user stylesheet", -> - userStylesheetPath = null - beforeEach -> - userStylesheetPath = path.join(temp.mkdirSync("atom"), 'styles.less') - fs.writeFileSync(userStylesheetPath, 'body {border-style: dotted !important;}') - spyOn(atom.styles, 'getUserStyleSheetPath').andReturn userStylesheetPath - - describe "when the user stylesheet changes", -> - beforeEach -> - jasmine.snapshotDeprecations() - - afterEach -> - jasmine.restoreDeprecationsSnapshot() - - it "reloads it", -> - [styleElementAddedHandler, styleElementRemovedHandler] = [] - - waitsForPromise -> - atom.themes.activateThemes() - - runs -> - atom.styles.onDidRemoveStyleElement styleElementRemovedHandler = jasmine.createSpy("styleElementRemovedHandler") - atom.styles.onDidAddStyleElement styleElementAddedHandler = jasmine.createSpy("styleElementAddedHandler") - - spyOn(atom.themes, 'loadUserStylesheet').andCallThrough() - - expect(getComputedStyle(document.body).borderStyle).toBe 'dotted' - fs.writeFileSync(userStylesheetPath, 'body {border-style: dashed}') - - waitsFor -> - atom.themes.loadUserStylesheet.callCount is 1 - - runs -> - expect(getComputedStyle(document.body).borderStyle).toBe 'dashed' - - expect(styleElementRemovedHandler).toHaveBeenCalled() - expect(styleElementRemovedHandler.argsForCall[0][0].textContent).toContain 'dotted' - - expect(styleElementAddedHandler).toHaveBeenCalled() - expect(styleElementAddedHandler.argsForCall[0][0].textContent).toContain 'dashed' - - styleElementRemovedHandler.reset() - fs.removeSync(userStylesheetPath) - - waitsFor -> - atom.themes.loadUserStylesheet.callCount is 2 - - runs -> - expect(styleElementRemovedHandler).toHaveBeenCalled() - expect(styleElementRemovedHandler.argsForCall[0][0].textContent).toContain 'dashed' - expect(getComputedStyle(document.body).borderStyle).toBe 'none' - - describe "when there is an error reading the stylesheet", -> - addErrorHandler = null - beforeEach -> - atom.themes.loadUserStylesheet() - spyOn(atom.themes.lessCache, 'cssForFile').andCallFake -> - throw new Error('EACCES permission denied "styles.less"') - atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy() - - it "creates an error notification and does not add the stylesheet", -> - atom.themes.loadUserStylesheet() - expect(addErrorHandler).toHaveBeenCalled() - note = addErrorHandler.mostRecentCall.args[0] - expect(note.getType()).toBe 'error' - expect(note.getMessage()).toContain 'Error loading' - expect(atom.styles.styleElementsBySourcePath[atom.styles.getUserStyleSheetPath()]).toBeUndefined() - - describe "when there is an error watching the user stylesheet", -> - addErrorHandler = null - beforeEach -> - {File} = require 'pathwatcher' - spyOn(File::, 'on').andCallFake (event) -> - if event.indexOf('contents-changed') > -1 - throw new Error('Unable to watch path') - spyOn(atom.themes, 'loadStylesheet').andReturn '' - atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy() - - it "creates an error notification", -> - atom.themes.loadUserStylesheet() - expect(addErrorHandler).toHaveBeenCalled() - note = addErrorHandler.mostRecentCall.args[0] - expect(note.getType()).toBe 'error' - expect(note.getMessage()).toContain 'Unable to watch path' - - it "adds a notification when a theme's stylesheet is invalid", -> - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - expect(-> atom.packages.activatePackage('theme-with-invalid-styles').then((->), (->))).not.toThrow() - expect(addErrorHandler.callCount).toBe 2 - expect(addErrorHandler.argsForCall[1][0].message).toContain("Failed to activate the theme-with-invalid-styles theme") - - describe "when a non-existent theme is present in the config", -> - beforeEach -> - console.warn.reset() - atom.config.set('core.themes', ['non-existent-dark-ui', 'non-existent-dark-syntax']) - - waitsForPromise -> - atom.themes.activateThemes() - - it 'uses the default dark UI and syntax themes and logs a warning', -> - activeThemeNames = atom.themes.getActiveThemeNames() - expect(console.warn.callCount).toBe 2 - expect(activeThemeNames.length).toBe(2) - expect(activeThemeNames).toContain('atom-dark-ui') - expect(activeThemeNames).toContain('atom-dark-syntax') - - describe "when in safe mode", -> - describe 'when the enabled UI and syntax themes are bundled with Atom', -> - beforeEach -> - atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-syntax']) - - waitsForPromise -> - atom.themes.activateThemes() - - it 'uses the enabled themes', -> - activeThemeNames = atom.themes.getActiveThemeNames() - expect(activeThemeNames.length).toBe(2) - expect(activeThemeNames).toContain('atom-light-ui') - expect(activeThemeNames).toContain('atom-dark-syntax') - - describe 'when the enabled UI and syntax themes are not bundled with Atom', -> - beforeEach -> - atom.config.set('core.themes', ['installed-dark-ui', 'installed-dark-syntax']) - - waitsForPromise -> - atom.themes.activateThemes() - - it 'uses the default dark UI and syntax themes', -> - activeThemeNames = atom.themes.getActiveThemeNames() - expect(activeThemeNames.length).toBe(2) - expect(activeThemeNames).toContain('atom-dark-ui') - expect(activeThemeNames).toContain('atom-dark-syntax') - - describe 'when the enabled UI theme is not bundled with Atom', -> - beforeEach -> - atom.config.set('core.themes', ['installed-dark-ui', 'atom-light-syntax']) - - waitsForPromise -> - atom.themes.activateThemes() - - it 'uses the default dark UI theme', -> - activeThemeNames = atom.themes.getActiveThemeNames() - expect(activeThemeNames.length).toBe(2) - expect(activeThemeNames).toContain('atom-dark-ui') - expect(activeThemeNames).toContain('atom-light-syntax') - - describe 'when the enabled syntax theme is not bundled with Atom', -> - beforeEach -> - atom.config.set('core.themes', ['atom-light-ui', 'installed-dark-syntax']) - - waitsForPromise -> - atom.themes.activateThemes() - - it 'uses the default dark syntax theme', -> - activeThemeNames = atom.themes.getActiveThemeNames() - expect(activeThemeNames.length).toBe(2) - expect(activeThemeNames).toContain('atom-light-ui') - expect(activeThemeNames).toContain('atom-dark-syntax') diff --git a/spec/theme-manager-spec.js b/spec/theme-manager-spec.js new file mode 100644 index 000000000..f4ed3b9f5 --- /dev/null +++ b/spec/theme-manager-spec.js @@ -0,0 +1,503 @@ +const path = require('path') +const fs = require('fs-plus') +const temp = require('temp').track() + +describe('atom.themes', function () { + beforeEach(function () { + spyOn(atom, 'inSpecMode').andReturn(false) + spyOn(console, 'warn') + }) + + afterEach(function () { + waitsForPromise(() => atom.themes.deactivateThemes()) + runs(function () { + try { + temp.cleanupSync() + } catch (error) {} + }) + }) + + describe('theme getters and setters', function () { + beforeEach(function () { + jasmine.snapshotDeprecations() + atom.packages.loadPackages() + }) + + afterEach(() => jasmine.restoreDeprecationsSnapshot()) + + describe('getLoadedThemes', () => + it('gets all the loaded themes', function () { + const themes = atom.themes.getLoadedThemes() + expect(themes.length).toBeGreaterThan(2) + }) + ) + + describe('getActiveThemes', () => + it('gets all the active themes', function () { + waitsForPromise(() => atom.themes.activateThemes()) + + runs(function () { + const names = atom.config.get('core.themes') + expect(names.length).toBeGreaterThan(0) + const themes = atom.themes.getActiveThemes() + expect(themes).toHaveLength(names.length) + }) + }) + ) + }) + + describe('when the core.themes config value contains invalid entry', () => + it('ignores theme', function () { + atom.config.set('core.themes', [ + 'atom-light-ui', + null, + undefined, + '', + false, + 4, + {}, + [], + 'atom-dark-ui' + ]) + + expect(atom.themes.getEnabledThemeNames()).toEqual(['atom-dark-ui', 'atom-light-ui']) + }) +) + + describe('::getImportPaths()', function () { + it('returns the theme directories before the themes are loaded', function () { + atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui', 'atom-light-ui']) + + const paths = atom.themes.getImportPaths() + + // syntax theme is not a dir at this time, so only two. + expect(paths.length).toBe(2) + expect(paths[0]).toContain('atom-light-ui') + expect(paths[1]).toContain('atom-dark-ui') + }) + + it('ignores themes that cannot be resolved to a directory', function () { + atom.config.set('core.themes', ['definitely-not-a-theme']) + expect(() => atom.themes.getImportPaths()).not.toThrow() + }) + }) + + describe('when the core.themes config value changes', function () { + it('add/removes stylesheets to reflect the new config value', function () { + let didChangeActiveThemesHandler + atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler = jasmine.createSpy()) + spyOn(atom.styles, 'getUserStyleSheetPath').andCallFake(() => null) + + waitsForPromise(() => atom.themes.activateThemes()) + + runs(function () { + didChangeActiveThemesHandler.reset() + atom.config.set('core.themes', []) + }) + + waitsFor('a', () => didChangeActiveThemesHandler.callCount === 1) + + runs(function () { + didChangeActiveThemesHandler.reset() + expect(document.querySelectorAll('style.theme')).toHaveLength(0) + atom.config.set('core.themes', ['atom-dark-ui']) + }) + + waitsFor('b', () => didChangeActiveThemesHandler.callCount === 1) + + runs(function () { + didChangeActiveThemesHandler.reset() + expect(document.querySelectorAll('style[priority="1"]')).toHaveLength(2) + expect(document.querySelector('style[priority="1"]').getAttribute('source-path')).toMatch(/atom-dark-ui/) + atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-ui']) + }) + + waitsFor('c', () => didChangeActiveThemesHandler.callCount === 1) + + runs(function () { + didChangeActiveThemesHandler.reset() + expect(document.querySelectorAll('style[priority="1"]')).toHaveLength(2) + expect(document.querySelectorAll('style[priority="1"]')[0].getAttribute('source-path')).toMatch(/atom-dark-ui/) + expect(document.querySelectorAll('style[priority="1"]')[1].getAttribute('source-path')).toMatch(/atom-light-ui/) + atom.config.set('core.themes', []) + }) + + waitsFor(() => didChangeActiveThemesHandler.callCount === 1) + + runs(function () { + didChangeActiveThemesHandler.reset() + expect(document.querySelectorAll('style[priority="1"]')).toHaveLength(2) + // atom-dark-ui has a directory path, the syntax one doesn't + atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui']) + }) + + waitsFor(() => didChangeActiveThemesHandler.callCount === 1) + + runs(function () { + expect(document.querySelectorAll('style[priority="1"]')).toHaveLength(2) + const importPaths = atom.themes.getImportPaths() + expect(importPaths.length).toBe(1) + expect(importPaths[0]).toContain('atom-dark-ui') + }) + }) + + it('adds theme-* classes to the workspace for each active theme', function () { + atom.config.set('core.themes', ['atom-dark-ui', 'atom-dark-syntax']) + + let didChangeActiveThemesHandler + atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler = jasmine.createSpy()) + waitsForPromise(() => atom.themes.activateThemes()) + + const workspaceElement = atom.workspace.getElement() + runs(function () { + expect(workspaceElement).toHaveClass('theme-atom-dark-ui') + + atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler = jasmine.createSpy()) + atom.config.set('core.themes', ['theme-with-ui-variables', 'theme-with-syntax-variables']) + }) + + waitsFor(() => didChangeActiveThemesHandler.callCount > 0) + + runs(function () { + // `theme-` twice as it prefixes the name with `theme-` + expect(workspaceElement).toHaveClass('theme-theme-with-ui-variables') + expect(workspaceElement).toHaveClass('theme-theme-with-syntax-variables') + expect(workspaceElement).not.toHaveClass('theme-atom-dark-ui') + expect(workspaceElement).not.toHaveClass('theme-atom-dark-syntax') + }) + }) + }) + + describe('when a theme fails to load', () => + it('logs a warning', function () { + console.warn.reset() + atom.packages.activatePackage('a-theme-that-will-not-be-found').then(function () {}, function () {}) + expect(console.warn.callCount).toBe(1) + expect(console.warn.argsForCall[0][0]).toContain("Could not resolve 'a-theme-that-will-not-be-found'") + }) + ) + + describe('::requireStylesheet(path)', function () { + beforeEach(() => jasmine.snapshotDeprecations()) + + afterEach(() => jasmine.restoreDeprecationsSnapshot()) + + it('synchronously loads css at the given path and installs a style tag for it in the head', function () { + let styleElementAddedHandler + atom.styles.onDidAddStyleElement(styleElementAddedHandler = jasmine.createSpy('styleElementAddedHandler')) + + const cssPath = getAbsolutePath(atom.project.getDirectories()[0], 'css.css') + const lengthBefore = document.querySelectorAll('head style').length + + atom.themes.requireStylesheet(cssPath) + expect(document.querySelectorAll('head style').length).toBe(lengthBefore + 1) + + expect(styleElementAddedHandler).toHaveBeenCalled() + + const element = document.querySelector('head style[source-path*="css.css"]') + expect(element.getAttribute('source-path')).toEqualPath(cssPath) + expect(element.textContent).toBe(fs.readFileSync(cssPath, 'utf8')) + + // doesn't append twice + styleElementAddedHandler.reset() + atom.themes.requireStylesheet(cssPath) + expect(document.querySelectorAll('head style').length).toBe(lengthBefore + 1) + expect(styleElementAddedHandler).not.toHaveBeenCalled() + + document.querySelectorAll('head style[id*="css.css"]').forEach((styleElement) => { + styleElement.remove() + }) + }) + + it('synchronously loads and parses less files at the given path and installs a style tag for it in the head', function () { + const lessPath = getAbsolutePath(atom.project.getDirectories()[0], 'sample.less') + const lengthBefore = document.querySelectorAll('head style').length + atom.themes.requireStylesheet(lessPath) + expect(document.querySelectorAll('head style').length).toBe(lengthBefore + 1) + + const element = document.querySelector('head style[source-path*="sample.less"]') + expect(element.getAttribute('source-path')).toEqualPath(lessPath) + expect(element.textContent.toLowerCase()).toBe(`\ +#header { + color: #4d926f; +} +h2 { + color: #4d926f; +} +\ +` + ) + + // doesn't append twice + atom.themes.requireStylesheet(lessPath) + expect(document.querySelectorAll('head style').length).toBe(lengthBefore + 1) + document.querySelectorAll('head style[id*="sample.less"]').forEach((styleElement) => { + styleElement.remove() + }) + }) + + it('supports requiring css and less stylesheets without an explicit extension', function () { + atom.themes.requireStylesheet(path.join(__dirname, 'fixtures', 'css')) + expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')) + .toEqualPath(getAbsolutePath(atom.project.getDirectories()[0], 'css.css')) + atom.themes.requireStylesheet(path.join(__dirname, 'fixtures', 'sample')) + expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')) + .toEqualPath(getAbsolutePath(atom.project.getDirectories()[0], 'sample.less')) + + document.querySelector('head style[source-path*="css.css"]').remove() + document.querySelector('head style[source-path*="sample.less"]').remove() + }) + + it('returns a disposable allowing styles applied by the given path to be removed', function () { + const cssPath = require.resolve('./fixtures/css.css') + + expect(getComputedStyle(document.body).fontWeight).not.toBe('bold') + const disposable = atom.themes.requireStylesheet(cssPath) + expect(getComputedStyle(document.body).fontWeight).toBe('bold') + + let styleElementRemovedHandler + atom.styles.onDidRemoveStyleElement(styleElementRemovedHandler = jasmine.createSpy('styleElementRemovedHandler')) + + disposable.dispose() + + expect(getComputedStyle(document.body).fontWeight).not.toBe('bold') + + expect(styleElementRemovedHandler).toHaveBeenCalled() + }) + }) + + describe('base style sheet loading', function () { + beforeEach(function () { + const workspaceElement = atom.workspace.getElement() + jasmine.attachToDOM(atom.workspace.getElement()) + workspaceElement.appendChild(document.createElement('atom-text-editor')) + + waitsForPromise(() => atom.themes.activateThemes()) + }) + + it("loads the correct values from the theme's ui-variables file", function () { + let didChangeActiveThemesHandler + atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler = jasmine.createSpy()) + atom.config.set('core.themes', ['theme-with-ui-variables', 'theme-with-syntax-variables']) + + waitsFor(() => didChangeActiveThemesHandler.callCount > 0) + + runs(function () { + // an override loaded in the base css + expect(getComputedStyle(atom.workspace.getElement())['background-color']).toBe('rgb(0, 0, 255)') + + // from within the theme itself + expect(getComputedStyle(document.querySelector('atom-text-editor')).paddingTop).toBe('150px') + expect(getComputedStyle(document.querySelector('atom-text-editor')).paddingRight).toBe('150px') + expect(getComputedStyle(document.querySelector('atom-text-editor')).paddingBottom).toBe('150px') + }) + }) + + describe('when there is a theme with incomplete variables', () => + it('loads the correct values from the fallback ui-variables', function () { + let didChangeActiveThemesHandler + atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler = jasmine.createSpy()) + atom.config.set('core.themes', ['theme-with-incomplete-ui-variables', 'theme-with-syntax-variables']) + + waitsFor(() => didChangeActiveThemesHandler.callCount > 0) + + runs(function () { + // an override loaded in the base css + expect(getComputedStyle(atom.workspace.getElement())['background-color']).toBe('rgb(0, 0, 255)') + + // from within the theme itself + expect(getComputedStyle(document.querySelector('atom-text-editor')).backgroundColor).toBe('rgb(0, 152, 255)') + }) + }) + ) + }) + + describe('user stylesheet', function () { + let userStylesheetPath + beforeEach(function () { + userStylesheetPath = path.join(temp.mkdirSync('atom'), 'styles.less') + fs.writeFileSync(userStylesheetPath, 'body {border-style: dotted !important;}') + spyOn(atom.styles, 'getUserStyleSheetPath').andReturn(userStylesheetPath) + }) + + describe('when the user stylesheet changes', function () { + beforeEach(() => jasmine.snapshotDeprecations()) + + afterEach(() => jasmine.restoreDeprecationsSnapshot()) + + it('reloads it', function () { + let styleElementAddedHandler, styleElementRemovedHandler + + waitsForPromise(() => atom.themes.activateThemes()) + + runs(function () { + atom.styles.onDidRemoveStyleElement(styleElementRemovedHandler = jasmine.createSpy('styleElementRemovedHandler')) + atom.styles.onDidAddStyleElement(styleElementAddedHandler = jasmine.createSpy('styleElementAddedHandler')) + + spyOn(atom.themes, 'loadUserStylesheet').andCallThrough() + + expect(getComputedStyle(document.body).borderStyle).toBe('dotted') + fs.writeFileSync(userStylesheetPath, 'body {border-style: dashed}') + }) + + waitsFor(() => atom.themes.loadUserStylesheet.callCount === 1) + + runs(function () { + expect(getComputedStyle(document.body).borderStyle).toBe('dashed') + + expect(styleElementRemovedHandler).toHaveBeenCalled() + expect(styleElementRemovedHandler.argsForCall[0][0].textContent).toContain('dotted') + + expect(styleElementAddedHandler).toHaveBeenCalled() + expect(styleElementAddedHandler.argsForCall[0][0].textContent).toContain('dashed') + + styleElementRemovedHandler.reset() + fs.removeSync(userStylesheetPath) + }) + + waitsFor(() => atom.themes.loadUserStylesheet.callCount === 2) + + runs(function () { + expect(styleElementRemovedHandler).toHaveBeenCalled() + expect(styleElementRemovedHandler.argsForCall[0][0].textContent).toContain('dashed') + expect(getComputedStyle(document.body).borderStyle).toBe('none') + }) + }) + }) + + describe('when there is an error reading the stylesheet', function () { + let addErrorHandler = null + beforeEach(function () { + atom.themes.loadUserStylesheet() + spyOn(atom.themes.lessCache, 'cssForFile').andCallFake(function () { + throw new Error('EACCES permission denied "styles.less"') + }) + atom.notifications.onDidAddNotification(addErrorHandler = jasmine.createSpy()) + }) + + it('creates an error notification and does not add the stylesheet', function () { + atom.themes.loadUserStylesheet() + expect(addErrorHandler).toHaveBeenCalled() + const note = addErrorHandler.mostRecentCall.args[0] + expect(note.getType()).toBe('error') + expect(note.getMessage()).toContain('Error loading') + expect(atom.styles.styleElementsBySourcePath[atom.styles.getUserStyleSheetPath()]).toBeUndefined() + }) + }) + + describe('when there is an error watching the user stylesheet', function () { + let addErrorHandler = null + beforeEach(function () { + const {File} = require('pathwatcher') + spyOn(File.prototype, 'on').andCallFake(function (event) { + if (event.indexOf('contents-changed') > -1) { + throw new Error('Unable to watch path') + } + }) + spyOn(atom.themes, 'loadStylesheet').andReturn('') + atom.notifications.onDidAddNotification(addErrorHandler = jasmine.createSpy()) + }) + + it('creates an error notification', function () { + atom.themes.loadUserStylesheet() + expect(addErrorHandler).toHaveBeenCalled() + const note = addErrorHandler.mostRecentCall.args[0] + expect(note.getType()).toBe('error') + expect(note.getMessage()).toContain('Unable to watch path') + }) + }) + + it("adds a notification when a theme's stylesheet is invalid", function () { + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + expect(() => atom.packages.activatePackage('theme-with-invalid-styles').then(function () {}, function () {})).not.toThrow() + expect(addErrorHandler.callCount).toBe(2) + expect(addErrorHandler.argsForCall[1][0].message).toContain('Failed to activate the theme-with-invalid-styles theme') + }) + }) + + describe('when a non-existent theme is present in the config', function () { + beforeEach(function () { + console.warn.reset() + atom.config.set('core.themes', ['non-existent-dark-ui', 'non-existent-dark-syntax']) + + waitsForPromise(() => atom.themes.activateThemes()) + }) + + it('uses the default dark UI and syntax themes and logs a warning', function () { + const activeThemeNames = atom.themes.getActiveThemeNames() + expect(console.warn.callCount).toBe(2) + expect(activeThemeNames.length).toBe(2) + expect(activeThemeNames).toContain('atom-dark-ui') + expect(activeThemeNames).toContain('atom-dark-syntax') + }) + }) + + describe('when in safe mode', function () { + describe('when the enabled UI and syntax themes are bundled with Atom', function () { + beforeEach(function () { + atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-syntax']) + + waitsForPromise(() => atom.themes.activateThemes()) + }) + + it('uses the enabled themes', function () { + const activeThemeNames = atom.themes.getActiveThemeNames() + expect(activeThemeNames.length).toBe(2) + expect(activeThemeNames).toContain('atom-light-ui') + expect(activeThemeNames).toContain('atom-dark-syntax') + }) + }) + + describe('when the enabled UI and syntax themes are not bundled with Atom', function () { + beforeEach(function () { + atom.config.set('core.themes', ['installed-dark-ui', 'installed-dark-syntax']) + + waitsForPromise(() => atom.themes.activateThemes()) + }) + + it('uses the default dark UI and syntax themes', function () { + const activeThemeNames = atom.themes.getActiveThemeNames() + expect(activeThemeNames.length).toBe(2) + expect(activeThemeNames).toContain('atom-dark-ui') + expect(activeThemeNames).toContain('atom-dark-syntax') + }) + }) + + describe('when the enabled UI theme is not bundled with Atom', function () { + beforeEach(function () { + atom.config.set('core.themes', ['installed-dark-ui', 'atom-light-syntax']) + + waitsForPromise(() => atom.themes.activateThemes()) + }) + + it('uses the default dark UI theme', function () { + const activeThemeNames = atom.themes.getActiveThemeNames() + expect(activeThemeNames.length).toBe(2) + expect(activeThemeNames).toContain('atom-dark-ui') + expect(activeThemeNames).toContain('atom-light-syntax') + }) + }) + + describe('when the enabled syntax theme is not bundled with Atom', function () { + beforeEach(function () { + atom.config.set('core.themes', ['atom-light-ui', 'installed-dark-syntax']) + + waitsForPromise(() => atom.themes.activateThemes()) + }) + + it('uses the default dark syntax theme', function () { + const activeThemeNames = atom.themes.getActiveThemeNames() + expect(activeThemeNames.length).toBe(2) + expect(activeThemeNames).toContain('atom-light-ui') + expect(activeThemeNames).toContain('atom-dark-syntax') + }) + }) + }) +}) + +function getAbsolutePath (directory, relativePath) { + if (directory) { + return directory.resolve(relativePath) + } +} From 1e9753d8a54a027c5902be0c9f8b10c665f271ec Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 2 Nov 2017 10:22:22 -0600 Subject: [PATCH 09/36] Fix select-word command between word and non-word chararacters In #15776, we accidentally stopped passing an option to the wordRegExp method that caused us to prefer word characters when selecting words at a boundary between word and non-word characters. --- spec/text-editor-spec.js | 8 ++++++-- src/cursor.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index cece5d753..382d020d4 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -2007,13 +2007,17 @@ describe('TextEditor', () => { describe('when the cursor is between two words', () => { it('selects the word the cursor is on', () => { - editor.setCursorScreenPosition([0, 4]) + editor.setCursorBufferPosition([0, 4]) editor.selectWordsContainingCursors() expect(editor.getSelectedText()).toBe('quicksort') - editor.setCursorScreenPosition([0, 3]) + editor.setCursorBufferPosition([0, 3]) editor.selectWordsContainingCursors() expect(editor.getSelectedText()).toBe('var') + + editor.setCursorBufferPosition([1, 22]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedText()).toBe('items') }) }) diff --git a/src/cursor.js b/src/cursor.js index 6cd0cc623..10bdef804 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -594,7 +594,7 @@ class Cursor extends Model { getCurrentWordBufferRange (options = {}) { const position = this.getBufferPosition() const ranges = this.editor.buffer.findAllInRangeSync( - options.wordRegex || this.wordRegExp(), + options.wordRegex || this.wordRegExp(options), new Range(new Point(position.row, 0), new Point(position.row, Infinity)) ) const range = ranges.find(range => From 3d3042baf2086e306845e383b863a6c8b5eb36d6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 2 Nov 2017 12:18:46 -0600 Subject: [PATCH 10/36] :arrow_up: command-palette --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c8681e18e..ebf9448c0 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "background-tips": "0.27.1", "bookmarks": "0.44.4", "bracket-matcher": "0.88.0", - "command-palette": "0.41.1", + "command-palette": "0.42.0", "dalek": "0.2.1", "deprecation-cop": "0.56.9", "dev-live-reload": "0.47.1", From cbf2d24d9ea726f1edbc3605080b0072ba69978e Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 11:31:06 -0700 Subject: [PATCH 11/36] :keyboard: fix typo --- src/main-process/atom-application.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index a5e3e6b0b..f6802705e 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -162,7 +162,7 @@ class AtomApplication # Public: Removes the {AtomWindow} from the global window list. removeWindow: (window) -> - @windowStacktack.removeWindow(window) + @windowStack.removeWindow(window) if @getAllWindows().length is 0 @applicationMenu?.enableWindowSpecificItems(false) if process.platform in ['win32', 'linux'] From 303cf30b51b68d78d6d440aa10aa8cfc7afd115b Mon Sep 17 00:00:00 2001 From: hansonw Date: Wed, 1 Nov 2017 15:40:00 -0700 Subject: [PATCH 12/36] Allow directory providers to implement onDidChangeFiles for custom pathwatchers --- spec/project-spec.js | 28 ++++++++++++++++++++++++++++ src/project.js | 12 ++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/spec/project-spec.js b/spec/project-spec.js index 63c065fa6..0f003b26b 100644 --- a/spec/project-spec.js +++ b/spec/project-spec.js @@ -335,9 +335,14 @@ describe('Project', () => { isRoot () { return true } existsSync () { return this.path.endsWith('does-exist') } contains (filePath) { return filePath.startsWith(this.path) } + onDidChangeFiles (callback) { + onDidChangeFilesCallback = callback + return {dispose: () => {}} + } } let serviceDisposable = null + let onDidChangeFilesCallback = null beforeEach(() => { serviceDisposable = atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { @@ -349,6 +354,7 @@ describe('Project', () => { } } }) + onDidChangeFilesCallback = null waitsFor(() => atom.project.directoryProviders.length > 0) }) @@ -383,6 +389,28 @@ describe('Project', () => { atom.project.setPaths(['ssh://foreign-directory:8080/does-exist']) expect(atom.project.getDirectories().length).toBe(0) }) + + it('uses the custom onDidChangeFiles as the watcher if available', () => { + // Ensure that all preexisting watchers are stopped + waitsForPromise(() => stopAllWatchers()) + + const remotePath = 'ssh://another-directory:8080/does-exist' + runs(() => atom.project.setPaths([remotePath])) + waitsForPromise(() => atom.project.getWatcherPromise(remotePath)) + + runs(() => { + expect(onDidChangeFilesCallback).not.toBeNull() + + const changeSpy = jasmine.createSpy('atom.project.onDidChangeFiles') + const disposable = atom.project.onDidChangeFiles(changeSpy) + + const events = [{action: 'created', path: remotePath + '/test.txt'}] + onDidChangeFilesCallback(events) + + expect(changeSpy).toHaveBeenCalledWith(events) + disposable.dispose() + }) + }) }) describe('.open(path)', () => { diff --git a/src/project.js b/src/project.js index 48541c395..92a11ec7a 100644 --- a/src/project.js +++ b/src/project.js @@ -338,13 +338,21 @@ class Project extends Model { } this.rootDirectories.push(directory) - this.watcherPromisesByPath[directory.getPath()] = watchPath(directory.getPath(), {}, events => { + + const didChangeCallback = events => { // Stop event delivery immediately on removal of a rootDirectory, even if its watcher // promise has yet to resolve at the time of removal if (this.rootDirectories.includes(directory)) { this.emitter.emit('did-change-files', events) } - }) + } + // We'll use the directory's custom onDidChangeFiles callback, if available. + // CustomDirectory::onDidChangeFiles should match the signature of + // Project::onDidChangeFiles below (although it may resolve asynchronously) + this.watcherPromisesByPath[directory.getPath()] = + directory.onDidChangeFiles != null + ? Promise.resolve(directory.onDidChangeFiles(didChangeCallback)) + : watchPath(directory.getPath(), {}, didChangeCallback) for (let watchedPath in this.watcherPromisesByPath) { if (!this.rootDirectories.find(dir => dir.getPath() === watchedPath)) { From 76ac08c49cd829ecb3b1383f7d9725acc9490628 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 2 Nov 2017 20:40:23 +0100 Subject: [PATCH 13/36] :arrow_up: open-on-github@1.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ebf9448c0..b19548235 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "markdown-preview": "0.159.18", "metrics": "1.2.6", "notifications": "0.69.2", - "open-on-github": "1.2.1", + "open-on-github": "1.3.0", "package-generator": "1.1.1", "settings-view": "0.252.2", "snippets": "1.1.9", From db0fd527cea4ff6bc203a599f772303c6e950dbc Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 13:35:29 -0700 Subject: [PATCH 14/36] Add test for core URI handler window-selection logic --- spec/main-process/atom-application.test.js | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 6434710ce..a209b4301 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -461,6 +461,30 @@ 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) + + await new Promise(res => setTimeout(res, 2000)) + + const fileA = path.join(dirAPath, 'file-a') + const fileB = path.join(dirBPath, 'file-b') + + atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileA}`])) + await new Promise(res => setTimeout(res, 1000)) + assert.equal(atomApplication.getLastFocusedWindow(), window1) + + atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileB}`])) + await new Promise(res => setTimeout(res, 1000)) + assert.equal(atomApplication.getLastFocusedWindow(), window2) + }) }) }) From 668397c1d0cac12e61c1894cfa1d832f37cd8f59 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 14:11:52 -0700 Subject: [PATCH 15/36] Fix flaky test --- spec/main-process/atom-application.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index a209b4301..c97c70746 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -479,11 +479,11 @@ describe('AtomApplication', function () { atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileA}`])) await new Promise(res => setTimeout(res, 1000)) - assert.equal(atomApplication.getLastFocusedWindow(), window1) + await conditionPromise(() => window.atomApplication.getLastFocusedWindow() === window1) atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileB}`])) await new Promise(res => setTimeout(res, 1000)) - assert.equal(atomApplication.getLastFocusedWindow(), window2) + await conditionPromise(() => window.atomApplication.getLastFocusedWindow() === window2) }) }) }) From 7639afe684c490db7aa612e6c3095551e5e4bf4d Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Thu, 2 Nov 2017 12:50:42 -0600 Subject: [PATCH 16/36] Judge resize of overlay by contentRect changing --- src/text-editor-component.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 2a77e30f8..91ea18361 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -126,7 +126,6 @@ class TextEditorComponent { this.blockDecorationResizeObserver = new ResizeObserver(this.didResizeBlockDecorations.bind(this)) this.lineComponentsByScreenLineId = new Map() this.overlayComponents = new Set() - this.overlayDimensionsByElement = new WeakMap() this.shouldRenderDummyScrollbars = true this.remeasureScrollbars = false this.pendingAutoscroll = null @@ -803,15 +802,9 @@ class TextEditorComponent { { key: overlayProps.element, overlayComponents: this.overlayComponents, - measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element), didResize: (overlayComponent) => { this.updateOverlayToRender(overlayProps) - overlayComponent.update(Object.assign( - { - measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element) - }, - overlayProps - )) + overlayComponent.update(overlayProps) } }, overlayProps @@ -1357,7 +1350,6 @@ class TextEditorComponent { let wrapperTop = contentClientRect.top + this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight() let wrapperLeft = contentClientRect.left + this.pixelLeftForRowAndColumn(row, column) const clientRect = element.getBoundingClientRect() - this.overlayDimensionsByElement.set(element, clientRect) if (avoidOverflow !== false) { const computedStyle = window.getComputedStyle(element) @@ -4226,17 +4218,26 @@ class OverlayComponent { this.element.style.zIndex = 4 this.element.style.top = (this.props.pixelTop || 0) + 'px' this.element.style.left = (this.props.pixelLeft || 0) + 'px' + this.currentContentRect = null // Synchronous DOM updates in response to resize events might trigger a // "loop limit exceeded" error. We disconnect the observer before // potentially mutating the DOM, and then reconnect it on the next tick. + // Note: ResizeObserver calls its callback when .observe is called this.resizeObserver = new ResizeObserver((entries) => { const {contentRect} = entries[0] - if (contentRect.width !== this.props.measuredDimensions.width || contentRect.height !== this.props.measuredDimensions.height) { + + if ( + this.currentContentRect && + (this.currentContentRect.width !== contentRect.width || + this.currentContentRect.height !== contentRect.height) + ) { this.resizeObserver.disconnect() this.props.didResize(this) process.nextTick(() => { this.resizeObserver.observe(this.props.element) }) } + + this.currentContentRect = contentRect }) this.didAttach() this.props.overlayComponents.add(this) From ada75ed1dd1ee354bcf43982cd2a9ee957952727 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 14:45:14 -0700 Subject: [PATCH 17/36] Fix bug in test --- spec/main-process/atom-application.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index c97c70746..70ebf686c 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -479,11 +479,11 @@ describe('AtomApplication', function () { atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileA}`])) await new Promise(res => setTimeout(res, 1000)) - await conditionPromise(() => window.atomApplication.getLastFocusedWindow() === window1) + await conditionPromise(() => atomApplication.getLastFocusedWindow() === window1) atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileB}`])) await new Promise(res => setTimeout(res, 1000)) - await conditionPromise(() => window.atomApplication.getLastFocusedWindow() === window2) + await conditionPromise(() => atomApplication.getLastFocusedWindow() === window2) }) }) }) From 667634191eeaa4629cad63f01fc6ceff2fbf8af3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 2 Nov 2017 16:15:36 -0600 Subject: [PATCH 18/36] Document hiddenInCommandPalette option in atom.commands.add --- src/command-registry.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/command-registry.js b/src/command-registry.js index ba75918ab..9e6d8c2e1 100644 --- a/src/command-registry.js +++ b/src/command-registry.js @@ -107,6 +107,13 @@ module.exports = class CommandRegistry { // otherwise be generated from the event name. // * `description`: Used by consumers to display detailed information about // the command. + // * `hiddenInCommandPalette`: If `true`, this command will not appear in + // the bundled command palette by default, but can still be shown with. + // the `Command Palette: Show Hidden Commands` command. This is a good + // option when you need to register large numbers of commands that don't + // make sense to be executed from the command palette. Please use this + // option conservatively, as it could reduce the discoverability of your + // package's commands. // // ## Arguments: Registering Multiple Commands // From 178756b62ae1245745f40473e1d44a689ae1a1fc Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 15:27:59 -0700 Subject: [PATCH 19/36] :white_check_mark: update test --- spec/main-process/atom-application.test.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 70ebf686c..69ddb3539 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -472,17 +472,13 @@ describe('AtomApplication', function () { const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath)])) await focusWindow(window2) - await new Promise(res => setTimeout(res, 2000)) - const fileA = path.join(dirAPath, 'file-a') const fileB = path.join(dirBPath, 'file-b') atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileA}`])) - await new Promise(res => setTimeout(res, 1000)) await conditionPromise(() => atomApplication.getLastFocusedWindow() === window1) atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileB}`])) - await new Promise(res => setTimeout(res, 1000)) await conditionPromise(() => atomApplication.getLastFocusedWindow() === window2) }) }) From 444597c8455501d8421d5dbe766adf28d4070187 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 15:49:06 -0700 Subject: [PATCH 20/36] Let's add some debugging --- spec/async-spec-helpers.js | 4 ++-- spec/main-process/atom-application.test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 69ddb3539..39863aa34 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -476,10 +476,10 @@ describe('AtomApplication', function () { const fileB = path.join(dirBPath, 'file-b') atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileA}`])) - await conditionPromise(() => atomApplication.getLastFocusedWindow() === window1) + await conditionPromise(() => atomApplication.getLastFocusedWindow() === window1, 'window1 to be focused') atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileB}`])) - await conditionPromise(() => atomApplication.getLastFocusedWindow() === window2) + await conditionPromise(() => atomApplication.getLastFocusedWindow() === window2, 'window2 to be focused') }) }) }) From 99bef8e7d1b2fdb1fe670a54fce4c3623b241343 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 16:09:57 -0700 Subject: [PATCH 21/36] More debugging --- spec/main-process/atom-application.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 39863aa34..a7096e49c 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -476,10 +476,10 @@ describe('AtomApplication', function () { const fileB = path.join(dirBPath, 'file-b') atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileA}`])) - await conditionPromise(() => atomApplication.getLastFocusedWindow() === window1, 'window1 to be focused') + await conditionPromise(() => atomApplication.getLastFocusedWindow() === window1, `window1 to be focused from ${fileA}`) atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileB}`])) - await conditionPromise(() => atomApplication.getLastFocusedWindow() === window2, 'window2 to be focused') + await conditionPromise(() => atomApplication.getLastFocusedWindow() === window2, `window2 to be focused from ${fileB}`) }) }) }) From 07ac7041d9385ea8aab7e7a89b75299ad1f66d2a Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 16:19:19 -0700 Subject: [PATCH 22/36] :arrow_up: settings-view@0.253.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b19548235..87b37cfb1 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "notifications": "0.69.2", "open-on-github": "1.3.0", "package-generator": "1.1.1", - "settings-view": "0.252.2", + "settings-view": "0.253.0", "snippets": "1.1.9", "spell-check": "0.72.3", "status-bar": "1.8.14", From 026782921145e492bfb1660f1d5cee763fa0ca78 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 16:45:52 -0700 Subject: [PATCH 23/36] Change the way we test this --- spec/main-process/atom-application.test.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index a7096e49c..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' @@ -473,13 +474,18 @@ describe('AtomApplication', function () { 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}` - atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileA}`])) - await conditionPromise(() => atomApplication.getLastFocusedWindow() === window1, `window1 to be focused from ${fileA}`) + sinon.spy(window1, 'sendURIMessage') + sinon.spy(window2, 'sendURIMessage') - atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileB}`])) - await conditionPromise(() => atomApplication.getLastFocusedWindow() === window2, `window2 to be focused from ${fileB}`) + 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}`) }) }) }) From 1ee1c6c30e60428a2727ecdd9679b48ccc7ab247 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 3 Nov 2017 07:44:06 -0400 Subject: [PATCH 24/36] =?UTF-8?q?=E2=98=A0=E2=98=95=E2=98=95=20Decaffeinat?= =?UTF-8?q?e=20src/token-iterator.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/token-iterator.coffee | 56 --------------------------- src/token-iterator.js | 79 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 56 deletions(-) delete mode 100644 src/token-iterator.coffee create mode 100644 src/token-iterator.js diff --git a/src/token-iterator.coffee b/src/token-iterator.coffee deleted file mode 100644 index f836d33d4..000000000 --- a/src/token-iterator.coffee +++ /dev/null @@ -1,56 +0,0 @@ -module.exports = -class TokenIterator - constructor: (@tokenizedBuffer) -> - - reset: (@line) -> - @index = null - @startColumn = 0 - @endColumn = 0 - @scopes = @line.openScopes.map (id) => @tokenizedBuffer.grammar.scopeForId(id) - @scopeStarts = @scopes.slice() - @scopeEnds = [] - this - - next: -> - {tags} = @line - - if @index? - @startColumn = @endColumn - @scopeEnds.length = 0 - @scopeStarts.length = 0 - @index++ - else - @index = 0 - - while @index < tags.length - tag = tags[@index] - if tag < 0 - scope = @tokenizedBuffer.grammar.scopeForId(tag) - if tag % 2 is 0 - if @scopeStarts[@scopeStarts.length - 1] is scope - @scopeStarts.pop() - else - @scopeEnds.push(scope) - @scopes.pop() - else - @scopeStarts.push(scope) - @scopes.push(scope) - @index++ - else - @endColumn += tag - @text = @line.text.substring(@startColumn, @endColumn) - return true - - false - - getScopes: -> @scopes - - getScopeStarts: -> @scopeStarts - - getScopeEnds: -> @scopeEnds - - getText: -> @text - - getBufferStart: -> @startColumn - - getBufferEnd: -> @endColumn diff --git a/src/token-iterator.js b/src/token-iterator.js new file mode 100644 index 000000000..a698fc748 --- /dev/null +++ b/src/token-iterator.js @@ -0,0 +1,79 @@ +module.exports = +class TokenIterator { + constructor (tokenizedBuffer) { + this.tokenizedBuffer = tokenizedBuffer + } + + reset (line) { + this.line = line + this.index = null + this.startColumn = 0 + this.endColumn = 0 + this.scopes = this.line.openScopes.map(id => this.tokenizedBuffer.grammar.scopeForId(id)) + this.scopeStarts = this.scopes.slice() + this.scopeEnds = [] + return this + } + + next () { + const {tags} = this.line + + if (this.index != null) { + this.startColumn = this.endColumn + this.scopeEnds.length = 0 + this.scopeStarts.length = 0 + this.index++ + } else { + this.index = 0 + } + + while (this.index < tags.length) { + const tag = tags[this.index] + if (tag < 0) { + const scope = this.tokenizedBuffer.grammar.scopeForId(tag) + if ((tag % 2) === 0) { + if (this.scopeStarts[this.scopeStarts.length - 1] === scope) { + this.scopeStarts.pop() + } else { + this.scopeEnds.push(scope) + } + this.scopes.pop() + } else { + this.scopeStarts.push(scope) + this.scopes.push(scope) + } + this.index++ + } else { + this.endColumn += tag + this.text = this.line.text.substring(this.startColumn, this.endColumn) + return true + } + } + + return false + } + + getScopes () { + return this.scopes + } + + getScopeStarts () { + return this.scopeStarts + } + + getScopeEnds () { + return this.scopeEnds + } + + getText () { + return this.text + } + + getBufferStart () { + return this.startColumn + } + + getBufferEnd () { + return this.endColumn + } +} From 7b5837afb896294ce4600a06a4ac212e1c8613f2 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 3 Nov 2017 07:53:28 -0400 Subject: [PATCH 25/36] =?UTF-8?q?=E2=98=A0=E2=98=95=E2=98=95=20Decaffeinat?= =?UTF-8?q?e=20spec/token-iterator-spec.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/token-iterator-spec.coffee | 37 ---------------------------- spec/token-iterator-spec.js | 43 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 37 deletions(-) delete mode 100644 spec/token-iterator-spec.coffee create mode 100644 spec/token-iterator-spec.js diff --git a/spec/token-iterator-spec.coffee b/spec/token-iterator-spec.coffee deleted file mode 100644 index 6ae01cd30..000000000 --- a/spec/token-iterator-spec.coffee +++ /dev/null @@ -1,37 +0,0 @@ -TextBuffer = require 'text-buffer' -TokenizedBuffer = require '../src/tokenized-buffer' - -describe "TokenIterator", -> - it "correctly terminates scopes at the beginning of the line (regression)", -> - grammar = atom.grammars.createGrammar('test', { - 'scopeName': 'text.broken' - 'name': 'Broken grammar' - 'patterns': [ - { - 'begin': 'start' - 'end': '(?=end)' - 'name': 'blue.broken' - } - { - 'match': '.' - 'name': 'yellow.broken' - } - ] - }) - - buffer = new TextBuffer(text: """ - start x - end x - x - """) - tokenizedBuffer = new TokenizedBuffer({ - buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert - }) - tokenizedBuffer.setGrammar(grammar) - - tokenIterator = tokenizedBuffer.tokenizedLines[1].getTokenIterator() - tokenIterator.next() - - expect(tokenIterator.getBufferStart()).toBe 0 - expect(tokenIterator.getScopeEnds()).toEqual [] - expect(tokenIterator.getScopeStarts()).toEqual ['text.broken', 'yellow.broken'] diff --git a/spec/token-iterator-spec.js b/spec/token-iterator-spec.js new file mode 100644 index 000000000..f6d43395c --- /dev/null +++ b/spec/token-iterator-spec.js @@ -0,0 +1,43 @@ +const TextBuffer = require('text-buffer') +const TokenizedBuffer = require('../src/tokenized-buffer') + +describe('TokenIterator', () => + it('correctly terminates scopes at the beginning of the line (regression)', () => { + const grammar = atom.grammars.createGrammar('test', { + 'scopeName': 'text.broken', + 'name': 'Broken grammar', + 'patterns': [ + { + 'begin': 'start', + 'end': '(?=end)', + 'name': 'blue.broken' + }, + { + 'match': '.', + 'name': 'yellow.broken' + } + ] + }) + + const buffer = new TextBuffer({text: `\ +start x +end x +x\ +`}) + const tokenizedBuffer = new TokenizedBuffer({ + buffer, + config: atom.config, + grammarRegistry: atom.grammars, + packageManager: atom.packages, + assert: atom.assert + }) + tokenizedBuffer.setGrammar(grammar) + + const tokenIterator = tokenizedBuffer.tokenizedLines[1].getTokenIterator() + tokenIterator.next() + + expect(tokenIterator.getBufferStart()).toBe(0) + expect(tokenIterator.getScopeEnds()).toEqual([]) + expect(tokenIterator.getScopeStarts()).toEqual(['text.broken', 'yellow.broken']) + }) +) From b7504a2a72838b3de293ae88d48e5ad7731d2c9d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 3 Nov 2017 06:17:07 -0600 Subject: [PATCH 26/36] :arrow_up: tree-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 87b37cfb1..d6656725a 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "symbols-view": "0.118.1", "tabs": "0.109.1", "timecop": "0.36.0", - "tree-view": "0.221.1", + "tree-view": "0.221.2", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", "whitespace": "0.37.5", From 5dd5b29ca9c2bb3bfa9c7170fb66227dbeaa515e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 3 Nov 2017 09:28:59 -0600 Subject: [PATCH 27/36] :arrow_up: fuzzy-finder --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d6656725a..6a7f57e33 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "encoding-selector": "0.23.7", "exception-reporting": "0.41.5", "find-and-replace": "0.213.0", - "fuzzy-finder": "1.7.2", + "fuzzy-finder": "1.7.3", "github": "0.8.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", From 6465f54bcd29aff07b462332d92d7b27db2101e5 Mon Sep 17 00:00:00 2001 From: Hubot Date: Fri, 3 Nov 2017 10:57:02 -0500 Subject: [PATCH 28/36] 1.24.0-dev --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6a7f57e33..09e5da8fe 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.23.0-dev", + "version": "1.24.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/main-process/main.js", "repository": { From ead3fdb46245625376ff3ca61cb337386a32ba03 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 3 Nov 2017 15:36:46 -0700 Subject: [PATCH 29/36] :arrow_up: github@0.8.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 09e5da8fe..dc814e954 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "exception-reporting": "0.41.5", "find-and-replace": "0.213.0", "fuzzy-finder": "1.7.3", - "github": "0.8.0", + "github": "0.8.1", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.8", From d89a8162621b1474cce27e5a4df60e941c2bbab8 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 4 Nov 2017 20:44:30 +0100 Subject: [PATCH 30/36] :arrow_up: timecop@0.36.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc814e954..b080c53d0 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "styleguide": "0.49.8", "symbols-view": "0.118.1", "tabs": "0.109.1", - "timecop": "0.36.0", + "timecop": "0.36.1", "tree-view": "0.221.2", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", From 4fd0345e8488d2fa09151316b4e99656e7861644 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sun, 5 Nov 2017 02:49:56 +0100 Subject: [PATCH 31/36] :arrow_up: timecop@0.36.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b080c53d0..92b80089a 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "styleguide": "0.49.8", "symbols-view": "0.118.1", "tabs": "0.109.1", - "timecop": "0.36.1", + "timecop": "0.36.2", "tree-view": "0.221.2", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", From 3f264fa2b0435fc7c2f7fa9d2f579dfe1d818fb9 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 6 Nov 2017 10:24:57 +0100 Subject: [PATCH 32/36] :arrow_up: styleguide@0.49.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 92b80089a..9aadf1484 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "snippets": "1.1.9", "spell-check": "0.72.3", "status-bar": "1.8.14", - "styleguide": "0.49.8", + "styleguide": "0.49.9", "symbols-view": "0.118.1", "tabs": "0.109.1", "timecop": "0.36.2", From 05fc82cb8653d0fbc1463acd3a50e340f074ce3a Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 6 Nov 2017 15:30:25 +0100 Subject: [PATCH 33/36] :arrow_up: status-bar@1.8.15 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9aadf1484..b010cd4a0 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "settings-view": "0.253.0", "snippets": "1.1.9", "spell-check": "0.72.3", - "status-bar": "1.8.14", + "status-bar": "1.8.15", "styleguide": "0.49.9", "symbols-view": "0.118.1", "tabs": "0.109.1", From 85f8b13a62ac79f2933ba9c784c07d1ba1c33d90 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 6 Nov 2017 09:44:05 -0800 Subject: [PATCH 34/36] :art: clean up git-repository.js --- src/git-repository.js | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/git-repository.js b/src/git-repository.js index 057c5fcb7..55d70c12c 100644 --- a/src/git-repository.js +++ b/src/git-repository.js @@ -1,15 +1,7 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS104: Avoid inline assignments - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const {join} = require('path') +const path = require('path') +const fs = require('fs-plus') const _ = require('underscore-plus') const {Emitter, Disposable, CompositeDisposable} = require('event-kit') -const fs = require('fs-plus') -const path = require('path') const GitUtils = require('git-utils') let nextId = 0 @@ -241,15 +233,15 @@ class GitRepository { // * `path` The {String} path to check. // // Returns a {Boolean}. - isSubmodule (path) { - if (!path) return false + isSubmodule (filePath) { + if (!filePath) return false - const repo = this.getRepo(path) - if (repo.isSubmodule(repo.relativize(path))) { + const repo = this.getRepo(filePath) + if (repo.isSubmodule(repo.relativize(filePath))) { return true } else { - // Check if the path is a working directory in a repo that isn't the root. - return repo !== this.getRepo() && repo.relativize(join(path, 'dir')) === 'dir' + // Check if the filePath is a working directory in a repo that isn't the root. + return repo !== this.getRepo() && repo.relativize(path.join(filePath, 'dir')) === 'dir' } } From 275fb0eb3645a883b5af0781f0137617f6867d7a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 6 Nov 2017 10:25:31 -0800 Subject: [PATCH 35/36] Convert GitRepository spec to JS --- spec/git-repository-spec.coffee | 371 --------------------------- spec/git-repository-spec.js | 433 ++++++++++++++++++++++++++++++++ 2 files changed, 433 insertions(+), 371 deletions(-) delete mode 100644 spec/git-repository-spec.coffee create mode 100644 spec/git-repository-spec.js diff --git a/spec/git-repository-spec.coffee b/spec/git-repository-spec.coffee deleted file mode 100644 index e4d1e0c7f..000000000 --- a/spec/git-repository-spec.coffee +++ /dev/null @@ -1,371 +0,0 @@ -temp = require('temp').track() -GitRepository = require '../src/git-repository' -fs = require 'fs-plus' -path = require 'path' -Project = require '../src/project' - -copyRepository = -> - workingDirPath = temp.mkdirSync('atom-spec-git') - fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) - fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) - workingDirPath - -describe "GitRepository", -> - repo = null - - beforeEach -> - gitPath = path.join(temp.dir, '.git') - fs.removeSync(gitPath) if fs.isDirectorySync(gitPath) - - afterEach -> - repo.destroy() if repo?.repo? - try - temp.cleanupSync() # These tests sometimes lag at shutting down resources - - describe "@open(path)", -> - it "returns null when no repository is found", -> - expect(GitRepository.open(path.join(temp.dir, 'nogit.txt'))).toBeNull() - - describe "new GitRepository(path)", -> - it "throws an exception when no repository is found", -> - expect(-> new GitRepository(path.join(temp.dir, 'nogit.txt'))).toThrow() - - describe ".getPath()", -> - it "returns the repository path for a .git directory path with a directory", -> - repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects')) - expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git') - - it "returns the repository path for a repository path", -> - repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git')) - expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git') - - describe ".isPathIgnored(path)", -> - it "returns true for an ignored path", -> - repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git')) - expect(repo.isPathIgnored('a.txt')).toBeTruthy() - - it "returns false for a non-ignored path", -> - repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git')) - expect(repo.isPathIgnored('b.txt')).toBeFalsy() - - describe ".isPathModified(path)", -> - [repo, filePath, newPath] = [] - - beforeEach -> - workingDirPath = copyRepository() - repo = new GitRepository(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') - newPath = path.join(workingDirPath, 'new-path.txt') - - describe "when the path is unstaged", -> - it "returns false if the path has not been modified", -> - expect(repo.isPathModified(filePath)).toBeFalsy() - - it "returns true if the path is modified", -> - fs.writeFileSync(filePath, "change") - expect(repo.isPathModified(filePath)).toBeTruthy() - - it "returns true if the path is deleted", -> - fs.removeSync(filePath) - expect(repo.isPathModified(filePath)).toBeTruthy() - - it "returns false if the path is new", -> - expect(repo.isPathModified(newPath)).toBeFalsy() - - describe ".isPathNew(path)", -> - [filePath, newPath] = [] - - beforeEach -> - workingDirPath = copyRepository() - repo = new GitRepository(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') - newPath = path.join(workingDirPath, 'new-path.txt') - fs.writeFileSync(newPath, "i'm new here") - - describe "when the path is unstaged", -> - it "returns true if the path is new", -> - expect(repo.isPathNew(newPath)).toBeTruthy() - - it "returns false if the path isn't new", -> - expect(repo.isPathNew(filePath)).toBeFalsy() - - describe ".checkoutHead(path)", -> - [filePath] = [] - - beforeEach -> - workingDirPath = copyRepository() - repo = new GitRepository(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') - - it "no longer reports a path as modified after checkout", -> - expect(repo.isPathModified(filePath)).toBeFalsy() - fs.writeFileSync(filePath, 'ch ch changes') - expect(repo.isPathModified(filePath)).toBeTruthy() - expect(repo.checkoutHead(filePath)).toBeTruthy() - expect(repo.isPathModified(filePath)).toBeFalsy() - - it "restores the contents of the path to the original text", -> - fs.writeFileSync(filePath, 'ch ch changes') - expect(repo.checkoutHead(filePath)).toBeTruthy() - expect(fs.readFileSync(filePath, 'utf8')).toBe '' - - it "fires a status-changed event if the checkout completes successfully", -> - fs.writeFileSync(filePath, 'ch ch changes') - repo.getPathStatus(filePath) - statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatus statusHandler - repo.checkoutHead(filePath) - expect(statusHandler.callCount).toBe 1 - expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: 0} - - repo.checkoutHead(filePath) - expect(statusHandler.callCount).toBe 1 - - describe ".checkoutHeadForEditor(editor)", -> - [filePath, editor] = [] - - beforeEach -> - spyOn(atom, "confirm") - - workingDirPath = copyRepository() - repo = new GitRepository(workingDirPath, {project: atom.project, config: atom.config, confirm: atom.confirm}) - filePath = path.join(workingDirPath, 'a.txt') - fs.writeFileSync(filePath, 'ch ch changes') - - waitsForPromise -> - atom.workspace.open(filePath) - - runs -> - editor = atom.workspace.getActiveTextEditor() - - it "displays a confirmation dialog by default", -> - return if process.platform is 'win32' # Permissions issues with this test on Windows - - atom.confirm.andCallFake ({buttons}) -> buttons.OK() - atom.config.set('editor.confirmCheckoutHeadRevision', true) - - repo.checkoutHeadForEditor(editor) - - expect(fs.readFileSync(filePath, 'utf8')).toBe '' - - it "does not display a dialog when confirmation is disabled", -> - return if process.platform is 'win32' # Flakey EPERM opening a.txt on Win32 - atom.config.set('editor.confirmCheckoutHeadRevision', false) - - repo.checkoutHeadForEditor(editor) - - expect(fs.readFileSync(filePath, 'utf8')).toBe '' - expect(atom.confirm).not.toHaveBeenCalled() - - describe ".destroy()", -> - it "throws an exception when any method is called after it is called", -> - repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git')) - repo.destroy() - expect(-> repo.getShortHead()).toThrow() - - describe ".getPathStatus(path)", -> - [filePath] = [] - - beforeEach -> - workingDirectory = copyRepository() - repo = new GitRepository(workingDirectory) - filePath = path.join(workingDirectory, 'file.txt') - - it "trigger a status-changed event when the new status differs from the last cached one", -> - statusHandler = jasmine.createSpy("statusHandler") - repo.onDidChangeStatus statusHandler - fs.writeFileSync(filePath, '') - status = repo.getPathStatus(filePath) - expect(statusHandler.callCount).toBe 1 - expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: status} - - fs.writeFileSync(filePath, 'abc') - status = repo.getPathStatus(filePath) - expect(statusHandler.callCount).toBe 1 - - describe ".getDirectoryStatus(path)", -> - [directoryPath, filePath] = [] - - beforeEach -> - workingDirectory = copyRepository() - repo = new GitRepository(workingDirectory) - directoryPath = path.join(workingDirectory, 'dir') - filePath = path.join(directoryPath, 'b.txt') - - it "gets the status based on the files inside the directory", -> - expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe false - fs.writeFileSync(filePath, 'abc') - repo.getPathStatus(filePath) - expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe true - - describe ".refreshStatus()", -> - [newPath, modifiedPath, cleanPath, workingDirectory] = [] - - beforeEach -> - workingDirectory = copyRepository() - repo = new GitRepository(workingDirectory, {project: atom.project, config: atom.config}) - modifiedPath = path.join(workingDirectory, 'file.txt') - newPath = path.join(workingDirectory, 'untracked.txt') - cleanPath = path.join(workingDirectory, 'other.txt') - fs.writeFileSync(cleanPath, 'Full of text') - fs.writeFileSync(newPath, '') - newPath = fs.absolute newPath # specs could be running under symbol path. - - it "returns status information for all new and modified files", -> - fs.writeFileSync(modifiedPath, 'making this path modified') - statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatuses statusHandler - repo.refreshStatus() - - waitsFor -> - statusHandler.callCount > 0 - - runs -> - expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() - - it 'caches the proper statuses when a subdir is open', -> - subDir = path.join(workingDirectory, 'dir') - fs.mkdirSync(subDir) - - filePath = path.join(subDir, 'b.txt') - fs.writeFileSync(filePath, '') - - atom.project.setPaths([subDir]) - - waitsForPromise -> - atom.workspace.open('b.txt') - - statusHandler = null - runs -> - repo = atom.project.getRepositories()[0] - - statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatuses statusHandler - repo.refreshStatus() - - waitsFor -> - statusHandler.callCount > 0 - - runs -> - status = repo.getCachedPathStatus(filePath) - expect(repo.isStatusModified(status)).toBe false - expect(repo.isStatusNew(status)).toBe false - - it "works correctly when the project has multiple folders (regression)", -> - atom.project.addPath(workingDirectory) - atom.project.addPath(path.join(__dirname, 'fixtures', 'dir')) - statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatuses statusHandler - - repo.refreshStatus() - - waitsFor -> - statusHandler.callCount > 0 - - runs -> - expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() - - it 'caches statuses that were looked up synchronously', -> - originalContent = 'undefined' - fs.writeFileSync(modifiedPath, 'making this path modified') - repo.getPathStatus('file.txt') - - fs.writeFileSync(modifiedPath, originalContent) - waitsForPromise -> repo.refreshStatus() - runs -> - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy() - - describe "buffer events", -> - [editor] = [] - - beforeEach -> - statusRefreshed = false - atom.project.setPaths([copyRepository()]) - atom.project.getRepositories()[0].onDidChangeStatuses -> statusRefreshed = true - - waitsForPromise -> - atom.workspace.open('other.txt').then (o) -> editor = o - - waitsFor 'repo to refresh', -> statusRefreshed - - it "emits a status-changed event when a buffer is saved", -> - editor.insertNewline() - - statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].onDidChangeStatus statusHandler - - waitsForPromise -> - editor.save() - - runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} - - it "emits a status-changed event when a buffer is reloaded", -> - fs.writeFileSync(editor.getPath(), 'changed') - - statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].onDidChangeStatus statusHandler - - waitsForPromise -> - editor.getBuffer().reload() - - runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} - - waitsForPromise -> - editor.getBuffer().reload() - - runs -> - expect(statusHandler.callCount).toBe 1 - - it "emits a status-changed event when a buffer's path changes", -> - fs.writeFileSync(editor.getPath(), 'changed') - - statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].onDidChangeStatus statusHandler - editor.getBuffer().emitter.emit 'did-change-path' - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} - editor.getBuffer().emitter.emit 'did-change-path' - expect(statusHandler.callCount).toBe 1 - - it "stops listening to the buffer when the repository is destroyed (regression)", -> - atom.project.getRepositories()[0].destroy() - expect(-> editor.save()).not.toThrow() - - describe "when a project is deserialized", -> - [buffer, project2, statusHandler] = [] - - afterEach -> - project2?.destroy() - - it "subscribes to all the serialized buffers in the project", -> - atom.project.setPaths([copyRepository()]) - - waitsForPromise -> - atom.workspace.open('file.txt') - - waitsForPromise -> - project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm, applicationDelegate: atom.applicationDelegate}) - project2.deserialize(atom.project.serialize({isUnloading: false})) - - waitsFor -> - buffer = project2.getBuffers()[0] - - waitsForPromise -> - originalContent = buffer.getText() - buffer.append('changes') - - statusHandler = jasmine.createSpy('statusHandler') - project2.getRepositories()[0].onDidChangeStatus statusHandler - buffer.save() - - runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256} diff --git a/spec/git-repository-spec.js b/spec/git-repository-spec.js new file mode 100644 index 000000000..1dfad182d --- /dev/null +++ b/spec/git-repository-spec.js @@ -0,0 +1,433 @@ +const path = require('path') +const fs = require('fs-plus') +const temp = require('temp').track() +const GitRepository = require('../src/git-repository') +const Project = require('../src/project') + +describe('GitRepository', () => { + let repo + + beforeEach(() => { + const gitPath = path.join(temp.dir, '.git') + if (fs.isDirectorySync(gitPath)) fs.removeSync(gitPath) + }) + + afterEach(() => { + if (repo && !repo.isDestroyed()) repo.destroy() + + // These tests sometimes lag at shutting down resources + try { + temp.cleanupSync() + } catch (error) {} + }) + + describe('@open(path)', () => { + it('returns null when no repository is found', () => { + expect(GitRepository.open(path.join(temp.dir, 'nogit.txt'))).toBeNull() + }) + }) + + describe('new GitRepository(path)', () => { + it('throws an exception when no repository is found', () => { + expect(() => new GitRepository(path.join(temp.dir, 'nogit.txt'))).toThrow() + }) + }) + + describe('.getPath()', () => { + it('returns the repository path for a .git directory path with a directory', () => { + repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects')) + expect(repo.getPath()).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) + }) + + it('returns the repository path for a repository path', () => { + repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git')) + expect(repo.getPath()).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) + }) + }) + + describe('.isPathIgnored(path)', () => { + it('returns true for an ignored path', () => { + repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git')) + expect(repo.isPathIgnored('a.txt')).toBeTruthy() + }) + + it('returns false for a non-ignored path', () => { + repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git')) + expect(repo.isPathIgnored('b.txt')).toBeFalsy() + }) + }) + + describe('.isPathModified(path)', () => { + let filePath, newPath + + beforeEach(() => { + const workingDirPath = copyRepository() + repo = new GitRepository(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + newPath = path.join(workingDirPath, 'new-path.txt') + }) + + describe('when the path is unstaged', () => { + it('returns false if the path has not been modified', () => { + expect(repo.isPathModified(filePath)).toBeFalsy() + }) + + it('returns true if the path is modified', () => { + fs.writeFileSync(filePath, 'change') + expect(repo.isPathModified(filePath)).toBeTruthy() + }) + + it('returns true if the path is deleted', () => { + fs.removeSync(filePath) + expect(repo.isPathModified(filePath)).toBeTruthy() + }) + + it('returns false if the path is new', () => { + expect(repo.isPathModified(newPath)).toBeFalsy() + }) + }) + }) + + describe('.isPathNew(path)', () => { + let filePath, newPath + + beforeEach(() => { + const workingDirPath = copyRepository() + repo = new GitRepository(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + newPath = path.join(workingDirPath, 'new-path.txt') + fs.writeFileSync(newPath, "i'm new here") + }) + + describe('when the path is unstaged', () => { + it('returns true if the path is new', () => { + expect(repo.isPathNew(newPath)).toBeTruthy() + }) + + it("returns false if the path isn't new", () => { + expect(repo.isPathNew(filePath)).toBeFalsy() + }) + }) + }) + + describe('.checkoutHead(path)', () => { + let filePath + + beforeEach(() => { + const workingDirPath = copyRepository() + repo = new GitRepository(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + }) + + it('no longer reports a path as modified after checkout', () => { + expect(repo.isPathModified(filePath)).toBeFalsy() + fs.writeFileSync(filePath, 'ch ch changes') + expect(repo.isPathModified(filePath)).toBeTruthy() + expect(repo.checkoutHead(filePath)).toBeTruthy() + expect(repo.isPathModified(filePath)).toBeFalsy() + }) + + it('restores the contents of the path to the original text', () => { + fs.writeFileSync(filePath, 'ch ch changes') + expect(repo.checkoutHead(filePath)).toBeTruthy() + expect(fs.readFileSync(filePath, 'utf8')).toBe('') + }) + + it('fires a status-changed event if the checkout completes successfully', () => { + fs.writeFileSync(filePath, 'ch ch changes') + repo.getPathStatus(filePath) + const statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatus(statusHandler) + repo.checkoutHead(filePath) + expect(statusHandler.callCount).toBe(1) + expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: 0}) + + repo.checkoutHead(filePath) + expect(statusHandler.callCount).toBe(1) + }) + }) + + describe('.checkoutHeadForEditor(editor)', () => { + let filePath, editor + + beforeEach(() => { + spyOn(atom, 'confirm') + + const workingDirPath = copyRepository() + repo = new GitRepository(workingDirPath, {project: atom.project, config: atom.config, confirm: atom.confirm}) + filePath = path.join(workingDirPath, 'a.txt') + fs.writeFileSync(filePath, 'ch ch changes') + + waitsForPromise(() => atom.workspace.open(filePath)) + + runs(() => editor = atom.workspace.getActiveTextEditor()) + }) + + it('displays a confirmation dialog by default', () => { + // Permissions issues with this test on Windows + if (process.platform === 'win32') return + + atom.confirm.andCallFake(({buttons}) => buttons.OK()) + atom.config.set('editor.confirmCheckoutHeadRevision', true) + + repo.checkoutHeadForEditor(editor) + + expect(fs.readFileSync(filePath, 'utf8')).toBe('') + }) + + it('does not display a dialog when confirmation is disabled', () => { + // Flakey EPERM opening a.txt on Win32 + if (process.platform === 'win32') return + atom.config.set('editor.confirmCheckoutHeadRevision', false) + + repo.checkoutHeadForEditor(editor) + + expect(fs.readFileSync(filePath, 'utf8')).toBe('') + expect(atom.confirm).not.toHaveBeenCalled() + }) + }) + + describe('.destroy()', () => { + it('throws an exception when any method is called after it is called', () => { + repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git')) + repo.destroy() + expect(() => repo.getShortHead()).toThrow() + }) + }) + + describe('.getPathStatus(path)', () => { + let filePath + + beforeEach(() => { + const workingDirectory = copyRepository() + repo = new GitRepository(workingDirectory) + filePath = path.join(workingDirectory, 'file.txt') + }) + + it('trigger a status-changed event when the new status differs from the last cached one', () => { + const statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatus(statusHandler) + fs.writeFileSync(filePath, '') + let status = repo.getPathStatus(filePath) + expect(statusHandler.callCount).toBe(1) + expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) + + fs.writeFileSync(filePath, 'abc') + status = repo.getPathStatus(filePath) + expect(statusHandler.callCount).toBe(1) + }) + }) + + describe('.getDirectoryStatus(path)', () => { + let directoryPath, filePath + + beforeEach(() => { + const workingDirectory = copyRepository() + repo = new GitRepository(workingDirectory) + directoryPath = path.join(workingDirectory, 'dir') + filePath = path.join(directoryPath, 'b.txt') + }) + + it('gets the status based on the files inside the directory', () => { + expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe(false) + fs.writeFileSync(filePath, 'abc') + repo.getPathStatus(filePath) + expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe(true) + }) + }) + + describe('.refreshStatus()', () => { + let newPath, modifiedPath, cleanPath, workingDirectory + + beforeEach(() => { + workingDirectory = copyRepository() + repo = new GitRepository(workingDirectory, {project: atom.project, config: atom.config}) + modifiedPath = path.join(workingDirectory, 'file.txt') + newPath = path.join(workingDirectory, 'untracked.txt') + cleanPath = path.join(workingDirectory, 'other.txt') + fs.writeFileSync(cleanPath, 'Full of text') + fs.writeFileSync(newPath, '') + newPath = fs.absolute(newPath) + }) // specs could be running under symbol path. + + it('returns status information for all new and modified files', () => { + fs.writeFileSync(modifiedPath, 'making this path modified') + const statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatuses(statusHandler) + repo.refreshStatus() + + waitsFor(() => statusHandler.callCount > 0) + + runs(() => { + expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + }) + }) + + it('caches the proper statuses when a subdir is open', () => { + const subDir = path.join(workingDirectory, 'dir') + fs.mkdirSync(subDir) + + const filePath = path.join(subDir, 'b.txt') + fs.writeFileSync(filePath, '') + + atom.project.setPaths([subDir]) + + waitsForPromise(() => atom.workspace.open('b.txt')) + + let statusHandler = null + runs(() => { + repo = atom.project.getRepositories()[0] + + statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatuses(statusHandler) + repo.refreshStatus() + }) + + waitsFor(() => statusHandler.callCount > 0) + + runs(() => { + const status = repo.getCachedPathStatus(filePath) + expect(repo.isStatusModified(status)).toBe(false) + expect(repo.isStatusNew(status)).toBe(false) + }) + }) + + it('works correctly when the project has multiple folders (regression)', () => { + atom.project.addPath(workingDirectory) + atom.project.addPath(path.join(__dirname, 'fixtures', 'dir')) + const statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatuses(statusHandler) + + repo.refreshStatus() + + waitsFor(() => statusHandler.callCount > 0) + + runs(() => { + expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + }) + }) + + it('caches statuses that were looked up synchronously', () => { + const originalContent = 'undefined' + fs.writeFileSync(modifiedPath, 'making this path modified') + repo.getPathStatus('file.txt') + + fs.writeFileSync(modifiedPath, originalContent) + waitsForPromise(() => repo.refreshStatus()) + runs(() => { + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy() + }) + }) + }) + + describe('buffer events', () => { + let editor + + beforeEach(() => { + let statusRefreshed = false + atom.project.setPaths([copyRepository()]) + atom.project.getRepositories()[0].onDidChangeStatuses(() => statusRefreshed = true) + + waitsForPromise(() => atom.workspace.open('other.txt').then(o => editor = o)) + + waitsFor('repo to refresh', () => statusRefreshed) + }) + + it('emits a status-changed event when a buffer is saved', () => { + editor.insertNewline() + + const statusHandler = jasmine.createSpy('statusHandler') + atom.project.getRepositories()[0].onDidChangeStatus(statusHandler) + + waitsForPromise(() => editor.save()) + + runs(() => { + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + }) + }) + + it('emits a status-changed event when a buffer is reloaded', () => { + fs.writeFileSync(editor.getPath(), 'changed') + + const statusHandler = jasmine.createSpy('statusHandler') + atom.project.getRepositories()[0].onDidChangeStatus(statusHandler) + + waitsForPromise(() => editor.getBuffer().reload()) + + runs(() => { + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + }) + + waitsForPromise(() => editor.getBuffer().reload()) + + runs(() => { + expect(statusHandler.callCount).toBe(1) + }) + }) + + it("emits a status-changed event when a buffer's path changes", () => { + fs.writeFileSync(editor.getPath(), 'changed') + + const statusHandler = jasmine.createSpy('statusHandler') + atom.project.getRepositories()[0].onDidChangeStatus(statusHandler) + editor.getBuffer().emitter.emit('did-change-path') + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + editor.getBuffer().emitter.emit('did-change-path') + expect(statusHandler.callCount).toBe(1) + }) + + it('stops listening to the buffer when the repository is destroyed (regression)', () => { + atom.project.getRepositories()[0].destroy() + expect(() => editor.save()).not.toThrow() + }) + }) + + describe('when a project is deserialized', () => { + let buffer, project2, statusHandler + + afterEach(() => { + if (project2) project2.destroy() + }) + + it('subscribes to all the serialized buffers in the project', () => { + atom.project.setPaths([copyRepository()]) + + waitsForPromise(() => atom.workspace.open('file.txt')) + + waitsForPromise(() => { + project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm, applicationDelegate: atom.applicationDelegate}) + return project2.deserialize(atom.project.serialize({isUnloading: false})) + }) + + waitsFor(() => buffer = project2.getBuffers()[0]) + + waitsForPromise(() => { + const originalContent = buffer.getText() + buffer.append('changes') + + statusHandler = jasmine.createSpy('statusHandler') + project2.getRepositories()[0].onDidChangeStatus(statusHandler) + return buffer.save() + }) + + runs(() => { + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: buffer.getPath(), pathStatus: 256}) + }) + }) + }) +}) + +function copyRepository () { + const workingDirPath = temp.mkdirSync('atom-spec-git') + fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) + fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) + return workingDirPath +} From 6e0b629389610120ae817cd79e6bd4f511889a1a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 6 Nov 2017 10:57:46 -0800 Subject: [PATCH 36/36] Use async and await in git-repository-spec --- spec/git-repository-spec.js | 160 ++++++++++++++---------------------- 1 file changed, 60 insertions(+), 100 deletions(-) diff --git a/spec/git-repository-spec.js b/spec/git-repository-spec.js index 1dfad182d..e03a9788a 100644 --- a/spec/git-repository-spec.js +++ b/spec/git-repository-spec.js @@ -1,3 +1,4 @@ +const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') const path = require('path') const fs = require('fs-plus') const temp = require('temp').track() @@ -150,7 +151,7 @@ describe('GitRepository', () => { describe('.checkoutHeadForEditor(editor)', () => { let filePath, editor - beforeEach(() => { + beforeEach(async () => { spyOn(atom, 'confirm') const workingDirPath = copyRepository() @@ -158,9 +159,7 @@ describe('GitRepository', () => { filePath = path.join(workingDirPath, 'a.txt') fs.writeFileSync(filePath, 'ch ch changes') - waitsForPromise(() => atom.workspace.open(filePath)) - - runs(() => editor = atom.workspace.getActiveTextEditor()) + editor = await atom.workspace.open(filePath) }) it('displays a confirmation dialog by default', () => { @@ -248,127 +247,89 @@ describe('GitRepository', () => { fs.writeFileSync(cleanPath, 'Full of text') fs.writeFileSync(newPath, '') newPath = fs.absolute(newPath) - }) // specs could be running under symbol path. - - it('returns status information for all new and modified files', () => { - fs.writeFileSync(modifiedPath, 'making this path modified') - const statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatuses(statusHandler) - repo.refreshStatus() - - waitsFor(() => statusHandler.callCount > 0) - - runs(() => { - expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() - }) }) - it('caches the proper statuses when a subdir is open', () => { + it('returns status information for all new and modified files', async () => { + const statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatuses(statusHandler) + fs.writeFileSync(modifiedPath, 'making this path modified') + + await repo.refreshStatus() + expect(statusHandler.callCount).toBe(1) + expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(repo.getCachedPathStatus(newPath) )).toBeTruthy() + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + }) + + it('caches the proper statuses when a subdir is open', async () => { const subDir = path.join(workingDirectory, 'dir') fs.mkdirSync(subDir) - const filePath = path.join(subDir, 'b.txt') fs.writeFileSync(filePath, '') - atom.project.setPaths([subDir]) + await atom.workspace.open('b.txt') + repo = atom.project.getRepositories()[0] - waitsForPromise(() => atom.workspace.open('b.txt')) - - let statusHandler = null - runs(() => { - repo = atom.project.getRepositories()[0] - - statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatuses(statusHandler) - repo.refreshStatus() - }) - - waitsFor(() => statusHandler.callCount > 0) - - runs(() => { - const status = repo.getCachedPathStatus(filePath) - expect(repo.isStatusModified(status)).toBe(false) - expect(repo.isStatusNew(status)).toBe(false) - }) + await repo.refreshStatus() + const status = repo.getCachedPathStatus(filePath) + expect(repo.isStatusModified(status)).toBe(false) + expect(repo.isStatusNew(status)).toBe(false) }) - it('works correctly when the project has multiple folders (regression)', () => { + it('works correctly when the project has multiple folders (regression)', async () => { atom.project.addPath(workingDirectory) atom.project.addPath(path.join(__dirname, 'fixtures', 'dir')) - const statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatuses(statusHandler) - repo.refreshStatus() - - waitsFor(() => statusHandler.callCount > 0) - - runs(() => { - expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() - }) + await repo.refreshStatus() + expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() }) - it('caches statuses that were looked up synchronously', () => { + it('caches statuses that were looked up synchronously', async () => { const originalContent = 'undefined' fs.writeFileSync(modifiedPath, 'making this path modified') repo.getPathStatus('file.txt') fs.writeFileSync(modifiedPath, originalContent) - waitsForPromise(() => repo.refreshStatus()) - runs(() => { - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy() - }) + await repo.refreshStatus() + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy() }) }) describe('buffer events', () => { let editor - beforeEach(() => { - let statusRefreshed = false + beforeEach(async () => { atom.project.setPaths([copyRepository()]) - atom.project.getRepositories()[0].onDidChangeStatuses(() => statusRefreshed = true) - - waitsForPromise(() => atom.workspace.open('other.txt').then(o => editor = o)) - - waitsFor('repo to refresh', () => statusRefreshed) + const refreshPromise = new Promise(resolve => atom.project.getRepositories()[0].onDidChangeStatuses(resolve)) + editor = await atom.workspace.open('other.txt') + await refreshPromise }) - it('emits a status-changed event when a buffer is saved', () => { + it('emits a status-changed event when a buffer is saved', async () => { editor.insertNewline() const statusHandler = jasmine.createSpy('statusHandler') atom.project.getRepositories()[0].onDidChangeStatus(statusHandler) - waitsForPromise(() => editor.save()) - - runs(() => { - expect(statusHandler.callCount).toBe(1) - expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - }) + await editor.save() + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) }) - it('emits a status-changed event when a buffer is reloaded', () => { + it('emits a status-changed event when a buffer is reloaded', async () => { fs.writeFileSync(editor.getPath(), 'changed') const statusHandler = jasmine.createSpy('statusHandler') atom.project.getRepositories()[0].onDidChangeStatus(statusHandler) - waitsForPromise(() => editor.getBuffer().reload()) + await editor.getBuffer().reload() + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - runs(() => { - expect(statusHandler.callCount).toBe(1) - expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - }) - - waitsForPromise(() => editor.getBuffer().reload()) - - runs(() => { - expect(statusHandler.callCount).toBe(1) - }) + await editor.getBuffer().reload() + expect(statusHandler.callCount).toBe(1) }) it("emits a status-changed event when a buffer's path changes", () => { @@ -396,31 +357,30 @@ describe('GitRepository', () => { if (project2) project2.destroy() }) - it('subscribes to all the serialized buffers in the project', () => { + it('subscribes to all the serialized buffers in the project', async () => { atom.project.setPaths([copyRepository()]) - waitsForPromise(() => atom.workspace.open('file.txt')) + await atom.workspace.open('file.txt') - waitsForPromise(() => { - project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm, applicationDelegate: atom.applicationDelegate}) - return project2.deserialize(atom.project.serialize({isUnloading: false})) + project2 = new Project({ + notificationManager: atom.notifications, + packageManager: atom.packages, + confirm: atom.confirm, + applicationDelegate: atom.applicationDelegate }) + await project2.deserialize(atom.project.serialize({isUnloading: false})) - waitsFor(() => buffer = project2.getBuffers()[0]) + buffer = project2.getBuffers()[0] - waitsForPromise(() => { - const originalContent = buffer.getText() - buffer.append('changes') + const originalContent = buffer.getText() + buffer.append('changes') - statusHandler = jasmine.createSpy('statusHandler') - project2.getRepositories()[0].onDidChangeStatus(statusHandler) - return buffer.save() - }) + statusHandler = jasmine.createSpy('statusHandler') + project2.getRepositories()[0].onDidChangeStatus(statusHandler) + await buffer.save() - runs(() => { - expect(statusHandler.callCount).toBe(1) - expect(statusHandler).toHaveBeenCalledWith({path: buffer.getPath(), pathStatus: 256}) - }) + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: buffer.getPath(), pathStatus: 256}) }) }) })