From 7c681905bd81948a09b54fc6013a917852148f12 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 31 May 2017 20:04:11 -0700 Subject: [PATCH 001/314] Add keyb accelerators to file change & window state dialogs. Fixes #2928 --- src/atom-environment.coffee | 4 ++-- src/pane.coffee | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index e97b14508..53d55a9f7 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -933,8 +933,8 @@ class AtomEnvironment extends Model "Would you like to add the #{nouns} to this window, permanently discarding the saved state, " + "or open the #{nouns} in a new window, restoring the saved state?" buttons: [ - 'Open in new window and recover state' - 'Add to this window and discard state' + '&Open in new window and recover state' + '&Add to this window and discard state' ] if btn is 0 @open diff --git a/src/pane.coffee b/src/pane.coffee index 31ee92f77..e5dc022a0 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -640,7 +640,7 @@ class Pane chosen = @applicationDelegate.confirm message: message detailedMessage: "Your changes will be lost if you close this item without saving." - buttons: [saveButtonText, "Cancel", "Don't Save"] + buttons: [saveButtonText, "Cancel", "&Don't Save"] switch chosen when 0 then saveFn(item, saveError) when 1 then false From ca6254dd6624efe51a51e7829ddec9b40ed25477 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Wed, 7 Jun 2017 16:24:28 -0700 Subject: [PATCH 002/314] Add methods for observing dock visibility --- spec/dock-spec.js | 12 ++++++++++++ src/dock.js | 37 ++++++++++++++++++++++++++++++------- src/workspace.js | 25 +++++++++++++++---------- 3 files changed, 57 insertions(+), 17 deletions(-) diff --git a/spec/dock-spec.js b/spec/dock-spec.js index e554094f2..d4db460ae 100644 --- a/spec/dock-spec.js +++ b/spec/dock-spec.js @@ -9,12 +9,15 @@ describe('Dock', () => { it('opens the dock and activates its active pane', () => { jasmine.attachToDOM(atom.workspace.getElement()) const dock = atom.workspace.getLeftDock() + const didChangeVisibleSpy = jasmine.createSpy() + dock.onDidChangeVisible(didChangeVisibleSpy) expect(dock.isVisible()).toBe(false) expect(document.activeElement).toBe(atom.workspace.getCenter().getActivePane().getElement()) dock.activate() expect(dock.isVisible()).toBe(true) expect(document.activeElement).toBe(dock.getActivePane().getElement()) + expect(didChangeVisibleSpy).toHaveBeenCalledWith(true) }) }) @@ -22,17 +25,24 @@ describe('Dock', () => { it('transfers focus back to the active center pane if the dock had focus', () => { jasmine.attachToDOM(atom.workspace.getElement()) const dock = atom.workspace.getLeftDock() + const didChangeVisibleSpy = jasmine.createSpy() + dock.onDidChangeVisible(didChangeVisibleSpy) + dock.activate() expect(document.activeElement).toBe(dock.getActivePane().getElement()) + expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(true) dock.hide() expect(document.activeElement).toBe(atom.workspace.getCenter().getActivePane().getElement()) + expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(false) dock.activate() expect(document.activeElement).toBe(dock.getActivePane().getElement()) + expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(true) dock.toggle() expect(document.activeElement).toBe(atom.workspace.getCenter().getActivePane().getElement()) + expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(false) // Don't change focus if the dock was not focused in the first place const modalElement = document.createElement('div') @@ -43,9 +53,11 @@ describe('Dock', () => { dock.show() expect(document.activeElement).toBe(modalElement) + expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(true) dock.hide() expect(document.activeElement).toBe(modalElement) + expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(false) }) }) diff --git a/src/dock.js b/src/dock.js index 68a34464b..30284f884 100644 --- a/src/dock.js +++ b/src/dock.js @@ -1,7 +1,7 @@ 'use strict' const _ = require('underscore-plus') -const {CompositeDisposable} = require('event-kit') +const {CompositeDisposable, Emitter} = require('event-kit') const PaneContainer = require('./pane-container') const TextEditor = require('./text-editor') const Grim = require('grim') @@ -35,7 +35,8 @@ module.exports = class Dock { this.notificationManager = params.notificationManager this.viewRegistry = params.viewRegistry this.didActivate = params.didActivate - this.didHide = params.didHide + + this.emitter = new Emitter() this.paneContainer = new PaneContainer({ location: this.location, @@ -53,6 +54,7 @@ module.exports = class Dock { } this.subscriptions = new CompositeDisposable( + this.emitter, this.paneContainer.onDidActivatePane(() => { this.show() this.didActivate(this) @@ -135,14 +137,12 @@ module.exports = class Dock { setState (newState) { const prevState = this.state const nextState = Object.assign({}, prevState, newState) - let didHide = false // Update the `shouldAnimate` state. This needs to be written to the DOM before updating the // class that changes the animated property. Normally we'd have to defer the class change a // frame to ensure the property is animated (or not) appropriately, however we luck out in this // case because the drag start always happens before the item is dragged into the toggle button. if (nextState.visible !== prevState.visible) { - didHide = !nextState.visible // Never animate toggling visiblity... nextState.shouldAnimate = false } else if (!nextState.visible && nextState.draggingItem && !prevState.draggingItem) { @@ -152,7 +152,11 @@ module.exports = class Dock { this.state = nextState this.render(this.state) - if (didHide) this.didHide(this) + + const {visible} = this.state + if (visible !== prevState.visible) { + this.emitter.emit('did-change-visible', visible) + } } render (state) { @@ -379,12 +383,31 @@ module.exports = class Dock { }) } - // PaneContainer-delegating methods - /* Section: Event Subscription */ + // Essential: Invoke the given callback when the visibility of the dock changes. + // + // * `callback` {Function} to be called when the visibility changes. + // * `visible` {Boolean} Is the dock now visible? + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeVisible (callback) { + return this.emitter.on('did-change-visible', callback) + } + + // Essential: Invoke the given callback with the current and all future visibilities of the dock. + // + // * `callback` {Function} to be called when the visibility changes. + // * `visible` {Boolean} Is the dock now visible? + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeVisible (callback) { + callback(this.isVisible()) + return this.onDidChangeVisible(callback) + } + // Essential: Invoke the given callback with all current and future panes items // in the dock. // diff --git a/src/workspace.js b/src/workspace.js index 033b7c8c5..e8679fd83 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -184,7 +184,6 @@ module.exports = class Workspace extends Model { this.didChangeActivePaneOnPaneContainer = this.didChangeActivePaneOnPaneContainer.bind(this) this.didChangeActivePaneItemOnPaneContainer = this.didChangeActivePaneItemOnPaneContainer.bind(this) this.didActivatePaneContainer = this.didActivatePaneContainer.bind(this) - this.didHideDock = this.didHideDock.bind(this) this.enablePersistence = params.enablePersistence this.packageManager = params.packageManager @@ -270,7 +269,6 @@ module.exports = class Workspace extends Model { deserializerManager: this.deserializerManager, notificationManager: this.notificationManager, viewRegistry: this.viewRegistry, - didHide: this.didHideDock, didActivate: this.didActivatePaneContainer, didChangeActivePane: this.didChangeActivePaneOnPaneContainer, didChangeActivePaneItem: this.didChangeActivePaneItemOnPaneContainer, @@ -321,6 +319,7 @@ module.exports = class Workspace extends Model { this.subscribeToFontSize() this.subscribeToAddedItems() this.subscribeToMovedItems() + this.subscribeToDockToggling() } consumeServices ({serviceHub}) { @@ -484,14 +483,6 @@ module.exports = class Workspace extends Model { } } - didHideDock (dock) { - const {activeElement} = document - const dockElement = dock.getElement() - if (dockElement === activeElement || dockElement.contains(activeElement)) { - this.getCenter().activate() - } - } - setDraggingItem (draggingItem) { _.values(this.paneContainers).forEach(dock => { dock.setDraggingItem(draggingItem) @@ -513,6 +504,20 @@ module.exports = class Workspace extends Model { }) } + subscribeToDockToggling () { + const docks = [this.getLeftDock(), this.getRightDock(), this.getBottomDock()] + docks.forEach(dock => { + dock.onDidChangeVisible(visible => { + if (visible) return + const {activeElement} = document + const dockElement = dock.getElement() + if (dockElement === activeElement || dockElement.contains(activeElement)) { + this.getCenter().activate() + } + }) + }) + } + subscribeToMovedItems () { for (const paneContainer of this.getPaneContainers()) { paneContainer.observePanes(pane => { From 5048f46cd78ace21129854aaf26615c104476b2b Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 12 Jun 2017 12:04:22 -0400 Subject: [PATCH 003/314] Update CSP to allow using eval from JavaScript Signed-off-by: Antonio Scandurra --- static/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/index.html b/static/index.html index 386481bb5..ab239dd5c 100644 --- a/static/index.html +++ b/static/index.html @@ -1,7 +1,7 @@ - + From 18e3f52f74a48b1536278b33c281ce39dfa135a3 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 13 Jun 2017 13:52:09 -0700 Subject: [PATCH 004/314] Appveyor switch to Node 6.9.4 from 6.8.0 --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 79f4f0cbd..236f90af9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,7 +20,7 @@ environment: ATOM_DEV_RESOURCE_PATH: c:\projects\atom matrix: - - NODE_VERSION: 6.8.0 + - NODE_VERSION: 6.9.4 matrix: fast_finish: true From 8c16455bdd735508a91c863878a08c334817ea58 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Thu, 15 Jun 2017 11:24:05 -0700 Subject: [PATCH 005/314] These caches get corrupted, let's just disable them for now --- appveyor.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 79f4f0cbd..11ab7b3ba 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -58,10 +58,6 @@ artifacts: name: atom-full.nupkg cache: - - '%APPVEYOR_BUILD_FOLDER%\script\node_modules' - - '%APPVEYOR_BUILD_FOLDER%\apm\node_modules' - - '%APPVEYOR_BUILD_FOLDER%\node_modules' - '%APPVEYOR_BUILD_FOLDER%\electron' - '%USERPROFILE%\.atom\.apm' - '%USERPROFILE%\.atom\compile-cache' - - '%USERPROFILE%\.atom\snapshot-cache' From 147e9dff429a8b985212348b2434763a197df06e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 15 Jun 2017 22:15:31 +0200 Subject: [PATCH 006/314] Document text decorations --- src/text-editor.coffee | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index e809ac4c5..bba314825 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1787,8 +1787,13 @@ class TextEditor extends Model # spanned by the `DisplayMarker`. # * `line-number` Adds the given `class` to the line numbers overlapping # the rows spanned by the `DisplayMarker`. - # * `highlight` Creates a `.highlight` div with the nested class with up - # to 3 nested regions that fill the area spanned by the `DisplayMarker`. + # * `text` Injects spans into all text overlapping the marked range, + # then adds the given `class` or `style` properties to these spans. + # Use this to manipulate the foreground color or styling of text in + # a given range. + # * `highlight` Creates an absolutely-positioned `.highlight` div + # containing nested divs to cover the marked region. For example, this + # is used to implement selections. # * `overlay` Positions the view associated with the given item at the # head or tail of the given `DisplayMarker`, depending on the `position` # property. @@ -1804,9 +1809,10 @@ class TextEditor extends Model # or render artificial cursors that don't actually exist in the model # by passing a marker that isn't actually associated with a cursor. # * `class` This CSS class will be applied to the decorated line number, - # line, highlight, or overlay. + # line, text spans, highlight regions, cursors, or overlay. # * `style` An {Object} containing CSS style properties to apply to the - # relevant DOM node. Currently this only works with a `type` of `cursor`. + # relevant DOM node. Currently this only works with a `type` of `cursor` + # or `text`. # * `item` (optional) An {HTMLElement} or a model {Object} with a # corresponding view registered. Only applicable to the `gutter`, # `overlay` and `block` decoration types. From 5e46afd63370ed1265c165de617a9da2cbc308da Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 15 Jun 2017 15:05:09 -0700 Subject: [PATCH 007/314] :arrow_up: line-ending-selector --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4a44079da..122306f07 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "image-view": "0.61.2", "incompatible-packages": "0.27.3", "keybinding-resolver": "0.38.0", - "line-ending-selector": "0.7.2", + "line-ending-selector": "0.7.3", "link": "0.31.3", "markdown-preview": "0.159.12", "metrics": "1.2.5", From 5ca738b84c206f0599dded04c28be27f241091d0 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 15 Jun 2017 21:59:12 -0400 Subject: [PATCH 008/314] Always run tests where directed --- src/main-process/atom-application.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 944f2783c..8e8fb8b55 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -120,7 +120,9 @@ class AtomApplication Promise.all(windowsClosePromises).then(=> @disposable.dispose()) launch: (options) -> - if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0 or options.test or options.benchmark or options.benchmarkTest + if options.test or options.benchmark or options.benchmarkTest + @openWithOptions(options) + else if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0 if @config.get('core.restorePreviousWindowsOnStart') is 'always' @loadState(_.deepClone(options)) @openWithOptions(options) From 137ccf4e07f7d021823f3d16e66a2cbd0c1dd5d3 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 15 Jun 2017 22:26:42 -0400 Subject: [PATCH 009/314] :arrow_up: autocomplete-atom-api@0.10.2 Brings in API changes since Atom 1.4.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 122306f07..a8bf3af91 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "solarized-light-syntax": "1.1.2", "about": "1.7.6", "archive-view": "0.63.3", - "autocomplete-atom-api": "0.10.1", + "autocomplete-atom-api": "0.10.2", "autocomplete-css": "0.16.2", "autocomplete-html": "0.8.0", "autocomplete-plus": "2.35.5", From 2707a53743ea516b9ca4c2f5e82bf532f9b8a126 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 16 Jun 2017 10:27:40 -0700 Subject: [PATCH 010/314] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a8bf3af91..1e68ba06e 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "sinon": "1.17.4", "@atom/source-map-support": "^0.3.4", "temp": "^0.8.3", - "text-buffer": "13.0.1", + "text-buffer": "13.0.2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From ccdae0fc860e2fa4b6e87fad98bbf1c2116b6768 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 16 Jun 2017 11:37:08 -0700 Subject: [PATCH 011/314] :arrow_up: text-buffer and whitespace --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1e68ba06e..a38a4bcc8 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "sinon": "1.17.4", "@atom/source-map-support": "^0.3.4", "temp": "^0.8.3", - "text-buffer": "13.0.2", + "text-buffer": "13.0.3", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", @@ -129,7 +129,7 @@ "tree-view": "0.217.2", "update-package-dependencies": "0.12.0", "welcome": "0.36.4", - "whitespace": "0.37.1", + "whitespace": "0.37.2", "wrap-guide": "0.40.2", "language-c": "0.58.1", "language-clojure": "0.22.3", From 17bec4262e750ca3ee1e928e54708d13cc0cd8b8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 19 Jun 2017 14:48:47 -0700 Subject: [PATCH 012/314] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a38a4bcc8..332e27e21 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "sinon": "1.17.4", "@atom/source-map-support": "^0.3.4", "temp": "^0.8.3", - "text-buffer": "13.0.3", + "text-buffer": "13.0.4", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From f9c0ed0a6dbc7c0f4927547352acf818b702e146 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 19 Jun 2017 14:48:51 -0700 Subject: [PATCH 013/314] Revert "Use a more recent c++ toolchain on travis" This reverts commit 958eed3a624930d95dc5fb39b98036ce2266038b. --- .travis.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index f9cfc4d24..71accd347 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ matrix: include: - os: linux dist: trusty - env: NODE_VERSION=6.9.4 DISPLAY=:99.0 CXX=g++-6 CC=gcc-6 + env: NODE_VERSION=6.9.4 DISPLAY=:99.0 CC=clang CXX=clang++ npm_config_clang=1 sudo: required @@ -51,11 +51,9 @@ addons: - out/atom-amd64.tar.gz target_paths: travis-artifacts/$TRAVIS_BUILD_ID apt: - sources: - - ubuntu-toolchain-r-test packages: - - gcc-6 - - g++-6 + - build-essential + - clang-3.3 - fakeroot - git - libsecret-1-dev From 92151fa54fc07ca52a4274f9ab6117b7e175db19 Mon Sep 17 00:00:00 2001 From: ungb Date: Mon, 19 Jun 2017 15:55:09 -0700 Subject: [PATCH 014/314] :arrow_up: autocomplete-plus@02.35.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a38a4bcc8..f77c24ac8 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "autocomplete-atom-api": "0.10.2", "autocomplete-css": "0.16.2", "autocomplete-html": "0.8.0", - "autocomplete-plus": "2.35.5", + "autocomplete-plus": "2.35.6", "autocomplete-snippets": "1.11.0", "autoflow": "0.29.0", "autosave": "0.24.3", From 02b8862386795f4391a9d522c0177e3e662c81b8 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 19 Jun 2017 22:29:47 -0400 Subject: [PATCH 015/314] :arrow_up: markdown-preview@0.159.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 40f163457..5cb25e8c3 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "keybinding-resolver": "0.38.0", "line-ending-selector": "0.7.3", "link": "0.31.3", - "markdown-preview": "0.159.12", + "markdown-preview": "0.159.13", "metrics": "1.2.5", "notifications": "0.67.2", "open-on-github": "1.2.1", From 7c9f39e3f1d05ee423e0093e6b83f042ce11c90a Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 21 Jun 2017 23:14:51 -0400 Subject: [PATCH 016/314] :arrow_up: tree-view@0.217.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5cb25e8c3..ac29ec7c1 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "symbols-view": "0.116.1", "tabs": "0.106.2", "timecop": "0.36.0", - "tree-view": "0.217.2", + "tree-view": "0.217.3", "update-package-dependencies": "0.12.0", "welcome": "0.36.4", "whitespace": "0.37.2", From 5a8dce117541bef77d3b8404dbc55383cb278f16 Mon Sep 17 00:00:00 2001 From: simurai Date: Sat, 24 Jun 2017 16:50:56 +0900 Subject: [PATCH 017/314] :arrow_up: image-view@v0.62.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ac29ec7c1..18a003168 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", - "image-view": "0.61.2", + "image-view": "0.62.0", "incompatible-packages": "0.27.3", "keybinding-resolver": "0.38.0", "line-ending-selector": "0.7.3", From 527210f72c206934283f12b4668dbbdda6f5b269 Mon Sep 17 00:00:00 2001 From: Tim van der Lippe Date: Sat, 24 Jun 2017 13:20:19 +0200 Subject: [PATCH 018/314] Add instructions for local development Finding out how to contribute to my first Atom package was problematic and took me quite some time to figure out how to locally run the packages. There were some spread out answers in the Atom discussion forum, but I think it is easier for new developers to read the CONTRIBUTING guide instead of using Google to find out how to contribute. --- CONTRIBUTING.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f74c9b3ab..e6ee13d47 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -197,6 +197,19 @@ Both issue lists are sorted by total number of comments. While not perfect, numb If you want to read about using Atom or developing packages in Atom, the [Atom Flight Manual](http://flight-manual.atom.io) is free and available online. You can find the source to the manual in [atom/flight-manual.atom.io](https://github.com/atom/flight-manual.atom.io). +#### Local development + +All packages can be developed locally, by checking out the corresponding repository and registering the package to Atom with `apm`: + +``` +$ git clone url-to-git-repository +$ cd path-to-package/ +$ apm link -d +$ atom -d . +``` + +By running Atom with the `-d` flag, you signal it to run with development packages installed. `apm link` makes sure that your local repository is loaded by Atom. + ### Pull Requests * Fill in [the required template](PULL_REQUEST_TEMPLATE.md) From a171380544109ccacb35c26e0ad7c8210e20e923 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 26 Jun 2017 11:37:11 +0200 Subject: [PATCH 019/314] Fix resetting styles in NodePool Previously, we were mistakenly not clearing out some styling properties like `marginTop`, thus causing e.g. line numbers to be misaligned. This was caused by manual updates to an element's style object, without a consequent update to the NodePool. With this commit we will now rely on `element.styleMap` (a DOM primitive) to detect which styles have been set on a element that is about to be recycled and, if the object being created doesn't use them, simply clear them out. --- src/text-editor-component.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 4e52c5d08..fdfeb2ff2 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -4034,7 +4034,6 @@ class NodePool { constructor () { this.elementsByType = {} this.textNodes = [] - this.stylesByNode = new WeakMap() } getElement (type, className, style) { @@ -4055,14 +4054,10 @@ class NodePool { if (element) { element.className = className - var existingStyle = this.stylesByNode.get(element) - if (existingStyle) { - for (var key in existingStyle) { - if (!style || !style[key]) element.style[key] = '' - } - } + element.styleMap.forEach((value, key) => { + if (!style || style[key] == null) element.style[key] = '' + }) if (style) Object.assign(element.style, style) - this.stylesByNode.set(element, style) while (element.firstChild) element.firstChild.remove() return element @@ -4070,7 +4065,6 @@ class NodePool { var newElement = document.createElement(type) if (className) newElement.className = className if (style) Object.assign(newElement.style, style) - this.stylesByNode.set(newElement, style) return newElement } } From 52ba6c7342f82578ff17fbd1d77124801dd3525b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 26 Jun 2017 12:18:45 +0200 Subject: [PATCH 020/314] Fix measuring block dec. if adding them before updating element's width --- spec/text-editor-component-spec.js | 25 +++++++++++++++++++++++++ src/text-editor-component.js | 6 ++++++ 2 files changed, 31 insertions(+) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 7c211ef29..b44838538 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -2035,6 +2035,31 @@ describe('TextEditorComponent', () => { expect(item6.previousSibling).toBe(lineNodeForScreenRow(component, 12)) }) + it('measures block decorations correctly when they are added before the component width has been updated', async () => { + { + const {editor, component, element} = buildComponent({autoHeight: false, width: 500, attach: false}) + const marker = editor.markScreenPosition([0, 0]) + const item = document.createElement('div') + item.textContent = 'block decoration' + const decoration = editor.decorateMarker(marker, {type: 'block', item}) + + jasmine.attachToDOM(element) + assertLinesAreAlignedWithLineNumbers(component) + } + + { + const {editor, component, element} = buildComponent({autoHeight: false, width: 800}) + const marker = editor.markScreenPosition([0, 0]) + const item = document.createElement('div') + item.textContent = 'block decoration that could wrap many times' + const decoration = editor.decorateMarker(marker, {type: 'block', item}) + + element.style.width = '50px' + await component.getNextUpdatePromise() + assertLinesAreAlignedWithLineNumbers(component) + } + }) + it('bases the width of the block decoration measurement area on the editor scroll width', async () => { const {component, element} = buildComponent({autoHeight: false, width: 150}) expect(component.refs.blockDecorationMeasurementArea.offsetWidth).toBe(component.getScrollWidth()) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 4e52c5d08..d2809f0cd 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -312,6 +312,11 @@ class TextEditorComponent { } }) + if (this.resizeBlockDecorationMeasurementsArea) { + this.resizeBlockDecorationMeasurementsArea = false + this.refs.blockDecorationMeasurementArea.style.width = this.getScrollWidth() + 'px' + } + this.blockDecorationsToMeasure.forEach((decoration) => { const {item} = decoration.getProperties() const decorationElement = TextEditor.viewForItem(item) @@ -1391,6 +1396,7 @@ class TextEditorComponent { if (!this.hasInitialMeasurements) this.measureDimensions() this.visible = true this.props.model.setVisible(true) + this.resizeBlockDecorationMeasurementsArea = true this.updateSync() this.flushPendingLogicalScrollPosition() } From 62ee913567cb8cba015b83dd2c5ebddd50848535 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 26 Jun 2017 16:29:31 +0200 Subject: [PATCH 021/314] Ensure custom title bar is always updated when document.title changes This commit uses a new private API on Workspace that emits an event every time the window title gets updated (e.g. as a result of an active pane item changing, the project paths changing, etc.). This fixes a bug that left the custom title bar with a stale document.title under some circumstances. Signed-off-by: Jason Rudolph --- spec/title-bar-spec.coffee | 40 +++++++++++++++++++++++++++----------- src/title-bar.coffee | 2 +- src/workspace.js | 5 +++++ 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/spec/title-bar-spec.coffee b/spec/title-bar-spec.coffee index 85bcba7e6..465af9135 100644 --- a/spec/title-bar-spec.coffee +++ b/spec/title-bar-spec.coffee @@ -1,22 +1,27 @@ TitleBar = require '../src/title-bar' +temp = require 'temp' describe "TitleBar", -> - it "updates the title based on document.title when the active pane item changes", -> + it "updates its title when document.title changes", -> titleBar = new TitleBar({ workspace: atom.workspace, themes: atom.themes, applicationDelegate: atom.applicationDelegate, }) + expect(titleBar.element.querySelector('.title').textContent).toBe(document.title) - expect(titleBar.element.querySelector('.title').textContent).toBe document.title - initialTitle = document.title + paneItem = new FakePaneItem('Title 1') + atom.workspace.getActivePane().activateItem(paneItem) + expect(document.title).toMatch('Title 1') + expect(titleBar.element.querySelector('.title').textContent).toBe(document.title) - atom.workspace.getActivePane().activateItem({ - getTitle: -> 'Test Title' - }) + paneItem.setTitle('Title 2') + expect(document.title).toMatch('Title 2') + expect(titleBar.element.querySelector('.title').textContent).toBe(document.title) - expect(document.title).not.toBe(initialTitle) - expect(titleBar.element.querySelector('.title').textContent).toBe document.title + atom.project.setPaths([temp.mkdirSync('project-1')]) + expect(document.title).toMatch('project-1') + expect(titleBar.element.querySelector('.title').textContent).toBe(document.title) it "can update the sheet offset for the current window based on its height", -> titleBar = new TitleBar({ @@ -24,6 +29,19 @@ describe "TitleBar", -> themes: atom.themes, applicationDelegate: atom.applicationDelegate, }) - expect(-> - titleBar.updateWindowSheetOffset() - ).not.toThrow() + expect(-> titleBar.updateWindowSheetOffset()).not.toThrow() + +class FakePaneItem + constructor: (title) -> + @title = title + + getTitle: -> + @title + + onDidChangeTitle: (callback) -> + @didChangeTitleCallback = callback + {dispose: => @didChangeTitleCallback = null} + + setTitle: (title) -> + @title = title + @didChangeTitleCallback?(title) diff --git a/src/title-bar.coffee b/src/title-bar.coffee index b81b08060..a56843627 100644 --- a/src/title-bar.coffee +++ b/src/title-bar.coffee @@ -10,7 +10,7 @@ class TitleBar @element.addEventListener 'dblclick', @dblclickHandler - @workspace.onDidChangeActivePaneItem => @updateTitle() + @workspace.onDidChangeWindowTitle => @updateTitle() @themes.onDidChangeActiveThemes => @updateWindowSheetOffset() @updateTitle() diff --git a/src/workspace.js b/src/workspace.js index 749ddda66..aac3adffa 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -587,6 +587,7 @@ module.exports = class Workspace extends Model { document.title = titleParts.join(' \u2014 ') this.applicationDelegate.setRepresentedFilename(representedPath) + this.emitter.emit('did-change-window-title') } // On macOS, fades the application window's proxy icon when the current file @@ -864,6 +865,10 @@ module.exports = class Workspace extends Model { return this.emitter.on('did-add-text-editor', callback) } + onDidChangeWindowTitle (callback) { + return this.emitter.on('did-change-window-title', callback) + } + /* Section: Opening */ From a7b199624507f57cd08d840b34a4b259ba0a286f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 26 Jun 2017 16:32:14 +0200 Subject: [PATCH 022/314] Convert spec/title-bar-spec.coffee to js Signed-off-by: Jason Rudolph --- spec/title-bar-spec.coffee | 47 ------------------------------- spec/title-bar-spec.js | 57 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 47 deletions(-) delete mode 100644 spec/title-bar-spec.coffee create mode 100644 spec/title-bar-spec.js diff --git a/spec/title-bar-spec.coffee b/spec/title-bar-spec.coffee deleted file mode 100644 index 465af9135..000000000 --- a/spec/title-bar-spec.coffee +++ /dev/null @@ -1,47 +0,0 @@ -TitleBar = require '../src/title-bar' -temp = require 'temp' - -describe "TitleBar", -> - it "updates its title when document.title changes", -> - titleBar = new TitleBar({ - workspace: atom.workspace, - themes: atom.themes, - applicationDelegate: atom.applicationDelegate, - }) - expect(titleBar.element.querySelector('.title').textContent).toBe(document.title) - - paneItem = new FakePaneItem('Title 1') - atom.workspace.getActivePane().activateItem(paneItem) - expect(document.title).toMatch('Title 1') - expect(titleBar.element.querySelector('.title').textContent).toBe(document.title) - - paneItem.setTitle('Title 2') - expect(document.title).toMatch('Title 2') - expect(titleBar.element.querySelector('.title').textContent).toBe(document.title) - - atom.project.setPaths([temp.mkdirSync('project-1')]) - expect(document.title).toMatch('project-1') - expect(titleBar.element.querySelector('.title').textContent).toBe(document.title) - - it "can update the sheet offset for the current window based on its height", -> - titleBar = new TitleBar({ - workspace: atom.workspace, - themes: atom.themes, - applicationDelegate: atom.applicationDelegate, - }) - expect(-> titleBar.updateWindowSheetOffset()).not.toThrow() - -class FakePaneItem - constructor: (title) -> - @title = title - - getTitle: -> - @title - - onDidChangeTitle: (callback) -> - @didChangeTitleCallback = callback - {dispose: => @didChangeTitleCallback = null} - - setTitle: (title) -> - @title = title - @didChangeTitleCallback?(title) diff --git a/spec/title-bar-spec.js b/spec/title-bar-spec.js new file mode 100644 index 000000000..c0cb806b5 --- /dev/null +++ b/spec/title-bar-spec.js @@ -0,0 +1,57 @@ +const TitleBar = require('../src/title-bar') +const temp = require('temp') + +describe('TitleBar', () => { + it('updates its title when document.title changes', () => { + const titleBar = new TitleBar({ + workspace: atom.workspace, + themes: atom.themes, + applicationDelegate: atom.applicationDelegate + }) + expect(titleBar.element.querySelector('.title').textContent).toBe(document.title) + + const paneItem = new FakePaneItem('Title 1') + atom.workspace.getActivePane().activateItem(paneItem) + expect(document.title).toMatch('Title 1') + expect(titleBar.element.querySelector('.title').textContent).toBe(document.title) + + paneItem.setTitle('Title 2') + expect(document.title).toMatch('Title 2') + expect(titleBar.element.querySelector('.title').textContent).toBe(document.title) + + atom.project.setPaths([temp.mkdirSync('project-1')]) + expect(document.title).toMatch('project-1') + expect(titleBar.element.querySelector('.title').textContent).toBe(document.title) + }) + + it('can update the sheet offset for the current window based on its height', () => { + const titleBar = new TitleBar({ + workspace: atom.workspace, + themes: atom.themes, + applicationDelegate: atom.applicationDelegate + }) + expect(() => titleBar.updateWindowSheetOffset()).not.toThrow() + }) +}) + +class FakePaneItem { + constructor (title) { + this.title = title + } + + getTitle () { + return this.title + } + + onDidChangeTitle (callback) { + this.didChangeTitleCallback = callback + return { + dispose: () => { this.didChangeTitleCallback = null } + } + } + + setTitle (title) { + this.title = title + if (this.didChangeTitleCallback) this.didChangeTitleCallback(title) + } +} From 9e8f07b926a33a3a25754361acd8bba0e1914bf7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 26 Jun 2017 16:34:33 +0200 Subject: [PATCH 023/314] Convert src/title-bar.coffee to js Signed-off-by: Jason Rudolph --- src/title-bar.coffee | 34 -------------------------------- src/title-bar.js | 47 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 34 deletions(-) delete mode 100644 src/title-bar.coffee create mode 100644 src/title-bar.js diff --git a/src/title-bar.coffee b/src/title-bar.coffee deleted file mode 100644 index a56843627..000000000 --- a/src/title-bar.coffee +++ /dev/null @@ -1,34 +0,0 @@ -module.exports = -class TitleBar - constructor: ({@workspace, @themes, @applicationDelegate}) -> - @element = document.createElement('div') - @element.classList.add('title-bar') - - @titleElement = document.createElement('div') - @titleElement.classList.add('title') - @element.appendChild(@titleElement) - - @element.addEventListener 'dblclick', @dblclickHandler - - @workspace.onDidChangeWindowTitle => @updateTitle() - @themes.onDidChangeActiveThemes => @updateWindowSheetOffset() - - @updateTitle() - @updateWindowSheetOffset() - - dblclickHandler: => - # User preference deciding which action to take on a title bar double-click - switch @applicationDelegate.getUserDefault('AppleActionOnDoubleClick', 'string') - when 'Minimize' - @applicationDelegate.minimizeWindow() - when 'Maximize' - if @applicationDelegate.isWindowMaximized() - @applicationDelegate.unmaximizeWindow() - else - @applicationDelegate.maximizeWindow() - - updateTitle: -> - @titleElement.textContent = document.title - - updateWindowSheetOffset: -> - @applicationDelegate.getCurrentWindow().setSheetOffset(@element.offsetHeight) diff --git a/src/title-bar.js b/src/title-bar.js new file mode 100644 index 000000000..260266308 --- /dev/null +++ b/src/title-bar.js @@ -0,0 +1,47 @@ +module.exports = +class TitleBar { + constructor ({workspace, themes, applicationDelegate}) { + this.dblclickHandler = this.dblclickHandler.bind(this) + this.workspace = workspace + this.themes = themes + this.applicationDelegate = applicationDelegate + this.element = document.createElement('div') + this.element.classList.add('title-bar') + + this.titleElement = document.createElement('div') + this.titleElement.classList.add('title') + this.element.appendChild(this.titleElement) + + this.element.addEventListener('dblclick', this.dblclickHandler) + + this.workspace.onDidChangeWindowTitle(() => this.updateTitle()) + this.themes.onDidChangeActiveThemes(() => this.updateWindowSheetOffset()) + + this.updateTitle() + this.updateWindowSheetOffset() + } + + dblclickHandler () { + // User preference deciding which action to take on a title bar double-click + switch (this.applicationDelegate.getUserDefault('AppleActionOnDoubleClick', 'string')) { + case 'Minimize': + this.applicationDelegate.minimizeWindow() + break + case 'Maximize': + if (this.applicationDelegate.isWindowMaximized()) { + this.applicationDelegate.unmaximizeWindow() + } else { + this.applicationDelegate.maximizeWindow() + } + break + } + } + + updateTitle () { + this.titleElement.textContent = document.title + } + + updateWindowSheetOffset () { + this.applicationDelegate.getCurrentWindow().setSheetOffset(this.element.offsetHeight) + } +} From 863faffd237cd592a6b34b8ab3e4707bb8e4b5f1 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Tue, 27 Jun 2017 16:08:16 -0700 Subject: [PATCH 024/314] Only expect a single 'did-destroy' event These events will only be fired a single time at most, so we should clean up the listeners after that. This should help minimize accidental memory leaks. --- src/cursor.coffee | 2 +- src/decoration.coffee | 2 +- src/git-repository.coffee | 2 +- src/gutter.coffee | 2 +- src/pane-axis.coffee | 2 +- src/pane.coffee | 2 +- src/panel-container.js | 2 +- src/panel.js | 2 +- src/selection.coffee | 2 +- src/text-editor.coffee | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/cursor.coffee b/src/cursor.coffee index 74922ff51..128fe7ff5 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -49,7 +49,7 @@ class Cursor extends Model # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroy: (callback) -> - @emitter.on 'did-destroy', callback + @emitter.once 'did-destroy', callback ### Section: Managing Cursor Position diff --git a/src/decoration.coffee b/src/decoration.coffee index 7be15d9f5..f18733f6e 100644 --- a/src/decoration.coffee +++ b/src/decoration.coffee @@ -105,7 +105,7 @@ class Decoration # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroy: (callback) -> - @emitter.on 'did-destroy', callback + @emitter.once 'did-destroy', callback ### Section: Decoration Details diff --git a/src/git-repository.coffee b/src/git-repository.coffee index f80152737..c7105baef 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -129,7 +129,7 @@ class GitRepository # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroy: (callback) -> - @emitter.on 'did-destroy', callback + @emitter.once 'did-destroy', callback ### Section: Event Subscription diff --git a/src/gutter.coffee b/src/gutter.coffee index 6b39398dd..4521eeeb2 100644 --- a/src/gutter.coffee +++ b/src/gutter.coffee @@ -48,7 +48,7 @@ class Gutter # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroy: (callback) -> - @emitter.on 'did-destroy', callback + @emitter.once 'did-destroy', callback ### Section: Visibility diff --git a/src/pane-axis.coffee b/src/pane-axis.coffee index 31c5e1664..c8fcc4108 100644 --- a/src/pane-axis.coffee +++ b/src/pane-axis.coffee @@ -69,7 +69,7 @@ class PaneAxis extends Model @emitter.on 'did-replace-child', fn onDidDestroy: (fn) -> - @emitter.on 'did-destroy', fn + @emitter.once 'did-destroy', fn onDidChangeFlexScale: (fn) -> @emitter.on 'did-change-flex-scale', fn diff --git a/src/pane.coffee b/src/pane.coffee index 9c0440e0a..3dde9678f 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -170,7 +170,7 @@ class Pane # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroy: (callback) -> - @emitter.on 'did-destroy', callback + @emitter.once 'did-destroy', callback # Public: Invoke the given callback when the value of the {::isActive} # property changes. diff --git a/src/panel-container.js b/src/panel-container.js index 65dd89f7a..6d5fb7398 100644 --- a/src/panel-container.js +++ b/src/panel-container.js @@ -40,7 +40,7 @@ module.exports = class PanelContainer { } onDidDestroy (callback) { - return this.emitter.on('did-destroy', callback) + return this.emitter.once('did-destroy', callback) } /* diff --git a/src/panel.js b/src/panel.js index c9beba72a..49b037be0 100644 --- a/src/panel.js +++ b/src/panel.js @@ -66,7 +66,7 @@ class Panel { // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroy (callback) { - return this.emitter.on('did-destroy', callback) + return this.emitter.once('did-destroy', callback) } /* diff --git a/src/selection.coffee b/src/selection.coffee index 935a15b13..e361d0b5c 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -54,7 +54,7 @@ class Selection extends Model # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroy: (callback) -> - @emitter.on 'did-destroy', callback + @emitter.once 'did-destroy', callback ### Section: Managing the selection range diff --git a/src/text-editor.coffee b/src/text-editor.coffee index bba314825..ddc435414 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -647,7 +647,7 @@ class TextEditor extends Model # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroy: (callback) -> - @emitter.on 'did-destroy', callback + @emitter.once 'did-destroy', callback # Extended: Calls your `callback` when a {Cursor} is added to the editor. # Immediately calls your callback for each existing cursor. From fc3915e656a8a69e6997c63581251e965cb2216e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 29 Jun 2017 11:59:10 -0400 Subject: [PATCH 025/314] :arrow_up: autocomplete-plus --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 18a003168..3e13bf149 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "autocomplete-atom-api": "0.10.2", "autocomplete-css": "0.16.2", "autocomplete-html": "0.8.0", - "autocomplete-plus": "2.35.6", + "autocomplete-plus": "2.35.7", "autocomplete-snippets": "1.11.0", "autoflow": "0.29.0", "autosave": "0.24.3", From 2a45e426977b4e92854b9bde13d1f0f0c81bdc10 Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 30 Jun 2017 14:38:21 +0900 Subject: [PATCH 026/314] :arrow_up: welcome@v0.36.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3e13bf149..5f13d0010 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "timecop": "0.36.0", "tree-view": "0.217.3", "update-package-dependencies": "0.12.0", - "welcome": "0.36.4", + "welcome": "0.36.5", "whitespace": "0.37.2", "wrap-guide": "0.40.2", "language-c": "0.58.1", From e686c4d7f84121afb368f74638b0139c41599feb Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Tue, 20 Jun 2017 13:00:34 +0200 Subject: [PATCH 027/314] Remove `invalidateBlockDecorationDimensions` from tests This is now automatically called by the mutation observer. --- spec/text-editor-component-spec.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index b44838538..862018cf8 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1941,8 +1941,6 @@ describe('TextEditorComponent', () => { item3.style.margin = '10px' item2.style.height = '33px' item2.style.margin = '0px' - component.invalidateBlockDecorationDimensions(decoration2) - component.invalidateBlockDecorationDimensions(decoration3) await component.getNextUpdatePromise() expect(component.getRenderedStartRow()).toBe(0) expect(component.getRenderedEndRow()).toBe(9) @@ -1973,7 +1971,6 @@ describe('TextEditorComponent', () => { item3.style.wordWrap = 'break-word' const contentWidthInCharacters = Math.floor(component.getScrollContainerClientWidth() / component.getBaseCharacterWidth()) item3.textContent = 'x'.repeat(contentWidthInCharacters * 2) - component.invalidateBlockDecorationDimensions(decoration3) await component.getNextUpdatePromise() // make the editor wider, so that the decoration doesn't wrap anymore. From b30f55bb576ca52782b6970c92223722309c556a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 30 Jun 2017 14:06:50 +0200 Subject: [PATCH 028/314] Invalidate block decorations height automatically if their size changes --- src/text-editor-component.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 0665bc185..ce332ed67 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -114,6 +114,9 @@ class TextEditorComponent { this.horizontalPositionsToMeasure = new Map() // Keys are rows with positions we want to measure, values are arrays of columns to measure this.horizontalPixelPositionsByScreenLineId = new Map() // Values are maps from column to horiontal pixel positions this.blockDecorationsToMeasure = new Set() + this.blockDecorationsByElement = new WeakMap() + this.heightsByBlockDecoration = new WeakMap() + this.blockDecorationResizeObserver = new ResizeObserver(this.didResizeBlockDecorations.bind(this)) this.lineNodesByScreenLineId = new Map() this.textNodesByScreenLineId = new Map() this.overlayComponents = new Set() @@ -322,6 +325,7 @@ class TextEditorComponent { const decorationElement = TextEditor.viewForItem(item) const {previousSibling, nextSibling} = decorationElement const height = nextSibling.getBoundingClientRect().top - previousSibling.getBoundingClientRect().bottom + this.heightsByBlockDecoration.set(decoration, height) this.lineTopIndex.resizeBlock(decoration, height) }) @@ -2347,11 +2351,14 @@ class TextEditorComponent { didAddBlockDecoration (decoration) { const marker = decoration.getMarker() - const {position} = decoration.getProperties() + const {item, position} = decoration.getProperties() + const element = TextEditor.viewForItem(item) const row = marker.getHeadScreenPosition().row this.lineTopIndex.insertBlock(decoration, row, 0, position === 'after') this.blockDecorationsToMeasure.add(decoration) + this.blockDecorationsByElement.set(element, decoration) + this.blockDecorationResizeObserver.observe(element) const didUpdateDisposable = marker.bufferMarker.onDidChange((e) => { if (!e.textChanged) { @@ -2361,6 +2368,9 @@ class TextEditorComponent { }) const didDestroyDisposable = decoration.onDidDestroy(() => { this.blockDecorationsToMeasure.delete(decoration) + this.heightsByBlockDecoration.delete(decoration) + this.blockDecorationsByElement.delete(element) + this.blockDecorationResizeObserver.unobserve(element) this.lineTopIndex.removeBlock(decoration) didUpdateDisposable.dispose() didDestroyDisposable.dispose() @@ -2368,6 +2378,19 @@ class TextEditorComponent { }) } + didResizeBlockDecorations (entries) { + if (!this.visible) return + + for (let i = 0; i < entries.length; i++) { + const {target, contentRect} = entries[i] + const decoration = this.blockDecorationsByElement.get(target) + const previousHeight = this.heightsByBlockDecoration.get(decoration) + if (this.element.contains(target) && contentRect.height !== previousHeight) { + this.invalidateBlockDecorationDimensions(decoration) + } + } + } + invalidateBlockDecorationDimensions (decoration) { this.blockDecorationsToMeasure.add(decoration) this.scheduleUpdate() From 10a7cb5badc56ac6b5d02476fe330d6e53f1cedd Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 30 Jun 2017 10:11:36 -0400 Subject: [PATCH 029/314] :arrow_up: atom-keymap --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f13d0010..8cf0859c4 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "electronVersion": "1.6.9", "dependencies": { "async": "0.2.6", - "atom-keymap": "8.1.2", + "atom-keymap": "8.2.1", "atom-select-list": "^0.1.0", "atom-ui": "0.4.1", "babel-core": "5.8.38", From 00a650bd559533d8cb34c8a6090fb3f707bba667 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 30 Jun 2017 15:42:40 -0700 Subject: [PATCH 030/314] :arrow_up: text-buffer Fixes #14894 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8cf0859c4..aea8fb1ed 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "sinon": "1.17.4", "@atom/source-map-support": "^0.3.4", "temp": "^0.8.3", - "text-buffer": "13.0.4", + "text-buffer": "13.0.5", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 116a90fe2c52a96b9501084975dbe0c4db03a660 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 30 Jun 2017 15:59:42 -0700 Subject: [PATCH 031/314] :arrow_up: find-and-replace --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aea8fb1ed..f314b9903 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "dev-live-reload": "0.47.1", "encoding-selector": "0.23.4", "exception-reporting": "0.41.4", - "find-and-replace": "0.208.3", + "find-and-replace": "0.209.0", "fuzzy-finder": "1.5.8", "github": "0.3.3", "git-diff": "1.3.6", From e7b4ad48f087a0006aa04d1604eb20ec543bc3bb Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 3 Jul 2017 16:10:41 -0600 Subject: [PATCH 032/314] Always render 'decoration' class on custom decorations --- spec/text-editor-component-spec.js | 10 +++++----- src/text-editor-component.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 7c211ef29..e36da68c1 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1727,17 +1727,17 @@ describe('TextEditorComponent', () => { let [decorationNode1, decorationNode2] = gutterA.getElement().firstChild.children const [decorationNode3] = gutterB.getElement().firstChild.children - expect(decorationNode1.className).toBe('a') + expect(decorationNode1.className).toBe('decoration a') expect(decorationNode1.getBoundingClientRect().top).toBe(clientTopForLine(component, 2)) expect(decorationNode1.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 5)) expect(decorationNode1.firstChild).toBeNull() - expect(decorationNode2.className).toBe('b') + expect(decorationNode2.className).toBe('decoration b') expect(decorationNode2.getBoundingClientRect().top).toBe(clientTopForLine(component, 6)) expect(decorationNode2.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 8)) expect(decorationNode2.firstChild).toBe(decorationElement1) - expect(decorationNode3.className).toBe('') + expect(decorationNode3.className).toBe('decoration') expect(decorationNode3.getBoundingClientRect().top).toBe(clientTopForLine(component, 9)) expect(decorationNode3.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 12) + component.getLineHeight()) expect(decorationNode3.firstChild).toBe(decorationElement2) @@ -1746,9 +1746,9 @@ describe('TextEditorComponent', () => { decoration2.setProperties({type: 'gutter', gutterName: 'a', item: decorationElement2}) decoration3.destroy() await component.getNextUpdatePromise() - expect(decorationNode1.className).toBe('c') + expect(decorationNode1.className).toBe('decoration c') expect(decorationNode1.firstChild).toBe(decorationElement1) - expect(decorationNode2.className).toBe('') + expect(decorationNode2.className).toBe('decoration') expect(decorationNode2.firstChild).toBe(decorationElement2) expect(gutterB.getElement().firstChild.children.length).toBe(0) }) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 4e52c5d08..5f4c28415 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1105,7 +1105,7 @@ class TextEditorComponent { const height = this.pixelPositionBeforeBlocksForRow(screenRange.end.row + 1) - top decorations.push({ - className: decoration.class, + className: 'decoration' + (decoration.class ? ' ' + decoration.class : ''), element: TextEditor.viewForItem(decoration.item), top, height From c78ecb3896cf729c836276726769a7aa83b702ec Mon Sep 17 00:00:00 2001 From: simurai Date: Tue, 4 Jul 2017 11:52:23 +0900 Subject: [PATCH 033/314] :arrow_up: one-dark/light-syntax@v1.8.0 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f314b9903..ee12e4434 100644 --- a/package.json +++ b/package.json @@ -80,8 +80,8 @@ "base16-tomorrow-light-theme": "1.5.0", "one-dark-ui": "1.10.5", "one-light-ui": "1.10.5", - "one-dark-syntax": "1.7.1", - "one-light-syntax": "1.7.1", + "one-dark-syntax": "1.8.0", + "one-light-syntax": "1.8.0", "solarized-dark-syntax": "1.1.2", "solarized-light-syntax": "1.1.2", "about": "1.7.6", From ea6cb7bd197a1022ad974a5f70addf2b5ab0f284 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 3 Jul 2017 20:41:02 -0700 Subject: [PATCH 034/314] :arrow_up: find-and-replace --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ee12e4434..9081c2266 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "dev-live-reload": "0.47.1", "encoding-selector": "0.23.4", "exception-reporting": "0.41.4", - "find-and-replace": "0.209.0", + "find-and-replace": "0.209.1", "fuzzy-finder": "1.5.8", "github": "0.3.3", "git-diff": "1.3.6", From 942dd03bd0c623a5ea9140306cb886726d8d0a07 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 5 Jul 2017 11:11:44 +0200 Subject: [PATCH 035/314] Assign screen-row to each line number as a data field --- spec/text-editor-component-spec.js | 21 +++++++++++++-------- src/text-editor-component.js | 13 ++++++++++--- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 862018cf8..83246ae91 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -667,19 +667,24 @@ describe('TextEditorComponent', () => { expect(element.classList.contains('has-selection')).toBe(false) }) - it('assigns a buffer-row to each line number as a data field', async () => { + it('assigns buffer-row and screen-row to each line number as data fields', async () => { const {editor, element, component} = buildComponent() editor.setSoftWrapped(true) await component.getNextUpdatePromise() await setEditorWidthInCharacters(component, 40) + { + const bufferRows = Array.from(element.querySelectorAll('.line-number:not(.dummy)')).map((e) => e.dataset.bufferRow) + const screenRows = Array.from(element.querySelectorAll('.line-number:not(.dummy)')).map((e) => e.dataset.screenRow) + expect(bufferRows).toEqual([ + '0', '1', '2', '3', '3', '4', '5', '6', '6', '6', + '7', '8', '8', '8', '9', '10', '11', '11', '12' + ]) + expect(screenRows).toEqual([ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '10', '11', '12', '13', '14', '15', '16', '17', '18' + ]) + } - expect( - Array.from(element.querySelectorAll('.line-number:not(.dummy)')) - .map((element) => element.dataset.bufferRow) - ).toEqual([ - '0', '1', '2', '3', '3', '4', '5', '6', '6', '6', - '7', '8', '8', '8', '9', '10', '11', '11', '12' - ]) }) it('does not blow away class names added to the element by packages when changing the class name', async () => { diff --git a/src/text-editor-component.js b/src/text-editor-component.js index ce332ed67..641585348 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -845,6 +845,7 @@ class TextEditorComponent { const renderedRowCount = this.getRenderedRowCount() const bufferRows = model.bufferRowsForScreenRows(startRow, endRow) + const screenRows = new Array(renderedRowCount) const keys = new Array(renderedRowCount) const foldableFlags = new Array(renderedRowCount) const softWrappedFlags = new Array(renderedRowCount) @@ -871,6 +872,7 @@ class TextEditorComponent { foldableFlags[i] = false } + screenRows[i] = row previousBufferRow = bufferRow } @@ -878,6 +880,7 @@ class TextEditorComponent { bufferRows.pop() this.lineNumbersToRender.bufferRows = bufferRows + this.lineNumbersToRender.screenRows = screenRows this.lineNumbersToRender.keys = keys this.lineNumbersToRender.foldableFlags = foldableFlags this.lineNumbersToRender.softWrappedFlags = softWrappedFlags @@ -2944,7 +2947,7 @@ class GutterContainerComponent { if (!isLineNumberGutterVisible) return null if (hasInitialMeasurements) { - const {maxDigits, keys, bufferRows, softWrappedFlags, foldableFlags} = lineNumbersToRender + const {maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags} = lineNumbersToRender return $(LineNumberGutterComponent, { ref: 'lineNumberGutter', element: gutter.getElement(), @@ -2955,6 +2958,7 @@ class GutterContainerComponent { maxDigits: maxDigits, keys: keys, bufferRows: bufferRows, + screenRows: screenRows, softWrappedFlags: softWrappedFlags, foldableFlags: foldableFlags, decorations: decorationsToRender.lineNumbers, @@ -2996,7 +3000,7 @@ class LineNumberGutterComponent { render () { const { rootComponent, showLineNumbers, height, width, lineHeight, startRow, endRow, rowsPerTile, - maxDigits, keys, bufferRows, softWrappedFlags, foldableFlags, decorations + maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags, decorations } = this.props let children = null @@ -3013,6 +3017,7 @@ class LineNumberGutterComponent { const softWrapped = softWrappedFlags[j] const foldable = foldableFlags[j] const bufferRow = bufferRows[j] + const screenRow = screenRows[j] let className = 'line-number' if (foldable) className = className + ' foldable' @@ -3031,6 +3036,7 @@ class LineNumberGutterComponent { className, width, bufferRow, + screenRow, number, nodePool: this.nodePool } @@ -3144,12 +3150,13 @@ class LineNumberGutterComponent { class LineNumberComponent { constructor (props) { - const {className, width, marginTop, bufferRow, number, nodePool} = props + const {className, width, marginTop, bufferRow, screenRow, number, nodePool} = props this.props = props const style = {width: width + 'px'} if (marginTop != null) style.marginTop = marginTop + 'px' this.element = nodePool.getElement('DIV', className, style) this.element.dataset.bufferRow = bufferRow + this.element.dataset.screenRow = screenRow if (number) this.element.appendChild(nodePool.getTextNode(number)) this.element.appendChild(nodePool.getElement('DIV', 'icon-right', null)) } From bcaf6553253b6833a661e48a38241d840cfc694a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 5 Jul 2017 11:12:48 +0200 Subject: [PATCH 036/314] Update buffer-row and screen-row data fields on each line number node --- spec/text-editor-component-spec.js | 14 ++++++++++++++ src/text-editor-component.js | 4 +++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 83246ae91..54a7e327a 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -685,6 +685,20 @@ describe('TextEditorComponent', () => { ]) } + editor.getBuffer().insert([2, 0], '\n') + await component.getNextUpdatePromise() + { + const bufferRows = Array.from(element.querySelectorAll('.line-number:not(.dummy)')).map((e) => e.dataset.bufferRow) + const screenRows = Array.from(element.querySelectorAll('.line-number:not(.dummy)')).map((e) => e.dataset.screenRow) + expect(bufferRows).toEqual([ + '0', '1', '2', '3', '4', '4', '5', '6', '7', '7', + '7', '8', '9', '9', '9', '10', '11', '12', '12', '13' + ]) + expect(screenRows).toEqual([ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '10', '11', '12', '13', '14', '15', '16', '17', '18', '19' + ]) + } }) it('does not blow away class names added to the element by packages when changing the class name', async () => { diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 641585348..8599c5b49 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -3167,8 +3167,10 @@ class LineNumberComponent { } update (props) { - const {nodePool, className, width, marginTop, number} = props + const {nodePool, className, width, marginTop, bufferRow, screenRow, number} = props + if (this.props.bufferRow !== bufferRow) this.element.dataset.bufferRow = bufferRow + if (this.props.screenRow !== screenRow) this.element.dataset.screenRow = screenRow if (this.props.className !== className) this.element.className = className if (this.props.width !== width) this.element.style.width = width + 'px' if (this.props.marginTop !== marginTop) { From 4d8f74d96073f234e860b37b3b59506137622780 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 5 Jul 2017 10:29:08 -0700 Subject: [PATCH 037/314] :arrow_up: github@0.3.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9081c2266..de00dea62 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "exception-reporting": "0.41.4", "find-and-replace": "0.209.1", "fuzzy-finder": "1.5.8", - "github": "0.3.3", + "github": "0.3.5", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", From e3704932a439708f8ab3420238ae48af939fe74c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 6 Jul 2017 15:50:17 -0400 Subject: [PATCH 038/314] :arrow_up: spell-check --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9081c2266..27492ad16 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "package-generator": "1.1.1", "settings-view": "0.250.0", "snippets": "1.1.4", - "spell-check": "0.71.4", + "spell-check": "0.72.0", "status-bar": "1.8.11", "styleguide": "0.49.6", "symbols-view": "0.116.1", From 1e118a247310f135e597f12586211e4a773df4a5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 6 Jul 2017 19:46:21 -0400 Subject: [PATCH 039/314] :arrow_up: settings-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 27492ad16..01519af64 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "notifications": "0.67.2", "open-on-github": "1.2.1", "package-generator": "1.1.1", - "settings-view": "0.250.0", + "settings-view": "0.251.0", "snippets": "1.1.4", "spell-check": "0.72.0", "status-bar": "1.8.11", From 0219a1de0f49a1c6b8010c87f9bb169225902f46 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Thu, 6 Jul 2017 22:38:46 -0700 Subject: [PATCH 040/314] :arrow_up: metrics --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 01519af64..1b25dddf8 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "line-ending-selector": "0.7.3", "link": "0.31.3", "markdown-preview": "0.159.13", - "metrics": "1.2.5", + "metrics": "1.2.6", "notifications": "0.67.2", "open-on-github": "1.2.1", "package-generator": "1.1.1", From e392154dad6caf824b4417242202af7e5791245a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Jul 2017 09:29:19 +0200 Subject: [PATCH 041/314] :arrow_up: text-buffer 13.0.6-0 Signed-off-by: Nathan Sobo --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b25dddf8..71bbc38c5 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "sinon": "1.17.4", "@atom/source-map-support": "^0.3.4", "temp": "^0.8.3", - "text-buffer": "13.0.5", + "text-buffer": "13.0.6-0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From e5fd511e235bec67878e4799438f4fae63569c0f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Jul 2017 10:09:09 +0200 Subject: [PATCH 042/314] :arrow_up: text-buffer 13.0.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 71bbc38c5..c021aebd9 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "sinon": "1.17.4", "@atom/source-map-support": "^0.3.4", "temp": "^0.8.3", - "text-buffer": "13.0.6-0", + "text-buffer": "13.0.6", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From d09536915537f5843402c94f1254efbfc94900f5 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 10 Jul 2017 01:12:49 -0400 Subject: [PATCH 043/314] :arrow_up: settings-view@0.251.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c021aebd9..596641b46 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "notifications": "0.67.2", "open-on-github": "1.2.1", "package-generator": "1.1.1", - "settings-view": "0.251.0", + "settings-view": "0.251.1", "snippets": "1.1.4", "spell-check": "0.72.0", "status-bar": "1.8.11", From 3c3a0e350366197fadaa80f742a78788fb9f2ec1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 10 Jul 2017 12:50:44 -0400 Subject: [PATCH 044/314] :arrow_up: atom-keymap --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 596641b46..c1ff8925d 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "electronVersion": "1.6.9", "dependencies": { "async": "0.2.6", - "atom-keymap": "8.2.1", + "atom-keymap": "8.2.2", "atom-select-list": "^0.1.0", "atom-ui": "0.4.1", "babel-core": "5.8.38", From 82619a989b8235f3044ea416bc17137704228b93 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 10 Jul 2017 14:27:55 -0600 Subject: [PATCH 045/314] :art: --- src/text-editor-component.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index b44fac331..fef64d654 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1645,10 +1645,11 @@ class TextEditorComponent { didMouseDownOnContent (event) { const {model} = this.props const {target, button, detail, ctrlKey, shiftKey, metaKey} = event + const platform = this.getPlatform() // Only handle mousedown events for left mouse button (or the middle mouse // button on Linux where it pastes the selection clipboard). - if (!(button === 0 || (this.getPlatform() === 'linux' && button === 1))) return + if (!(button === 0 || (platform === 'linux' && button === 1))) return const screenPosition = this.screenPositionForMouseEvent(event) @@ -1659,14 +1660,14 @@ class TextEditorComponent { } // Handle middle mouse button only on Linux (paste clipboard) - if (this.getPlatform() === 'linux' && button === 1) { + if (platform === 'linux' && button === 1) { const selection = clipboard.readText('selection') model.setCursorScreenPosition(screenPosition, {autoscroll: false}) model.insertText(selection) return } - const addOrRemoveSelection = metaKey || (ctrlKey && this.getPlatform() !== 'darwin') + const addOrRemoveSelection = metaKey || (ctrlKey && platform !== 'darwin') switch (detail) { case 1: From cd27b49dc471db63c82850d119e7c98293f251a2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 10 Jul 2017 14:28:21 -0600 Subject: [PATCH 046/314] Don't handle ctrl-click events on macOS It brings up the context menu, so we shouldn't change the cursor position --- src/text-editor-component.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index fef64d654..dfcdbc150 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1667,6 +1667,9 @@ class TextEditorComponent { return } + // Ctrl-click brings up the context menu on macOS + if (platform === 'darwin' && ctrlKey) return + const addOrRemoveSelection = metaKey || (ctrlKey && platform !== 'darwin') switch (detail) { From f6d2f966bfb57492632c65c23e318dd792bbdced Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 10 Jul 2017 15:53:23 -0600 Subject: [PATCH 047/314] Fix middle-mouse-button paste on Linux Chrome now synthesizes a textInput event on mouseup for middle mouse button clicks, which rendered our custom JS for handling that case redundant. --- spec/text-editor-component-spec.js | 37 +++++++----------------------- src/text-editor-component.js | 25 ++++++++++---------- 2 files changed, 21 insertions(+), 41 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 555ee3015..2028ae8bd 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -2412,10 +2412,13 @@ describe('TextEditorComponent', () => { ctrlKey: true }) ) - expect(editor.getCursorScreenPositions()).toEqual([[1, 4]]) + expect(editor.getSelectedScreenRanges()).toEqual([ + [[1, 16], [1, 16]] + ]) // ctrl-click adds cursors on platforms *other* than macOS component.props.platform = 'win32' + editor.setCursorScreenPosition([1, 4]) component.didMouseDownOnContent( Object.assign(clientPositionForCharacter(component, 1, 16), { detail: 1, @@ -2675,42 +2678,18 @@ describe('TextEditorComponent', () => { expect(component.getScrollLeft()).toBe(maxScrollLeft) }) - it('pastes the previously selected text when clicking the middle mouse button on Linux', async () => { - spyOn(electron.ipcRenderer, 'send').andCallFake(function (eventName, selectedText) { - if (eventName === 'write-text-to-selection-clipboard') { - clipboard.writeText(selectedText, 'selection') - } - }) - + it('positions the cursor on clicking the middle mouse button on Linux', async () => { + // The browser synthesizes the paste as a textInput event on mouseup + // so it is not possible to test it here. const {component, editor} = buildComponent({platform: 'linux'}) - // Middle mouse pasting. editor.setSelectedBufferRange([[1, 6], [1, 10]]) - await conditionPromise(() => TextEditor.clipboard.read() === 'sort') component.didMouseDownOnContent({ button: 1, clientX: clientLeftForCharacter(component, 10, 0), clientY: clientTopForLine(component, 10) }) - expect(TextEditor.clipboard.read()).toBe('sort') - expect(editor.lineTextForBufferRow(10)).toBe('sort') - editor.undo() - - // Ensure left clicks don't interfere. - editor.setSelectedBufferRange([[1, 2], [1, 5]]) - await conditionPromise(() => TextEditor.clipboard.read() === 'var') - component.didMouseDownOnContent({ - button: 0, - detail: 1, - clientX: clientLeftForCharacter(component, 10, 0), - clientY: clientTopForLine(component, 10) - }) - component.didMouseDownOnContent({ - button: 1, - clientX: clientLeftForCharacter(component, 10, 0), - clientY: clientTopForLine(component, 10) - }) - expect(editor.lineTextForBufferRow(10)).toBe('var') + expect(editor.getSelectedBufferRange()).toEqual([[10, 0], [10, 0]]) }) }) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index dfcdbc150..cf9e5c53f 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1647,9 +1647,21 @@ class TextEditorComponent { const {target, button, detail, ctrlKey, shiftKey, metaKey} = event const platform = this.getPlatform() + // On Linux, position the cursor on middle mouse button click. A + // textInput event with the contents of the selection clipboard will be + // dispatched by the browser automatically on mouseup. + if (platform === 'linux' && button === 1) { + const screenPosition = this.screenPositionForMouseEvent(event) + model.setCursorScreenPosition(screenPosition, {autoscroll: false}) + return + } + // Only handle mousedown events for left mouse button (or the middle mouse // button on Linux where it pastes the selection clipboard). - if (!(button === 0 || (platform === 'linux' && button === 1))) return + if (button !== 0) return + + // Ctrl-click brings up the context menu on macOS + if (platform === 'darwin' && ctrlKey) return const screenPosition = this.screenPositionForMouseEvent(event) @@ -1659,17 +1671,6 @@ class TextEditorComponent { return } - // Handle middle mouse button only on Linux (paste clipboard) - if (platform === 'linux' && button === 1) { - const selection = clipboard.readText('selection') - model.setCursorScreenPosition(screenPosition, {autoscroll: false}) - model.insertText(selection) - return - } - - // Ctrl-click brings up the context menu on macOS - if (platform === 'darwin' && ctrlKey) return - const addOrRemoveSelection = metaKey || (ctrlKey && platform !== 'darwin') switch (detail) { From 07c7e53b1eb620064e9d81004438f3028d22151a Mon Sep 17 00:00:00 2001 From: ungb Date: Mon, 10 Jul 2017 16:26:47 -0700 Subject: [PATCH 048/314] fix lint errors --- src/text-editor-component.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index cf9e5c53f..037e45462 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -5,7 +5,6 @@ const {Point, Range} = require('text-buffer') const LineTopIndex = require('line-top-index') const TextEditor = require('./text-editor') const {isPairedCharacter} = require('./text-utils') -const clipboard = require('./safe-clipboard') const electron = require('electron') const $ = etch.dom From 68b7bf46dd051a042802b4d8b86ec7fd3b3f596e Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 11 Jul 2017 17:04:54 -0700 Subject: [PATCH 049/314] Make loadSettings a property so we can change it --- src/main-process/atom-window.coffee | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index b0b516b10..5d1add870 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -54,14 +54,14 @@ class AtomWindow @browserWindow = new BrowserWindow(options) @handleEvents() - loadSettings = Object.assign({}, settings) - loadSettings.appVersion = app.getVersion() - loadSettings.resourcePath = @resourcePath - loadSettings.devMode ?= false - loadSettings.safeMode ?= false - loadSettings.atomHome = process.env.ATOM_HOME - loadSettings.clearWindowState ?= false - loadSettings.initialPaths ?= + @loadSettings = Object.assign({}, settings) + @loadSettings.appVersion = app.getVersion() + @loadSettings.resourcePath = @resourcePath + @loadSettings.devMode ?= false + @loadSettings.safeMode ?= false + @loadSettings.atomHome = process.env.ATOM_HOME + @loadSettings.clearWindowState ?= false + @loadSettings.initialPaths ?= for {pathToOpen} in locationsToOpen when pathToOpen stat = fs.statSyncNoException(pathToOpen) or null if stat?.isDirectory() @@ -72,17 +72,17 @@ class AtomWindow parentDirectory else pathToOpen - loadSettings.initialPaths.sort() + @loadSettings.initialPaths.sort() # Only send to the first non-spec window created if @constructor.includeShellLoadTime and not @isSpec @constructor.includeShellLoadTime = false - loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime + @loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime - @representedDirectoryPaths = loadSettings.initialPaths - @env = loadSettings.env if loadSettings.env? + @representedDirectoryPaths = @loadSettings.initialPaths + @env = @loadSettings.env if @loadSettings.env? - @browserWindow.loadSettingsJSON = JSON.stringify(loadSettings) + @browserWindow.loadSettingsJSON = JSON.stringify(@loadSettings) @browserWindow.on 'window:loaded', => @disableZoom() From 269623c9baf67d81abc6cb4ecffca22cbc2f1ec2 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 11 Jul 2017 20:46:09 -0700 Subject: [PATCH 050/314] Update loadSettingsJSON when paths change, fixed #13933 --- src/main-process/atom-window.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index 5d1add870..f20239e66 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -309,6 +309,8 @@ class AtomWindow setRepresentedDirectoryPaths: (@representedDirectoryPaths) -> @representedDirectoryPaths.sort() + @loadSettings.initialPaths = @representedDirectoryPaths + @browserWindow.loadSettingsJSON = JSON.stringify(@loadSettings) @atomApplication.saveState() copy: -> @browserWindow.copy() From 4354b8b7b30f456f5d2b2dccf3293982ad064c59 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 12 Jul 2017 11:32:44 -0700 Subject: [PATCH 051/314] Add --existing-artifacts switch to script\build --- script/build | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/script/build b/script/build index 21561353b..e25659ec7 100755 --- a/script/build +++ b/script/build @@ -14,6 +14,7 @@ const yargs = require('yargs') const argv = yargs .usage('Usage: $0 [options]') .help('help') + .describe('existing-artifacts', 'Use existing Atom binaries (skip clean/transpile/cache)') .describe('code-sign', 'Code-sign executables (macOS and Windows only)') .describe('create-windows-installer', 'Create installer (Windows only)') .describe('create-debian-package', 'Create .deb package (Linux only)') @@ -52,18 +53,21 @@ process.on('unhandledRejection', function (e) { process.exit(1) }) -checkChromedriverVersion() -cleanOutputDirectory() -copyAssets() -transpilePackagesWithCustomTranspilerPaths() -transpileBabelPaths() -transpileCoffeeScriptPaths() -transpileCsonPaths() -transpilePegJsPaths() -generateModuleCache() -prebuildLessCache() -generateMetadata() -generateAPIDocs() +if (!argv.existingArtifacts) { + checkChromedriverVersion() + cleanOutputDirectory() + copyAssets() + transpilePackagesWithCustomTranspilerPaths() + transpileBabelPaths() + transpileCoffeeScriptPaths() + transpileCsonPaths() + transpilePegJsPaths() + generateModuleCache() + prebuildLessCache() + generateMetadata() + generateAPIDocs() +} + dumpSymbols() .then(packageApplication) .then(packagedAppPath => generateStartupSnapshot(packagedAppPath).then(() => packagedAppPath)) From 2d66dd93fb93c3faa137c3c15116f8148f9e46e3 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 12 Jul 2017 14:17:53 -0700 Subject: [PATCH 052/314] Create Windows installer after the tests have run --- appveyor.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 11ab7b3ba..0cbcdcec9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -31,19 +31,20 @@ install: - npm install -g npm build_script: - - IF NOT EXIST C:\sqtemp MKDIR C:\sqtemp - - SET SQUIRREL_TEMP=C:\sqtemp - CD %APPVEYOR_BUILD_FOLDER% - - IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] ( - script\build.cmd --code-sign --create-windows-installer --compress-artifacts - ) ELSE ( - script\build.cmd --code-sign --compress-artifacts - ) + - script\build.cmd --code-sign --compress-artifacts test_script: - script\lint.cmd - script\test.cmd +after_test: + - IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] ( + IF NOT EXIST C:\sqtemp MKDIR C:\sqtemp + SET SQUIRREL_TEMP=C:\sqtemp + script\build.cmd --existing-artifacts --code-sign --create-windows-installer + ) + deploy: off artifacts: - path: out\AtomSetup.exe From ef983f63cf0c4437fc51cd0522944203f55bf28e Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 12 Jul 2017 14:58:31 -0700 Subject: [PATCH 053/314] Revert GH package version for testing --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5d8542515..c1ff8925d 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "exception-reporting": "0.41.4", "find-and-replace": "0.209.1", "fuzzy-finder": "1.5.8", - "github": "0.3.5", + "github": "0.3.3", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", From dbe75c9362858f4a35d28036b31ee2673aa1da5c Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 12 Jul 2017 15:52:08 -0700 Subject: [PATCH 054/314] Pin npm to 5.1 so we can build. 5.2 causes errors with eslinter-plugin-react --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 65e244dd4..9d972adec 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -28,7 +28,7 @@ matrix: install: - SET PATH=C:\Program Files\Atom\resources\cli;%PATH% - ps: Install-Product node $env:NODE_VERSION $env:PLATFORM - - npm install -g npm + - npm install -g npm@5.1 build_script: - IF NOT EXIST C:\sqtemp MKDIR C:\sqtemp From a6abd8c70313998f04d33917d7b5dc199464a2d4 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 12 Jul 2017 23:20:15 -0400 Subject: [PATCH 055/314] :arrow_up: language-javascript@0.127.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c1ff8925d..3fcccdffb 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "language-html": "0.47.3", "language-hyperlink": "0.16.1", "language-java": "0.27.2", - "language-javascript": "0.126.1", + "language-javascript": "0.127.0", "language-json": "0.19.1", "language-less": "0.32.0", "language-make": "0.22.3", From c7f4f058d27027ed15261cf7fe80695d4ad3e4f8 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 12 Jul 2017 23:21:48 -0400 Subject: [PATCH 056/314] :arrow_up: language-css@0.42.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3fcccdffb..57b861312 100644 --- a/package.json +++ b/package.json @@ -135,7 +135,7 @@ "language-clojure": "0.22.3", "language-coffee-script": "0.48.7", "language-csharp": "0.14.2", - "language-css": "0.42.2", + "language-css": "0.42.3", "language-gfm": "0.89.1", "language-git": "0.19.1", "language-go": "0.44.1", From b953ba2cd0c22851c347cedeb626b4752e9d5adf Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 12 Jul 2017 23:23:38 -0400 Subject: [PATCH 057/314] :arrow_up: language-sass@0.60.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 57b861312..f843340cf 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,7 @@ "language-python": "0.45.3", "language-ruby": "0.71.1", "language-ruby-on-rails": "0.25.2", - "language-sass": "0.59.0", + "language-sass": "0.60.0", "language-shellscript": "0.25.1", "language-source": "0.9.0", "language-sql": "0.25.6", From 216d9c16def87fe901794349ced2885dcea4306a Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 12 Jul 2017 23:25:28 -0400 Subject: [PATCH 058/314] :arrow_up: language-less@0.33.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f843340cf..46da89e49 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "language-java": "0.27.2", "language-javascript": "0.127.0", "language-json": "0.19.1", - "language-less": "0.32.0", + "language-less": "0.33.0", "language-make": "0.22.3", "language-mustache": "0.14.1", "language-objective-c": "0.15.1", From 2beb1158431a56c748e3f74f21c41668629309eb Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 12 Jul 2017 23:26:44 -0400 Subject: [PATCH 059/314] :arrow_up: language-clojure@0.22.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 46da89e49..33e58ac62 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "whitespace": "0.37.2", "wrap-guide": "0.40.2", "language-c": "0.58.1", - "language-clojure": "0.22.3", + "language-clojure": "0.22.4", "language-coffee-script": "0.48.7", "language-csharp": "0.14.2", "language-css": "0.42.3", From aa641ccefb4b06885849b533ebe70e09ff0e37ca Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 12 Jul 2017 23:27:56 -0400 Subject: [PATCH 060/314] :arrow_up: language-coffee-script@0.48.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 33e58ac62..9a7ff997a 100644 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "wrap-guide": "0.40.2", "language-c": "0.58.1", "language-clojure": "0.22.4", - "language-coffee-script": "0.48.7", + "language-coffee-script": "0.48.8", "language-csharp": "0.14.2", "language-css": "0.42.3", "language-gfm": "0.89.1", From 518d3bacea9404119bbc7eeacd652c44210ef512 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 12 Jul 2017 23:29:16 -0400 Subject: [PATCH 061/314] :arrow_up: language-yaml@0.30.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9a7ff997a..667b94b4f 100644 --- a/package.json +++ b/package.json @@ -162,7 +162,7 @@ "language-todo": "0.29.1", "language-toml": "0.18.1", "language-xml": "0.35.1", - "language-yaml": "0.30.0" + "language-yaml": "0.30.1" }, "private": true, "scripts": { From 6226c31a0c1b534391883834991f030eddfe9182 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 12 Jul 2017 23:30:13 -0400 Subject: [PATCH 062/314] :arrow_up: language-xml@0.35.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 667b94b4f..ddf1b721d 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "language-text": "0.7.3", "language-todo": "0.29.1", "language-toml": "0.18.1", - "language-xml": "0.35.1", + "language-xml": "0.35.2", "language-yaml": "0.30.1" }, "private": true, From bbbd02378f96c3f374e5aa4a57dedd7edae212b7 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 12 Jul 2017 23:36:06 -0400 Subject: [PATCH 063/314] :arrow_up: language-php@0.40.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ddf1b721d..7499770aa 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "language-mustache": "0.14.1", "language-objective-c": "0.15.1", "language-perl": "0.37.0", - "language-php": "0.39.0", + "language-php": "0.40.0", "language-property-list": "0.9.1", "language-python": "0.45.3", "language-ruby": "0.71.1", From 1fc6bc9a02728fbe9608b82bfbfe9dde31f8de6d Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 12 Jul 2017 23:37:39 -0400 Subject: [PATCH 064/314] :arrow_up: language-sql@0.25.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7499770aa..79765a3c2 100644 --- a/package.json +++ b/package.json @@ -157,7 +157,7 @@ "language-sass": "0.60.0", "language-shellscript": "0.25.1", "language-source": "0.9.0", - "language-sql": "0.25.6", + "language-sql": "0.25.7", "language-text": "0.7.3", "language-todo": "0.29.1", "language-toml": "0.18.1", From 31b1550734b24a15d4a19fb3ba62a4d419be1fef Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 12 Jul 2017 23:39:21 -0400 Subject: [PATCH 065/314] :arrow_up: language-ruby@0.71.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 79765a3c2..85054cd06 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,7 @@ "language-php": "0.40.0", "language-property-list": "0.9.1", "language-python": "0.45.3", - "language-ruby": "0.71.1", + "language-ruby": "0.71.2", "language-ruby-on-rails": "0.25.2", "language-sass": "0.60.0", "language-shellscript": "0.25.1", From 278c4f7106c4d7141063d76f03fdc1940f922288 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 12 Jul 2017 23:40:37 -0400 Subject: [PATCH 066/314] :arrow_up: language-hyperlink@0.16.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 85054cd06..b2128034f 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "language-git": "0.19.1", "language-go": "0.44.1", "language-html": "0.47.3", - "language-hyperlink": "0.16.1", + "language-hyperlink": "0.16.2", "language-java": "0.27.2", "language-javascript": "0.127.0", "language-json": "0.19.1", From 91d0f1468697844ba146feb45cd3d8b7fa56ebd8 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 12 Jul 2017 23:42:08 -0400 Subject: [PATCH 067/314] :arrow_up: language-gfm@0.90.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b2128034f..2ff1690a2 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "language-coffee-script": "0.48.8", "language-csharp": "0.14.2", "language-css": "0.42.3", - "language-gfm": "0.89.1", + "language-gfm": "0.90.0", "language-git": "0.19.1", "language-go": "0.44.1", "language-html": "0.47.3", From fc44548199f7037958c797a78e6b27e5c2c9e7b1 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 12 Jul 2017 23:42:56 -0400 Subject: [PATCH 068/314] :arrow_up: language-shellscript@0.25.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ff1690a2..81e63dcf2 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,7 @@ "language-ruby": "0.71.2", "language-ruby-on-rails": "0.25.2", "language-sass": "0.60.0", - "language-shellscript": "0.25.1", + "language-shellscript": "0.25.2", "language-source": "0.9.0", "language-sql": "0.25.7", "language-text": "0.7.3", From 3dc39a73b8511d9048e4590adf001bd0667bcdf9 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 12 Jul 2017 23:44:40 -0400 Subject: [PATCH 069/314] :arrow_up: language-python@0.45.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 81e63dcf2..b98bb95ff 100644 --- a/package.json +++ b/package.json @@ -151,7 +151,7 @@ "language-perl": "0.37.0", "language-php": "0.40.0", "language-property-list": "0.9.1", - "language-python": "0.45.3", + "language-python": "0.45.4", "language-ruby": "0.71.2", "language-ruby-on-rails": "0.25.2", "language-sass": "0.60.0", From 52bfe3eee5f416bae20e41d5e9c5e443e180a03d Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 12 Jul 2017 23:48:32 -0400 Subject: [PATCH 070/314] :arrow_up: autocomplete-css@0.16.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c1ff8925d..b0670c556 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "about": "1.7.6", "archive-view": "0.63.3", "autocomplete-atom-api": "0.10.2", - "autocomplete-css": "0.16.2", + "autocomplete-css": "0.16.3", "autocomplete-html": "0.8.0", "autocomplete-plus": "2.35.7", "autocomplete-snippets": "1.11.0", From d0588cd812de3706e5fe193ab1d71c00f41deb67 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Thu, 13 Jul 2017 02:32:43 -0700 Subject: [PATCH 071/314] Check if incoming element is non-null before attempting to append to it --- src/text-editor-component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 037e45462..dd11884cc 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -3271,7 +3271,7 @@ class CustomGutterDecorationComponent { if (newProps.className !== oldProps.className) this.element.className = newProps.className || '' if (newProps.element !== oldProps.element) { if (this.element.firstChild) this.element.firstChild.remove() - this.element.appendChild(newProps.element) + if (newProps.element != null) this.element.appendChild(newProps.element) } } } From c3f7edc104df3ad8e325fc1dbfa75a50776cc767 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 13 Jul 2017 15:11:36 +0200 Subject: [PATCH 072/314] Swap underlying editor correctly when calling setModel on editor element Previously, when `setModel` was called, we forgot to update the pointer to the component in the newly supplied editor. This was causing the element to not update in response to model updates but only as a result of focus or visibility changes. We suspect this regressed during the rewrite of the editor rendering layer. With this commit we will now correctly swap the element's underlying editor by updating the component pointer on the newly supplied editor. Also, if the element was already attached to another editor, we will null out the component reference on it, because one instance of `TextEditorElement` can only represent one instance of `TextEditor`. --- spec/text-editor-element-spec.js | 27 +++++++++++++++++++++++++++ src/text-editor-component.js | 4 ++++ 2 files changed, 31 insertions(+) diff --git a/spec/text-editor-element-spec.js b/spec/text-editor-element-spec.js index c92c6f144..a9692bc21 100644 --- a/spec/text-editor-element-spec.js +++ b/spec/text-editor-element-spec.js @@ -201,6 +201,33 @@ describe('TextEditorElement', () => { }) }) + describe('::setModel', () => { + describe('when the element does not have an editor yet', () => { + it('uses the supplied one', () => { + const element = buildTextEditorElement({attach: false}) + const editor = new TextEditor() + element.setModel(editor) + jasmine.attachToDOM(element) + expect(editor.element).toBe(element) + expect(element.getModel()).toBe(editor) + }) + }) + + describe('when the element already has an editor', () => { + it('unbinds it and then swaps it with the supplied one', async () => { + const element = buildTextEditorElement({attach: true}) + const previousEditor = element.getModel() + expect(previousEditor.element).toBe(element) + + const newEditor = new TextEditor() + element.setModel(newEditor) + expect(previousEditor.element).not.toBe(element) + expect(newEditor.element).toBe(element) + expect(element.getModel()).toBe(newEditor) + }) + }) + }) + describe('::onDidAttach and ::onDidDetach', () => it('invokes callbacks when the element is attached and detached', () => { const element = buildTextEditorElement({attach: false}) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 037e45462..a93cbabba 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -174,6 +174,10 @@ class TextEditorComponent { } update (props) { + if (props.model !== this.props.model) { + this.props.model.component = null + props.model.component = this + } this.props = props this.scheduleUpdate() } From 1d42590a5bf8eb9e9fe69fab5eda746df2457905 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 13 Jul 2017 10:12:37 -0600 Subject: [PATCH 073/314] Force legacy scrollbars in text-editor-element-spec --- spec/text-editor-element-spec.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/text-editor-element-spec.js b/spec/text-editor-element-spec.js index a9692bc21..684d160a8 100644 --- a/spec/text-editor-element-spec.js +++ b/spec/text-editor-element-spec.js @@ -9,6 +9,10 @@ describe('TextEditorElement', () => { beforeEach(() => { jasmineContent = document.body.querySelector('#jasmine-content') + // Force scrollbars to be visible regardless of local system configuration + const scrollbarStyle = document.createElement('style') + scrollbarStyle.textContent = '::-webkit-scrollbar { -webkit-appearance: none }' + jasmine.attachToDOM(scrollbarStyle) }) function buildTextEditorElement (options = {}) { From e88e0fc55cfb260e94410b7bc63a9c527f3602e2 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Thu, 13 Jul 2017 09:22:23 -0700 Subject: [PATCH 074/314] Use npm 5.1.0 for building Atom --- .travis.yml | 2 +- appveyor.yml | 2 +- circle.yml | 2 +- script/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 71accd347..790259016 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ install: - source /tmp/.nvm/nvm.sh - nvm install $NODE_VERSION - nvm use --delete-prefix $NODE_VERSION - - npm install -g npm + - npm install -g npm@5.1.0 - script/build --create-debian-package --create-rpm-package --compress-artifacts script: diff --git a/appveyor.yml b/appveyor.yml index 9d972adec..30b321e35 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -28,7 +28,7 @@ matrix: install: - SET PATH=C:\Program Files\Atom\resources\cli;%PATH% - ps: Install-Product node $env:NODE_VERSION $env:PLATFORM - - npm install -g npm@5.1 + - npm install -g npm@5.1.0 build_script: - IF NOT EXIST C:\sqtemp MKDIR C:\sqtemp diff --git a/circle.yml b/circle.yml index 5890c45e9..34ff7e2f8 100644 --- a/circle.yml +++ b/circle.yml @@ -18,7 +18,7 @@ dependencies: - curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.3/install.sh | bash - nvm install 6.9.4 - nvm use 6.9.4 - - npm install -g npm + - npm install -g npm@5.1.0 override: - script/build --code-sign --compress-artifacts diff --git a/script/package.json b/script/package.json index e5c09c286..9f1f36a4d 100644 --- a/script/package.json +++ b/script/package.json @@ -22,7 +22,7 @@ "minidump": "0.9.0", "mkdirp": "0.5.1", "normalize-package-data": "2.3.5", - "npm": "3.10.5", + "npm": "5.1.0", "passwd-user": "2.1.0", "pegjs": "0.9.0", "runas": "3.1.1", From 08e5dcf325b04f8d7229a8c906775832d1913ff6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 13 Jul 2017 09:48:18 -0700 Subject: [PATCH 075/314] :arrow_up: find-and-replace --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b0670c556..a3f6e4b82 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "dev-live-reload": "0.47.1", "encoding-selector": "0.23.4", "exception-reporting": "0.41.4", - "find-and-replace": "0.209.1", + "find-and-replace": "0.209.2", "fuzzy-finder": "1.5.8", "github": "0.3.3", "git-diff": "1.3.6", From dc73841673cfdd1a4314775b34f7df6879e33a3b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 13 Jul 2017 14:37:58 -0600 Subject: [PATCH 076/314] Add test for null-guarding element during gutter decoration update --- spec/text-editor-component-spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 2028ae8bd..7a4608872 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1762,13 +1762,13 @@ describe('TextEditorComponent', () => { expect(decorationNode3.firstChild).toBe(decorationElement2) decoration1.setProperties({type: 'gutter', gutterName: 'a', class: 'c', item: decorationElement1}) - decoration2.setProperties({type: 'gutter', gutterName: 'a', item: decorationElement2}) + decoration2.setProperties({type: 'gutter', gutterName: 'a'}) decoration3.destroy() await component.getNextUpdatePromise() expect(decorationNode1.className).toBe('decoration c') expect(decorationNode1.firstChild).toBe(decorationElement1) expect(decorationNode2.className).toBe('decoration') - expect(decorationNode2.firstChild).toBe(decorationElement2) + expect(decorationNode2.firstChild).toBeNull() expect(gutterB.getElement().firstChild.children.length).toBe(0) }) }) From 19e671c18154772484be0ec76c8ae658aff3d9ef Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 13 Jul 2017 14:35:25 -0700 Subject: [PATCH 077/314] :arrow_up: github@0.3.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c1ff8925d..7e4512d86 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "exception-reporting": "0.41.4", "find-and-replace": "0.209.1", "fuzzy-finder": "1.5.8", - "github": "0.3.3", + "github": "0.3.7", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", From e365031fb2e57b863e30f02e18227bed6d40420a Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 13 Jul 2017 15:19:26 -0700 Subject: [PATCH 078/314] :arrow_up: github@0.3.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3e9392b31..7d4cd7305 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "exception-reporting": "0.41.4", "find-and-replace": "0.209.2", "fuzzy-finder": "1.5.8", - "github": "0.3.7", + "github": "0.3.8", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", From 73ee17f61afce2a77e45bb5d999eb4dbe4b68f5a Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 13 Jul 2017 23:40:33 -0400 Subject: [PATCH 079/314] :arrow_up: language-javascript@0.127.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b98bb95ff..6cdf8883c 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "language-html": "0.47.3", "language-hyperlink": "0.16.2", "language-java": "0.27.2", - "language-javascript": "0.127.0", + "language-javascript": "0.127.1", "language-json": "0.19.1", "language-less": "0.33.0", "language-make": "0.22.3", From 9b99c863e03178ee05d6234a12d90bc084786283 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 14 Jul 2017 00:14:04 -0400 Subject: [PATCH 080/314] Add JSDoc to the mix --- spec/workspace-spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index 0fe1d7afa..489b45120 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -1582,6 +1582,7 @@ i = /test/; #FIXME\ expect(atom2.grammars.getGrammars().map(grammar => grammar.name).sort()).toEqual([ 'CoffeeScript', 'CoffeeScript (Literate)', + 'JSDoc', 'JavaScript', 'Null Grammar', 'Regular Expression Replacement (JavaScript)', From 1e38da6519a4dace2f40ec0099bceec5e64b239a Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 14 Jul 2017 09:56:28 -0400 Subject: [PATCH 081/314] :arrow_up: image-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7d4cd7305..61c3a9530 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", - "image-view": "0.62.0", + "image-view": "0.62.1", "incompatible-packages": "0.27.3", "keybinding-resolver": "0.38.0", "line-ending-selector": "0.7.3", From 3d02234cbdf701a851d9b1239d67dcdbc8808146 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 14 Jul 2017 19:08:14 -0400 Subject: [PATCH 082/314] :arrow_up: symbols-view@0.117.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a00db7b9..1ac8956eb 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "spell-check": "0.72.0", "status-bar": "1.8.11", "styleguide": "0.49.6", - "symbols-view": "0.116.1", + "symbols-view": "0.117.0", "tabs": "0.106.2", "timecop": "0.36.0", "tree-view": "0.217.3", From 3c2fc9494d480a82b3ddd427626b64b985db9000 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 15 Jul 2017 23:52:09 -0400 Subject: [PATCH 083/314] :arrow_up: settings-view@0.251.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1ac8956eb..0aba351ff 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "notifications": "0.67.2", "open-on-github": "1.2.1", "package-generator": "1.1.1", - "settings-view": "0.251.1", + "settings-view": "0.251.2", "snippets": "1.1.4", "spell-check": "0.72.0", "status-bar": "1.8.11", From 5fb82836c37a0d4a7765ce6828bd3ec943f76e76 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 16 Jul 2017 08:19:39 -0400 Subject: [PATCH 084/314] Use npm 5.3.0 for building Atom --- .travis.yml | 2 +- appveyor.yml | 2 +- circle.yml | 2 +- script/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 790259016..62040612a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ install: - source /tmp/.nvm/nvm.sh - nvm install $NODE_VERSION - nvm use --delete-prefix $NODE_VERSION - - npm install -g npm@5.1.0 + - npm install -g npm@5.3.0 - script/build --create-debian-package --create-rpm-package --compress-artifacts script: diff --git a/appveyor.yml b/appveyor.yml index 30b321e35..ba57543ce 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -28,7 +28,7 @@ matrix: install: - SET PATH=C:\Program Files\Atom\resources\cli;%PATH% - ps: Install-Product node $env:NODE_VERSION $env:PLATFORM - - npm install -g npm@5.1.0 + - npm install -g npm@5.3.0 build_script: - IF NOT EXIST C:\sqtemp MKDIR C:\sqtemp diff --git a/circle.yml b/circle.yml index 34ff7e2f8..8b578bebd 100644 --- a/circle.yml +++ b/circle.yml @@ -18,7 +18,7 @@ dependencies: - curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.3/install.sh | bash - nvm install 6.9.4 - nvm use 6.9.4 - - npm install -g npm@5.1.0 + - npm install -g npm@5.3.0 override: - script/build --code-sign --compress-artifacts diff --git a/script/package.json b/script/package.json index 9f1f36a4d..350fc2b0e 100644 --- a/script/package.json +++ b/script/package.json @@ -22,7 +22,7 @@ "minidump": "0.9.0", "mkdirp": "0.5.1", "normalize-package-data": "2.3.5", - "npm": "5.1.0", + "npm": "5.3.0", "passwd-user": "2.1.0", "pegjs": "0.9.0", "runas": "3.1.1", From d61967bff479acbdc6216dc9618879ebe8d23910 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 17 Jul 2017 00:00:53 -0400 Subject: [PATCH 085/314] :arrow_up: language-go@0.44.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0aba351ff..d9579372c 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "language-css": "0.42.3", "language-gfm": "0.90.0", "language-git": "0.19.1", - "language-go": "0.44.1", + "language-go": "0.44.2", "language-html": "0.47.3", "language-hyperlink": "0.16.2", "language-java": "0.27.2", From 1040a36a0caa76ab66682c07834235d06b436e1e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 17 Jul 2017 13:34:39 -0400 Subject: [PATCH 086/314] :arrow_up: find-and-replace --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d9579372c..4ba771397 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "dev-live-reload": "0.47.1", "encoding-selector": "0.23.4", "exception-reporting": "0.41.4", - "find-and-replace": "0.209.2", + "find-and-replace": "0.209.3", "fuzzy-finder": "1.5.8", "github": "0.3.8", "git-diff": "1.3.6", From 715576fc8b4044db41c567f78fb3e83e72bbc64a Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 17 Jul 2017 10:56:49 -0700 Subject: [PATCH 087/314] Normalize access keys (shortcuts) on buttons in dialogs --- src/application-delegate.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index fdb7119e1..78ea42087 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -143,6 +143,7 @@ class ApplicationDelegate message: message detail: detailedMessage buttons: buttonLabels + normalizeAccessKeys: true }) if _.isArray(buttons) From fe147ccf2e477167e46f19f36a452197f03b9910 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 17 Jul 2017 13:00:40 -0700 Subject: [PATCH 088/314] Only dumpSymbols if rebuilding binaries during build --- script/build | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/script/build b/script/build index e25659ec7..c2b977c81 100755 --- a/script/build +++ b/script/build @@ -53,6 +53,8 @@ process.on('unhandledRejection', function (e) { process.exit(1) }) +let artifactsPromise = Promise.resolve() + if (!argv.existingArtifacts) { checkChromedriverVersion() cleanOutputDirectory() @@ -66,9 +68,10 @@ if (!argv.existingArtifacts) { prebuildLessCache() generateMetadata() generateAPIDocs() + artifactsPromise = dumpSymbols() } -dumpSymbols() +artifactsPromise .then(packageApplication) .then(packagedAppPath => generateStartupSnapshot(packagedAppPath).then(() => packagedAppPath)) .then(packagedAppPath => { From dcbb72bbec58ccced4bd5423ff629b3a0b19fc15 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 17 Jul 2017 14:41:12 -0700 Subject: [PATCH 089/314] Do not dump symbols when using existing binaries. Change command switch name --- script/build | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/script/build b/script/build index c2b977c81..c3efddd3b 100755 --- a/script/build +++ b/script/build @@ -14,7 +14,7 @@ const yargs = require('yargs') const argv = yargs .usage('Usage: $0 [options]') .help('help') - .describe('existing-artifacts', 'Use existing Atom binaries (skip clean/transpile/cache)') + .describe('existing-binaries', 'Use existing Atom binaries (skip clean/transpile/cache)') .describe('code-sign', 'Code-sign executables (macOS and Windows only)') .describe('create-windows-installer', 'Create installer (Windows only)') .describe('create-debian-package', 'Create .deb package (Linux only)') @@ -53,9 +53,9 @@ process.on('unhandledRejection', function (e) { process.exit(1) }) -let artifactsPromise = Promise.resolve() +let binariesPromise = Promise.resolve() -if (!argv.existingArtifacts) { +if (!argv.existingBinaries) { checkChromedriverVersion() cleanOutputDirectory() copyAssets() @@ -68,10 +68,10 @@ if (!argv.existingArtifacts) { prebuildLessCache() generateMetadata() generateAPIDocs() - artifactsPromise = dumpSymbols() + binariesPromise = dumpSymbols() } -artifactsPromise +binariesPromise .then(packageApplication) .then(packagedAppPath => generateStartupSnapshot(packagedAppPath).then(() => packagedAppPath)) .then(packagedAppPath => { From bad6325484ab23999d1f7e494c726643b8673014 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 17 Jul 2017 15:13:05 -0700 Subject: [PATCH 090/314] Actually switch over to using the new flag on AppVeyor --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 0cbcdcec9..81b2dfc97 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -42,7 +42,7 @@ after_test: - IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] ( IF NOT EXIST C:\sqtemp MKDIR C:\sqtemp SET SQUIRREL_TEMP=C:\sqtemp - script\build.cmd --existing-artifacts --code-sign --create-windows-installer + script\build.cmd --existing-binaries --code-sign --create-windows-installer ) deploy: off From 78efec6a495796b945e316935363bcfdd4be6218 Mon Sep 17 00:00:00 2001 From: ungb Date: Mon, 17 Jul 2017 16:43:42 -0700 Subject: [PATCH 091/314] :arrow_up: image-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4ba771397..03d2c2216 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", - "image-view": "0.62.1", + "image-view": "0.62.2", "incompatible-packages": "0.27.3", "keybinding-resolver": "0.38.0", "line-ending-selector": "0.7.3", From d5e3eba4723466d1b742aa1898ef8d389c607bb5 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 18 Jul 2017 00:32:24 -0400 Subject: [PATCH 092/314] :arrow_up: autocomplete-css@0.17.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 03d2c2216..326bcd4e0 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "about": "1.7.6", "archive-view": "0.63.3", "autocomplete-atom-api": "0.10.2", - "autocomplete-css": "0.16.3", + "autocomplete-css": "0.17.0", "autocomplete-html": "0.8.0", "autocomplete-plus": "2.35.7", "autocomplete-snippets": "1.11.0", From 1186a9896ceb67c147031ec7ca0b527e0daa000f Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 18 Jul 2017 00:33:49 -0400 Subject: [PATCH 093/314] :arrow_up: language-sql@0.25.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 326bcd4e0..00f4a2689 100644 --- a/package.json +++ b/package.json @@ -157,7 +157,7 @@ "language-sass": "0.60.0", "language-shellscript": "0.25.2", "language-source": "0.9.0", - "language-sql": "0.25.7", + "language-sql": "0.25.8", "language-text": "0.7.3", "language-todo": "0.29.1", "language-toml": "0.18.1", From f62e0bceeff9f707b881bc7f09c54bc882b2dd67 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 18 Jul 2017 10:01:50 -0400 Subject: [PATCH 094/314] :arrow_down: autocomplete-css@0.16.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 00f4a2689..8a867dd8a 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "about": "1.7.6", "archive-view": "0.63.3", "autocomplete-atom-api": "0.10.2", - "autocomplete-css": "0.17.0", + "autocomplete-css": "0.16.3", "autocomplete-html": "0.8.0", "autocomplete-plus": "2.35.7", "autocomplete-snippets": "1.11.0", From 8205ff7bb89403bfc154debc6ca57b9607144c44 Mon Sep 17 00:00:00 2001 From: Lee Dohm <1038121+lee-dohm@users.noreply.github.com> Date: Tue, 18 Jul 2017 14:04:56 -0700 Subject: [PATCH 095/314] :arrow_up: bracket-matcher@0.87.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8a867dd8a..eeec8b698 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "autosave": "0.24.3", "background-tips": "0.27.1", "bookmarks": "0.44.4", - "bracket-matcher": "0.86.0", + "bracket-matcher": "0.87.0", "command-palette": "0.40.4", "dalek": "0.2.1", "deprecation-cop": "0.56.7", From 48abb16edbb105e41c2558c88a31ab45a2a3ba6c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 18 Jul 2017 14:31:47 -0700 Subject: [PATCH 096/314] Fix exception in screenPositionForPixelPosition when content updates are pending Signed-off-by: Nathan Sobo --- spec/text-editor-component-spec.js | 9 +++++++++ src/text-editor-component.js | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 7a4608872..cb21f68a3 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -3335,6 +3335,15 @@ describe('TextEditorComponent', () => { expect(left).toBe(clientLeftForCharacter(referenceComponent, 12, 1) - referenceContentRect.left) } }) + + it('does not get the component into an inconsistent state when the model has unflushed changes (regression)', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 2, autoHeight: false, text: ''}) + await setEditorHeightInLines(component, 10) + + const updatePromise = editor.getBuffer().append("hi\n") + component.screenPositionForPixelPosition({top: 800, left: 1}) + await updatePromise + }) }) describe('screenPositionForPixelPosition', () => { diff --git a/src/text-editor-component.js b/src/text-editor-component.js index a3a51a3aa..bffa5db50 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -251,20 +251,17 @@ class TextEditorComponent { this.measureBlockDecorations() - this.measuredContent = false this.updateSyncBeforeMeasuringContent() if (useScheduler === true) { const scheduler = etch.getScheduler() scheduler.readDocument(() => { this.measureContentDuringUpdateSync() - this.measuredContent = true scheduler.updateDocument(() => { this.updateSyncAfterMeasuringContent() }) }) } else { this.measureContentDuringUpdateSync() - this.measuredContent = true this.updateSyncAfterMeasuringContent() } } @@ -341,6 +338,7 @@ class TextEditorComponent { } updateSyncBeforeMeasuringContent () { + this.measuredContent = false this.derivedDimensionsCache = {} this.updateModelSoftWrapColumn() if (this.pendingAutoscroll) { @@ -384,6 +382,8 @@ class TextEditorComponent { } this.pendingAutoscroll = null } + + this.measuredContent = true } updateSyncAfterMeasuringContent () { From d51ed933e644adb2057c26989c249d1ed3dbcb3f Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 18 Jul 2017 17:34:54 -0400 Subject: [PATCH 097/314] :arrow_up: bracket-matcher@0.87.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eeec8b698..321179e69 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "autosave": "0.24.3", "background-tips": "0.27.1", "bookmarks": "0.44.4", - "bracket-matcher": "0.87.0", + "bracket-matcher": "0.87.1", "command-palette": "0.40.4", "dalek": "0.2.1", "deprecation-cop": "0.56.7", From bfc8a961f64b6a9e7716de3fc0f095d6cd4a6d9b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 18 Jul 2017 16:18:20 -0600 Subject: [PATCH 098/314] Try an apm that depends on node-gyp explicitly --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index cb756f217..c8ac6c9e2 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.18.2" + "atom-package-manager": "1.18.3-0" } } From 1c7f2c33b6379e53fa7f66e881a136de59d56864 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 18 Jul 2017 20:26:45 -0600 Subject: [PATCH 099/314] :arrow_up: apm --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index c8ac6c9e2..ba8d06f1d 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.18.3-0" + "atom-package-manager": "1.18.3" } } From d15e65a2c0101df280a60c9d99c03ff8153fffca Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 18 Jul 2017 17:38:04 -0700 Subject: [PATCH 100/314] Output apm version info during build --- script/bootstrap | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/script/bootstrap b/script/bootstrap index 6b83aa3ea..430d7959a 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -2,6 +2,7 @@ 'use strict' +const childProcess = require('child_process') const CONFIG = require('./config') const cleanDependencies = require('./lib/clean-dependencies') const deleteMsbuildFromPath = require('./lib/delete-msbuild-from-path') @@ -26,6 +27,11 @@ if (process.platform === 'win32') deleteMsbuildFromPath() installScriptDependencies() installApm() +childProcess.execFileSync( + CONFIG.getApmBinPath(), + ['--version'], + {stdio: 'inherit'} +) runApmInstall(CONFIG.repositoryRootPath) dependenciesFingerprint.write() From db2800dac95eca82dc50858423e2d8da372d16dd Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 19 Jul 2017 11:48:31 -0700 Subject: [PATCH 101/314] Sign manually without using Squirrel --- script/build | 49 +++++++++++++++----------- script/lib/code-sign-on-windows.js | 16 ++++----- script/lib/create-windows-installer.js | 46 +----------------------- 3 files changed, 35 insertions(+), 76 deletions(-) diff --git a/script/build b/script/build index c3efddd3b..930aed929 100755 --- a/script/build +++ b/script/build @@ -10,6 +10,7 @@ require('./bootstrap') require('coffee-script/register') require('colors') +const path = require('path') const yargs = require('yargs') const argv = yargs .usage('Usage: $0 [options]') @@ -75,34 +76,40 @@ binariesPromise .then(packageApplication) .then(packagedAppPath => generateStartupSnapshot(packagedAppPath).then(() => packagedAppPath)) .then(packagedAppPath => { - if (process.platform === 'darwin') { - if (argv.codeSign) { - codeSignOnMac(packagedAppPath) - } else { - console.log('Skipping code-signing. Specify the --code-sign option to perform code-signing'.gray) - } - } else if (process.platform === 'win32') { - if (argv.createWindowsInstaller) { - return createWindowsInstaller(packagedAppPath, argv.codeSign).then(() => packagedAppPath) - } else { - console.log('Skipping creating installer. Specify the --create-windows-installer option to create a Squirrel-based Windows installer.'.gray) + switch (process.platform) { + case 'darwin': { if (argv.codeSign) { - codeSignOnWindows(packagedAppPath) + codeSignOnMac(packagedAppPath) } else { console.log('Skipping code-signing. Specify the --code-sign option to perform code-signing'.gray) } } - } else if (process.platform === 'linux') { - if (argv.createDebianPackage) { - createDebianPackage(packagedAppPath) - } else { - console.log('Skipping creating debian package. Specify the --create-debian-package option to create it.'.gray) + case 'win32': { + if (argv.codeSign) { + codeSignOnWindows(path.join(packagedAppPath, 'Atom.exe')) + } else { + console.log('Skipping code-signing. Specify the --code-sign option to perform code-signing'.gray) + } + if (argv.createWindowsInstaller) { + return createWindowsInstaller(packagedAppPath) + .then(() => argv.codeSign && codeSignOnWindows(path.join(CONFIG.buildOutputPath, 'AtomSetup.exe'))) + .then(() => packagedAppPath) + } else { + console.log('Skipping creating installer. Specify the --create-windows-installer option to create a Squirrel-based Windows installer.'.gray) + } } + case 'linux': { + if (argv.createDebianPackage) { + createDebianPackage(packagedAppPath) + } else { + console.log('Skipping creating debian package. Specify the --create-debian-package option to create it.'.gray) + } - if (argv.createRpmPackage) { - createRpmPackage(packagedAppPath) - } else { - console.log('Skipping creating rpm package. Specify the --create-rpm-package option to create it.'.gray) + if (argv.createRpmPackage) { + createRpmPackage(packagedAppPath) + } else { + console.log('Skipping creating rpm package. Specify the --create-rpm-package option to create it.'.gray) + } } } diff --git a/script/lib/code-sign-on-windows.js b/script/lib/code-sign-on-windows.js index a4137c726..d37acdd59 100644 --- a/script/lib/code-sign-on-windows.js +++ b/script/lib/code-sign-on-windows.js @@ -4,10 +4,7 @@ const os = require('os') const path = require('path') const {spawnSync} = require('child_process') -// This is only used when specifying --code-sign WITHOUT --create-windows-installer -// as Squirrel has to take care of code-signing in order to correctly wrap during the building of setup - -module.exports = function (packagedAppPath) { +module.exports = function (fileToSignPath) { if (!process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL && !process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) { console.log('Skipping code signing because the ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL environment variable is not defined'.gray) return @@ -19,25 +16,24 @@ module.exports = function (packagedAppPath) { downloadFileFromGithub(process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath) } try { - console.log(`Code-signing application at ${packagedAppPath}`) - signFile(path.join(packagedAppPath, 'atom.exe')) + console.log(`Code-signing executable at ${fileToSignPath}`) + signFile(fileToSignPath) } finally { if (!process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) { - console.log(`Deleting certificate at ${certPath}`) fs.removeSync(certPath) } } - function signFile (filePath) { + function signFile (fileToSignPath) { const signCommand = path.resolve(__dirname, '..', 'node_modules', 'electron-winstaller', 'vendor', 'signtool.exe') - const args = [ // Changing any of these should also be done in create-windows-installer.js + const args = [ 'sign', `/f ${certPath}`, // Signing cert file `/p ${process.env.ATOM_WIN_CODE_SIGNING_CERT_PASSWORD}`, // Signing cert password '/fd sha256', // File digest algorithm '/tr http://timestamp.digicert.com', // Time stamp server '/td sha256', // Times stamp algorithm - `"${filePath}"` + `"${fileToSignPath}"` ] const result = spawnSync(signCommand, args, {stdio: 'inherit', shell: true}) if (result.status !== 0) { diff --git a/script/lib/create-windows-installer.js b/script/lib/create-windows-installer.js index e8494b06d..ddc46d484 100644 --- a/script/lib/create-windows-installer.js +++ b/script/lib/create-windows-installer.js @@ -1,16 +1,13 @@ 'use strict' -const downloadFileFromGithub = require('./download-file-from-github') const electronInstaller = require('electron-winstaller') const fs = require('fs-extra') const glob = require('glob') -const os = require('os') const path = require('path') -const spawnSync = require('./spawn-sync') const CONFIG = require('../config') -module.exports = (packagedAppPath, codeSign) => { +module.exports = (packagedAppPath) => { const archSuffix = process.arch === 'ia32' ? '' : '-' + process.arch const options = { appDirectory: packagedAppPath, @@ -23,32 +20,7 @@ module.exports = (packagedAppPath, codeSign) => { setupIcon: path.join(CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'atom.ico') } - const signing = codeSign && (process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL || process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) - let certPath = process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH - - if (signing) { - if (!certPath) { - certPath = path.join(os.tmpdir(), 'win.p12') - downloadFileFromGithub(process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath) - } - - var signParams = [] // Changing any of these should also be done in code-sign-on-windows.js - signParams.push(`/f ${certPath}`) // Signing cert file - signParams.push(`/p ${process.env.ATOM_WIN_CODE_SIGNING_CERT_PASSWORD}`) // Signing cert password - signParams.push('/fd sha256') // File digest algorithm - signParams.push('/tr http://timestamp.digicert.com') // Time stamp server - signParams.push('/td sha256') // Times stamp algorithm - options.signWithParams = signParams.join(' ') - } else { - console.log('Skipping code-signing. Specify the --code-sign option and provide a ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL environment variable to perform code-signing'.gray) - } - const cleanUp = () => { - if (fs.existsSync(certPath) && !process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) { - console.log(`Deleting certificate at ${certPath}`) - fs.removeSync(certPath) - } - for (let nupkgPath of glob.sync(`${CONFIG.buildOutputPath}/*.nupkg`)) { if (!nupkgPath.includes(CONFIG.appMetadata.version)) { console.log(`Deleting downloaded nupkg for previous version at ${nupkgPath} to prevent it from being stored as an artifact`) @@ -57,24 +29,8 @@ module.exports = (packagedAppPath, codeSign) => { } } - // Squirrel signs its own copy of the executables but we need them for the portable ZIP - const extractSignedExes = () => { - if (signing) { - for (let nupkgPath of glob.sync(`${CONFIG.buildOutputPath}/*-full.nupkg`)) { - if (nupkgPath.includes(CONFIG.appMetadata.version)) { - nupkgPath = path.resolve(nupkgPath) // Switch from forward-slash notation - console.log(`Extracting signed executables from ${nupkgPath} for use in portable zip`) - spawnSync('7z.exe', ['e', nupkgPath, 'lib\\net45\\*.exe', '-aoa', `-o${packagedAppPath}`]) - spawnSync(process.env.COMSPEC, ['/c', 'move', '/y', path.join(packagedAppPath, 'squirrel.exe'), path.join(packagedAppPath, 'update.exe')]) - return - } - } - } - } - console.log(`Creating Windows Installer for ${packagedAppPath}`) return electronInstaller.createWindowsInstaller(options) - .then(extractSignedExes) .then(cleanUp, error => { cleanUp() return Promise.reject(error) From b6fb15fce31fdc96c424e47057bd3f452154e41b Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 19 Jul 2017 16:21:56 -0400 Subject: [PATCH 102/314] :memo: [ci skip] --- src/workspace.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workspace.js b/src/workspace.js index aac3adffa..3bf112461 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -612,7 +612,7 @@ module.exports = class Workspace extends Model { // editors in the workspace. // // * `callback` {Function} to be called with current and future text editors. - // * `editor` An {TextEditor} that is present in {::getTextEditors} at the time + // * `editor` A {TextEditor} that is present in {::getTextEditors} at the time // of subscription or that is added at some later time. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. From 372cef218c1f68301536bf238351435a03df0946 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 19 Jul 2017 21:06:49 -0400 Subject: [PATCH 103/314] :memo: [ci skip] --- src/text-editor.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index ddc435414..6962bf10a 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1582,6 +1582,8 @@ class TextEditor extends Model # undo history, no changes will be made to the buffer and this method will # return `false`. # + # * `checkpoint` The checkpoint to revert to. + # # Returns a {Boolean} indicating whether the operation succeeded. revertToCheckpoint: (checkpoint) -> @buffer.revertToCheckpoint(checkpoint) @@ -1591,6 +1593,8 @@ class TextEditor extends Model # If the given checkpoint is no longer present in the undo history, no # grouping will be performed and this method will return `false`. # + # * `checkpoint` The checkpoint from which to group changes. + # # Returns a {Boolean} indicating whether the operation succeeded. groupChangesSinceCheckpoint: (checkpoint) -> @buffer.groupChangesSinceCheckpoint(checkpoint) From 8130850e2af2f8ca15365b4df558b3b49b028b6a Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Thu, 20 Jul 2017 09:33:44 -0700 Subject: [PATCH 104/314] Bring in latest code signing changes from 1.19-releases by hand. --- script/build | 9 +++++++-- script/lib/code-sign-on-windows.js | 12 +++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/script/build b/script/build index 930aed929..acc54cdac 100755 --- a/script/build +++ b/script/build @@ -54,6 +54,7 @@ process.on('unhandledRejection', function (e) { process.exit(1) }) +const CONFIG = require('./config') let binariesPromise = Promise.resolve() if (!argv.existingBinaries) { @@ -86,13 +87,17 @@ binariesPromise } case 'win32': { if (argv.codeSign) { - codeSignOnWindows(path.join(packagedAppPath, 'Atom.exe')) + const executablesToSign = [ path.join(packagedAppPath, 'Atom.exe') ] + if (argv.createWindowsInstaller) { + executablesToSign.push(path.join(__dirname, 'node_modules', 'electron-winstaller', 'vendor', 'Update.exe')) + } + codeSignOnWindows(executablesToSign) } else { console.log('Skipping code-signing. Specify the --code-sign option to perform code-signing'.gray) } if (argv.createWindowsInstaller) { return createWindowsInstaller(packagedAppPath) - .then(() => argv.codeSign && codeSignOnWindows(path.join(CONFIG.buildOutputPath, 'AtomSetup.exe'))) + .then(() => argv.codeSign && codeSignOnWindows([ path.join(CONFIG.buildOutputPath, 'AtomSetup.exe') ])) .then(() => packagedAppPath) } else { console.log('Skipping creating installer. Specify the --create-windows-installer option to create a Squirrel-based Windows installer.'.gray) diff --git a/script/lib/code-sign-on-windows.js b/script/lib/code-sign-on-windows.js index d37acdd59..a73ea90be 100644 --- a/script/lib/code-sign-on-windows.js +++ b/script/lib/code-sign-on-windows.js @@ -4,7 +4,7 @@ const os = require('os') const path = require('path') const {spawnSync} = require('child_process') -module.exports = function (fileToSignPath) { +module.exports = function (filesToSign) { if (!process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL && !process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) { console.log('Skipping code signing because the ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL environment variable is not defined'.gray) return @@ -16,15 +16,17 @@ module.exports = function (fileToSignPath) { downloadFileFromGithub(process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath) } try { - console.log(`Code-signing executable at ${fileToSignPath}`) - signFile(fileToSignPath) + for (const fileToSign of filesToSign) { + console.log(`Code-signing executable at ${fileToSign}`) + signFile(fileToSign) + } } finally { if (!process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) { fs.removeSync(certPath) } } - function signFile (fileToSignPath) { + function signFile (fileToSign) { const signCommand = path.resolve(__dirname, '..', 'node_modules', 'electron-winstaller', 'vendor', 'signtool.exe') const args = [ 'sign', @@ -33,7 +35,7 @@ module.exports = function (fileToSignPath) { '/fd sha256', // File digest algorithm '/tr http://timestamp.digicert.com', // Time stamp server '/td sha256', // Times stamp algorithm - `"${fileToSignPath}"` + `"${fileToSign}"` ] const result = spawnSync(signCommand, args, {stdio: 'inherit', shell: true}) if (result.status !== 0) { From 6635e08b47f278494e7c3760fcbbf2ed5c5e8bcb Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Thu, 20 Jul 2017 09:36:40 -0700 Subject: [PATCH 105/314] Update to node used on build servers --- docs/build-instructions/windows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build-instructions/windows.md b/docs/build-instructions/windows.md index 93d540b0c..a6c327ec8 100644 --- a/docs/build-instructions/windows.md +++ b/docs/build-instructions/windows.md @@ -2,7 +2,7 @@ ## Requirements -* Node.js 6.x or later (the architecture of node available to the build system will determine whether you build 32-bit or 64-bit Atom) +* Node.js 6.9.4 or later (the architecture of node available to the build system will determine whether you build 32-bit or 64-bit Atom) * Python v2.7.x * The python.exe must be available at `%SystemDrive%\Python27\python.exe`. If it is installed elsewhere create a symbolic link to the directory containing the python.exe using: `mklink /d %SystemDrive%\Python27 D:\elsewhere\Python27` * 7zip (7z.exe available from the command line) - for creating distribution zip files From 43cea29c428a4527008cdee2b8aea06147556aa7 Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 21 Jul 2017 10:20:33 +0900 Subject: [PATCH 106/314] :arrow_up: find-and-replace@v0.209.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 321179e69..21139c962 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "dev-live-reload": "0.47.1", "encoding-selector": "0.23.4", "exception-reporting": "0.41.4", - "find-and-replace": "0.209.3", + "find-and-replace": "0.209.4", "fuzzy-finder": "1.5.8", "github": "0.3.8", "git-diff": "1.3.6", From c84d705e584e708022b04d87d436804808c3840d Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 21 Jul 2017 10:40:48 +0900 Subject: [PATCH 107/314] :arrow_up: image-view@v0.62.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 21139c962..b04797ee2 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", - "image-view": "0.62.2", + "image-view": "0.62.3", "incompatible-packages": "0.27.3", "keybinding-resolver": "0.38.0", "line-ending-selector": "0.7.3", From 743c7afe75ef2cbd4c21241f6a1aab52d32754c6 Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 21 Jul 2017 11:40:25 +0900 Subject: [PATCH 108/314] :arrow_up: tree-view@v0.217.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b04797ee2..a835e7b4f 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "symbols-view": "0.117.0", "tabs": "0.106.2", "timecop": "0.36.0", - "tree-view": "0.217.3", + "tree-view": "0.217.4", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", "whitespace": "0.37.2", From 57e023e74fffebaf50e56caba314de9fef7a966f Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 21 Jul 2017 00:41:21 -0400 Subject: [PATCH 109/314] :arrow_up: tree-view@0.217.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a835e7b4f..8887713af 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "symbols-view": "0.117.0", "tabs": "0.106.2", "timecop": "0.36.0", - "tree-view": "0.217.4", + "tree-view": "0.217.5", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", "whitespace": "0.37.2", From cbbd91d19e87bcda731fc25789e476f22d7b7f6c Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 21 Jul 2017 13:15:06 -0400 Subject: [PATCH 110/314] :arrow_up: find-and-replace@0.209.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8887713af..87aad2d78 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "dev-live-reload": "0.47.1", "encoding-selector": "0.23.4", "exception-reporting": "0.41.4", - "find-and-replace": "0.209.4", + "find-and-replace": "0.209.5", "fuzzy-finder": "1.5.8", "github": "0.3.8", "git-diff": "1.3.6", From 940d8bcc2fb168783813714e8b5e243bfd933107 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 21 Jul 2017 13:29:57 -0400 Subject: [PATCH 111/314] :arrow_up: autocomplete-css@0.17.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 87aad2d78..17ba31755 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "about": "1.7.6", "archive-view": "0.63.3", "autocomplete-atom-api": "0.10.2", - "autocomplete-css": "0.16.3", + "autocomplete-css": "0.17.1", "autocomplete-html": "0.8.0", "autocomplete-plus": "2.35.7", "autocomplete-snippets": "1.11.0", From 2f036de2e8f97aba7856ac716aae57c139649666 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 21 Jul 2017 13:33:44 -0400 Subject: [PATCH 112/314] :arrow_up: autocomplete-css@0.17.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 17ba31755..0eae4f900 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "about": "1.7.6", "archive-view": "0.63.3", "autocomplete-atom-api": "0.10.2", - "autocomplete-css": "0.17.1", + "autocomplete-css": "0.17.2", "autocomplete-html": "0.8.0", "autocomplete-plus": "2.35.7", "autocomplete-snippets": "1.11.0", From 0e503cd81f7b9c7c2fcc13a389c2d75bebf2c5e7 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 21 Jul 2017 13:41:44 -0400 Subject: [PATCH 113/314] :arrow_up: language-coffee-script@0.48.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0eae4f900..20c6ca0db 100644 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "wrap-guide": "0.40.2", "language-c": "0.58.1", "language-clojure": "0.22.4", - "language-coffee-script": "0.48.8", + "language-coffee-script": "0.48.9", "language-csharp": "0.14.2", "language-css": "0.42.3", "language-gfm": "0.90.0", From d9828cc85e4345cd4c88ceb8fa38990330131ceb Mon Sep 17 00:00:00 2001 From: t9md Date: Tue, 25 Jul 2017 03:49:18 +0900 Subject: [PATCH 114/314] should not use bind for map's callback. map pass item index as 2nd arg, so bind is not appropriate for the function which take 2nd arg. --- src/pane.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pane.coffee b/src/pane.coffee index 1fefa361b..64f215bd7 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -637,7 +637,7 @@ class Pane # Public: Destroy all items. destroyItems: -> Promise.all( - @getItems().map(@destroyItem.bind(this)) + @getItems().map((item) => @destroyItem(item)) ) # Public: Destroy all items except for the active item. @@ -645,7 +645,7 @@ class Pane Promise.all( @getItems() .filter((item) => item isnt @activeItem) - .map(@destroyItem.bind(this)) + .map((item) => @destroyItem(item)) ) promptToSaveItem: (item, options={}) -> @@ -950,7 +950,7 @@ class Pane # Returns a {Promise} that resolves once the pane is either closed, or the # closing has been cancelled. close: -> - Promise.all(@getItems().map(@promptToSaveItem.bind(this))).then (results) => + Promise.all(@getItems().map((item) => @promptToSaveItem(item))).then (results) => @destroy() unless results.includes(false) handleSaveError: (error, item) -> @@ -989,4 +989,4 @@ promisify = (callback) -> try Promise.resolve(callback()) catch error - Promise.reject(error) \ No newline at end of file + Promise.reject(error) From a38b438338cb5b5145a71fd296a21a8c29663dd6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 27 Jul 2017 08:48:06 -0700 Subject: [PATCH 115/314] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 20c6ca0db..06061f4d0 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "sinon": "1.17.4", "@atom/source-map-support": "^0.3.4", "temp": "^0.8.3", - "text-buffer": "13.0.6", + "text-buffer": "13.0.7", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From bae01b6594219f39041023a6f2c08919e1d3c060 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 28 Jul 2017 14:33:50 -0700 Subject: [PATCH 116/314] :arrow_up: text-buffer (prerelease) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 06061f4d0..2e3bf48ba 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "sinon": "1.17.4", "@atom/source-map-support": "^0.3.4", "temp": "^0.8.3", - "text-buffer": "13.0.7", + "text-buffer": "13.0.8-2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 4254825f3cc6fdcf3ed9d6ee491d56523d89ef40 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 27 Jul 2017 08:44:56 -0700 Subject: [PATCH 117/314] Remove duplicate mocking Since this was newly introduced, let's only set it up when clock mocking is specifically requested to minimize breakage. We really need to deprecate the global spec helper someday. It's bad news. /cc @hansonw --- spec/spec-helper.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 2379cc650..6ac492995 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -64,7 +64,6 @@ beforeEach -> atom.project.setPaths([specProjectPath]) window.resetTimeouts() - spyOn(Date, 'now').andCallFake -> window.now spyOn(_._, "now").andCallFake -> window.now spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout @@ -180,7 +179,6 @@ jasmine.useRealClock = -> jasmine.unspy(window, 'setTimeout') jasmine.unspy(window, 'clearTimeout') jasmine.unspy(_._, 'now') - jasmine.unspy(Date, 'now') # The clock is halfway mocked now in a sad and terrible way... only setTimeout # and clearTimeout are included. This method will also include setInterval. We From 899e9d3f8aeb7e8633c98df76e33e52c66bc5cac Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 31 Jul 2017 09:50:31 -0600 Subject: [PATCH 118/314] Explicitly request to use mock clock to get Date.now mocking --- spec/pane-spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/pane-spec.js b/spec/pane-spec.js index c36abbf6a..f05da0e28 100644 --- a/spec/pane-spec.js +++ b/spec/pane-spec.js @@ -1350,6 +1350,7 @@ describe('Pane', () => { let editor1, pane, eventCount beforeEach(async () => { + jasmine.useMockClock() editor1 = await atom.workspace.open('sample.txt', {pending: true}) pane = atom.workspace.getActivePane() eventCount = 0 From f33bca4fd9178ae6b9a77473f8227e5601e8d936 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 31 Jul 2017 10:14:44 -0600 Subject: [PATCH 119/314] Try always mocking Date.now in our global spec helper --- spec/pane-spec.js | 1 - spec/spec-helper.coffee | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/pane-spec.js b/spec/pane-spec.js index f05da0e28..c36abbf6a 100644 --- a/spec/pane-spec.js +++ b/spec/pane-spec.js @@ -1350,7 +1350,6 @@ describe('Pane', () => { let editor1, pane, eventCount beforeEach(async () => { - jasmine.useMockClock() editor1 = await atom.workspace.open('sample.txt', {pending: true}) pane = atom.workspace.getActivePane() eventCount = 0 diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 6ac492995..eec8ce5fb 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -65,6 +65,7 @@ beforeEach -> window.resetTimeouts() spyOn(_._, "now").andCallFake -> window.now + spyOn(Date, 'now').andCallFake(-> window.now) spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout @@ -179,6 +180,7 @@ jasmine.useRealClock = -> jasmine.unspy(window, 'setTimeout') jasmine.unspy(window, 'clearTimeout') jasmine.unspy(_._, 'now') + jasmine.unspy(Date, 'now') # The clock is halfway mocked now in a sad and terrible way... only setTimeout # and clearTimeout are included. This method will also include setInterval. We @@ -186,8 +188,6 @@ jasmine.useRealClock = -> jasmine.useMockClock = -> spyOn(window, 'setInterval').andCallFake(fakeSetInterval) spyOn(window, 'clearInterval').andCallFake(fakeClearInterval) - spyOn(Date, 'now').andCallFake(-> window.now) - addCustomMatchers = (spec) -> spec.addMatchers From b72d8078dc4b32468a30b6c5d922417602002fdc Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 31 Jul 2017 16:41:50 -0400 Subject: [PATCH 120/314] Use relative positioning for gutters --- static/text-editor.less | 1 + 1 file changed, 1 insertion(+) diff --git a/static/text-editor.less b/static/text-editor.less index 06518ac0f..8917d8dd1 100644 --- a/static/text-editor.less +++ b/static/text-editor.less @@ -20,6 +20,7 @@ atom-text-editor { min-width: 1em; box-sizing: border-box; background-color: inherit; + position: relative; } .gutter:hover { From 1c7aa3999697f7bd6a80c95d1011c74b204b200e Mon Sep 17 00:00:00 2001 From: Bryant Ung Date: Mon, 31 Jul 2017 14:39:18 -0700 Subject: [PATCH 121/314] :arrow_down: bracket-matcher reverting bracket-matcher due to issue https://github.com/atom/bracket-matcher/issues/303 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 06061f4d0..5df5784af 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "autosave": "0.24.3", "background-tips": "0.27.1", "bookmarks": "0.44.4", - "bracket-matcher": "0.87.1", + "bracket-matcher": "0.87.0", "command-palette": "0.40.4", "dalek": "0.2.1", "deprecation-cop": "0.56.7", From fe52743b82009c1bd66f7cea56b3926eade703a4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 31 Jul 2017 15:24:13 -0700 Subject: [PATCH 122/314] :arrow_up: text-buffer (prerelease) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2e3bf48ba..e6f8a66a7 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "sinon": "1.17.4", "@atom/source-map-support": "^0.3.4", "temp": "^0.8.3", - "text-buffer": "13.0.8-2", + "text-buffer": "13.0.8-3", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From dbfc792df00bb9a18dcfff2a5ec7ea9d2b9682e4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 31 Jul 2017 16:28:37 -0700 Subject: [PATCH 123/314] :arrow_up: text-buffer (prerelease) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e6f8a66a7..4113a73e2 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "sinon": "1.17.4", "@atom/source-map-support": "^0.3.4", "temp": "^0.8.3", - "text-buffer": "13.0.8-3", + "text-buffer": "13.0.8-4", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 81913776e865b791fe2ed4d9fc2908644db1c188 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 28 Jul 2017 13:11:48 -0700 Subject: [PATCH 124/314] Add the 1.x compatibility branch of jasmine-reporters --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 5df5784af..17092c0aa 100644 --- a/package.json +++ b/package.json @@ -193,5 +193,8 @@ "HTMLElement", "snapshotResult" ] + }, + "devDependencies": { + "jasmine-reporters": "^1.1.0" } } From a7105980a335286eb178364375c4ecd5ec354fd0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 28 Jul 2017 17:56:00 -0700 Subject: [PATCH 125/314] If TEST_JUNIT_XML_PATH is set, output JUnit XML format test results --- spec/jasmine-test-runner.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/jasmine-test-runner.coffee b/spec/jasmine-test-runner.coffee index dcfe4448e..e6c594cef 100644 --- a/spec/jasmine-test-runner.coffee +++ b/spec/jasmine-test-runner.coffee @@ -7,6 +7,10 @@ module.exports = ({logFile, headless, testPaths, buildAtomEnvironment}) -> window[key] = value for key, value of require '../vendor/jasmine' require 'jasmine-tagged' + if process.env.TEST_JUNIT_XML_PATH + require 'jasmine-reporters' + jasmine.getEnv().addReporter new jasmine.JUnitXmlReporter(process.env.TEST_JUNIT_XML_PATH, true, true) + # Allow document.title to be assigned in specs without screwing up spec window title documentTitle = null Object.defineProperty document, 'title', From 37842fd2d4c3810d1e64309ac9edd662a998484c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 28 Jul 2017 17:56:27 -0700 Subject: [PATCH 126/314] Populate TEST_JUNIT_XML_PATH on CircleCI --- script/test | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/script/test b/script/test index fde922e94..dbabf605a 100755 --- a/script/test +++ b/script/test @@ -32,18 +32,29 @@ if (process.platform === 'darwin') { throw new Error('Running tests on this platform is not supported.') } +function prepareEnv (suiteName) { + const env = {} + + if (process.env.CIRCLE_TEST_REPORTS) { + // CircleCI + // Tell Jasmine to output this suite's results to the directory that Circle expects to find JUnit XML files in. + const outputPath = path.join(process.env.CIRCLE_TEST_REPORTS, suiteName, 'test-results.xml') + env.TEST_JUNIT_XML_PATH = outputPath + } + + return env +} + function runCoreMainProcessTests (callback) { const testPath = path.join(CONFIG.repositoryRootPath, 'spec', 'main-process') const testArguments = [ '--resource-path', resourcePath, '--test', '--main-process', testPath ] + const testEnv = Object.assign({}, prepareEnv('core-main-process'), process.env, {ATOM_GITHUB_INLINE_GIT_EXEC: 'true'}) console.log('Executing core main process tests'.bold.green) - const cp = childProcess.spawn(executablePath, testArguments, { - stdio: 'inherit', - env: Object.assign({}, process.env, {ATOM_GITHUB_INLINE_GIT_EXEC: 'true'}) - }) + const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit', env: testEnv}) cp.on('error', error => { callback(error) }) cp.on('close', exitCode => { callback(null, exitCode) }) } @@ -54,9 +65,10 @@ function runCoreRenderProcessTests (callback) { '--resource-path', resourcePath, '--test', testPath ] + const testEnv = prepareEnv('core-render-process') console.log('Executing core render process tests'.bold.green) - const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit'}) + const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit', env: testEnv}) cp.on('error', error => { callback(error) }) cp.on('close', exitCode => { callback(null, exitCode) }) } @@ -87,6 +99,7 @@ for (let packageName in CONFIG.appMetadata.packageDependencies) { '--resource-path', resourcePath, '--test', testFolder ] + const testEnv = prepareEnv(`bundled-package-${packageName}`) const pkgJsonPath = path.join(repositoryPackagePath, 'package.json') const nodeModulesPath = path.join(repositoryPackagePath, 'node_modules') @@ -105,7 +118,7 @@ for (let packageName in CONFIG.appMetadata.packageDependencies) { } else { console.log(`Executing ${packageName} tests`.bold.green) } - const cp = childProcess.spawn(executablePath, testArguments) + const cp = childProcess.spawn(executablePath, testArguments, {env: testEnv}) let stderrOutput = '' cp.stderr.on('data', data => { stderrOutput += data }) cp.stdout.on('data', data => { stderrOutput += data }) @@ -127,9 +140,10 @@ for (let packageName in CONFIG.appMetadata.packageDependencies) { function runBenchmarkTests (callback) { const benchmarksPath = path.join(CONFIG.repositoryRootPath, 'benchmarks') const testArguments = ['--benchmark-test', benchmarksPath] + const testEnv = prepareEnv('benchmark') console.log('Executing benchmark tests'.bold.green) - const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit'}) + const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit', env: testEnv}) cp.on('error', error => { callback(error) }) cp.on('close', exitCode => { callback(null, exitCode) }) } From 3e581d493678d670a7fd267822a9cfade822ac8d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sat, 29 Jul 2017 16:26:03 -0400 Subject: [PATCH 127/314] Set TEST_JUNIT_XML_PATH and upload results on AppVeyor --- appveyor.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 72b536755..b30fed1cf 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,6 +18,7 @@ platform: environment: global: ATOM_DEV_RESOURCE_PATH: c:\projects\atom + TEST_JUNIT_XML_PATH: c:\projects\atom\junit-results.xml matrix: - NODE_VERSION: 6.9.4 @@ -39,6 +40,9 @@ test_script: - script\test.cmd after_test: + - ps: | + $wc = New-Object 'System.Net.WebClient' + $wc.UploadFile("https://ci.appveyor.com/api/testresults/xunit/$($env:APPVEYOR_JOB_ID)", $env:TEST_JUNIT_XML_PATH) - IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] ( IF NOT EXIST C:\sqtemp MKDIR C:\sqtemp SET SQUIRREL_TEMP=C:\sqtemp From 830038f1ac8cee584b9664b22327316b165978f9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sat, 29 Jul 2017 16:35:00 -0400 Subject: [PATCH 128/314] Use TEST_JUNIT_XML_ROOT instead of the CircleCI-specific root --- circle.yml | 1 + script/test | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/circle.yml b/circle.yml index 8b578bebd..b5791e7ad 100644 --- a/circle.yml +++ b/circle.yml @@ -3,6 +3,7 @@ machine: XCODE_SCHEME: test XCODE_WORKSPACE: test XCODE_PROJECT: test + TEST_JUNIT_XML_ROOT: ${CIRCLE_TEST_REPORTS} xcode: version: 7.3 diff --git a/script/test b/script/test index dbabf605a..4ba9675fc 100755 --- a/script/test +++ b/script/test @@ -35,10 +35,10 @@ if (process.platform === 'darwin') { function prepareEnv (suiteName) { const env = {} - if (process.env.CIRCLE_TEST_REPORTS) { - // CircleCI - // Tell Jasmine to output this suite's results to the directory that Circle expects to find JUnit XML files in. - const outputPath = path.join(process.env.CIRCLE_TEST_REPORTS, suiteName, 'test-results.xml') + if (process.env.TEST_JUNIT_XML_ROOT) { + // Tell Jasmine to output this suite's results as a JUnit XML file to a subdirectory of the root, so that a + // CI system can interpret it. + const outputPath = path.join(process.env.TEST_JUNIT_XML_ROOT, suiteName, 'test-results.xml') env.TEST_JUNIT_XML_PATH = outputPath } From 29005fad25b4cd6812181091df1e94875b12a2c0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sat, 29 Jul 2017 16:35:24 -0400 Subject: [PATCH 129/314] Set TEST_JUNIT_XML_ROOT on AppVeyor --- appveyor.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index b30fed1cf..c60956c3a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,7 +18,7 @@ platform: environment: global: ATOM_DEV_RESOURCE_PATH: c:\projects\atom - TEST_JUNIT_XML_PATH: c:\projects\atom\junit-results.xml + TEST_JUNIT_XML_ROOT: c:\projects\junit-test-results matrix: - NODE_VERSION: 6.9.4 @@ -27,6 +27,7 @@ matrix: fast_finish: true install: + - IF NOT EXIST %TEST_JUNIT_XML_ROOT% MKDIR %TEST_JUNIT_XML_ROOT% - SET PATH=C:\Program Files\Atom\resources\cli;%PATH% - ps: Install-Product node $env:NODE_VERSION $env:PLATFORM - npm install -g npm@5.3.0 From fa89d53c8c1b190f45e6b6bc6bab9dd47f1cbe64 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sat, 29 Jul 2017 16:42:53 -0400 Subject: [PATCH 130/314] Default to passing process.env to all suites in script/test --- script/test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/test b/script/test index 4ba9675fc..79cc265d1 100755 --- a/script/test +++ b/script/test @@ -33,7 +33,7 @@ if (process.platform === 'darwin') { } function prepareEnv (suiteName) { - const env = {} + const env = Object.assign({}, process.env) if (process.env.TEST_JUNIT_XML_ROOT) { // Tell Jasmine to output this suite's results as a JUnit XML file to a subdirectory of the root, so that a From 6813e4e94500f605d43b018359dd0907f9147cb8 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sat, 29 Jul 2017 17:14:44 -0400 Subject: [PATCH 131/314] These are JUnit results, not XUnit --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index c60956c3a..69fcacf55 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -43,7 +43,7 @@ test_script: after_test: - ps: | $wc = New-Object 'System.Net.WebClient' - $wc.UploadFile("https://ci.appveyor.com/api/testresults/xunit/$($env:APPVEYOR_JOB_ID)", $env:TEST_JUNIT_XML_PATH) + $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", $env:TEST_JUNIT_XML_PATH) - IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] ( IF NOT EXIST C:\sqtemp MKDIR C:\sqtemp SET SQUIRREL_TEMP=C:\sqtemp From e53cb5e5eff317a7e441a64e3968bbc0df07efee Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 31 Jul 2017 09:46:57 -0400 Subject: [PATCH 132/314] Powershell by means of StackOverflow --- appveyor.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 69fcacf55..eedc58e29 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -43,7 +43,11 @@ test_script: after_test: - ps: | $wc = New-Object 'System.Net.WebClient' - $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", $env:TEST_JUNIT_XML_PATH) + $endpoint = "https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)" + Get-ChildItem -Path $env:TEST_JUNIT_XML_ROOT -Recurse -Include *.xml | ForEach-Object { + Write-Output "Uploading JUnit XML $($_)" + $wc.UploadFile($endpoint, $_) + } - IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] ( IF NOT EXIST C:\sqtemp MKDIR C:\sqtemp SET SQUIRREL_TEMP=C:\sqtemp From 3690b682c8f0a573b3e0dd0ae895b408b779f06e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 31 Jul 2017 10:59:46 -0400 Subject: [PATCH 133/314] Better filtering --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index eedc58e29..0d5487996 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -44,7 +44,7 @@ after_test: - ps: | $wc = New-Object 'System.Net.WebClient' $endpoint = "https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)" - Get-ChildItem -Path $env:TEST_JUNIT_XML_ROOT -Recurse -Include *.xml | ForEach-Object { + Get-ChildItem -Path $env:TEST_JUNIT_XML_ROOT\* -Recurse -File -Name -Include *.xml | ForEach-Object { Write-Output "Uploading JUnit XML $($_)" $wc.UploadFile($endpoint, $_) } From 0a0e5e9cea0635995e93991679d263ef7cee4045 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 31 Jul 2017 11:46:09 -0400 Subject: [PATCH 134/314] Still trying to get that Get-ChildItem call right --- appveyor.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 0d5487996..70cbec4a2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -44,7 +44,8 @@ after_test: - ps: | $wc = New-Object 'System.Net.WebClient' $endpoint = "https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)" - Get-ChildItem -Path $env:TEST_JUNIT_XML_ROOT\* -Recurse -File -Name -Include *.xml | ForEach-Object { + Write-Output "Searching for JUnit XML output beneath $($env:TEST_JUNIT_XML_ROOT)" + Get-ChildItem -Path "$($env:TEST_JUNIT_XML_ROOT)\*" -Recurse -File -Name -Include "*.xml" | ForEach-Object { Write-Output "Uploading JUnit XML $($_)" $wc.UploadFile($endpoint, $_) } From db2b196404cc4e541d04e1a080a8738ebc0c2903 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 31 Jul 2017 13:09:36 -0400 Subject: [PATCH 135/314] Diagnostics for AppVeyor --- spec/jasmine-test-runner.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/jasmine-test-runner.coffee b/spec/jasmine-test-runner.coffee index e6c594cef..3084c729a 100644 --- a/spec/jasmine-test-runner.coffee +++ b/spec/jasmine-test-runner.coffee @@ -9,6 +9,7 @@ module.exports = ({logFile, headless, testPaths, buildAtomEnvironment}) -> if process.env.TEST_JUNIT_XML_PATH require 'jasmine-reporters' + console.log "Producing JUnit XML output at #{process.env.TEST_JUNIT_XML_PATH}." jasmine.getEnv().addReporter new jasmine.JUnitXmlReporter(process.env.TEST_JUNIT_XML_PATH, true, true) # Allow document.title to be assigned in specs without screwing up spec window title From ca4f372c88303ae64accedfd468c445a15a09c09 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 31 Jul 2017 13:28:05 -0400 Subject: [PATCH 136/314] Let's dump that directory maybe? --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index 70cbec4a2..428a247ff 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -45,6 +45,7 @@ after_test: $wc = New-Object 'System.Net.WebClient' $endpoint = "https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)" Write-Output "Searching for JUnit XML output beneath $($env:TEST_JUNIT_XML_ROOT)" + Get-ChildItem -Path "$($env:TEST_JUNIT_XML_ROOT)\*" -Recurse -File -Name Get-ChildItem -Path "$($env:TEST_JUNIT_XML_ROOT)\*" -Recurse -File -Name -Include "*.xml" | ForEach-Object { Write-Output "Uploading JUnit XML $($_)" $wc.UploadFile($endpoint, $_) From 2574ca57360afb35ec9cf1c4094bd3f66d277fdf Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 31 Jul 2017 14:18:08 -0400 Subject: [PATCH 137/314] More diagnostics --- spec/jasmine-test-runner.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/jasmine-test-runner.coffee b/spec/jasmine-test-runner.coffee index 3084c729a..ba6246d8e 100644 --- a/spec/jasmine-test-runner.coffee +++ b/spec/jasmine-test-runner.coffee @@ -11,6 +11,9 @@ module.exports = ({logFile, headless, testPaths, buildAtomEnvironment}) -> require 'jasmine-reporters' console.log "Producing JUnit XML output at #{process.env.TEST_JUNIT_XML_PATH}." jasmine.getEnv().addReporter new jasmine.JUnitXmlReporter(process.env.TEST_JUNIT_XML_PATH, true, true) + else + console.log "TEST_JUNIT_XML_PATH was falsy" + console.log require('util').inspect process.env # Allow document.title to be assigned in specs without screwing up spec window title documentTitle = null From 5eef79c90498d43925f5547af0bb6aefddf5c9bc Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 31 Jul 2017 14:20:20 -0400 Subject: [PATCH 138/314] Quoting shenanigans maybe? --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 428a247ff..b74da2b4e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -45,8 +45,8 @@ after_test: $wc = New-Object 'System.Net.WebClient' $endpoint = "https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)" Write-Output "Searching for JUnit XML output beneath $($env:TEST_JUNIT_XML_ROOT)" - Get-ChildItem -Path "$($env:TEST_JUNIT_XML_ROOT)\*" -Recurse -File -Name - Get-ChildItem -Path "$($env:TEST_JUNIT_XML_ROOT)\*" -Recurse -File -Name -Include "*.xml" | ForEach-Object { + Get-ChildItem -Path $env:TEST_JUNIT_XML_ROOT) -Recurse -File -Name + Get-ChildItem -Path $env:TEST_JUNIT_XML_ROOT) -Recurse -File -Name -Include "*.xml" | ForEach-Object { Write-Output "Uploading JUnit XML $($_)" $wc.UploadFile($endpoint, $_) } From 76af36939adddc9bf6606a2386a21a5a3040969b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 31 Jul 2017 14:56:24 -0400 Subject: [PATCH 139/314] Extra ) --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index b74da2b4e..87233a295 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -45,8 +45,8 @@ after_test: $wc = New-Object 'System.Net.WebClient' $endpoint = "https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)" Write-Output "Searching for JUnit XML output beneath $($env:TEST_JUNIT_XML_ROOT)" - Get-ChildItem -Path $env:TEST_JUNIT_XML_ROOT) -Recurse -File -Name - Get-ChildItem -Path $env:TEST_JUNIT_XML_ROOT) -Recurse -File -Name -Include "*.xml" | ForEach-Object { + Get-ChildItem -Path $env:TEST_JUNIT_XML_ROOT -Recurse -File -Name + Get-ChildItem -Path $env:TEST_JUNIT_XML_ROOT -Recurse -File -Name -Include "*.xml" | ForEach-Object { Write-Output "Uploading JUnit XML $($_)" $wc.UploadFile($endpoint, $_) } From adec6fe981b208d405e50d589fe6bf1799aa73c7 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 31 Jul 2017 15:07:05 -0400 Subject: [PATCH 140/314] Dump the environment before running core main process tests --- script/test | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/script/test b/script/test index 79cc265d1..fe90160a8 100755 --- a/script/test +++ b/script/test @@ -51,7 +51,9 @@ function runCoreMainProcessTests (callback) { '--resource-path', resourcePath, '--test', '--main-process', testPath ] - const testEnv = Object.assign({}, prepareEnv('core-main-process'), process.env, {ATOM_GITHUB_INLINE_GIT_EXEC: 'true'}) + const testEnv = Object.assign({}, prepareEnv('core-main-process'), {ATOM_GITHUB_INLINE_GIT_EXEC: 'true'}) + + console.log(`test environment: ${require('util').inspect(testEnv)}`) console.log('Executing core main process tests'.bold.green) const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit', env: testEnv}) From 8ea491f223f781fc44ac4fca69ac894c023ec0da Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 09:02:27 -0400 Subject: [PATCH 141/314] Use mocha Multi and JUnit reporters --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 17092c0aa..c2c57e58e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "license": "MIT", "electronVersion": "1.6.9", "dependencies": { + "@atom/source-map-support": "^0.3.4", "async": "0.2.6", "atom-keymap": "8.2.2", "atom-select-list": "^0.1.0", @@ -38,6 +39,7 @@ "glob": "^7.1.1", "grim": "1.5.0", "jasmine-json": "~0.0", + "jasmine-reporters": "^2.2.1", "jasmine-tagged": "^1.1.4", "key-path-helpers": "^0.4.0", "less-cache": "1.1.0", @@ -45,6 +47,8 @@ "marked": "^0.3.6", "minimatch": "^3.0.3", "mocha": "2.5.1", + "mocha-junit-reporter": "^1.13.0", + "mocha-multi-reporters": "^1.1.4", "mock-spawn": "^0.2.6", "normalize-package-data": "^2.0.0", "nslog": "^3", @@ -63,7 +67,6 @@ "semver": "^4.3.3", "service-hub": "^0.7.4", "sinon": "1.17.4", - "@atom/source-map-support": "^0.3.4", "temp": "^0.8.3", "text-buffer": "13.0.7", "typescript-simple": "1.0.0", @@ -193,8 +196,5 @@ "HTMLElement", "snapshotResult" ] - }, - "devDependencies": { - "jasmine-reporters": "^1.1.0" } } From 41cb5ce07665b0eab12486292ad734cd3cabc54a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 09:02:44 -0400 Subject: [PATCH 142/314] Configure Mocha to use the JUnit XML reporter --- spec/main-process/mocha-test-runner.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/spec/main-process/mocha-test-runner.js b/spec/main-process/mocha-test-runner.js index 0fe5a5125..d1634fd72 100644 --- a/spec/main-process/mocha-test-runner.js +++ b/spec/main-process/mocha-test-runner.js @@ -7,7 +7,24 @@ import {assert} from 'chai' export default function (testPaths) { global.assert = assert - const mocha = new Mocha({reporter: 'spec'}) + let reporterOptions = { + reporterEnabled: 'list' + } + + if (process.env.TEST_JUNIT_XML_PATH) { + console.log(`Mocha: Producing JUnit XML output at ${process.env.TEST_JUNIT_XML_PATH}.`) + reporterOptions = { + reporterEnabled: 'list, mocha-junit-reporter', + mochaJunitReporterReporterOptions: { + mochaFile: process.env.TEST_JUNIT_XML_PATH + } + } + } + + const mocha = new Mocha({ + reporter: 'mocha-multi-reporters', + reporterOptions + }) for (let testPath of testPaths) { if (fs.isDirectorySync(testPath)) { for (let testFilePath of fs.listTreeSync(testPath)) { From 2ff3e654e0fc611c783f429f4b4f30b26b21225b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 09:37:44 -0400 Subject: [PATCH 143/314] Move test result uploading to on_finish on AppVeyor --- appveyor.yml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 87233a295..5c438da0e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -41,15 +41,6 @@ test_script: - script\test.cmd after_test: - - ps: | - $wc = New-Object 'System.Net.WebClient' - $endpoint = "https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)" - Write-Output "Searching for JUnit XML output beneath $($env:TEST_JUNIT_XML_ROOT)" - Get-ChildItem -Path $env:TEST_JUNIT_XML_ROOT -Recurse -File -Name - Get-ChildItem -Path $env:TEST_JUNIT_XML_ROOT -Recurse -File -Name -Include "*.xml" | ForEach-Object { - Write-Output "Uploading JUnit XML $($_)" - $wc.UploadFile($endpoint, $_) - } - IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] ( IF NOT EXIST C:\sqtemp MKDIR C:\sqtemp SET SQUIRREL_TEMP=C:\sqtemp @@ -73,3 +64,13 @@ cache: - '%APPVEYOR_BUILD_FOLDER%\electron' - '%USERPROFILE%\.atom\.apm' - '%USERPROFILE%\.atom\compile-cache' + +on_finish: + - ps: | + $wc = New-Object 'System.Net.WebClient' + $endpoint = "https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)" + Write-Output "Searching for JUnit XML output beneath $($env:TEST_JUNIT_XML_ROOT)" + Get-ChildItem -Path $env:TEST_JUNIT_XML_ROOT -Recurse -File -Name -Include "*.xml" | ForEach-Object { + Write-Output "Uploading JUnit XML $($_)" + $wc.UploadFile($endpoint, $_) + } From 9ab9643b113db50807abd0df1e66ad8cf4ab9288 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 11:24:31 -0400 Subject: [PATCH 144/314] Use full paths for JUnit XML files --- appveyor.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 5c438da0e..32a67e18e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -71,6 +71,7 @@ on_finish: $endpoint = "https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)" Write-Output "Searching for JUnit XML output beneath $($env:TEST_JUNIT_XML_ROOT)" Get-ChildItem -Path $env:TEST_JUNIT_XML_ROOT -Recurse -File -Name -Include "*.xml" | ForEach-Object { - Write-Output "Uploading JUnit XML $($_)" - $wc.UploadFile($endpoint, $_) + $full = "$($env:TEST_JUNIT_XML_ROOT)\$($_)" + Write-Output "Uploading JUnit XML file $($full)" + $wc.UploadFile($endpoint, $full) } From cc753be2ca4da3f03aff39cdea5c1df5196bfc1c Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 1 Aug 2017 09:20:52 -0700 Subject: [PATCH 145/314] :arrow_up: github@0.3.9-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5df5784af..4dde1ca03 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "exception-reporting": "0.41.4", "find-and-replace": "0.209.5", "fuzzy-finder": "1.5.8", - "github": "0.3.8", + "github": "0.3.9-0", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", From e0b6ff6ebb82dbdfaad610d3284ca79ea3d493a3 Mon Sep 17 00:00:00 2001 From: Alan Yee Date: Tue, 1 Aug 2017 09:47:20 -0700 Subject: [PATCH 146/314] Update README.md -Added Homebrew install instructions for macOS -Made explicit HTTPS calls -Minor url cleanup --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 74a5b4cee..16c5f9eda 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![macOS Build Status](https://circleci.com/gh/atom/atom/tree/master.svg?style=shield)](https://circleci.com/gh/atom/atom) [![Linux Build Status](https://travis-ci.org/atom/atom.svg?branch=master)](https://travis-ci.org/atom/atom) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/1tkktwh654w07eim?svg=true)](https://ci.appveyor.com/project/Atom/atom) [![Dependency Status](https://david-dm.org/atom/atom.svg)](https://david-dm.org/atom/atom) -[![Join the Atom Community on Slack](http://atom-slack.herokuapp.com/badge.svg)](http://atom-slack.herokuapp.com/) +[![Join the Atom Community on Slack](https://atom-slack.herokuapp.com/badge.svg)](https://atom-slack.herokuapp.com) Atom is a hackable text editor for the 21st century, built on [Electron](https://github.com/atom/electron), and based on everything we love about our favorite editors. We designed it to be deeply customizable, but still approachable using the default configuration. @@ -16,14 +16,14 @@ By participating, you are expected to uphold this code. Please report unacceptab ## Documentation -If you want to read about using Atom or developing packages in Atom, the [Atom Flight Manual](http://flight-manual.atom.io) is free and available online. You can find the source to the manual in [atom/flight-manual.atom.io](https://github.com/atom/flight-manual.atom.io). +If you want to read about using Atom or developing packages in Atom, the [Atom Flight Manual](https://flight-manual.atom.io) is free and available online. You can find the source to the manual in [atom/flight-manual.atom.io](https://github.com/atom/flight-manual.atom.io). The [API reference](https://atom.io/docs/api) for developing packages is also documented on Atom.io. ## Installing ### Prerequisites -- [Git](https://git-scm.com/) +- [Git](https://git-scm.com) ### macOS @@ -31,6 +31,8 @@ Download the latest [Atom release](https://github.com/atom/atom/releases/latest) Atom will automatically update when a new release is available. +Using (Homebrew)[https://brew.sh]? Run to `brew cask install atom` install the latest version of Atom. + ### Windows Download the latest [Atom installer](https://github.com/atom/atom/releases/latest). AtomSetup.exe is 32-bit, AtomSetup-x64.exe for 64-bit systems. @@ -40,7 +42,7 @@ Atom will automatically update when a new release is available. You can also download `atom-windows.zip` (32-bit) or `atom-x64-windows.zip` (64-bit) from the [releases page](https://github.com/atom/atom/releases/latest). The `.zip` version will not automatically update. -Using [chocolatey](https://chocolatey.org/)? Run `cinst Atom` to install the latest version of Atom. +Using [Chocolatey](https://chocolatey.org)? Run `cinst Atom` to install the latest version of Atom. ### Debian Linux (Ubuntu) From 7295e5de6fb7dda1a620f16f13b515c0e1a25324 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 1 Aug 2017 09:53:03 -0700 Subject: [PATCH 147/314] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4113a73e2..b526d664b 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "sinon": "1.17.4", "@atom/source-map-support": "^0.3.4", "temp": "^0.8.3", - "text-buffer": "13.0.8-4", + "text-buffer": "13.0.8", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 73ac74cce9d4dcafc4a3075c5cc191b7c2d91f47 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 16 Jun 2017 10:29:32 -0400 Subject: [PATCH 148/314] Use nsfw for file watching --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 1ed1c87fb..826ef5926 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "mocha": "2.5.1", "mock-spawn": "^0.2.6", "normalize-package-data": "^2.0.0", + "nsfw": "^1.0.15", "nslog": "^3", "oniguruma": "6.2.1", "pathwatcher": "7.1.0", From a1ccd49b8e0a336a1b39e672b1f2b5646558eb27 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Jun 2017 15:08:34 -0400 Subject: [PATCH 149/314] Use a tree-backed registry to deduplicate and consolidate native watchers --- spec/native-watcher-registry-spec.js | 113 +++++++++++++ src/native-watcher-registry.js | 240 +++++++++++++++++++++++++++ 2 files changed, 353 insertions(+) create mode 100644 spec/native-watcher-registry-spec.js create mode 100644 src/native-watcher-registry.js diff --git a/spec/native-watcher-registry-spec.js b/spec/native-watcher-registry-spec.js new file mode 100644 index 000000000..fd8d898ec --- /dev/null +++ b/spec/native-watcher-registry-spec.js @@ -0,0 +1,113 @@ +/** @babel */ + +import path from 'path' + +import NativeWatcherRegistry from '../src/native-watcher-registry' + +class MockWatcher { + constructor () { + this.native = null + } + + attachToNative (native) { + this.native = native + this.native.attached.push(this) + } +} + +class MockNative { + constructor (name) { + this.name = name + this.attached = [] + this.disposed = false + this.stopped = false + } + + reattachTo (newNative) { + for (const watcher of this.attached) { + watcher.attachToNative(newNative) + } + + this.attached = [] + } + + dispose() { + this.disposed = true + } + + stop() { + this.stopped = true + } +} + +describe('NativeWatcherRegistry', function () { + let registry, watcher + + beforeEach(function () { + registry = new NativeWatcherRegistry() + watcher = new MockWatcher() + }) + + it('attaches a Watcher to a newly created NativeWatcher for a new directory', function() { + const NATIVE = new MockNative('created') + registry.attach('/some/path', watcher, () => NATIVE) + + expect(watcher.native).toBe(NATIVE) + }) + + it('reuses an existing NativeWatcher on the same directory', function () { + const EXISTING = new MockNative('existing') + registry.attach('/existing/path', new MockWatcher(), () => EXISTING) + + registry.attach('/existing/path', watcher, () => new MockNative('no')) + + expect(watcher.native).toBe(EXISTING) + }) + + it('attaches to an existing NativeWatcher on a parent directory', function () { + const EXISTING = new MockNative('existing') + registry.attach('/existing/path', new MockWatcher(), () => EXISTING) + + registry.attach('/existing/path/sub/directory/', watcher, () => new MockNative('no')) + + expect(watcher.native).toBe(EXISTING) + }) + + it('adopts Watchers from NativeWatchers on child directories', function () { + const EXISTING0 = new MockNative('existing0') + const watcher0 = new MockWatcher() + registry.attach('/existing/path/child/directory/zero', watcher0, () => EXISTING0) + + const EXISTING1 = new MockNative('existing1') + const watcher1 = new MockWatcher() + registry.attach('/existing/path/child/directory/one', watcher1, () => EXISTING1) + + const EXISTING2 = new MockNative('existing2') + const watcher2 = new MockWatcher() + registry.attach('/another/path', watcher2, () => EXISTING2) + + expect(watcher0.native).toBe(EXISTING0) + expect(watcher1.native).toBe(EXISTING1) + expect(watcher2.native).toBe(EXISTING2) + + // Consolidate all three watchers beneath the same native watcher on the parent directory + const CREATED = new MockNative('created') + registry.attach('/existing/path/', watcher, () => CREATED) + + expect(watcher.native).toBe(CREATED) + + expect(watcher0.native).toBe(CREATED) + expect(EXISTING0.stopped).toBe(true) + expect(EXISTING0.disposed).toBe(true) + + expect(watcher1.native).toBe(CREATED) + expect(EXISTING1.stopped).toBe(true) + expect(EXISTING1.disposed).toBe(true) + + expect(watcher2.native).toBe(EXISTING2) + expect(EXISTING2.stopped).toBe(false) + expect(EXISTING2.disposed).toBe(false) + }) + + it('removes NativeWatchers when all Watchers have been disposed') +}) diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js new file mode 100644 index 000000000..e78c87c9c --- /dev/null +++ b/src/native-watcher-registry.js @@ -0,0 +1,240 @@ +/** @babel */ + +import path from 'path' + +// Private: Non-leaf node in a tree used by the {NativeWatcherRegistry} to cover the allocated {Watcher} instances with +// the most efficient set of {NativeWatcher} instances possible. Each {RegistryNode} maps to a directory in the +// filesystem tree. +class RegistryNode { + + // Private: Construct a new, empty node representing a node with no watchers. + constructor () { + this.children = {} + } + + // Private: Recursively discover any existing watchers corresponding to a path. + // + // * `pathSegments` filesystem path of a new {Watcher} already split into an Array of directory names. + // + // Returns: A {ParentResult} if the exact requested directory or a parent directory is being watched, a + // {ChildrenResult} if one or more child paths are being watched, or a {MissingResult} if no relevant watchers + // exist. + lookup(pathSegments) { + if (pathSegments.length === 0) { + return new ChildrenResult(this.leaves()) + } + + const child = this.children[pathSegments[0]] + if (child === undefined) { + return new MissingResult(this) + } + + return child.lookup(pathSegments.slice(1)) + } + + // Private: Insert a new {RegistryWatcherNode} into the tree, creating new intermediate {RegistryNode} instances as + // needed. Any existing children of the watched directory are removed. + // + // * `pathSegments` filesystem path of the new {Watcher}, already split into an Array of directory names. + // * `leaf` initialized {RegistryWatcherNode} to insert + // + // Returns: The root of a new tree with the {RegistryWatcherNode} inserted at the correct location. Callers should + // replace their node references with the returned value. + insert(pathSegments, leaf) { + if (pathSegments.length === 0) { + return leaf + } + + const pathKey = pathSegments[0] + let child = this.children[pathKey] + if (child === undefined) { + child = new RegistryNode() + } + this.children[pathKey] = child.insert(pathSegments.slice(1), leaf) + return this + } + + // Private: Discover all {RegistryWatcherNode} instances beneath this tree node. + // + // Returns: A possibly empty {Array} of {RegistryWatcherNode} instances that are the descendants of this node. + leaves() { + const results = [] + for (const p of Object.keys(this.children)) { + results.push(...this.children[p].leaves()) + } + return results + } +} + +// Private: Leaf node within a {NativeWatcherRegistry} tree. Represents a directory that is covered by a +// {NativeWatcher}. +class RegistryWatcherNode { + + // Private: Allocate a new node to track a {NativeWatcher}. + // + // * `nativeWatcher` An existing {NativeWatcher} instance. + constructor (nativeWatcher) { + this.nativeWatcher = nativeWatcher + } + + // Private: Accessor for the {NativeWatcher}. + getNativeWatcher () { + return this.nativeWatcher + } + + // Private: Identify how this watcher relates to a request to watch a directory tree. + // + // * `pathSegments` filesystem path of a new {Watcher} already split into an Array of directory names. + // + // Returns: A {ParentResult} referencing this node. + lookup(pathSegments) { + return new ParentResult(this, pathSegments) + } + + // Private: Discover this {RegistryWatcherNode} instance. + // + // Returns: An {Array} containing this node. + leaves() { + return [this] + } +} + +// Private: A {RegisteryNode} traversal result that's returned when neither a directory, its children, nor its parents +// are present in the tree. +class MissingResult { + // Private: Instantiate a new {MissingResult}. + // + // * `lastParent` the final succesfully traversed {RegistryNode}. + constructor (lastParent) { + this.lastParent = lastParent + } + + // Private: Dispatch within a map of callback actions. + // + // * `actions` {Object} containing a `missing` key that maps to a callback to be invoked when no results were returned + // by {RegistryNode.lookup}. The callback will be called with the last parent node that was encountered during the + // traversal. + // + // Returns: the result of the `actions` callback. + when (actions) { + return actions.missing(this.lastParent) + } +} + +// Private: A {RegistryNode.lookup} traversal result that's returned when a parent or an exact match of the requested +// directory is being watched by an existing {RegistryWatcherNode}. +class ParentResult { + + // Private: Instantiate a new {ParentResult}. + // + // * `parent` the {RegistryWatcherNode} that was discovered. + // * `remainingPathSegments` an {Array} of the directories that lie between the leaf node's watched directory and + // the requested directory. This will be empty for exact matches. + constructor (parent, remainingPathSegments) { + this.parent = parent + this.remainingPathSegments = remainingPathSegments + } + + // Private: Dispatch within a map of callback actions. + // + // * `actions` {Object} containing a `parent` key that maps to a callback to be invoked when a parent of a requested + // requested directory is returned by a {RegistryNode.lookup} call. The callback will be called with the + // {RegistryWatcherNode} instance and an {Array} of the {String} path segments that separate the parent node + // and the requested directory. + // + // Returns: the result of the `actions` callback. + when (actions) { + return actions.parent(this.parent, this.remainingPathSegments) + } +} + +// Private: A {RegistryNode.lookup} traversal result that's returned when one or more children of the requested +// directory are already being watched. +class ChildrenResult { + + // Private: Instantiate a new {ChildrenResult}. + // + // * `children` {Array} of the {RegistryWatcherNode} instances that were discovered. + constructor (children) { + this.children = children + } + + // Private: Dispatch within a map of callback actions. + // + // * `actions` {Object} containing a `children` key that maps to a callback to be invoked when a parent of a requested + // requested directory is returned by a {RegistryNode.lookup} call. The callback will be called with the + // {RegistryWatcherNode} instance. + // + // Returns: the result of the `actions` callback. + when (actions) { + return actions.children(this.children) + } +} + +// Private: Track the directories being monitored by native filesystem watchers. Minimize the number of native watchers +// allocated to receive events for a desired set of directories by: +// +// 1. Subscribing to the same underlying {NativeWatcher} when watching the same directory multiple times. +// 2. Subscribing to an existing {NativeWatcher} on a parent of a desired directory. +// 3. Replacing multiple {NativeWatcher} instances on child directories with a single new {NativeWatcher} on the +// parent. +export default class NativeWatcherRegistry { + + // Private: Instantiate an empty registry. + constructor () { + this.tree = new RegistryNode() + } + + // Private: Attach a watcher to a directory, assigning it a {NativeWatcher}. If a suitable {NativeWatcher} already + // exists, it will be attached to the new {Watcher} with an appropriate subpath configuration. Otherwise, the + // `createWatcher` callback will be invoked to create a new {NativeWatcher}, which will be registered in the tree + // and attached to the watcher. + // + // If any pre-existing child watchers are removed as a result of this operation, {NativeWatcher.onWillReattach} will + // be broadcast on each with the new parent watcher as an event payload to give child watchers a chance to attach to + // the new watcher. + // + // * `directory` a normalized path to be watched as a {String}. + // * `watcher` an unattached {Watcher}. + // * `createNative` callback to be invoked if no existing {NativeWatcher} covers the {Watcher}. It should + // synchronously return a new {NativeWatcher} instance watching {directory}. + attach (directory, watcher, createNative) { + const pathSegments = directory.split(path.sep).filter(segment => segment.length > 0) + + const attachToNew = () => { + const native = createNative() + const leaf = new RegistryWatcherNode(native) + this.tree = this.tree.insert(pathSegments, leaf) + + watcher.attachToNative(native, '') + + return native + } + + this.tree.lookup(pathSegments).when({ + parent: (parent, remaining) => { + // An existing NativeWatcher is watching a parent directory of the requested path. Attach this Watcher to + // it as a filtering watcher. + const native = parent.getNativeWatcher() + const subpath = remaining.length === 0 ? '' : path.join(...remaining) + + watcher.attachToNative(native, subpath) + }, + children: children => { + const newNative = attachToNew() + + // One or more NativeWatchers exist on child directories of the requested path. + for (let i = 0; i < children.length; i++) { + const child = children[i] + const childNative = child.getNativeWatcher() + childNative.reattachTo(newNative, directory) + childNative.dispose() + + // Don't await this Promise. Subscribers can listen for `onDidStop` to be notified if they choose. + childNative.stop() + } + }, + missing: attachToNew + }) + } +} From 908e5ad1e93112dc46a812bf46025fbdc2c30ca6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Jun 2017 07:36:07 -0400 Subject: [PATCH 150/314] FileSystemManager that hands out Watchers to subscribe to filesystem events --- src/atom-environment.coffee | 5 + src/filesystem-manager.js | 201 ++++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 src/filesystem-manager.js diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index b37acddd1..12a1ad192 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -46,6 +46,7 @@ TextBuffer = require 'text-buffer' Gutter = require './gutter' TextEditorRegistry = require './text-editor-registry' AutoUpdateManager = require './auto-update-manager' +FileSystemManager = require './filesystem-manager' # Essential: Atom global for dealing with packages, themes, menus, and the window. # @@ -117,6 +118,9 @@ class AtomEnvironment extends Model # Private: An {AutoUpdateManager} instance autoUpdater: null + # Public: A {FileSystemManager} instance + filesystem: null + saveStateDebounceInterval: 1000 ### @@ -137,6 +141,7 @@ class AtomEnvironment extends Model @views = new ViewRegistry(this) TextEditor.setScheduler(@views) @notifications = new NotificationManager + @filesystem = new FileSystemManager @updateProcessEnv ?= updateProcessEnv # For testing @stateStore = new StateStore('AtomEnvironments', 1) diff --git a/src/filesystem-manager.js b/src/filesystem-manager.js new file mode 100644 index 000000000..de9efe36a --- /dev/null +++ b/src/filesystem-manager.js @@ -0,0 +1,201 @@ +/** @babel */ + +import fs from 'fs' +import path from 'path' + +import {Emitter, Disposable, CompositeDisposable} from 'event-kit' +import nsfw from 'nsfw' +const {MODIFIED, CREATED, DELETED, RENAMED} = nsfw.actions + +import NativeWatcherRegistry from './native-watcher-registry' + +// Private: Associate native watcher action type flags with descriptive String equivalents. +const ACTION_MAP = new Map([ + [nsfw.actions.MODIFIED, 'changed'], + [nsfw.actions.CREATED, 'added'], + [nsfw.actions.DELETED, 'deleted'], + [nsfw.actions.RENAMED, 'renamed'] +]) + +// Private: Interface with and normalize events from a native OS filesystem watcher. +class NativeWatcher { + + // Private: Initialize a native watcher on a path. + // + // Events will not be produced until {start()} is called. + constructor (normalizedPath) { + this.normalizedPath = normalizedPath + this.emitter = new Emitter() + + this.watcher = null + this.running = false + this.refCount = 0 + } + + // Private: Begin watching for filesystem events. + // + // Has no effect if the watcher has already been started. + async start () { + if (this.running) { + return + } + + this.watcher = await nsfw( + this.normalizedPath, + this.onEvents.bind(this), + { + debounceMS: 100, + errorCallback: this.onError.bind(this) + } + ) + + await this.watcher.start() + + this.running = true + this.emitter.emit('did-start') + } + + // Private: Return true if the underlying watcher has been started. + isRunning () { + return this.running + } + + // Private: Register a callback to be invoked when the filesystem watcher has been initialized. + // + // Returns: A {Disposable} to revoke the subscription. + onDidStart (callback) { + return this.emitter.on('did-start', callback) + } + + // Private: Register a callback to be invoked with normalized filesystem events as they arrive. + // + // Returns: A {Disposable} to revoke the subscription. + onDidChange (callback) { + return this.emitter.on('did-change', callback) + } + + // Private: Register a callback to be invoked when a {Watcher} should attach to a different {NativeWatcher}. + // + // Returns: A {Disposable} to revoke the subscription. + onShouldDetach (callback) { + return this.emitter.on('should-detach', callback) + } + + // Private: Register a callback to be invoked when the filesystem watcher has been initialized. + // + // Returns: A {Disposable} to revoke the subscription. + onDidStop (callback) { + return this.emitter.on('did-stop', callback) + } + + // Private: Broadcast an `onShouldDetach` event to prompt any {Watcher} instances bound here to attach to a new + // {NativeWatcher} instead. + reattachTo (other) { + this.emitter.emit('should-detach', other) + } + + // Private: Stop the native watcher and release any operating system resources associated with it. + // + // Has no effect if the watcher is not running. + async stop() { + if (!this.running) { + return + } + + await this.watcher.stop() + this.running = false + this.emitter.emit('did-stop') + } + + // Private: Callback function invoked by the native watcher when a debounced group of filesystem events arrive. + // Normalize and re-broadcast them to any subscribers. + // + // * `events` An Array of filesystem events. + onEvents (events) { + this.emitter.emit('did-change', events.map(event => { + const type = ACTION_MAP.get(event.action) || `unexpected (${event.action})` + const oldFileName = event.file || event.oldFile + const newFileName = event.newFile + const oldPath = path.join(event.directory, oldFileName) + const newPath = newFileName && path.join(event.directory, newFileName) + + return {oldPath, newPath, type} + })) + } + + // Private: Callback function invoked by the native watcher when an error occurs. + // + // * `err` The native filesystem error. + onError (err) { + // + } +} + +class Watcher { + constructor (watchedPath) { + this.watchedPath = watchedPath + this.normalizedPath = null + + this.emitter = new Emitter() + this.subs = new CompositeDisposable() + } + + onDidStart (callback) { + return this.emitter.on('did-start', callback) + } + + onDidChange (callback) { + return this.emitter.on('did-change', callback) + } + + attachToNative (native) { + this.subs.dispose() + + if (native.isRunning()) { + this.emitter.emit('did-start') + } else { + this.subs.add(native.onDidStart(payload => { + this.emitter.emit('did-start', payload) + })) + } + + this.subs.add(native.onDidChange(events => { + // TODO does event.oldPath resolve symlinks? + const filtered = events.filter(event => event.oldPath.startsWith(this.normalizedPath)) + + if (filtered.length > 0) { + this.emitter.emit('did-change', filtered) + } + })) + + this.subs.add(native.onShouldDetach( + this.attachToNative.bind(this) + )) + } + + dispose () { + this.emitter.dispose() + this.subs.dispose() + } +} + +export default class FileSystemManager { + constructor () { + this.nativeWatchers = new NativeWatcherRegistry() + } + + getWatcher (rootPath) { + const watcher = new Watcher(rootPath) + + (async () => { + const normalizedPath = await new Promise((resolve, reject) => { + fs.realpath(rootPath, (err, real) => (err ? reject(err) : resolve(real))) + }) + watcher.normalizedPath = normalizedPath + + this.nativeWatchers.attach(normalizedPath, watcher, () => new NativeWatcher(normalizedPath)) + })() + + return watcher + } +} From 366ee19bd951651cb9f56127e5afbd3fdb760de4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 20 Jun 2017 14:38:56 -0400 Subject: [PATCH 151/314] :shirt: make the linter happy --- spec/native-watcher-registry-spec.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/spec/native-watcher-registry-spec.js b/spec/native-watcher-registry-spec.js index fd8d898ec..e5e880667 100644 --- a/spec/native-watcher-registry-spec.js +++ b/spec/native-watcher-registry-spec.js @@ -1,7 +1,5 @@ /** @babel */ -import path from 'path' - import NativeWatcherRegistry from '../src/native-watcher-registry' class MockWatcher { @@ -31,11 +29,11 @@ class MockNative { this.attached = [] } - dispose() { + dispose () { this.disposed = true } - stop() { + stop () { this.stopped = true } } @@ -48,7 +46,7 @@ describe('NativeWatcherRegistry', function () { watcher = new MockWatcher() }) - it('attaches a Watcher to a newly created NativeWatcher for a new directory', function() { + it('attaches a Watcher to a newly created NativeWatcher for a new directory', function () { const NATIVE = new MockNative('created') registry.attach('/some/path', watcher, () => NATIVE) From bd76773412e609284b5cb372ed8545bb9705f1ea Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 20 Jun 2017 14:48:39 -0400 Subject: [PATCH 152/314] :shirt: lint lint lint --- src/native-watcher-registry.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index e78c87c9c..c0b7d7c0d 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -19,7 +19,7 @@ class RegistryNode { // Returns: A {ParentResult} if the exact requested directory or a parent directory is being watched, a // {ChildrenResult} if one or more child paths are being watched, or a {MissingResult} if no relevant watchers // exist. - lookup(pathSegments) { + lookup (pathSegments) { if (pathSegments.length === 0) { return new ChildrenResult(this.leaves()) } @@ -40,7 +40,7 @@ class RegistryNode { // // Returns: The root of a new tree with the {RegistryWatcherNode} inserted at the correct location. Callers should // replace their node references with the returned value. - insert(pathSegments, leaf) { + insert (pathSegments, leaf) { if (pathSegments.length === 0) { return leaf } @@ -57,7 +57,7 @@ class RegistryNode { // Private: Discover all {RegistryWatcherNode} instances beneath this tree node. // // Returns: A possibly empty {Array} of {RegistryWatcherNode} instances that are the descendants of this node. - leaves() { + leaves () { const results = [] for (const p of Object.keys(this.children)) { results.push(...this.children[p].leaves()) @@ -87,14 +87,14 @@ class RegistryWatcherNode { // * `pathSegments` filesystem path of a new {Watcher} already split into an Array of directory names. // // Returns: A {ParentResult} referencing this node. - lookup(pathSegments) { + lookup (pathSegments) { return new ParentResult(this, pathSegments) } // Private: Discover this {RegistryWatcherNode} instance. // // Returns: An {Array} containing this node. - leaves() { + leaves () { return [this] } } From 9c9625eb766bb75f53d1a2e807cc4efa66f1e66e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 20 Jun 2017 15:44:05 -0400 Subject: [PATCH 153/314] Helpers to promisify functions in specs --- spec/async-spec-helpers.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/spec/async-spec-helpers.js b/spec/async-spec-helpers.js index 8bc36c913..84a592c6a 100644 --- a/spec/async-spec-helpers.js +++ b/spec/async-spec-helpers.js @@ -72,3 +72,27 @@ export function emitterEventPromise (emitter, event, timeout = 15000) { }) }) } + +export function promisify (original) { + return function (...args) { + return new Promise((resolve, reject) => { + args.push((err, ...results) => { + if (err) { + reject(err) + } else { + resolve(...results) + } + }) + + return original(...args) + }) + } +} + +export function promisifySome (obj, fnNames) { + const result = {} + for (fnName of fnNames) { + result[fnName] = promisify(obj[fnName]) + } + return result +} From e4c48a5c8c887878f6e410d61776a0281661a17a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 20 Jun 2017 15:44:57 -0400 Subject: [PATCH 154/314] :shirt: for FileSystemManager --- src/filesystem-manager.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/filesystem-manager.js b/src/filesystem-manager.js index de9efe36a..6bdbb9f01 100644 --- a/src/filesystem-manager.js +++ b/src/filesystem-manager.js @@ -3,9 +3,8 @@ import fs from 'fs' import path from 'path' -import {Emitter, Disposable, CompositeDisposable} from 'event-kit' +import {Emitter, CompositeDisposable} from 'event-kit' import nsfw from 'nsfw' -const {MODIFIED, CREATED, DELETED, RENAMED} = nsfw.actions import NativeWatcherRegistry from './native-watcher-registry' @@ -29,7 +28,6 @@ class NativeWatcher { this.watcher = null this.running = false - this.refCount = 0 } // Private: Begin watching for filesystem events. @@ -97,7 +95,7 @@ class NativeWatcher { // Private: Stop the native watcher and release any operating system resources associated with it. // // Has no effect if the watcher is not running. - async stop() { + async stop () { if (!this.running) { return } @@ -127,7 +125,7 @@ class NativeWatcher { // // * `err` The native filesystem error. onError (err) { - // + console.error(err) } } @@ -187,14 +185,15 @@ export default class FileSystemManager { getWatcher (rootPath) { const watcher = new Watcher(rootPath) - (async () => { + const init = async () => { const normalizedPath = await new Promise((resolve, reject) => { fs.realpath(rootPath, (err, real) => (err ? reject(err) : resolve(real))) }) watcher.normalizedPath = normalizedPath this.nativeWatchers.attach(normalizedPath, watcher, () => new NativeWatcher(normalizedPath)) - })() + } + init() return watcher } From 39085ce3dca2b977a9fb2dd74a94f1b0d38f8b9c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 20 Jun 2017 15:45:36 -0400 Subject: [PATCH 155/314] First few FileSystemManager specs --- spec/filesystem-manager-spec.js | 137 ++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 spec/filesystem-manager-spec.js diff --git a/spec/filesystem-manager-spec.js b/spec/filesystem-manager-spec.js new file mode 100644 index 000000000..dbc79fa52 --- /dev/null +++ b/spec/filesystem-manager-spec.js @@ -0,0 +1,137 @@ +/** @babel */ + +import {it, beforeEach, afterEach, promisifySome} from './async-spec-helpers' +import tempCb from 'temp' +import fsCb from 'fs-plus' +import path from 'path' + +import {CompositeDisposable} from 'event-kit' +import FileSystemManager from '../src/filesystem-manager' + +tempCb.track() + +const fs = promisifySome(fsCb, ['writeFile', 'mkdir', 'symlink', 'appendFile', 'realpath']) +const temp = promisifySome(tempCb, ['mkdir', 'cleanup']) + +describe('FileSystemManager', function () { + let subs, manager + + beforeEach(function () { + subs = new CompositeDisposable() + manager = new FileSystemManager() + }) + + afterEach(async function () { + subs.dispose() + + const cleanup = [temp.cleanup()] + const nativeWatchers = manager.nativeWatchers.tree.leaves().map(node => node.getNativeWatcher()) + for (const native of nativeWatchers) { + cleanup.push(native.stop()) + } + await Promise.all(cleanup) + }) + + function waitForEvent (fn) { + return new Promise(resolve => { + subs.add(fn(resolve)) + }) + } + + function waitForChanges (watcher, ...fileNames) { + const waiting = new Set(fileNames) + const relevantEvents = [] + + return new Promise(resolve => { + const sub = watcher.onDidChange(events => { + for (const event of events) { + if (waiting.delete(event.oldPath)) { + relevantEvents.push(event) + } + } + + if (waiting.size === 0) { + resolve(relevantEvents) + sub.dispose() + } + }) + }) + } + + describe('getWatcher()', function () { + it('broadcasts onDidStart when the watcher begins listening', async function () { + const rootDir = await temp.mkdir('atom-fsmanager-') + + const watcher = manager.getWatcher(rootDir) + await waitForEvent(cb => watcher.onDidStart(cb)) + }) + + it('reuses an existing native watcher and broadcasts onDidStart immediately if attached to an existing watcher', async function () { + const rootDir = await temp.mkdir('atom-fsmanager-') + + const watcher0 = manager.getWatcher(rootDir) + await waitForEvent(cb => watcher0.onDidStart(cb)) + + const watcher1 = manager.getWatcher(rootDir) + await waitForEvent(cb => watcher1.onDidStart(cb)) + + expect(watcher0.native).toBe(watcher1.native) + }) + + it('reuses an existing native watcher on a parent directory and filters events', async function () { + const rootDir = await temp.mkdir('atom-fsmanager-').then(fs.realpath) + + const rootFile = path.join(rootDir, 'rootfile.txt') + const subDir = path.join(rootDir, 'subdir') + const subFile = path.join(subDir, 'subfile.txt') + await Promise.all([ + fs.mkdir(subDir).then( + fs.writeFile(subFile, 'subfile\n', {encoding: 'utf8'}) + ), + fs.writeFile(rootFile, 'rootfile\n', {encoding: 'utf8'}) + ]) + + const rootWatcher = manager.getWatcher(rootDir) + await waitForEvent(cb => rootWatcher.onDidStart(cb)) + + const childWatcher = manager.getWatcher(subDir) + await waitForEvent(cb => childWatcher.onDidStart(cb)) + + expect(rootWatcher.native).toBe(childWatcher.native) + + const firstRootChange = waitForChanges(rootWatcher, subFile) + const firstChildChange = waitForChanges(childWatcher, subFile) + + console.log(`changing ${subFile}`) + await fs.appendFile(subFile, 'changes\n', {encoding: 'utf8'}) + const firstPayloads = await Promise.all([firstRootChange, firstChildChange]) + + for (const events of firstPayloads) { + expect(events.length).toBe(1) + expect(events[0].oldPath).toBe(subFile) + } + + const nextRootEvent = waitForChanges(rootWatcher, rootFile) + await fs.appendFile(rootFile, 'changes\n', {encoding: 'utf8'}) + + const nextPayload = await nextRootEvent + + expect(nextPayload.length).toBe(1) + expect(nextPayload[0].oldPath).toBe(rootFile) + }) + + xit('adopts existing child watchers and filters events appropriately to them') + + describe('event normalization', function () { + xit('normalizes "changed" events') + xit('normalizes "added" events') + xit('normalizes "deleted" events') + xit('normalizes "renamed" events') + }) + + describe('symlinks', function () { + xit('reports events with symlink paths') + xit('uses the same native watcher even for symlink paths') + }) + }) +}) From 21e381033c65d62428499d2880bdfb8a63c0c1e9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 20 Jun 2017 15:45:55 -0400 Subject: [PATCH 156/314] Start native watchers when attached --- src/filesystem-manager.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/filesystem-manager.js b/src/filesystem-manager.js index 6bdbb9f01..5d4c6408a 100644 --- a/src/filesystem-manager.js +++ b/src/filesystem-manager.js @@ -155,6 +155,8 @@ class Watcher { this.subs.add(native.onDidStart(payload => { this.emitter.emit('did-start', payload) })) + + native.start() } this.subs.add(native.onDidChange(events => { From baf71492a23fc5d74e7a4bb90eccece425d2835a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Jun 2017 10:21:40 -0400 Subject: [PATCH 157/314] `.dispose()` all subscribers on a NativeWatcher --- src/filesystem-manager.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/filesystem-manager.js b/src/filesystem-manager.js index 5d4c6408a..7cea7f298 100644 --- a/src/filesystem-manager.js +++ b/src/filesystem-manager.js @@ -105,6 +105,11 @@ class NativeWatcher { this.emitter.emit('did-stop') } + // Private: Detach any event subscribers. + dispose () { + this.emitter.dispose() + } + // Private: Callback function invoked by the native watcher when a debounced group of filesystem events arrive. // Normalize and re-broadcast them to any subscribers. // From 8d86acf19cca7dc2713da3fc78b0ea5a74d08b85 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Jun 2017 10:21:55 -0400 Subject: [PATCH 158/314] Don't report errors after stop --- src/filesystem-manager.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/filesystem-manager.js b/src/filesystem-manager.js index 7cea7f298..8f98650e5 100644 --- a/src/filesystem-manager.js +++ b/src/filesystem-manager.js @@ -130,6 +130,10 @@ class NativeWatcher { // // * `err` The native filesystem error. onError (err) { + if (!this.isRunning()) { + return + } + console.error(err) } } From 9f518736e107adf12f6bcbc27a95d367efef9f74 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Jun 2017 10:22:13 -0400 Subject: [PATCH 159/314] Track the current NativeWatcher assigned to a Watcher --- src/filesystem-manager.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/filesystem-manager.js b/src/filesystem-manager.js index 8f98650e5..c043dc0b6 100644 --- a/src/filesystem-manager.js +++ b/src/filesystem-manager.js @@ -142,6 +142,7 @@ class Watcher { constructor (watchedPath) { this.watchedPath = watchedPath this.normalizedPath = null + this.native = null this.emitter = new Emitter() this.subs = new CompositeDisposable() @@ -157,6 +158,7 @@ class Watcher { attachToNative (native) { this.subs.dispose() + this.native = native if (native.isRunning()) { this.emitter.emit('did-start') From 12c961c8b9a894382a16422445f87280ba698ec4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Jun 2017 10:22:26 -0400 Subject: [PATCH 160/314] Maintain a Set of living NativeWatcher instances --- src/filesystem-manager.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/filesystem-manager.js b/src/filesystem-manager.js index c043dc0b6..4f900f4f3 100644 --- a/src/filesystem-manager.js +++ b/src/filesystem-manager.js @@ -193,6 +193,7 @@ class Watcher { export default class FileSystemManager { constructor () { this.nativeWatchers = new NativeWatcherRegistry() + this.liveWatchers = new Set() } getWatcher (rootPath) { @@ -204,7 +205,17 @@ export default class FileSystemManager { }) watcher.normalizedPath = normalizedPath - this.nativeWatchers.attach(normalizedPath, watcher, () => new NativeWatcher(normalizedPath)) + this.nativeWatchers.attach(normalizedPath, watcher, () => { + const nativeWatcher = new NativeWatcher(normalizedPath) + + this.liveWatchers.add(nativeWatcher) + const sub = nativeWatcher.onDidStop(() => { + this.liveWatchers.delete(nativeWatcher) + sub.dispose() + }) + + return nativeWatcher + }) } init() From be681d132406c71fad1cf31deca16f7459b9bda9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Jun 2017 10:24:06 -0400 Subject: [PATCH 161/314] Use a private utility function to wait for all native watchers to stop --- spec/filesystem-manager-spec.js | 10 +++------- src/atom-environment.coffee | 2 +- src/filesystem-manager.js | 8 ++++++++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/spec/filesystem-manager-spec.js b/spec/filesystem-manager-spec.js index dbc79fa52..1863f9aea 100644 --- a/spec/filesystem-manager-spec.js +++ b/spec/filesystem-manager-spec.js @@ -6,7 +6,7 @@ import fsCb from 'fs-plus' import path from 'path' import {CompositeDisposable} from 'event-kit' -import FileSystemManager from '../src/filesystem-manager' +import FileSystemManager, {stopAllWatchers} from '../src/filesystem-manager' tempCb.track() @@ -24,12 +24,8 @@ describe('FileSystemManager', function () { afterEach(async function () { subs.dispose() - const cleanup = [temp.cleanup()] - const nativeWatchers = manager.nativeWatchers.tree.leaves().map(node => node.getNativeWatcher()) - for (const native of nativeWatchers) { - cleanup.push(native.stop()) - } - await Promise.all(cleanup) + await stopAllWatchers(manager) + await temp.cleanup() }) function waitForEvent (fn) { diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 12a1ad192..7deaafbdb 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -46,7 +46,7 @@ TextBuffer = require 'text-buffer' Gutter = require './gutter' TextEditorRegistry = require './text-editor-registry' AutoUpdateManager = require './auto-update-manager' -FileSystemManager = require './filesystem-manager' +FileSystemManager = require('./filesystem-manager').default # Essential: Atom global for dealing with packages, themes, menus, and the window. # diff --git a/src/filesystem-manager.js b/src/filesystem-manager.js index 4f900f4f3..4e0c93bdb 100644 --- a/src/filesystem-manager.js +++ b/src/filesystem-manager.js @@ -222,3 +222,11 @@ export default class FileSystemManager { return watcher } } + +// Private: Return a Promise that resolves when all {NativeWatcher} instances associated with a FileSystemManager +// have stopped listening. This is useful for `afterEach()` blocks in unit tests. +export function stopAllWatchers (manager) { + return Promise.all( + Array.from(manager.liveWatchers, watcher => watcher.stop()) + ) +} From 882095eea672fc984bfb273290d0124cbfa4a042 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Jun 2017 10:24:26 -0400 Subject: [PATCH 162/314] Test child watcher adoption --- spec/filesystem-manager-spec.js | 49 ++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/spec/filesystem-manager-spec.js b/spec/filesystem-manager-spec.js index 1863f9aea..63a19b49e 100644 --- a/spec/filesystem-manager-spec.js +++ b/spec/filesystem-manager-spec.js @@ -116,7 +116,54 @@ describe('FileSystemManager', function () { expect(nextPayload[0].oldPath).toBe(rootFile) }) - xit('adopts existing child watchers and filters events appropriately to them') + it('adopts existing child watchers and filters events appropriately to them', async function () { + const parentDir = await temp.mkdir('atom-fsmanager-').then(fs.realpath) + + // Create the directory tree + const rootFile = path.join(parentDir, 'rootfile.txt') + const subDir0 = path.join(parentDir, 'subdir0') + const subFile0 = path.join(subDir0, 'subfile1.txt') + const subDir1 = path.join(parentDir, 'subdir1') + const subFile1 = path.join(subDir1, 'subfile1.txt') + await Promise.all([ + fs.writeFile(rootFile, 'rootfile\n', {encoding: 'utf8'}), + fs.mkdir(subDir0).then( + fs.writeFile(subFile0, 'subfile 0\n', {encoding: 'utf8'}) + ), + fs.mkdir(subDir1).then( + fs.writeFile(subFile1, 'subfile 1\n', {encoding: 'utf8'}) + ) + ]) + + // Begin the child watchers + const subWatcher0 = manager.getWatcher(subDir0) + const subWatcher1 = manager.getWatcher(subDir1) + + await Promise.all( + [subWatcher0, subWatcher1].map(watcher => waitForEvent(cb => watcher.onDidStart(cb))) + ) + expect(subWatcher0.native).not.toBe(subWatcher1.native) + + // Create the parent watcher + const parentWatcher = manager.getWatcher(parentDir) + await waitForEvent(cb => parentWatcher.onDidStart(cb)) + + expect(subWatcher0.native).toBe(parentWatcher.native) + expect(subWatcher1.native).toBe(parentWatcher.native) + + // Ensure events are filtered correctly + await Promise.all([ + fs.appendFile(rootFile, 'change\n', {encoding: 'utf8'}), + fs.appendFile(subFile0, 'change\n', {encoding: 'utf8'}), + fs.appendFile(subFile1, 'change\n', {encoding: 'utf8'}) + ]) + + await Promise.all([ + waitForChanges(subWatcher0, subFile0), + waitForChanges(subWatcher1, subFile1), + waitForChanges(parentWatcher, rootFile, subFile0, subFile1) + ]) + }) describe('event normalization', function () { xit('normalizes "changed" events') From 0325a77d591f12d56d604fde9b0657a2436dc1ba Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Jun 2017 11:08:29 -0400 Subject: [PATCH 163/314] Test NativeWatcher removal --- spec/native-watcher-registry-spec.js | 45 +++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/spec/native-watcher-registry-spec.js b/spec/native-watcher-registry-spec.js index e5e880667..4830a421f 100644 --- a/spec/native-watcher-registry-spec.js +++ b/spec/native-watcher-registry-spec.js @@ -1,5 +1,8 @@ /** @babel */ +import path from 'path' +import {Emitter} from 'event-kit' + import NativeWatcherRegistry from '../src/native-watcher-registry' class MockWatcher { @@ -19,6 +22,8 @@ class MockNative { this.attached = [] this.disposed = false this.stopped = false + + this.emitter = new Emitter() } reattachTo (newNative) { @@ -29,12 +34,17 @@ class MockNative { this.attached = [] } + onDidStop (callback) { + return this.emitter.on('did-stop', callback) + } + dispose () { this.disposed = true } stop () { this.stopped = true + this.emitter.emit('did-stop') } } @@ -107,5 +117,38 @@ describe('NativeWatcherRegistry', function () { expect(EXISTING2.disposed).toBe(false) }) - it('removes NativeWatchers when all Watchers have been disposed') + describe('removing NativeWatchers', function () { + it('happens when they stop', function () { + const STOPPED = new MockNative('stopped') + const stoppedWatcher = new MockWatcher() + const stoppedPath = ['watcher', 'that', 'will', 'be', 'stopped'] + registry.attach(path.join(...stoppedPath), stoppedWatcher, () => STOPPED) + + const RUNNING = new MockNative('running') + const runningWatcher = new MockWatcher() + const runningPath = ['watcher', 'that', 'will', 'continue', 'to', 'exist'] + registry.attach(path.join(...runningPath), runningWatcher, () => RUNNING) + + STOPPED.stop() + + const runningNode = registry.tree.lookup(runningPath).when({ + parent: node => node, + missing: () => false, + children: () => false + }) + expect(runningNode).toBeTruthy() + expect(runningNode.getNativeWatcher()).toBe(RUNNING) + + const stoppedNode = registry.tree.lookup(stoppedPath).when({ + parent: () => false, + missing: () => true, + children: () => false + }) + expect(stoppedNode).toBe(true) + }) + + it("keeps a parent watcher that's still running") + + it('reassigns new child watchers when a parent watcher is stopped') + }) }) From f3a4c74158cb40bdccb8b52fbb3f189b0c5bdb19 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Jun 2017 11:09:01 -0400 Subject: [PATCH 164/314] :fire: console.log --- spec/filesystem-manager-spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/filesystem-manager-spec.js b/spec/filesystem-manager-spec.js index 63a19b49e..019c2b0a9 100644 --- a/spec/filesystem-manager-spec.js +++ b/spec/filesystem-manager-spec.js @@ -98,7 +98,6 @@ describe('FileSystemManager', function () { const firstRootChange = waitForChanges(rootWatcher, subFile) const firstChildChange = waitForChanges(childWatcher, subFile) - console.log(`changing ${subFile}`) await fs.appendFile(subFile, 'changes\n', {encoding: 'utf8'}) const firstPayloads = await Promise.all([firstRootChange, firstChildChange]) From 4194e7f3b5d41a99261dec5406e631f502c4b7d7 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Jun 2017 11:33:49 -0400 Subject: [PATCH 165/314] Remove stopped watcher nodes with the power of RECURSION :sparkles: --- src/native-watcher-registry.js | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index c0b7d7c0d..adc86e6f7 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -54,6 +54,37 @@ class RegistryNode { return this } + // Private: Remove a {RegistryWatcherNode} by the exact watched directory. + // + // * `pathSegments` absolute pre-split filesystem path of the node to remove. + // + // Returns: The root of a new tree with the {RegistryWatcherNode} removed. Callers should replace their node + // references with the returned value. + remove (pathSegments) { + if (pathSegments.length === 0) { + // Attempt to remove a path with child watchers. Do nothing. + return this + } + + const pathKey = pathSegments[0] + const child = this.children[pathKey] + if (child === undefined) { + // Attempt to remove a path that isn't watched. Do nothing. + return this + } + + // Recurse + const newChild = child.remove(pathSegments.slice(1)) + if (newChild === null) { + delete this.children[pathKey] + } else { + this.children[pathKey] = newChild + } + + // Remove this node if all of its children have been removed + return Object.keys(this.children).length === 0 ? null : this + } + // Private: Discover all {RegistryWatcherNode} instances beneath this tree node. // // Returns: A possibly empty {Array} of {RegistryWatcherNode} instances that are the descendants of this node. @@ -91,6 +122,15 @@ class RegistryWatcherNode { return new ParentResult(this, pathSegments) } + // Private: Remove this leaf node if the watcher's exact path matches. + // + // * `pathSegments` filesystem path of the node to remove. + // + // Returns: {null} if the `pathSegments` are an exact match, {this} otherwise. + remove (pathSegments) { + return pathSegments.length === 0 ? null : this + } + // Private: Discover this {RegistryWatcherNode} instance. // // Returns: An {Array} containing this node. @@ -206,6 +246,11 @@ export default class NativeWatcherRegistry { const leaf = new RegistryWatcherNode(native) this.tree = this.tree.insert(pathSegments, leaf) + const sub = native.onDidStop(() => { + this.tree = this.tree.remove(pathSegments) + sub.dispose() + }) + watcher.attachToNative(native, '') return native From c8882ca92b639be77cdacc5a228d61d8af233622 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Jun 2017 11:42:50 -0400 Subject: [PATCH 166/314] Start and stop NativeWatchers automatically using `onDidChange` subs --- src/filesystem-manager.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/filesystem-manager.js b/src/filesystem-manager.js index 4e0c93bdb..5f3a50bae 100644 --- a/src/filesystem-manager.js +++ b/src/filesystem-manager.js @@ -3,7 +3,7 @@ import fs from 'fs' import path from 'path' -import {Emitter, CompositeDisposable} from 'event-kit' +import {Emitter, Disposable, CompositeDisposable} from 'event-kit' import nsfw from 'nsfw' import NativeWatcherRegistry from './native-watcher-registry' @@ -65,11 +65,23 @@ class NativeWatcher { return this.emitter.on('did-start', callback) } - // Private: Register a callback to be invoked with normalized filesystem events as they arrive. + // Private: Register a callback to be invoked with normalized filesystem events as they arrive. Starts the watcher + // automatically if it is not already running. The watcher will be stopped automatically when all subscribers + // dispose their subscriptions. // // Returns: A {Disposable} to revoke the subscription. onDidChange (callback) { - return this.emitter.on('did-change', callback) + if (!this.isRunning()) { + this.start() + } + + const sub = this.emitter.on('did-change', callback) + return new Disposable(() => { + sub.dispose() + if (this.emitter.listenerCountForEventName('did-change') === 0) { + this.stop() + } + }) } // Private: Register a callback to be invoked when a {Watcher} should attach to a different {NativeWatcher}. From d858e37058dcbd302edd077549db612107078d4d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Jun 2017 08:32:54 -0400 Subject: [PATCH 167/314] Support pending specs with an empty body --- spec/async-spec-helpers.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/async-spec-helpers.js b/spec/async-spec-helpers.js index 84a592c6a..e3cf26fa7 100644 --- a/spec/async-spec-helpers.js +++ b/spec/async-spec-helpers.js @@ -20,6 +20,11 @@ export function afterEach (fn) { ['it', 'fit', 'ffit', 'fffit'].forEach(function (name) { module.exports[name] = function (description, fn) { + if (fn === undefined) { + global[name](description) + return + } + global[name](description, function () { const result = fn() if (result instanceof Promise) { From 246e87b660bec50d18e8e51cbef03483e0ef69a8 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Jun 2017 08:33:13 -0400 Subject: [PATCH 168/314] :shirt: keep standard happy --- spec/async-spec-helpers.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/async-spec-helpers.js b/spec/async-spec-helpers.js index e3cf26fa7..56550cd9f 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) { 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 condition') } } } @@ -96,7 +96,7 @@ export function promisify (original) { export function promisifySome (obj, fnNames) { const result = {} - for (fnName of fnNames) { + for (const fnName of fnNames) { result[fnName] = promisify(obj[fnName]) } return result From 53dcc00bfcda6787d0fc6ac280b91f376020f91b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Jun 2017 08:33:40 -0400 Subject: [PATCH 169/314] Don't cleanup temp between runs to prevent reused directory names --- spec/filesystem-manager-spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/filesystem-manager-spec.js b/spec/filesystem-manager-spec.js index 019c2b0a9..55024c305 100644 --- a/spec/filesystem-manager-spec.js +++ b/spec/filesystem-manager-spec.js @@ -25,7 +25,6 @@ describe('FileSystemManager', function () { subs.dispose() await stopAllWatchers(manager) - await temp.cleanup() }) function waitForEvent (fn) { From 6e6c0a5ef9d333faf4e72fd28120c45171e0dbb1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Jun 2017 08:35:06 -0400 Subject: [PATCH 170/314] Use getStartPromise() in specs --- spec/filesystem-manager-spec.js | 75 ++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/spec/filesystem-manager-spec.js b/spec/filesystem-manager-spec.js index 55024c305..a37aee108 100644 --- a/spec/filesystem-manager-spec.js +++ b/spec/filesystem-manager-spec.js @@ -27,12 +27,6 @@ describe('FileSystemManager', function () { await stopAllWatchers(manager) }) - function waitForEvent (fn) { - return new Promise(resolve => { - subs.add(fn(resolve)) - }) - } - function waitForChanges (watcher, ...fileNames) { const waiting = new Set(fileNames) const relevantEvents = [] @@ -54,21 +48,53 @@ describe('FileSystemManager', function () { } describe('getWatcher()', function () { - it('broadcasts onDidStart when the watcher begins listening', async function () { + it('resolves getStartPromise() when the watcher begins listening', async function () { const rootDir = await temp.mkdir('atom-fsmanager-') const watcher = manager.getWatcher(rootDir) - await waitForEvent(cb => watcher.onDidStart(cb)) + watcher.onDidChange(() => {}) + + await watcher.getStartPromise() }) - it('reuses an existing native watcher and broadcasts onDidStart immediately if attached to an existing watcher', async function () { + it('does not start actually watching until an onDidChange subscriber is registered', async function () { + const rootDir = await temp.mkdir('atom-fsmanager-') + const watcher = manager.getWatcher(rootDir) + + let started = false + const startPromise = watcher.getStartPromise().then(() => { + started = true + }) + + expect(watcher.native).toBe(null) + expect(watcher.normalizedPath).toBe(null) + expect(started).toBe(false) + + await watcher.getNormalizedPathPromise() + + expect(watcher.native).toBe(null) + expect(watcher.normalizedPath).not.toBe(null) + expect(started).toBe(false) + + watcher.onDidChange(() => {}) + await startPromise + + expect(watcher.native).not.toBe(null) + expect(started).toBe(true) + }) + + it('automatically stops and removes the watcher when all onDidChange subscribers dispose') + + it('reuses an existing native watcher and resolves getStartPromise immediately if attached to a running watcher', async function () { const rootDir = await temp.mkdir('atom-fsmanager-') const watcher0 = manager.getWatcher(rootDir) - await waitForEvent(cb => watcher0.onDidStart(cb)) + watcher0.onDidChange(() => {}) + await watcher0.getStartPromise() const watcher1 = manager.getWatcher(rootDir) - await waitForEvent(cb => watcher1.onDidStart(cb)) + watcher1.onDidChange(() => {}) + await watcher1.getStartPromise() expect(watcher0.native).toBe(watcher1.native) }) @@ -87,17 +113,20 @@ describe('FileSystemManager', function () { ]) const rootWatcher = manager.getWatcher(rootDir) - await waitForEvent(cb => rootWatcher.onDidStart(cb)) - const childWatcher = manager.getWatcher(subDir) - await waitForEvent(cb => childWatcher.onDidStart(cb)) expect(rootWatcher.native).toBe(childWatcher.native) const firstRootChange = waitForChanges(rootWatcher, subFile) const firstChildChange = waitForChanges(childWatcher, subFile) + await Promise.all([ + rootWatcher.getStartPromise(), + childWatcher.getStartPromise() + ]) + await fs.appendFile(subFile, 'changes\n', {encoding: 'utf8'}) + const firstPayloads = await Promise.all([firstRootChange, firstChildChange]) for (const events of firstPayloads) { @@ -123,6 +152,7 @@ describe('FileSystemManager', function () { const subFile0 = path.join(subDir0, 'subfile1.txt') const subDir1 = path.join(parentDir, 'subdir1') const subFile1 = path.join(subDir1, 'subfile1.txt') + await Promise.all([ fs.writeFile(rootFile, 'rootfile\n', {encoding: 'utf8'}), fs.mkdir(subDir0).then( @@ -135,16 +165,23 @@ describe('FileSystemManager', function () { // Begin the child watchers const subWatcher0 = manager.getWatcher(subDir0) + const subWatcherChanges0 = waitForChanges(subWatcher0, subFile0) + const subWatcher1 = manager.getWatcher(subDir1) + const subWatcherChanges1 = waitForChanges(subWatcher1, subFile1) await Promise.all( - [subWatcher0, subWatcher1].map(watcher => waitForEvent(cb => watcher.onDidStart(cb))) + [subWatcher0, subWatcher1].map(watcher => { + return watcher.getStartPromise() + }) ) expect(subWatcher0.native).not.toBe(subWatcher1.native) // Create the parent watcher const parentWatcher = manager.getWatcher(parentDir) - await waitForEvent(cb => parentWatcher.onDidStart(cb)) + const parentWatcherChanges = waitForChanges(parentWatcher, rootFile, subFile0, subFile1) + + await parentWatcher.getStartPromise() expect(subWatcher0.native).toBe(parentWatcher.native) expect(subWatcher1.native).toBe(parentWatcher.native) @@ -157,9 +194,9 @@ describe('FileSystemManager', function () { ]) await Promise.all([ - waitForChanges(subWatcher0, subFile0), - waitForChanges(subWatcher1, subFile1), - waitForChanges(parentWatcher, rootFile, subFile0, subFile1) + subWatcherChanges0, + subWatcherChanges1, + parentWatcherChanges ]) }) From 9c8ed35b26c3ccc78fadd0ede991d4d16e0c5657 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Jun 2017 08:36:29 -0400 Subject: [PATCH 171/314] Provide native watcher creation function to the NativeWatcherRegistry constructor --- spec/native-watcher-registry-spec.js | 153 +++++++++++++++++++-------- src/native-watcher-registry.js | 18 ++-- 2 files changed, 119 insertions(+), 52 deletions(-) diff --git a/spec/native-watcher-registry-spec.js b/spec/native-watcher-registry-spec.js index 4830a421f..d76d91547 100644 --- a/spec/native-watcher-registry-spec.js +++ b/spec/native-watcher-registry-spec.js @@ -1,15 +1,22 @@ /** @babel */ +import {it, beforeEach} from './async-spec-helpers' + import path from 'path' import {Emitter} from 'event-kit' import NativeWatcherRegistry from '../src/native-watcher-registry' class MockWatcher { - constructor () { + constructor (normalizedPath) { + this.normalizedPath = normalizedPath this.native = null } + getNormalizedPathPromise () { + return Promise.resolve(this.normalizedPath) + } + attachToNative (native) { this.native = native this.native.attached.push(this) @@ -49,85 +56,143 @@ class MockNative { } describe('NativeWatcherRegistry', function () { - let registry, watcher + let createNative, registry beforeEach(function () { - registry = new NativeWatcherRegistry() - watcher = new MockWatcher() + registry = new NativeWatcherRegistry(normalizedPath => createNative(normalizedPath)) }) - it('attaches a Watcher to a newly created NativeWatcher for a new directory', function () { + it('attaches a Watcher to a newly created NativeWatcher for a new directory', async function () { + const watcher = new MockWatcher(path.join('some', 'path')) const NATIVE = new MockNative('created') - registry.attach('/some/path', watcher, () => NATIVE) + createNative = () => NATIVE + + await registry.attach(watcher) expect(watcher.native).toBe(NATIVE) }) - it('reuses an existing NativeWatcher on the same directory', function () { + it('reuses an existing NativeWatcher on the same directory', async function () { const EXISTING = new MockNative('existing') - registry.attach('/existing/path', new MockWatcher(), () => EXISTING) + const existingPath = path.join('existing', 'path') + let firstTime = true + createNative = () => { + if (firstTime) { + firstTime = false + return EXISTING + } - registry.attach('/existing/path', watcher, () => new MockNative('no')) + return new MockNative('nope') + } + await registry.attach(new MockWatcher(existingPath)) + + const watcher = new MockWatcher(existingPath) + await registry.attach(watcher) expect(watcher.native).toBe(EXISTING) }) - it('attaches to an existing NativeWatcher on a parent directory', function () { + it('attaches to an existing NativeWatcher on a parent directory', async function () { const EXISTING = new MockNative('existing') - registry.attach('/existing/path', new MockWatcher(), () => EXISTING) + const parentDir = path.join('existing', 'path') + const subDir = path.join(parentDir, 'sub', 'directory') + let firstTime = true + createNative = () => { + if (firstTime) { + firstTime = false + return EXISTING + } - registry.attach('/existing/path/sub/directory/', watcher, () => new MockNative('no')) + return new MockNative('nope') + } + await registry.attach(new MockWatcher(parentDir)) + + const watcher = new MockWatcher(subDir) + await registry.attach(watcher) expect(watcher.native).toBe(EXISTING) }) - it('adopts Watchers from NativeWatchers on child directories', function () { - const EXISTING0 = new MockNative('existing0') - const watcher0 = new MockWatcher() - registry.attach('/existing/path/child/directory/zero', watcher0, () => EXISTING0) + it('adopts Watchers from NativeWatchers on child directories', async function () { + const parentDir = path.join('existing', 'path') + const childDir0 = path.join(parentDir, 'child', 'directory', 'zero') + const childDir1 = path.join(parentDir, 'child', 'directory', 'one') + const otherDir = path.join('another', 'path') - const EXISTING1 = new MockNative('existing1') - const watcher1 = new MockWatcher() - registry.attach('/existing/path/child/directory/one', watcher1, () => EXISTING1) + const CHILD0 = new MockNative('existing0') + const CHILD1 = new MockNative('existing1') + const OTHER = new MockNative('existing2') + const PARENT = new MockNative('parent') - const EXISTING2 = new MockNative('existing2') - const watcher2 = new MockWatcher() - registry.attach('/another/path', watcher2, () => EXISTING2) + createNative = dir => { + if (dir === childDir0) { + return CHILD0 + } else if (dir === childDir1) { + return CHILD1 + } else if (dir === otherDir) { + return OTHER + } else if (dir === parentDir) { + return PARENT + } else { + throw new Error(`Unexpected path: ${dir}`) + } + } - expect(watcher0.native).toBe(EXISTING0) - expect(watcher1.native).toBe(EXISTING1) - expect(watcher2.native).toBe(EXISTING2) + const watcher0 = new MockWatcher(childDir0) + await registry.attach(watcher0) + + const watcher1 = new MockWatcher(childDir1) + await registry.attach(watcher1) + + const watcher2 = new MockWatcher(otherDir) + await registry.attach(watcher2) + + expect(watcher0.native).toBe(CHILD0) + expect(watcher1.native).toBe(CHILD1) + expect(watcher2.native).toBe(OTHER) // Consolidate all three watchers beneath the same native watcher on the parent directory - const CREATED = new MockNative('created') - registry.attach('/existing/path/', watcher, () => CREATED) + const watcher = new MockWatcher(parentDir) + await registry.attach(watcher) - expect(watcher.native).toBe(CREATED) + expect(watcher.native).toBe(PARENT) - expect(watcher0.native).toBe(CREATED) - expect(EXISTING0.stopped).toBe(true) - expect(EXISTING0.disposed).toBe(true) + expect(watcher0.native).toBe(PARENT) + expect(CHILD0.stopped).toBe(true) + expect(CHILD0.disposed).toBe(true) - expect(watcher1.native).toBe(CREATED) - expect(EXISTING1.stopped).toBe(true) - expect(EXISTING1.disposed).toBe(true) + expect(watcher1.native).toBe(PARENT) + expect(CHILD1.stopped).toBe(true) + expect(CHILD1.disposed).toBe(true) - expect(watcher2.native).toBe(EXISTING2) - expect(EXISTING2.stopped).toBe(false) - expect(EXISTING2.disposed).toBe(false) + expect(watcher2.native).toBe(OTHER) + expect(OTHER.stopped).toBe(false) + expect(OTHER.disposed).toBe(false) }) describe('removing NativeWatchers', function () { - it('happens when they stop', function () { + it('happens when they stop', async function () { const STOPPED = new MockNative('stopped') - const stoppedWatcher = new MockWatcher() - const stoppedPath = ['watcher', 'that', 'will', 'be', 'stopped'] - registry.attach(path.join(...stoppedPath), stoppedWatcher, () => STOPPED) - const RUNNING = new MockNative('running') - const runningWatcher = new MockWatcher() + + const stoppedPath = ['watcher', 'that', 'will', 'be', 'stopped'] const runningPath = ['watcher', 'that', 'will', 'continue', 'to', 'exist'] - registry.attach(path.join(...runningPath), runningWatcher, () => RUNNING) + + createNative = dir => { + if (dir === path.join(...stoppedPath)) { + return STOPPED + } else if (dir === path.join(...runningPath)) { + return RUNNING + } else { + throw new Error(`Unexpected path: ${dir}`) + } + } + + const stoppedWatcher = new MockWatcher(path.join(...stoppedPath)) + await registry.attach(stoppedWatcher) + + const runningWatcher = new MockWatcher(path.join(...runningPath)) + await registry.attach(runningWatcher) STOPPED.stop() diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index adc86e6f7..169201310 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -221,8 +221,12 @@ class ChildrenResult { export default class NativeWatcherRegistry { // Private: Instantiate an empty registry. - constructor () { + // + // * `createNative` {Function} that will be called with a normalized filesystem path to create a new native + // filesystem watcher. + constructor (createNative) { this.tree = new RegistryNode() + this.createNative = createNative } // Private: Attach a watcher to a directory, assigning it a {NativeWatcher}. If a suitable {NativeWatcher} already @@ -234,15 +238,13 @@ export default class NativeWatcherRegistry { // be broadcast on each with the new parent watcher as an event payload to give child watchers a chance to attach to // the new watcher. // - // * `directory` a normalized path to be watched as a {String}. // * `watcher` an unattached {Watcher}. - // * `createNative` callback to be invoked if no existing {NativeWatcher} covers the {Watcher}. It should - // synchronously return a new {NativeWatcher} instance watching {directory}. - attach (directory, watcher, createNative) { - const pathSegments = directory.split(path.sep).filter(segment => segment.length > 0) + async attach (watcher) { + const normalizedDirectory = await watcher.getNormalizedPathPromise() + const pathSegments = normalizedDirectory.split(path.sep).filter(segment => segment.length > 0) const attachToNew = () => { - const native = createNative() + const native = this.createNative(normalizedDirectory) const leaf = new RegistryWatcherNode(native) this.tree = this.tree.insert(pathSegments, leaf) @@ -272,7 +274,7 @@ export default class NativeWatcherRegistry { for (let i = 0; i < children.length; i++) { const child = children[i] const childNative = child.getNativeWatcher() - childNative.reattachTo(newNative, directory) + childNative.reattachTo(newNative, normalizedDirectory) childNative.dispose() // Don't await this Promise. Subscribers can listen for `onDidStop` to be notified if they choose. From 80a9126fdba0a51ca5a54b172906d8aeb19d6c80 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Jun 2017 08:37:46 -0400 Subject: [PATCH 172/314] Start NativeWatchers lazily and stop them opportunistically --- src/filesystem-manager.js | 109 +++++++++++++++++++++++++++----------- 1 file changed, 77 insertions(+), 32 deletions(-) diff --git a/src/filesystem-manager.js b/src/filesystem-manager.js index 5f3a50bae..8da8bdab0 100644 --- a/src/filesystem-manager.js +++ b/src/filesystem-manager.js @@ -151,21 +151,60 @@ class NativeWatcher { } class Watcher { - constructor (watchedPath) { + constructor (watchedPath, nativeWatcherRegistry) { this.watchedPath = watchedPath + this.nativeWatcherRegistry = nativeWatcherRegistry + this.normalizedPath = null this.native = null + this.changeCallbacks = new Map() + + this.normalizedPathPromise = new Promise((resolve, reject) => { + fs.realpath(watchedPath, (err, real) => { + if (err) { + reject(err) + return + } + + this.normalizedPath = real + resolve(real) + }) + }) + + this.startPromise = new Promise(resolve => { + this.resolveStartPromise = resolve + }) this.emitter = new Emitter() this.subs = new CompositeDisposable() } - onDidStart (callback) { - return this.emitter.on('did-start', callback) + getNormalizedPathPromise () { + return this.normalizedPathPromise + } + + getStartPromise () { + return this.startPromise } onDidChange (callback) { - return this.emitter.on('did-change', callback) + if (this.native) { + const sub = this.native.onDidChange(events => this.onNativeEvents(events, callback)) + this.changeCallbacks.set(callback, sub) + + this.native.start() + } else { + // Attach and retry + this.nativeWatcherRegistry.attach(this).then(() => { + this.onDidChange(callback) + }) + } + + return new Disposable(() => { + const sub = this.changeCallbacks.get(callback) + this.changeCallbacks.delete(callback) + sub.dispose() + }) } attachToNative (native) { @@ -173,30 +212,45 @@ class Watcher { this.native = native if (native.isRunning()) { - this.emitter.emit('did-start') + this.resolveStartPromise() } else { - this.subs.add(native.onDidStart(payload => { - this.emitter.emit('did-start', payload) + this.subs.add(native.onDidStart(() => { + this.resolveStartPromise() })) + } + // Transfer any native event subscriptions to the new NativeWatcher. + for (const [callback, formerSub] of this.changeCallbacks) { + const newSub = native.onDidChange(events => this.onNativeEvents(events, callback)) + this.changeCallbacks.set(callback, newSub) + formerSub.dispose() + } + + if (this.changeCallbacks.size > 0) { native.start() } - this.subs.add(native.onDidChange(events => { - // TODO does event.oldPath resolve symlinks? - const filtered = events.filter(event => event.oldPath.startsWith(this.normalizedPath)) - - if (filtered.length > 0) { - this.emitter.emit('did-change', filtered) + this.subs.add(native.onShouldDetach(replacement => { + if (replacement !== native) { + this.attachToNative(replacement) } })) + } - this.subs.add(native.onShouldDetach( - this.attachToNative.bind(this) - )) + onNativeEvents (events, callback) { + // TODO does event.oldPath resolve symlinks? + const filtered = events.filter(event => event.oldPath.startsWith(this.normalizedPath)) + + if (filtered.length > 0) { + callback(filtered) + } } dispose () { + for (const sub of this.changeCallbacks.values()) { + sub.dispose() + } + this.emitter.dispose() this.subs.dispose() } @@ -204,20 +258,10 @@ class Watcher { export default class FileSystemManager { constructor () { - this.nativeWatchers = new NativeWatcherRegistry() this.liveWatchers = new Set() - } - getWatcher (rootPath) { - const watcher = new Watcher(rootPath) - - const init = async () => { - const normalizedPath = await new Promise((resolve, reject) => { - fs.realpath(rootPath, (err, real) => (err ? reject(err) : resolve(real))) - }) - watcher.normalizedPath = normalizedPath - - this.nativeWatchers.attach(normalizedPath, watcher, () => { + this.nativeWatchers = new NativeWatcherRegistry( + normalizedPath => { const nativeWatcher = new NativeWatcher(normalizedPath) this.liveWatchers.add(nativeWatcher) @@ -227,11 +271,12 @@ export default class FileSystemManager { }) return nativeWatcher - }) - } - init() + } + ) + } - return watcher + getWatcher (rootPath) { + return new Watcher(rootPath, this.nativeWatchers) } } From 7ec79a00fc76297dccc767967be178ae1e0e20b6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Jun 2017 08:39:36 -0400 Subject: [PATCH 173/314] Set running = false before the asynchronous stop operation --- src/filesystem-manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/filesystem-manager.js b/src/filesystem-manager.js index 8da8bdab0..3096e237a 100644 --- a/src/filesystem-manager.js +++ b/src/filesystem-manager.js @@ -111,9 +111,9 @@ class NativeWatcher { if (!this.running) { return } + this.running = false await this.watcher.stop() - this.running = false this.emitter.emit('did-stop') } From 6d17fc880d2d3f876078a35aee4e2b5f629e50bf Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Jun 2017 09:00:16 -0400 Subject: [PATCH 174/314] Opportunistic native watcher stopping --- spec/filesystem-manager-spec.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/spec/filesystem-manager-spec.js b/spec/filesystem-manager-spec.js index a37aee108..bece1d09e 100644 --- a/spec/filesystem-manager-spec.js +++ b/spec/filesystem-manager-spec.js @@ -83,7 +83,23 @@ describe('FileSystemManager', function () { expect(started).toBe(true) }) - it('automatically stops and removes the watcher when all onDidChange subscribers dispose') + it('automatically stops and removes the watcher when all onDidChange subscribers dispose', async function () { + const dir = await temp.mkdir('atom-fsmanager-') + const watcher = manager.getWatcher(dir) + + const sub0 = watcher.onDidChange(() => {}) + const sub1 = watcher.onDidChange(() => {}) + + await watcher.getStartPromise() + expect(watcher.native).not.toBe(null) + expect(watcher.native.isRunning()).toBe(true) + + sub0.dispose() + expect(watcher.native.isRunning()).toBe(true) + + sub1.dispose() + expect(watcher.native.isRunning()).toBe(false) + }) it('reuses an existing native watcher and resolves getStartPromise immediately if attached to a running watcher', async function () { const rootDir = await temp.mkdir('atom-fsmanager-') From b34a9d69720e940f4e8be4435a341abdec0b0118 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Jun 2017 12:37:26 -0400 Subject: [PATCH 175/314] Only resolve the waitForChanges promise once --- spec/filesystem-manager-spec.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/filesystem-manager-spec.js b/spec/filesystem-manager-spec.js index bece1d09e..29e0ca435 100644 --- a/spec/filesystem-manager-spec.js +++ b/spec/filesystem-manager-spec.js @@ -29,6 +29,7 @@ describe('FileSystemManager', function () { function waitForChanges (watcher, ...fileNames) { const waiting = new Set(fileNames) + let fired = false const relevantEvents = [] return new Promise(resolve => { @@ -39,7 +40,8 @@ describe('FileSystemManager', function () { } } - if (waiting.size === 0) { + if (!fired && waiting.size === 0) { + fired = true resolve(relevantEvents) sub.dispose() } From f75aa1ae030a795c5507c293bbb98274172c07d0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Jun 2017 12:44:07 -0400 Subject: [PATCH 176/314] Use onWillStop() instead of onDidStop() This will prevent new Watchers from attaching to NativeWatchers that are in the process of stopping. --- spec/filesystem-manager-spec.js | 92 ++++++++++++++++++---------- spec/native-watcher-registry-spec.js | 6 +- src/filesystem-manager.js | 62 ++++++++++++++----- src/native-watcher-registry.js | 4 +- 4 files changed, 110 insertions(+), 54 deletions(-) diff --git a/spec/filesystem-manager-spec.js b/spec/filesystem-manager-spec.js index 29e0ca435..380419f7f 100644 --- a/spec/filesystem-manager-spec.js +++ b/spec/filesystem-manager-spec.js @@ -93,14 +93,17 @@ describe('FileSystemManager', function () { const sub1 = watcher.onDidChange(() => {}) await watcher.getStartPromise() - expect(watcher.native).not.toBe(null) - expect(watcher.native.isRunning()).toBe(true) + const native = watcher.native + expect(native).not.toBe(null) + expect(native.isRunning()).toBe(true) sub0.dispose() - expect(watcher.native.isRunning()).toBe(true) + expect(watcher.native).toBe(native) + expect(native.isRunning()).toBe(true) sub1.dispose() - expect(watcher.native.isRunning()).toBe(false) + expect(watcher.native).toBeNull() + expect(native.isRunning()).toBe(false) }) it('reuses an existing native watcher and resolves getStartPromise immediately if attached to a running watcher', async function () { @@ -117,48 +120,73 @@ describe('FileSystemManager', function () { expect(watcher0.native).toBe(watcher1.native) }) - it('reuses an existing native watcher on a parent directory and filters events', async function () { - const rootDir = await temp.mkdir('atom-fsmanager-').then(fs.realpath) + it("reuses existing native watchers even while they're still starting", async function () { + const rootDir = await temp.mkdir('atom-fsmanager-') + const watcher0 = manager.getWatcher(rootDir) + watcher0.onDidChange(() => {}) + await watcher0.getAttachedPromise() + expect(watcher0.native.isRunning()).toBe(false) + + const watcher1 = manager.getWatcher(rootDir) + watcher1.onDidChange(() => {}) + await watcher1.getAttachedPromise() + + expect(watcher0.native).toBe(watcher1.native) + + await Promise.all([watcher0.getStartPromise(), watcher1.getStartPromise()]) + }) + + it("doesn't attach new watchers to a native watcher that's stopping", async function () { + const rootDir = await temp.mkdir('atom-fsmanager-') + + const watcher0 = manager.getWatcher(rootDir) + const sub = watcher0.onDidChange(() => {}) + await watcher0.getStartPromise() + const native0 = watcher0.native + + sub.dispose() + + const watcher1 = manager.getWatcher(rootDir) + watcher1.onDidChange(() => {}) + + expect(watcher1.native).not.toBe(native0) + }) + + it('reuses an existing native watcher on a parent directory and filters events', async function () { + const rootDir = await temp.mkdir('atom-fsmanager-0-').then(fs.realpath) const rootFile = path.join(rootDir, 'rootfile.txt') const subDir = path.join(rootDir, 'subdir') const subFile = path.join(subDir, 'subfile.txt') - await Promise.all([ - fs.mkdir(subDir).then( - fs.writeFile(subFile, 'subfile\n', {encoding: 'utf8'}) - ), - fs.writeFile(rootFile, 'rootfile\n', {encoding: 'utf8'}) - ]) + await fs.mkdir(subDir) + + // Keep the watchers alive with an undisposed subscription const rootWatcher = manager.getWatcher(rootDir) + rootWatcher.onDidChange(() => {}) const childWatcher = manager.getWatcher(subDir) - - expect(rootWatcher.native).toBe(childWatcher.native) - - const firstRootChange = waitForChanges(rootWatcher, subFile) - const firstChildChange = waitForChanges(childWatcher, subFile) + childWatcher.onDidChange(() => {}) await Promise.all([ rootWatcher.getStartPromise(), childWatcher.getStartPromise() ]) - await fs.appendFile(subFile, 'changes\n', {encoding: 'utf8'}) + expect(rootWatcher.native).toBe(childWatcher.native) + expect(rootWatcher.native.isRunning()).toBe(true) - const firstPayloads = await Promise.all([firstRootChange, firstChildChange]) + const firstChanges = Promise.all([ + waitForChanges(rootWatcher, subFile), + waitForChanges(childWatcher, subFile) + ]) - for (const events of firstPayloads) { - expect(events.length).toBe(1) - expect(events[0].oldPath).toBe(subFile) - } + await fs.writeFile(subFile, 'subfile\n', {encoding: 'utf8'}) + await firstChanges const nextRootEvent = waitForChanges(rootWatcher, rootFile) - await fs.appendFile(rootFile, 'changes\n', {encoding: 'utf8'}) + await fs.writeFile(rootFile, 'rootfile\n', {encoding: 'utf8'}) - const nextPayload = await nextRootEvent - - expect(nextPayload.length).toBe(1) - expect(nextPayload[0].oldPath).toBe(rootFile) + await nextRootEvent }) it('adopts existing child watchers and filters events appropriately to them', async function () { @@ -181,17 +209,17 @@ describe('FileSystemManager', function () { ) ]) - // Begin the child watchers + // Begin the child watchers and keep them alive const subWatcher0 = manager.getWatcher(subDir0) + subWatcher0.onDidChange(() => {}) const subWatcherChanges0 = waitForChanges(subWatcher0, subFile0) const subWatcher1 = manager.getWatcher(subDir1) + subWatcher1.onDidChange(() => {}) const subWatcherChanges1 = waitForChanges(subWatcher1, subFile1) await Promise.all( - [subWatcher0, subWatcher1].map(watcher => { - return watcher.getStartPromise() - }) + [subWatcher0, subWatcher1].map(watcher => watcher.getStartPromise()) ) expect(subWatcher0.native).not.toBe(subWatcher1.native) diff --git a/spec/native-watcher-registry-spec.js b/spec/native-watcher-registry-spec.js index d76d91547..206aefc76 100644 --- a/spec/native-watcher-registry-spec.js +++ b/spec/native-watcher-registry-spec.js @@ -41,8 +41,8 @@ class MockNative { this.attached = [] } - onDidStop (callback) { - return this.emitter.on('did-stop', callback) + onWillStop (callback) { + return this.emitter.on('will-stop', callback) } dispose () { @@ -51,7 +51,7 @@ class MockNative { stop () { this.stopped = true - this.emitter.emit('did-stop') + this.emitter.emit('will-stop') } } diff --git a/src/filesystem-manager.js b/src/filesystem-manager.js index 3096e237a..277c7cbfe 100644 --- a/src/filesystem-manager.js +++ b/src/filesystem-manager.js @@ -16,6 +16,14 @@ const ACTION_MAP = new Map([ [nsfw.actions.RENAMED, 'renamed'] ]) +// Private: Possible states of a {NativeWatcher}. +export const WATCHER_STATE = { + STOPPED: Symbol('stopped'), + STARTING: Symbol('starting'), + RUNNING: Symbol('running'), + STOPPING: Symbol('stopping') +} + // Private: Interface with and normalize events from a native OS filesystem watcher. class NativeWatcher { @@ -27,16 +35,17 @@ class NativeWatcher { this.emitter = new Emitter() this.watcher = null - this.running = false + this.state = WATCHER_STATE.STOPPED } // Private: Begin watching for filesystem events. // // Has no effect if the watcher has already been started. async start () { - if (this.running) { + if (this.state !== WATCHER_STATE.STOPPED) { return } + this.state = WATCHER_STATE.STARTING this.watcher = await nsfw( this.normalizedPath, @@ -49,13 +58,14 @@ class NativeWatcher { await this.watcher.start() - this.running = true + + this.state = WATCHER_STATE.RUNNING this.emitter.emit('did-start') } - // Private: Return true if the underlying watcher has been started. + // Private: Return true if the underlying watcher is actively listening for filesystem events. isRunning () { - return this.running + return this.state === WATCHER_STATE.RUNNING } // Private: Register a callback to be invoked when the filesystem watcher has been initialized. @@ -71,9 +81,7 @@ class NativeWatcher { // // Returns: A {Disposable} to revoke the subscription. onDidChange (callback) { - if (!this.isRunning()) { - this.start() - } + this.start() const sub = this.emitter.on('did-change', callback) return new Disposable(() => { @@ -91,7 +99,14 @@ class NativeWatcher { return this.emitter.on('should-detach', callback) } - // Private: Register a callback to be invoked when the filesystem watcher has been initialized. + // Private: Register a callback to be invoked when a {NativeWatcher} is about to be stopped. + // + // Returns: A {Disposable} to revoke the subscription. + onWillStop (callback) { + return this.emitter.on('will-stop', callback) + } + + // Private: Register a callback to be invoked when the filesystem watcher has been stopped. // // Returns: A {Disposable} to revoke the subscription. onDidStop (callback) { @@ -108,12 +123,15 @@ class NativeWatcher { // // Has no effect if the watcher is not running. async stop () { - if (!this.running) { + if (this.state !== WATCHER_STATE.RUNNING) { return } - this.running = false + this.state = WATCHER_STATE.STOPPING + this.emitter.emit('will-stop') await this.watcher.stop() + this.state = WATCHER_STATE.STOPPED + this.emitter.emit('did-stop') } @@ -171,6 +189,9 @@ class Watcher { }) }) + this.attachedPromise = new Promise(resolve => { + this.resolveAttachedPromise = resolve + }) this.startPromise = new Promise(resolve => { this.resolveStartPromise = resolve }) @@ -183,6 +204,10 @@ class Watcher { return this.normalizedPathPromise } + getAttachedPromise () { + return this.attachedPromise + } + getStartPromise () { return this.startPromise } @@ -194,7 +219,7 @@ class Watcher { this.native.start() } else { - // Attach and retry + // Attach to a new native listener and retry this.nativeWatcherRegistry.attach(this).then(() => { this.onDidChange(callback) }) @@ -226,15 +251,18 @@ class Watcher { formerSub.dispose() } - if (this.changeCallbacks.size > 0) { - native.start() - } - this.subs.add(native.onShouldDetach(replacement => { if (replacement !== native) { this.attachToNative(replacement) } })) + + this.subs.add(native.onWillStop(() => { + this.subs.dispose() + this.native = null + })) + + this.resolveAttachedPromise() } onNativeEvents (events, callback) { @@ -265,7 +293,7 @@ export default class FileSystemManager { const nativeWatcher = new NativeWatcher(normalizedPath) this.liveWatchers.add(nativeWatcher) - const sub = nativeWatcher.onDidStop(() => { + const sub = nativeWatcher.onWillStop(() => { this.liveWatchers.delete(nativeWatcher) sub.dispose() }) diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index 169201310..95a5a6483 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -248,8 +248,8 @@ export default class NativeWatcherRegistry { const leaf = new RegistryWatcherNode(native) this.tree = this.tree.insert(pathSegments, leaf) - const sub = native.onDidStop(() => { - this.tree = this.tree.remove(pathSegments) + const sub = native.onWillStop(() => { + this.tree = this.tree.remove(pathSegments) || new RegistryNode() sub.dispose() }) From c2810b626c7407caee775f765286357ea227f12d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Jun 2017 13:39:47 -0400 Subject: [PATCH 177/314] Propagate errors to subscribers with an onDidError callback --- src/filesystem-manager.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/filesystem-manager.js b/src/filesystem-manager.js index 277c7cbfe..47ec98af4 100644 --- a/src/filesystem-manager.js +++ b/src/filesystem-manager.js @@ -113,6 +113,13 @@ class NativeWatcher { return this.emitter.on('did-stop', callback) } + // Private: Register a callback to be invoked with any errors reported from the watcher. + // + // Returns: A {Disposable} to revoke the subscription. + onDidError (callback) { + return this.emitter.on('did-error', callback) + } + // Private: Broadcast an `onShouldDetach` event to prompt any {Watcher} instances bound here to attach to a new // {NativeWatcher} instead. reattachTo (other) { @@ -160,11 +167,7 @@ class NativeWatcher { // // * `err` The native filesystem error. onError (err) { - if (!this.isRunning()) { - return - } - - console.error(err) + this.emitter.emit('did-error', err) } } @@ -232,6 +235,10 @@ class Watcher { }) } + onDidError (callback) { + return this.emitter.on('did-error', callback) + } + attachToNative (native) { this.subs.dispose() this.native = native @@ -251,6 +258,10 @@ class Watcher { formerSub.dispose() } + this.subs.add(native.onDidError(err => { + this.emitter.emit('did-error', err) + })) + this.subs.add(native.onShouldDetach(replacement => { if (replacement !== native) { this.attachToNative(replacement) From 60e6da9097156c43589002d42cb3927f1b2067ec Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Jun 2017 14:04:29 -0400 Subject: [PATCH 178/314] Weird-ass concurrent mkdir() error --- spec/filesystem-manager-spec.js | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/spec/filesystem-manager-spec.js b/spec/filesystem-manager-spec.js index 380419f7f..fda6c32f8 100644 --- a/spec/filesystem-manager-spec.js +++ b/spec/filesystem-manager-spec.js @@ -11,7 +11,7 @@ import FileSystemManager, {stopAllWatchers} from '../src/filesystem-manager' tempCb.track() const fs = promisifySome(fsCb, ['writeFile', 'mkdir', 'symlink', 'appendFile', 'realpath']) -const temp = promisifySome(tempCb, ['mkdir', 'cleanup']) +const temp = promisifySome(tempCb, ['mkdir']) describe('FileSystemManager', function () { let subs, manager @@ -51,7 +51,7 @@ describe('FileSystemManager', function () { describe('getWatcher()', function () { it('resolves getStartPromise() when the watcher begins listening', async function () { - const rootDir = await temp.mkdir('atom-fsmanager-') + const rootDir = await temp.mkdir('atom-fsmanager-test-') const watcher = manager.getWatcher(rootDir) watcher.onDidChange(() => {}) @@ -60,7 +60,7 @@ describe('FileSystemManager', function () { }) it('does not start actually watching until an onDidChange subscriber is registered', async function () { - const rootDir = await temp.mkdir('atom-fsmanager-') + const rootDir = await temp.mkdir('atom-fsmanager-test-') const watcher = manager.getWatcher(rootDir) let started = false @@ -86,7 +86,7 @@ describe('FileSystemManager', function () { }) it('automatically stops and removes the watcher when all onDidChange subscribers dispose', async function () { - const dir = await temp.mkdir('atom-fsmanager-') + const dir = await temp.mkdir('atom-fsmanager-test-') const watcher = manager.getWatcher(dir) const sub0 = watcher.onDidChange(() => {}) @@ -107,7 +107,7 @@ describe('FileSystemManager', function () { }) it('reuses an existing native watcher and resolves getStartPromise immediately if attached to a running watcher', async function () { - const rootDir = await temp.mkdir('atom-fsmanager-') + const rootDir = await temp.mkdir('atom-fsmanager-test-') const watcher0 = manager.getWatcher(rootDir) watcher0.onDidChange(() => {}) @@ -121,7 +121,7 @@ describe('FileSystemManager', function () { }) it("reuses existing native watchers even while they're still starting", async function () { - const rootDir = await temp.mkdir('atom-fsmanager-') + const rootDir = await temp.mkdir('atom-fsmanager-test-') const watcher0 = manager.getWatcher(rootDir) watcher0.onDidChange(() => {}) @@ -138,7 +138,7 @@ describe('FileSystemManager', function () { }) it("doesn't attach new watchers to a native watcher that's stopping", async function () { - const rootDir = await temp.mkdir('atom-fsmanager-') + const rootDir = await temp.mkdir('atom-fsmanager-test-') const watcher0 = manager.getWatcher(rootDir) const sub = watcher0.onDidChange(() => {}) @@ -154,7 +154,7 @@ describe('FileSystemManager', function () { }) it('reuses an existing native watcher on a parent directory and filters events', async function () { - const rootDir = await temp.mkdir('atom-fsmanager-0-').then(fs.realpath) + const rootDir = await temp.mkdir('atom-fsmanager-test-').then(fs.realpath) const rootFile = path.join(rootDir, 'rootfile.txt') const subDir = path.join(rootDir, 'subdir') const subFile = path.join(subDir, 'subfile.txt') @@ -190,23 +190,21 @@ describe('FileSystemManager', function () { }) it('adopts existing child watchers and filters events appropriately to them', async function () { - const parentDir = await temp.mkdir('atom-fsmanager-').then(fs.realpath) + const parentDir = await temp.mkdir('atom-fsmanager-test-').then(fs.realpath) // Create the directory tree const rootFile = path.join(parentDir, 'rootfile.txt') const subDir0 = path.join(parentDir, 'subdir0') - const subFile0 = path.join(subDir0, 'subfile1.txt') + const subFile0 = path.join(subDir0, 'subfile0.txt') const subDir1 = path.join(parentDir, 'subdir1') const subFile1 = path.join(subDir1, 'subfile1.txt') + await fs.mkdir(subDir0) + await fs.mkdir(subDir1) await Promise.all([ fs.writeFile(rootFile, 'rootfile\n', {encoding: 'utf8'}), - fs.mkdir(subDir0).then( - fs.writeFile(subFile0, 'subfile 0\n', {encoding: 'utf8'}) - ), - fs.mkdir(subDir1).then( - fs.writeFile(subFile1, 'subfile 1\n', {encoding: 'utf8'}) - ) + fs.writeFile(subFile0, 'subfile 0\n', {encoding: 'utf8'}), + fs.writeFile(subFile1, 'subfile 1\n', {encoding: 'utf8'}) ]) // Begin the child watchers and keep them alive From a5f217fd516d530dfcb2e3c0cb7ca8e7686855b9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 23 Jun 2017 09:33:47 -0400 Subject: [PATCH 179/314] WIP work on rewatching child directories --- spec/native-watcher-registry-spec.js | 62 +++++++++++++++++++++++++++- src/native-watcher-registry.js | 35 ++++++++++------ 2 files changed, 82 insertions(+), 15 deletions(-) diff --git a/spec/native-watcher-registry-spec.js b/spec/native-watcher-registry-spec.js index 206aefc76..107e0efd3 100644 --- a/spec/native-watcher-registry-spec.js +++ b/spec/native-watcher-registry-spec.js @@ -212,8 +212,66 @@ describe('NativeWatcherRegistry', function () { expect(stoppedNode).toBe(true) }) - it("keeps a parent watcher that's still running") + it('reassigns new child watchers when a parent watcher is stopped', async function () { + const CHILD0 = new MockNative('child0') + const CHILD1 = new MockNative('child1') + const PARENT = new MockNative('parent') - it('reassigns new child watchers when a parent watcher is stopped') + const parentDir = path.join('parent') + const childDir0 = path.join(parentDir, 'child0') + const childDir1 = path.join(parentDir, 'child1') + + createNative = dir => { + if (dir === parentDir) { + return PARENT + } else if (dir === childDir0) { + return CHILD0 + } else if (dir === childDir1) { + return CHILD1 + } else { + throw new Error(`Unexpected directory ${dir}`) + } + } + + const parentWatcher = new MockWatcher(parentDir) + const childWatcher0 = new MockWatcher(childDir0) + const childWatcher1 = new MockWatcher(childDir1) + + await registry.attach(parentWatcher) + await Promise.all([ + registry.attach(childWatcher0), + registry.attach(childWatcher1) + ]) + + // All three watchers should share the parent watcher's native watcher. + expect(parentWatcher.native).toBe(PARENT) + expect(childWatcher0.native).toBe(PARENT) + expect(childWatcher1.native).toBe(PARENT) + + // Stopping the parent should detach and recreate the child watchers. + // (Here, they'll be the same watcher instances used before, because of the fake createNative implementation.) + PARENT.stop() + + expect(childWatcher0.native).toBe(CHILD0) + expect(childWatcher1.native).toBe(CHILD1) + + expect(registry.tree.lookup(['parent']).when({ + parent: () => false, + missing: () => false, + children: () => true + })).toBe(true) + + expect(registry.tree.lookup(['parent', 'child0']).when({ + parent: () => true, + missing: () => false, + children: () => false + })).toBe(true) + + expect(registry.tree.lookup(['parent', 'child1']).when({ + parent: () => true, + missing: () => false, + children: () => false + })).toBe(true) + }) }) }) diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index 95a5a6483..3d83bbf82 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -21,7 +21,7 @@ class RegistryNode { // exist. lookup (pathSegments) { if (pathSegments.length === 0) { - return new ChildrenResult(this.leaves()) + return new ChildrenResult(this.leaves([])) } const child = this.children[pathSegments[0]] @@ -85,13 +85,17 @@ class RegistryNode { return Object.keys(this.children).length === 0 ? null : this } - // Private: Discover all {RegistryWatcherNode} instances beneath this tree node. + // Private: Discover all {RegistryWatcherNode} instances beneath this tree node and the child paths + // that they are watching. // - // Returns: A possibly empty {Array} of {RegistryWatcherNode} instances that are the descendants of this node. - leaves () { + // * `prefix` {Array} of intermediate path segments to prepend to the resulting child paths. + // + // Returns: A possibly empty {Array} of `{node, path}` objects describing {RegistryWatcherNode} + // instances beneath this node. + leaves (prefix) { const results = [] for (const p of Object.keys(this.children)) { - results.push(...this.children[p].leaves()) + results.push(...this.children[p].leaves(prefix + [p])) } return results } @@ -104,8 +108,11 @@ class RegistryWatcherNode { // Private: Allocate a new node to track a {NativeWatcher}. // // * `nativeWatcher` An existing {NativeWatcher} instance. - constructor (nativeWatcher) { + // * `childPaths` {Array} of child directories that are currently the responsibility of this + // {NativeWatcher}, if any + constructor (nativeWatcher, childPaths) { this.nativeWatcher = nativeWatcher + this.childPaths = new Set(childPaths) } // Private: Accessor for the {NativeWatcher}. @@ -133,9 +140,11 @@ class RegistryWatcherNode { // Private: Discover this {RegistryWatcherNode} instance. // - // Returns: An {Array} containing this node. - leaves () { - return [this] + // * `prefix` {Array} of intermediate path segments to prepend to the resulting child paths. + // + // Returns: An {Array} containing a `{node, path}` object describing this node. + leaves (prefix) { + return [{node: this, path: prefix}] } } @@ -243,9 +252,9 @@ export default class NativeWatcherRegistry { const normalizedDirectory = await watcher.getNormalizedPathPromise() const pathSegments = normalizedDirectory.split(path.sep).filter(segment => segment.length > 0) - const attachToNew = () => { + const attachToNew = (childPaths) => { const native = this.createNative(normalizedDirectory) - const leaf = new RegistryWatcherNode(native) + const leaf = new RegistryWatcherNode(native, childPaths) this.tree = this.tree.insert(pathSegments, leaf) const sub = native.onWillStop(() => { @@ -268,7 +277,7 @@ export default class NativeWatcherRegistry { watcher.attachToNative(native, subpath) }, children: children => { - const newNative = attachToNew() + const newNative = attachToNew([]) // One or more NativeWatchers exist on child directories of the requested path. for (let i = 0; i < children.length; i++) { @@ -281,7 +290,7 @@ export default class NativeWatcherRegistry { childNative.stop() } }, - missing: attachToNew + missing: () => attachToNew([]) }) } } From 0c5674a56c29754b8f4fa281cc29c79ea1237a9d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 23 Jun 2017 16:08:38 -0400 Subject: [PATCH 180/314] Split subtrees into child watchers on parent watcher removal --- spec/native-watcher-registry-spec.js | 28 +++-- src/native-watcher-registry.js | 177 +++++++++++++++++++-------- 2 files changed, 140 insertions(+), 65 deletions(-) diff --git a/spec/native-watcher-registry-spec.js b/spec/native-watcher-registry-spec.js index 107e0efd3..6efa6c219 100644 --- a/spec/native-watcher-registry-spec.js +++ b/spec/native-watcher-registry-spec.js @@ -17,9 +17,14 @@ class MockWatcher { return Promise.resolve(this.normalizedPath) } - attachToNative (native) { - this.native = native - this.native.attached.push(this) + attachToNative (native, nativePath) { + if (this.normalizedPath.startsWith(nativePath)) { + if (this.native) { + this.native.attached = this.native.attached.filter(each => each !== this) + } + this.native = native + this.native.attached.push(this) + } } } @@ -33,12 +38,10 @@ class MockNative { this.emitter = new Emitter() } - reattachTo (newNative) { + reattachTo (newNative, nativePath) { for (const watcher of this.attached) { - watcher.attachToNative(newNative) + watcher.attachToNative(newNative, nativePath) } - - this.attached = [] } onWillStop (callback) { @@ -196,7 +199,7 @@ describe('NativeWatcherRegistry', function () { STOPPED.stop() - const runningNode = registry.tree.lookup(runningPath).when({ + const runningNode = registry.tree.root.lookup(runningPath).when({ parent: node => node, missing: () => false, children: () => false @@ -204,7 +207,7 @@ describe('NativeWatcherRegistry', function () { expect(runningNode).toBeTruthy() expect(runningNode.getNativeWatcher()).toBe(RUNNING) - const stoppedNode = registry.tree.lookup(stoppedPath).when({ + const stoppedNode = registry.tree.root.lookup(stoppedPath).when({ parent: () => false, missing: () => true, children: () => false @@ -249,25 +252,24 @@ describe('NativeWatcherRegistry', function () { expect(childWatcher1.native).toBe(PARENT) // Stopping the parent should detach and recreate the child watchers. - // (Here, they'll be the same watcher instances used before, because of the fake createNative implementation.) PARENT.stop() expect(childWatcher0.native).toBe(CHILD0) expect(childWatcher1.native).toBe(CHILD1) - expect(registry.tree.lookup(['parent']).when({ + expect(registry.tree.root.lookup(['parent']).when({ parent: () => false, missing: () => false, children: () => true })).toBe(true) - expect(registry.tree.lookup(['parent', 'child0']).when({ + expect(registry.tree.root.lookup(['parent', 'child0']).when({ parent: () => true, missing: () => false, children: () => false })).toBe(true) - expect(registry.tree.lookup(['parent', 'child1']).when({ + expect(registry.tree.root.lookup(['parent', 'child1']).when({ parent: () => true, missing: () => false, children: () => false diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index 3d83bbf82..3ce127a85 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -2,6 +2,63 @@ import path from 'path' +class RegistryTree { + + constructor (basePathSegments, createNative) { + this.basePathSegments = basePathSegments + this.root = new RegistryNode() + this.createNative = createNative + } + + add (pathSegments, attachToNative) { + const absolutePathSegments = this.basePathSegments.concat(pathSegments) + const absolutePath = path.join(...absolutePathSegments) + + const attachToNew = (childPaths) => { + const native = this.createNative(absolutePath) + const leaf = new RegistryWatcherNode(native, absolutePathSegments, childPaths) + this.root = this.root.insert(pathSegments, leaf) + + const sub = native.onWillStop(() => { + sub.dispose() + this.root = this.root.remove(pathSegments, this.createNative) || new RegistryNode() + }) + + attachToNative(native, absolutePath) + return native + } + + this.root.lookup(pathSegments).when({ + parent: (parent, remaining) => { + // An existing NativeWatcher is watching the same directory or a parent directory of the requested path. + // Attach this Watcher to it as a filtering watcher and record it as a dependent child path. + const native = parent.getNativeWatcher() + parent.addChildPath(remaining) + attachToNative(native, path.join(...parent.getAbsolutePathSegments())) + }, + children: children => { + // One or more NativeWatchers exist on child directories of the requested path. Create a new native watcher + // on the parent directory, note the subscribed child paths, and cleanly stop the child native watchers. + const newNative = attachToNew(children.map(child => child.path)) + + for (let i = 0; i < children.length; i++) { + const childNode = children[i].node + const childNative = childNode.getNativeWatcher() + childNative.reattachTo(newNative, absolutePath) + childNative.dispose() + childNative.stop() + } + }, + missing: () => attachToNew([]) + }) + } + + getRoot () { + return this.root + } + +} + // Private: Non-leaf node in a tree used by the {NativeWatcherRegistry} to cover the allocated {Watcher} instances with // the most efficient set of {NativeWatcher} instances possible. Each {RegistryNode} maps to a directory in the // filesystem tree. @@ -57,10 +114,12 @@ class RegistryNode { // Private: Remove a {RegistryWatcherNode} by the exact watched directory. // // * `pathSegments` absolute pre-split filesystem path of the node to remove. + // * `createSplitNative` callback to be invoked with each child path segment {Array} if the {RegistryWatcherNode} + // is split into child watchers rather than removed outright. See {RegistryWatcherNode.remove}. // // Returns: The root of a new tree with the {RegistryWatcherNode} removed. Callers should replace their node // references with the returned value. - remove (pathSegments) { + remove (pathSegments, createSplitNative) { if (pathSegments.length === 0) { // Attempt to remove a path with child watchers. Do nothing. return this @@ -74,7 +133,7 @@ class RegistryNode { } // Recurse - const newChild = child.remove(pathSegments.slice(1)) + const newChild = child.remove(pathSegments.slice(1), createSplitNative) if (newChild === null) { delete this.children[pathKey] } else { @@ -95,7 +154,7 @@ class RegistryNode { leaves (prefix) { const results = [] for (const p of Object.keys(this.children)) { - results.push(...this.children[p].leaves(prefix + [p])) + results.push(...this.children[p].leaves(prefix.concat([p]))) } return results } @@ -108,11 +167,38 @@ class RegistryWatcherNode { // Private: Allocate a new node to track a {NativeWatcher}. // // * `nativeWatcher` An existing {NativeWatcher} instance. + // * `absolutePathSegments` The absolute path to this {NativeWatcher}'s directory as an {Array} of + // path segments. // * `childPaths` {Array} of child directories that are currently the responsibility of this - // {NativeWatcher}, if any - constructor (nativeWatcher, childPaths) { + // {NativeWatcher}, if any. Directories are represented as arrays of the path segments between this + // node's directory and the watched child path. + constructor (nativeWatcher, absolutePathSegments, childPaths) { this.nativeWatcher = nativeWatcher - this.childPaths = new Set(childPaths) + this.absolutePathSegments = absolutePathSegments + + // Store child paths as joined strings so they work as Set members. + this.childPaths = new Set() + for (let i = 0; i < childPaths.length; i++) { + this.childPaths.add(path.join(...childPaths[i])) + } + } + + // Private: Assume responsibility for a new child path. If this node is removed, it will instead + // split into a subtree with a new {RegistryWatcherNode} for each child path. + // + // * `childPathSegments` the {Array} of path segments between this node's directory and the watched + // child directory. + addChildPath (childPathSegments) { + this.childPaths.add(path.join(...childPathSegments)) + } + + // Private: Stop assuming responsbility for a previously assigned child path. If this node is + // removed, the named child path will no longer be allocated a {RegistryWatcherNode}. + // + // * `childPathSegments` the {Array} of path segments between this node's directory and the no longer + // watched child directory. + removeChildPath (childPathSegments) { + this.childPaths.delete(path.join(...childPathSegments)) } // Private: Accessor for the {NativeWatcher}. @@ -120,22 +206,47 @@ class RegistryWatcherNode { return this.nativeWatcher } + getAbsolutePathSegments () { + return this.absolutePathSegments + } + // Private: Identify how this watcher relates to a request to watch a directory tree. // // * `pathSegments` filesystem path of a new {Watcher} already split into an Array of directory names. - // + //g // Returns: A {ParentResult} referencing this node. lookup (pathSegments) { return new ParentResult(this, pathSegments) } - // Private: Remove this leaf node if the watcher's exact path matches. + // Private: Remove this leaf node if the watcher's exact path matches. If this node is covering additional + // {Watcher} instances on child paths, it will be split into a subtree. // // * `pathSegments` filesystem path of the node to remove. + // * `createSplitNative` callback invoked with each {Array} of absolute child path segments to create a native + // watcher on a subtree of this node. // - // Returns: {null} if the `pathSegments` are an exact match, {this} otherwise. - remove (pathSegments) { - return pathSegments.length === 0 ? null : this + // Returns: If `pathSegments` match this watcher's path exactly, returns `null` if this node has no `childPaths` + // or a new {RegistryNode} on a newly allocated subtree if it did. If `pathSegments` does not match the watcher's + // path, it's an attempt to remove a subnode that doesn't exist, so the remove call has no effect and returns + // `this` unaltered. + remove (pathSegments, createSplitNative) { + if (pathSegments.length !== 0) { + return this + } else if (this.childPaths.size > 0) { + let newSubTree = new RegistryTree(this.absolutePathSegments, createSplitNative) + + for (const childPath of this.childPaths) { + const childPathSegments = childPath.split(path.sep) + newSubTree.add(childPathSegments, (native, attachmentPath) => { + this.nativeWatcher.reattachTo(native, attachmentPath) + }) + } + + return newSubTree.getRoot() + } else { + return null + } } // Private: Discover this {RegistryWatcherNode} instance. @@ -234,8 +345,7 @@ export default class NativeWatcherRegistry { // * `createNative` {Function} that will be called with a normalized filesystem path to create a new native // filesystem watcher. constructor (createNative) { - this.tree = new RegistryNode() - this.createNative = createNative + this.tree = new RegistryTree([], createNative) } // Private: Attach a watcher to a directory, assigning it a {NativeWatcher}. If a suitable {NativeWatcher} already @@ -252,45 +362,8 @@ export default class NativeWatcherRegistry { const normalizedDirectory = await watcher.getNormalizedPathPromise() const pathSegments = normalizedDirectory.split(path.sep).filter(segment => segment.length > 0) - const attachToNew = (childPaths) => { - const native = this.createNative(normalizedDirectory) - const leaf = new RegistryWatcherNode(native, childPaths) - this.tree = this.tree.insert(pathSegments, leaf) - - const sub = native.onWillStop(() => { - this.tree = this.tree.remove(pathSegments) || new RegistryNode() - sub.dispose() - }) - - watcher.attachToNative(native, '') - - return native - } - - this.tree.lookup(pathSegments).when({ - parent: (parent, remaining) => { - // An existing NativeWatcher is watching a parent directory of the requested path. Attach this Watcher to - // it as a filtering watcher. - const native = parent.getNativeWatcher() - const subpath = remaining.length === 0 ? '' : path.join(...remaining) - - watcher.attachToNative(native, subpath) - }, - children: children => { - const newNative = attachToNew([]) - - // One or more NativeWatchers exist on child directories of the requested path. - for (let i = 0; i < children.length; i++) { - const child = children[i] - const childNative = child.getNativeWatcher() - childNative.reattachTo(newNative, normalizedDirectory) - childNative.dispose() - - // Don't await this Promise. Subscribers can listen for `onDidStop` to be notified if they choose. - childNative.stop() - } - }, - missing: () => attachToNew([]) + this.tree.add(pathSegments, (native, nativePath) => { + watcher.attachToNative(native, nativePath) }) } } From 2b79295d0b0db887d252a566899b30c224955f27 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 23 Jun 2017 16:09:34 -0400 Subject: [PATCH 181/314] (Untested) work to adapt to the registry API changes --- src/filesystem-manager.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/filesystem-manager.js b/src/filesystem-manager.js index 47ec98af4..28d1aed2b 100644 --- a/src/filesystem-manager.js +++ b/src/filesystem-manager.js @@ -122,8 +122,11 @@ class NativeWatcher { // Private: Broadcast an `onShouldDetach` event to prompt any {Watcher} instances bound here to attach to a new // {NativeWatcher} instead. - reattachTo (other) { - this.emitter.emit('should-detach', other) + // + // * `replacement` the new {NativeWatcher} instance that a live {Watcher} instance should reattach to instead. + // * `watchedPath` absolute path watched by the new {NativeWatcher}. + reattachTo (replacement, watchedPath) { + this.emitter.emit('should-detach', {replacement, watchedPath}) } // Private: Stop the native watcher and release any operating system resources associated with it. @@ -262,8 +265,8 @@ class Watcher { this.emitter.emit('did-error', err) })) - this.subs.add(native.onShouldDetach(replacement => { - if (replacement !== native) { + this.subs.add(native.onShouldDetach(({replacement, watchedPath}) => { + if (replacement !== native && this.normalizedPath.startsWith(watchedPath)) { this.attachToNative(replacement) } })) From 6cc3e4b6d2ec11ea5c356a294b033a8a8ec17b42 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 26 Jun 2017 09:00:22 -0400 Subject: [PATCH 182/314] Test case for consolidating child watchers during split --- spec/native-watcher-registry-spec.js | 59 ++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/spec/native-watcher-registry-spec.js b/spec/native-watcher-registry-spec.js index 6efa6c219..c3427d2c6 100644 --- a/spec/native-watcher-registry-spec.js +++ b/spec/native-watcher-registry-spec.js @@ -275,5 +275,64 @@ describe('NativeWatcherRegistry', function () { children: () => false })).toBe(true) }) + + it('consolidates children when splitting a parent watcher', async function () { + const CHILD0 = new MockNative('child0') + const PARENT = new MockNative('parent') + + const parentDir = path.join('parent') + const childDir0 = path.join(parentDir, 'child0') + const childDir1 = path.join(parentDir, 'child0', 'child1') + + createNative = dir => { + if (dir === parentDir) { + return PARENT + } else if (dir === childDir0) { + return CHILD0 + } else { + throw new Error(`Unexpected directory ${dir}`) + } + } + + const parentWatcher = new MockWatcher(parentDir) + const childWatcher0 = new MockWatcher(childDir0) + const childWatcher1 = new MockWatcher(childDir1) + + await registry.attach(parentWatcher) + await Promise.all([ + registry.attach(childWatcher0), + registry.attach(childWatcher1) + ]) + + // All three watchers should share the parent watcher's native watcher. + expect(parentWatcher.native).toBe(PARENT) + expect(childWatcher0.native).toBe(PARENT) + expect(childWatcher1.native).toBe(PARENT) + + // Stopping the parent should detach and create the child watchers. Both child watchers should + // share the same native watcher. + PARENT.stop() + + expect(childWatcher0.native).toBe(CHILD0) + expect(childWatcher1.native).toBe(CHILD0) + + expect(registry.tree.root.lookup(['parent']).when({ + parent: () => false, + missing: () => false, + children: () => true + })).toBe(true) + + expect(registry.tree.root.lookup(['parent', 'child0']).when({ + parent: () => true, + missing: () => false, + children: () => false + })).toBe(true) + + expect(registry.tree.root.lookup(['parent', 'child0', 'child1']).when({ + parent: () => true, + missing: () => false, + children: () => false + })).toBe(true) + }) }) }) From 0b17b3524457ba3a25c2ea645507dcd0881d1702 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 26 Jun 2017 09:00:35 -0400 Subject: [PATCH 183/314] :shirt: :burn: whitespace --- src/filesystem-manager.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/filesystem-manager.js b/src/filesystem-manager.js index 28d1aed2b..14273423c 100644 --- a/src/filesystem-manager.js +++ b/src/filesystem-manager.js @@ -58,7 +58,6 @@ class NativeWatcher { await this.watcher.start() - this.state = WATCHER_STATE.RUNNING this.emitter.emit('did-start') } From d4edc6b8949b7bdb844e2b566e58a72901af20ee Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 26 Jun 2017 09:01:04 -0400 Subject: [PATCH 184/314] Extra character for some reason? --- src/native-watcher-registry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index 3ce127a85..9b78f5505 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -213,7 +213,7 @@ class RegistryWatcherNode { // Private: Identify how this watcher relates to a request to watch a directory tree. // // * `pathSegments` filesystem path of a new {Watcher} already split into an Array of directory names. - //g + // // Returns: A {ParentResult} referencing this node. lookup (pathSegments) { return new ParentResult(this, pathSegments) From ba7275dc4fb6e21f44977bfe09c5432af868bd9e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 26 Jun 2017 09:16:35 -0400 Subject: [PATCH 185/314] Dump the tree structure to a string for debugging --- src/native-watcher-registry.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index 9b78f5505..c3ba01d39 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -57,6 +57,10 @@ class RegistryTree { return this.root } + print () { + return this.root.print() + } + } // Private: Non-leaf node in a tree used by the {NativeWatcherRegistry} to cover the allocated {Watcher} instances with @@ -158,6 +162,19 @@ class RegistryNode { } return results } + + print (indent = 0) { + let spaces = '' + for (let i = 0; i < indent; i++) { + spaces += ' ' + } + + let result = '' + for (const p of Object.keys(this.children)) { + result += `${spaces}${p}\n${this.children[p].print(indent + 2)}` + } + return result + } } // Private: Leaf node within a {NativeWatcherRegistry} tree. Represents a directory that is covered by a @@ -257,6 +274,20 @@ class RegistryWatcherNode { leaves (prefix) { return [{node: this, path: prefix}] } + + print (indent = 0) { + let result = '' + for (let i = 0; i < indent; i++) { + result += ' ' + } + result += '[watcher' + if (this.childPaths.size > 0) { + result += ` +${this.childPaths.size}` + } + result += ']\n' + + return result + } } // Private: A {RegisteryNode} traversal result that's returned when neither a directory, its children, nor its parents From 2ae70aac08db432721037e3e0aaf06a2deeb527a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 26 Jun 2017 09:51:45 -0400 Subject: [PATCH 186/314] Document RegistryTree. --- src/native-watcher-registry.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index c3ba01d39..6a88fe9f4 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -2,14 +2,35 @@ import path from 'path' +// Private: Map userland filesystem watcher subscriptions efficiently to deliver filesystem change notifications to +// each watcher with the most efficient coverage of native watchers. +// +// * If two watchers subscribe to the same directory, use a single native watcher for each. +// * Re-use a native watcher watching a parent directory for a watcher on a child directory. If the parent directory +// watcher is removed, it will be split into child watchers. +// * If any child directories already being watched, stop and replace them with a watcher on the parent directory. +// +// Uses a Trie whose structure mirrors the directory structure. class RegistryTree { + // Private: Construct a tree with no native watchers. + // + // * `basePathSegments` the position of this tree's root relative to the filesystem's root as an {Array} of directory + // names. + // * `createNative` {Function} used to construct new native watchers. It should accept an absolute path as an argument + // and return a new {NativeWatcher}. constructor (basePathSegments, createNative) { this.basePathSegments = basePathSegments this.root = new RegistryNode() this.createNative = createNative } + // Private: Identify the native watcher that should be used to produce events at a watched path, creating a new one + // if necessary. + // + // * `pathSegments` the path to watch represented as an {Array} of directory names relative to this {RegistryTree}'s + // root. + // * `attachToNative` {Function} invoked with the appropriate native watcher and the absolute path to its watch root. add (pathSegments, attachToNative) { const absolutePathSegments = this.basePathSegments.concat(pathSegments) const absolutePath = path.join(...absolutePathSegments) @@ -53,10 +74,12 @@ class RegistryTree { }) } + // Private: Access the root node of the tree. getRoot () { return this.root } + // Private: Return a {String} representation of this tree's structure for diagnostics and testing. print () { return this.root.print() } From 2d8f812f5681181533febd0176d0645bf2f1bb35 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 26 Jun 2017 09:56:13 -0400 Subject: [PATCH 187/314] More documentation touchups. --- src/native-watcher-registry.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index 6a88fe9f4..77da22669 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -86,9 +86,9 @@ class RegistryTree { } -// Private: Non-leaf node in a tree used by the {NativeWatcherRegistry} to cover the allocated {Watcher} instances with -// the most efficient set of {NativeWatcher} instances possible. Each {RegistryNode} maps to a directory in the -// filesystem tree. +// Private: Non-leaf node in a {RegistryTree} used by the {NativeWatcherRegistry} to cover the allocated {Watcher} +// instances with the most efficient set of {NativeWatcher} instances possible. Each {RegistryNode} maps to a directory +// in the filesystem tree. class RegistryNode { // Private: Construct a new, empty node representing a node with no watchers. @@ -138,7 +138,7 @@ class RegistryNode { return this } - // Private: Remove a {RegistryWatcherNode} by the exact watched directory. + // Private: Remove a {RegistryWatcherNode} by its exact watched directory. // // * `pathSegments` absolute pre-split filesystem path of the node to remove. // * `createSplitNative` callback to be invoked with each child path segment {Array} if the {RegistryWatcherNode} @@ -186,6 +186,7 @@ class RegistryNode { return results } + // Private: Return a {String} representation of this subtree for diagnostics and testing. print (indent = 0) { let spaces = '' for (let i = 0; i < indent; i++) { @@ -246,6 +247,7 @@ class RegistryWatcherNode { return this.nativeWatcher } + // Private: Return the absolute path watched by this {NativeWatcher} as an {Array} of directory names. getAbsolutePathSegments () { return this.absolutePathSegments } @@ -298,6 +300,8 @@ class RegistryWatcherNode { return [{node: this, path: prefix}] } + // Private: Return a {String} representation of this watcher for diagnostics and testing. Indicates the number of + // child paths that this node's {NativeWatcher} is responsible for. print (indent = 0) { let result = '' for (let i = 0; i < indent; i++) { @@ -316,6 +320,7 @@ class RegistryWatcherNode { // Private: A {RegisteryNode} traversal result that's returned when neither a directory, its children, nor its parents // are present in the tree. class MissingResult { + // Private: Instantiate a new {MissingResult}. // // * `lastParent` the final succesfully traversed {RegistryNode}. From 7aeca7fc8ca5f93c4ab870aa8d67dc7692c2bd95 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 31 Jul 2017 14:52:11 -0400 Subject: [PATCH 188/314] :fire: FileSystemManager --- src/atom-environment.coffee | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 7deaafbdb..b37acddd1 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -46,7 +46,6 @@ TextBuffer = require 'text-buffer' Gutter = require './gutter' TextEditorRegistry = require './text-editor-registry' AutoUpdateManager = require './auto-update-manager' -FileSystemManager = require('./filesystem-manager').default # Essential: Atom global for dealing with packages, themes, menus, and the window. # @@ -118,9 +117,6 @@ class AtomEnvironment extends Model # Private: An {AutoUpdateManager} instance autoUpdater: null - # Public: A {FileSystemManager} instance - filesystem: null - saveStateDebounceInterval: 1000 ### @@ -141,7 +137,6 @@ class AtomEnvironment extends Model @views = new ViewRegistry(this) TextEditor.setScheduler(@views) @notifications = new NotificationManager - @filesystem = new FileSystemManager @updateProcessEnv ?= updateProcessEnv # For testing @stateStore = new StateStore('AtomEnvironments', 1) From 7aab9925a89480960c300a7ada6dc3deaa82fb80 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 31 Jul 2017 14:54:10 -0400 Subject: [PATCH 189/314] Rename filesystem-manager to path-watcher --- ...{filesystem-manager.js => path-watcher.js} | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) rename src/{filesystem-manager.js => path-watcher.js} (92%) diff --git a/src/filesystem-manager.js b/src/path-watcher.js similarity index 92% rename from src/filesystem-manager.js rename to src/path-watcher.js index 14273423c..4a0e435ec 100644 --- a/src/filesystem-manager.js +++ b/src/path-watcher.js @@ -24,6 +24,22 @@ export const WATCHER_STATE = { STOPPING: Symbol('stopping') } +const LIVE = new Set() + +const REGISTRY = new NativeWatcherRegistry( + normalizedPath => { + const nativeWatcher = new NativeWatcher(normalizedPath) + + LIVE.add(nativeWatcher) + const sub = nativeWatcher.onWillStop(() => { + LIVE.delete(nativeWatcher) + sub.dispose() + }) + + return nativeWatcher + } +) + // Private: Interface with and normalize events from a native OS filesystem watcher. class NativeWatcher { @@ -173,8 +189,8 @@ class NativeWatcher { } } -class Watcher { - constructor (watchedPath, nativeWatcherRegistry) { +export default class PathWatcher { + constructor (watchedPath, options) { this.watchedPath = watchedPath this.nativeWatcherRegistry = nativeWatcherRegistry @@ -297,34 +313,10 @@ class Watcher { } } -export default class FileSystemManager { - constructor () { - this.liveWatchers = new Set() - - this.nativeWatchers = new NativeWatcherRegistry( - normalizedPath => { - const nativeWatcher = new NativeWatcher(normalizedPath) - - this.liveWatchers.add(nativeWatcher) - const sub = nativeWatcher.onWillStop(() => { - this.liveWatchers.delete(nativeWatcher) - sub.dispose() - }) - - return nativeWatcher - } - ) - } - - getWatcher (rootPath) { - return new Watcher(rootPath, this.nativeWatchers) - } -} - // Private: Return a Promise that resolves when all {NativeWatcher} instances associated with a FileSystemManager // have stopped listening. This is useful for `afterEach()` blocks in unit tests. -export function stopAllWatchers (manager) { +export function stopAllWatchers () { return Promise.all( - Array.from(manager.liveWatchers, watcher => watcher.stop()) + Array.from(LIVE, watcher => watcher.stop()) ) } From 99d6f911cfdf1d0849f004ff49ff9a1810b8ee70 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 31 Jul 2017 16:24:56 -0400 Subject: [PATCH 190/314] Rename the filesystem-manager-spec too --- ...m-manager-spec.js => path-watcher-spec.js} | 52 +++++++------------ 1 file changed, 19 insertions(+), 33 deletions(-) rename spec/{filesystem-manager-spec.js => path-watcher-spec.js} (83%) diff --git a/spec/filesystem-manager-spec.js b/spec/path-watcher-spec.js similarity index 83% rename from spec/filesystem-manager-spec.js rename to spec/path-watcher-spec.js index fda6c32f8..6d56c93de 100644 --- a/spec/filesystem-manager-spec.js +++ b/spec/path-watcher-spec.js @@ -6,25 +6,23 @@ import fsCb from 'fs-plus' import path from 'path' import {CompositeDisposable} from 'event-kit' -import FileSystemManager, {stopAllWatchers} from '../src/filesystem-manager' +import PathWatcher, {stopAllWatchers} from '../src/path-watcher' tempCb.track() const fs = promisifySome(fsCb, ['writeFile', 'mkdir', 'symlink', 'appendFile', 'realpath']) const temp = promisifySome(tempCb, ['mkdir']) -describe('FileSystemManager', function () { - let subs, manager +describe('PathWatcher', function () { + let subs beforeEach(function () { subs = new CompositeDisposable() - manager = new FileSystemManager() }) afterEach(async function () { subs.dispose() - - await stopAllWatchers(manager) + await stopAllWatchers() }) function waitForChanges (watcher, ...fileNames) { @@ -49,11 +47,11 @@ describe('FileSystemManager', function () { }) } - describe('getWatcher()', function () { + describe('new PatchWatcher()', function () { it('resolves getStartPromise() when the watcher begins listening', async function () { const rootDir = await temp.mkdir('atom-fsmanager-test-') - const watcher = manager.getWatcher(rootDir) + const watcher = new PathWatcher(rootDir) watcher.onDidChange(() => {}) await watcher.getStartPromise() @@ -61,7 +59,7 @@ describe('FileSystemManager', function () { it('does not start actually watching until an onDidChange subscriber is registered', async function () { const rootDir = await temp.mkdir('atom-fsmanager-test-') - const watcher = manager.getWatcher(rootDir) + const watcher = new PathWatcher(rootDir) let started = false const startPromise = watcher.getStartPromise().then(() => { @@ -87,7 +85,7 @@ describe('FileSystemManager', function () { it('automatically stops and removes the watcher when all onDidChange subscribers dispose', async function () { const dir = await temp.mkdir('atom-fsmanager-test-') - const watcher = manager.getWatcher(dir) + const watcher = new PathWatcher(dir) const sub0 = watcher.onDidChange(() => {}) const sub1 = watcher.onDidChange(() => {}) @@ -109,11 +107,11 @@ describe('FileSystemManager', function () { it('reuses an existing native watcher and resolves getStartPromise immediately if attached to a running watcher', async function () { const rootDir = await temp.mkdir('atom-fsmanager-test-') - const watcher0 = manager.getWatcher(rootDir) + const watcher0 = new PathWatcher(rootDir) watcher0.onDidChange(() => {}) await watcher0.getStartPromise() - const watcher1 = manager.getWatcher(rootDir) + const watcher1 = new PathWatcher(rootDir) watcher1.onDidChange(() => {}) await watcher1.getStartPromise() @@ -123,12 +121,12 @@ describe('FileSystemManager', function () { it("reuses existing native watchers even while they're still starting", async function () { const rootDir = await temp.mkdir('atom-fsmanager-test-') - const watcher0 = manager.getWatcher(rootDir) + const watcher0 = new PathWatcher(rootDir) watcher0.onDidChange(() => {}) await watcher0.getAttachedPromise() expect(watcher0.native.isRunning()).toBe(false) - const watcher1 = manager.getWatcher(rootDir) + const watcher1 = new PathWatcher(rootDir) watcher1.onDidChange(() => {}) await watcher1.getAttachedPromise() @@ -140,14 +138,14 @@ describe('FileSystemManager', function () { it("doesn't attach new watchers to a native watcher that's stopping", async function () { const rootDir = await temp.mkdir('atom-fsmanager-test-') - const watcher0 = manager.getWatcher(rootDir) + const watcher0 = new PathWatcher(rootDir) const sub = watcher0.onDidChange(() => {}) await watcher0.getStartPromise() const native0 = watcher0.native sub.dispose() - const watcher1 = manager.getWatcher(rootDir) + const watcher1 = new PathWatcher(rootDir) watcher1.onDidChange(() => {}) expect(watcher1.native).not.toBe(native0) @@ -162,9 +160,9 @@ describe('FileSystemManager', function () { await fs.mkdir(subDir) // Keep the watchers alive with an undisposed subscription - const rootWatcher = manager.getWatcher(rootDir) + const rootWatcher = new PathWatcher(rootDir) rootWatcher.onDidChange(() => {}) - const childWatcher = manager.getWatcher(subDir) + const childWatcher = new PathWatcher(subDir) childWatcher.onDidChange(() => {}) await Promise.all([ @@ -208,11 +206,11 @@ describe('FileSystemManager', function () { ]) // Begin the child watchers and keep them alive - const subWatcher0 = manager.getWatcher(subDir0) + const subWatcher0 = new PathWatcher(subDir0) subWatcher0.onDidChange(() => {}) const subWatcherChanges0 = waitForChanges(subWatcher0, subFile0) - const subWatcher1 = manager.getWatcher(subDir1) + const subWatcher1 = new PathWatcher(subDir1) subWatcher1.onDidChange(() => {}) const subWatcherChanges1 = waitForChanges(subWatcher1, subFile1) @@ -222,7 +220,7 @@ describe('FileSystemManager', function () { expect(subWatcher0.native).not.toBe(subWatcher1.native) // Create the parent watcher - const parentWatcher = manager.getWatcher(parentDir) + const parentWatcher = new PathWatcher(parentDir) const parentWatcherChanges = waitForChanges(parentWatcher, rootFile, subFile0, subFile1) await parentWatcher.getStartPromise() @@ -243,17 +241,5 @@ describe('FileSystemManager', function () { parentWatcherChanges ]) }) - - describe('event normalization', function () { - xit('normalizes "changed" events') - xit('normalizes "added" events') - xit('normalizes "deleted" events') - xit('normalizes "renamed" events') - }) - - describe('symlinks', function () { - xit('reports events with symlink paths') - xit('uses the same native watcher even for symlink paths') - }) }) }) From 4f0b52d2ab6faaa3ebec2bd88c960fd369d9a7cd Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 10:56:17 -0400 Subject: [PATCH 191/314] Move the global watcher registry to a lazily initialized manager --- src/path-watcher.js | 67 +++++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/src/path-watcher.js b/src/path-watcher.js index 4a0e435ec..92d961584 100644 --- a/src/path-watcher.js +++ b/src/path-watcher.js @@ -24,22 +24,6 @@ export const WATCHER_STATE = { STOPPING: Symbol('stopping') } -const LIVE = new Set() - -const REGISTRY = new NativeWatcherRegistry( - normalizedPath => { - const nativeWatcher = new NativeWatcher(normalizedPath) - - LIVE.add(nativeWatcher) - const sub = nativeWatcher.onWillStop(() => { - LIVE.delete(nativeWatcher) - sub.dispose() - }) - - return nativeWatcher - } -) - // Private: Interface with and normalize events from a native OS filesystem watcher. class NativeWatcher { @@ -189,8 +173,8 @@ class NativeWatcher { } } -export default class PathWatcher { - constructor (watchedPath, options) { +export class PathWatcher { + constructor (nativeWatcherRegistry, watchedPath, options) { this.watchedPath = watchedPath this.nativeWatcherRegistry = nativeWatcherRegistry @@ -313,10 +297,51 @@ export default class PathWatcher { } } +class PathWatcherManager { + static instance () { + if (!PathWatcherManager.theManager) { + PathWatcherManager.theManager = new PathWatcherManager() + } + return PathWatcherManager.theManager + } + + constructor () { + this.live = new Set() + this.nativeRegistry = new NativeWatcherRegistry( + normalizedPath => { + const nativeWatcher = new NativeWatcher(normalizedPath) + + this.live.add(nativeWatcher) + const sub = nativeWatcher.onWillStop(() => { + this.live.delete(nativeWatcher) + sub.dispose() + }) + + return nativeWatcher + } + ) + } + + createWatcher (rootPath, options, eventCallback) { + console.log(`watching root path = ${rootPath}`) + const watcher = new PathWatcher(this.nativeRegistry, rootPath, options) + watcher.onDidChange(eventCallback) + return watcher + } + + stopAllWatchers () { + return Promise.all( + Array.from(this.live, watcher => watcher.stop()) + ) + } +} + +export default function watchPath (rootPath, options, eventCallback) { + return PathWatcherManager.instance().createWatcher(rootPath, options, eventCallback) +} + // Private: Return a Promise that resolves when all {NativeWatcher} instances associated with a FileSystemManager // have stopped listening. This is useful for `afterEach()` blocks in unit tests. export function stopAllWatchers () { - return Promise.all( - Array.from(LIVE, watcher => watcher.stop()) - ) + return PathWatcherManager.instance().stopAllWatchers() } From 9c874c921e31098bf9a6216fb6056dd46f9377b2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 10:56:50 -0400 Subject: [PATCH 192/314] Use the watchPath API in specs --- spec/path-watcher-spec.js | 89 +++++++-------------------------------- 1 file changed, 15 insertions(+), 74 deletions(-) diff --git a/spec/path-watcher-spec.js b/spec/path-watcher-spec.js index 6d56c93de..2530e1561 100644 --- a/spec/path-watcher-spec.js +++ b/spec/path-watcher-spec.js @@ -6,7 +6,7 @@ import fsCb from 'fs-plus' import path from 'path' import {CompositeDisposable} from 'event-kit' -import PathWatcher, {stopAllWatchers} from '../src/path-watcher' +import watchPath, {stopAllWatchers} from '../src/path-watcher' tempCb.track() @@ -47,72 +47,21 @@ describe('PathWatcher', function () { }) } - describe('new PatchWatcher()', function () { + describe('watchPath()', function () { it('resolves getStartPromise() when the watcher begins listening', async function () { const rootDir = await temp.mkdir('atom-fsmanager-test-') - const watcher = new PathWatcher(rootDir) - watcher.onDidChange(() => {}) - + const watcher = watchPath(rootDir, {}, () => {}) await watcher.getStartPromise() }) - it('does not start actually watching until an onDidChange subscriber is registered', async function () { - const rootDir = await temp.mkdir('atom-fsmanager-test-') - const watcher = new PathWatcher(rootDir) - - let started = false - const startPromise = watcher.getStartPromise().then(() => { - started = true - }) - - expect(watcher.native).toBe(null) - expect(watcher.normalizedPath).toBe(null) - expect(started).toBe(false) - - await watcher.getNormalizedPathPromise() - - expect(watcher.native).toBe(null) - expect(watcher.normalizedPath).not.toBe(null) - expect(started).toBe(false) - - watcher.onDidChange(() => {}) - await startPromise - - expect(watcher.native).not.toBe(null) - expect(started).toBe(true) - }) - - it('automatically stops and removes the watcher when all onDidChange subscribers dispose', async function () { - const dir = await temp.mkdir('atom-fsmanager-test-') - const watcher = new PathWatcher(dir) - - const sub0 = watcher.onDidChange(() => {}) - const sub1 = watcher.onDidChange(() => {}) - - await watcher.getStartPromise() - const native = watcher.native - expect(native).not.toBe(null) - expect(native.isRunning()).toBe(true) - - sub0.dispose() - expect(watcher.native).toBe(native) - expect(native.isRunning()).toBe(true) - - sub1.dispose() - expect(watcher.native).toBeNull() - expect(native.isRunning()).toBe(false) - }) - it('reuses an existing native watcher and resolves getStartPromise immediately if attached to a running watcher', async function () { const rootDir = await temp.mkdir('atom-fsmanager-test-') - const watcher0 = new PathWatcher(rootDir) - watcher0.onDidChange(() => {}) + const watcher0 = watchPath(rootDir, {}, () => {}) await watcher0.getStartPromise() - const watcher1 = new PathWatcher(rootDir) - watcher1.onDidChange(() => {}) + const watcher1 = watchPath(rootDir, {}, () => {}) await watcher1.getStartPromise() expect(watcher0.native).toBe(watcher1.native) @@ -121,13 +70,11 @@ describe('PathWatcher', function () { it("reuses existing native watchers even while they're still starting", async function () { const rootDir = await temp.mkdir('atom-fsmanager-test-') - const watcher0 = new PathWatcher(rootDir) - watcher0.onDidChange(() => {}) + const watcher0 = watchPath(rootDir, {}, () => {}) await watcher0.getAttachedPromise() expect(watcher0.native.isRunning()).toBe(false) - const watcher1 = new PathWatcher(rootDir) - watcher1.onDidChange(() => {}) + const watcher1 = watchPath(rootDir, {}, () => {}) await watcher1.getAttachedPromise() expect(watcher0.native).toBe(watcher1.native) @@ -138,15 +85,13 @@ describe('PathWatcher', function () { it("doesn't attach new watchers to a native watcher that's stopping", async function () { const rootDir = await temp.mkdir('atom-fsmanager-test-') - const watcher0 = new PathWatcher(rootDir) - const sub = watcher0.onDidChange(() => {}) + const watcher0 = watchPath(rootDir, {}, () => {}) await watcher0.getStartPromise() const native0 = watcher0.native - sub.dispose() + watcher0.dispose() - const watcher1 = new PathWatcher(rootDir) - watcher1.onDidChange(() => {}) + const watcher1 = watchPath(rootDir, {}, () => {}) expect(watcher1.native).not.toBe(native0) }) @@ -160,10 +105,8 @@ describe('PathWatcher', function () { await fs.mkdir(subDir) // Keep the watchers alive with an undisposed subscription - const rootWatcher = new PathWatcher(rootDir) - rootWatcher.onDidChange(() => {}) - const childWatcher = new PathWatcher(subDir) - childWatcher.onDidChange(() => {}) + const rootWatcher = watchPath(rootDir, {}, () => {}) + const childWatcher = watchPath(subDir, {}, () => {}) await Promise.all([ rootWatcher.getStartPromise(), @@ -206,12 +149,10 @@ describe('PathWatcher', function () { ]) // Begin the child watchers and keep them alive - const subWatcher0 = new PathWatcher(subDir0) - subWatcher0.onDidChange(() => {}) + const subWatcher0 = watchPath(subDir0, {}, () => {}) const subWatcherChanges0 = waitForChanges(subWatcher0, subFile0) - const subWatcher1 = new PathWatcher(subDir1) - subWatcher1.onDidChange(() => {}) + const subWatcher1 = watchPath(subDir1, {}, () => {}) const subWatcherChanges1 = waitForChanges(subWatcher1, subFile1) await Promise.all( @@ -220,7 +161,7 @@ describe('PathWatcher', function () { expect(subWatcher0.native).not.toBe(subWatcher1.native) // Create the parent watcher - const parentWatcher = new PathWatcher(parentDir) + const parentWatcher = watchPath(parentDir, {}, () => {}) const parentWatcherChanges = waitForChanges(parentWatcher, rootFile, subFile0, subFile1) await parentWatcher.getStartPromise() From 3c967b07efe4df5b4345afc4009ea971bb38924a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 10:57:16 -0400 Subject: [PATCH 193/314] Use a cross-platform way to generate absolute paths for specs --- spec/native-watcher-registry-spec.js | 32 ++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/spec/native-watcher-registry-spec.js b/spec/native-watcher-registry-spec.js index c3427d2c6..4144afbe4 100644 --- a/spec/native-watcher-registry-spec.js +++ b/spec/native-watcher-registry-spec.js @@ -7,6 +7,24 @@ import {Emitter} from 'event-kit' import NativeWatcherRegistry from '../src/native-watcher-registry' +function findRootDirectory () { + let current = process.cwd() + while (true) { + let next = path.resolve(current, '..') + if (next === current) { + return next + } else { + current = next + } + } +} +const ROOT = findRootDirectory() + +function absolute(parts) { + const candidate = path.join(...parts) + return path.isAbsolute(candidate) ? candidate : path.join(ROOT, candidate) +} + class MockWatcher { constructor (normalizedPath) { this.normalizedPath = normalizedPath @@ -66,7 +84,7 @@ describe('NativeWatcherRegistry', function () { }) it('attaches a Watcher to a newly created NativeWatcher for a new directory', async function () { - const watcher = new MockWatcher(path.join('some', 'path')) + const watcher = new MockWatcher(absolute('some', 'path')) const NATIVE = new MockNative('created') createNative = () => NATIVE @@ -77,7 +95,7 @@ describe('NativeWatcherRegistry', function () { it('reuses an existing NativeWatcher on the same directory', async function () { const EXISTING = new MockNative('existing') - const existingPath = path.join('existing', 'path') + const existingPath = absolute('existing', 'path') let firstTime = true createNative = () => { if (firstTime) { @@ -97,7 +115,7 @@ describe('NativeWatcherRegistry', function () { it('attaches to an existing NativeWatcher on a parent directory', async function () { const EXISTING = new MockNative('existing') - const parentDir = path.join('existing', 'path') + const parentDir = absolute('existing', 'path') const subDir = path.join(parentDir, 'sub', 'directory') let firstTime = true createNative = () => { @@ -117,10 +135,10 @@ describe('NativeWatcherRegistry', function () { }) it('adopts Watchers from NativeWatchers on child directories', async function () { - const parentDir = path.join('existing', 'path') + const parentDir = absolute('existing', 'path') const childDir0 = path.join(parentDir, 'child', 'directory', 'zero') const childDir1 = path.join(parentDir, 'child', 'directory', 'one') - const otherDir = path.join('another', 'path') + const otherDir = absolute('another', 'path') const CHILD0 = new MockNative('existing0') const CHILD1 = new MockNative('existing1') @@ -220,7 +238,7 @@ describe('NativeWatcherRegistry', function () { const CHILD1 = new MockNative('child1') const PARENT = new MockNative('parent') - const parentDir = path.join('parent') + const parentDir = absolute('parent') const childDir0 = path.join(parentDir, 'child0') const childDir1 = path.join(parentDir, 'child1') @@ -280,7 +298,7 @@ describe('NativeWatcherRegistry', function () { const CHILD0 = new MockNative('child0') const PARENT = new MockNative('parent') - const parentDir = path.join('parent') + const parentDir = absolute('parent') const childDir0 = path.join(parentDir, 'child0') const childDir1 = path.join(parentDir, 'child0', 'child1') From 6fdeedd4abd2ca86811dcd7daa1d73c3145341a7 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 11:12:23 -0400 Subject: [PATCH 194/314] Introduce a helper to re-join split absolute paths regardless of platform --- src/native-watcher-registry.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index 77da22669..fa880b452 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -2,6 +2,12 @@ import path from 'path' +// Private: re-join the segments split from an absolute path to form another absolute path. +function absolute(...parts) { + const candidate = path.join(...parts) + return path.isAbsolute(candidate) ? candidate : path.join(path.sep, candidate) +} + // Private: Map userland filesystem watcher subscriptions efficiently to deliver filesystem change notifications to // each watcher with the most efficient coverage of native watchers. // @@ -10,7 +16,7 @@ import path from 'path' // watcher is removed, it will be split into child watchers. // * If any child directories already being watched, stop and replace them with a watcher on the parent directory. // -// Uses a Trie whose structure mirrors the directory structure. +// Uses a trie whose structure mirrors the directory structure. class RegistryTree { // Private: Construct a tree with no native watchers. @@ -33,7 +39,7 @@ class RegistryTree { // * `attachToNative` {Function} invoked with the appropriate native watcher and the absolute path to its watch root. add (pathSegments, attachToNative) { const absolutePathSegments = this.basePathSegments.concat(pathSegments) - const absolutePath = path.join(...absolutePathSegments) + const absolutePath = absolute(...absolutePathSegments) const attachToNew = (childPaths) => { const native = this.createNative(absolutePath) @@ -55,7 +61,7 @@ class RegistryTree { // Attach this Watcher to it as a filtering watcher and record it as a dependent child path. const native = parent.getNativeWatcher() parent.addChildPath(remaining) - attachToNative(native, path.join(...parent.getAbsolutePathSegments())) + attachToNative(native, absolute(...parent.getAbsolutePathSegments())) }, children: children => { // One or more NativeWatchers exist on child directories of the requested path. Create a new native watcher From 3fab3fed36a25f61e996f01148dd02eb6ab1eeaa Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 11:21:56 -0400 Subject: [PATCH 195/314] Consistent path handling in specs --- spec/native-watcher-registry-spec.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/spec/native-watcher-registry-spec.js b/spec/native-watcher-registry-spec.js index 4144afbe4..1663f3290 100644 --- a/spec/native-watcher-registry-spec.js +++ b/spec/native-watcher-registry-spec.js @@ -20,7 +20,7 @@ function findRootDirectory () { } const ROOT = findRootDirectory() -function absolute(parts) { +function absolute(...parts) { const candidate = path.join(...parts) return path.isAbsolute(candidate) ? candidate : path.join(ROOT, candidate) } @@ -196,28 +196,30 @@ describe('NativeWatcherRegistry', function () { const STOPPED = new MockNative('stopped') const RUNNING = new MockNative('running') - const stoppedPath = ['watcher', 'that', 'will', 'be', 'stopped'] - const runningPath = ['watcher', 'that', 'will', 'continue', 'to', 'exist'] + const stoppedPath = absolute('watcher', 'that', 'will', 'be', 'stopped') + const stoppedPathParts = stoppedPath.split(path.sep).filter(part => part.length > 0) + const runningPath = absolute('watcher', 'that', 'will', 'continue', 'to', 'exist') + const runningPathParts = runningPath.split(path.sep).filter(part => part.length > 0) createNative = dir => { - if (dir === path.join(...stoppedPath)) { + if (dir === stoppedPath) { return STOPPED - } else if (dir === path.join(...runningPath)) { + } else if (dir === runningPath) { return RUNNING } else { throw new Error(`Unexpected path: ${dir}`) } } - const stoppedWatcher = new MockWatcher(path.join(...stoppedPath)) + const stoppedWatcher = new MockWatcher(stoppedPath) await registry.attach(stoppedWatcher) - const runningWatcher = new MockWatcher(path.join(...runningPath)) + const runningWatcher = new MockWatcher(runningPath) await registry.attach(runningWatcher) STOPPED.stop() - const runningNode = registry.tree.root.lookup(runningPath).when({ + const runningNode = registry.tree.root.lookup(runningPathParts).when({ parent: node => node, missing: () => false, children: () => false @@ -225,7 +227,7 @@ describe('NativeWatcherRegistry', function () { expect(runningNode).toBeTruthy() expect(runningNode.getNativeWatcher()).toBe(RUNNING) - const stoppedNode = registry.tree.root.lookup(stoppedPath).when({ + const stoppedNode = registry.tree.root.lookup(stoppedPathParts).when({ parent: () => false, missing: () => true, children: () => false From 53ea43001945f2cac87c8d0639aef984bb9f93b8 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 11:22:03 -0400 Subject: [PATCH 196/314] Update spec name --- spec/path-watcher-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/path-watcher-spec.js b/spec/path-watcher-spec.js index 2530e1561..74d7980aa 100644 --- a/spec/path-watcher-spec.js +++ b/spec/path-watcher-spec.js @@ -13,7 +13,7 @@ tempCb.track() const fs = promisifySome(fsCb, ['writeFile', 'mkdir', 'symlink', 'appendFile', 'realpath']) const temp = promisifySome(tempCb, ['mkdir']) -describe('PathWatcher', function () { +describe('watchPath', function () { let subs beforeEach(function () { From afdb2f13a6fd7ef1442c0bc6ab50cf22e4d4d248 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 13:54:24 -0400 Subject: [PATCH 197/314] Doooooocs --- src/path-watcher.js | 105 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 3 deletions(-) diff --git a/src/path-watcher.js b/src/path-watcher.js index 92d961584..6e6801f8c 100644 --- a/src/path-watcher.js +++ b/src/path-watcher.js @@ -173,7 +173,22 @@ class NativeWatcher { } } +// Extended: Manage a subscription to filesystem events that occur beneath a root directory. Construct these by calling +// {watchPath}. +// +// Maybe PathWatchers may be backed by the same native watcher to conserve operation system resources. Native watchers are +// started when at least one subscription is registered, and stopped when all subscriptions are disposed. +// +// Acts as a {Disposable}. +// export class PathWatcher { + + // Private: Instantiate a new PathWatcher. Call {watchPath} instead. + // + // * `nativeWatcherRegistry` {NativeWatcherRegistry} used to find and consolidate redundant watchers. + // * `watchedPath` {String} containing the absolute path to the root of the watched filesystem tree. + // * `options` See {watchPath} for options. + // constructor (nativeWatcherRegistry, watchedPath, options) { this.watchedPath = watchedPath this.nativeWatcherRegistry = nativeWatcherRegistry @@ -205,18 +220,45 @@ export class PathWatcher { this.subs = new CompositeDisposable() } + // Private: Return a {Promise} that will resolve with the normalized root path. getNormalizedPathPromise () { return this.normalizedPathPromise } + // Private: Return a {Promise} that will resolve the first time that this watcher is attached to a native watcher. getAttachedPromise () { return this.attachedPromise } + // Extended: Return a {Promise} that will resolve when the underlying native watcher is ready to begin sending events. + // When testing filesystem watchers, it's important to await this promise before making filesystem changes that you + // intend to assert about because there will be a delay between the instantiation of the watcher and the activation + // of the underlying OS resources that feed it events. + // + // ```js + // const {watchPath} = require('atom') + // const ROOT = path.join(__dirname, 'fixtures') + // const FILE = path.join(ROOT, 'filename.txt') + // + // describe('something', function () { + // it("doesn't miss events", async function () { + // const watcher = watchPath(ROOT, {}, events => {}) + // await watcher.getStartPromise() + // fs.writeFile(FILE, 'contents\n', err => { + // // The watcher is listening and the event should be received asyncronously + // } + // }) + // }) + // ``` getStartPromise () { return this.startPromise } + // Private: Attach another {Function} to be called with each batch of filesystem events. See {watchPath} for the + // spec of the callback's argument. + // + // Returns a {Disposable} that will stop the underlying watcher when all callbacks mapped to it have been disposed. + // onDidChange (callback) { if (this.native) { const sub = this.native.onDidChange(events => this.onNativeEvents(events, callback)) @@ -237,10 +279,15 @@ export class PathWatcher { }) } + // Extended: Invoke a {Function} when any errors related to this watcher are reported. + // + // Returns a {Disposable}. + // onDidError (callback) { return this.emitter.on('did-error', callback) } + // Private: Wire this watcher to an operating system-level native watcher implementation. attachToNative (native) { this.subs.dispose() this.native = native @@ -278,15 +325,19 @@ export class PathWatcher { this.resolveAttachedPromise() } + // Private: Invoked when the attached native watcher creates a batch of native filesystem events. The native watcher's + // events may include events for paths above this watcher's root path, so filter them to only include the relevant + // ones, then re-broadcast them to our subscribers. onNativeEvents (events, callback) { - // TODO does event.oldPath resolve symlinks? - const filtered = events.filter(event => event.oldPath.startsWith(this.normalizedPath)) + const filtered = events.filter(event => event.path.startsWith(this.normalizedPath)) if (filtered.length > 0) { callback(filtered) } } + // Extended: Unsubscribe all subscribers from filesystem events. The native watcher resources may take some time to + // be cleaned up, but the watcher will stop broadcasting events immediately. dispose () { for (const sub of this.changeCallbacks.values()) { sub.dispose() @@ -297,7 +348,12 @@ export class PathWatcher { } } +// Private: Globally tracked state used to de-duplicate related [PathWatchers]{PathWatcher}. class PathWatcherManager { + + // Private: Access or lazily initialize the singleton manager instance. + // + // Returns the one and only {PathWatcherManager}. static instance () { if (!PathWatcherManager.theManager) { PathWatcherManager.theManager = new PathWatcherManager() @@ -305,6 +361,7 @@ class PathWatcherManager { return PathWatcherManager.theManager } + // Private: Initialize global {PathWatcher} state. constructor () { this.live = new Set() this.nativeRegistry = new NativeWatcherRegistry( @@ -322,13 +379,16 @@ class PathWatcherManager { ) } + // Private: Create a {PathWatcher} tied to this global state. See {watchPath} for detailed arguments. createWatcher (rootPath, options, eventCallback) { - console.log(`watching root path = ${rootPath}`) const watcher = new PathWatcher(this.nativeRegistry, rootPath, options) watcher.onDidChange(eventCallback) return watcher } + // Private: Stop all living watchers. + // + // Returns a {Promise} that resolves when all native watcher resources are disposed. stopAllWatchers () { return Promise.all( Array.from(this.live, watcher => watcher.stop()) @@ -336,6 +396,45 @@ class PathWatcherManager { } } +// Extended: Invoke a callback with each filesystem event that occurs beneath a specified path. If you only need to +// watch events within the project's root paths, use {Project::onDidChangeFiles} instead. +// +// watchPath handles the efficient re-use of operating system resources across living watchers. Watching the same path +// more than once, or the child of a watched path, will re-use the existing native watcher. +// +// * `rootPath` {String} specifies the absolute path to the root of the filesystem content to watch. +// * `options` Control the watcher's behavior. +// * `recursive` If true, passing the path to a directory will recursively watch all changes beneath that +// directory. If false, only the file or directory itself will be watched. +// * `eventCallback` {Function} or other callable to be called each time a batch of filesystem events is observed. +// * `events` {Array} of objects that describe the events that have occurred. +// * `type` {String} describing the filesystem action that occurred. One of `"created"`, `"modified"`, `"deleted"`, +// or `"renamed"`. +// * `path` {String} containing the absolute path to the filesystem entry that was acted upon. +// * `oldPath` For rename events, {String} containing the filesystem entry's former absolute path. +// +// Returns a {PathWatcher}. Note that every {PathWatcher} is a {Disposable}, so they can be managed by +// [CompositeDisposables]{CompositeDisposable} if desired. +// +// ```js +// const {watchPath} = require('atom') +// +// const disposable = watchPath('/var/log', {}, events => { +// console.log(`Received batch of ${events.length} events.`) +// for (const event of events) { +// console.log(`Event action: ${event.type}`) // "created", "modified", "deleted", "renamed" +// console.log(`Event path: ${event.path}`) // absolute path to the filesystem entry that was touched +// if (event.type === 'renamed') { +// console.log(`.. renamed from: ${event.oldPath}`) +// } +// } +// }) +// +// // Immediately stop receiving filesystem events. If this is the last watcher, asynchronously release any OS +// // resources required to subscribe to these events. +// disposable.dispose() +// ``` +// export default function watchPath (rootPath, options, eventCallback) { return PathWatcherManager.instance().createWatcher(rootPath, options, eventCallback) } From ba11070d16994595845a8e3d17a6469170331bd1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 14:01:15 -0400 Subject: [PATCH 198/314] Translate nsfw events to the events we're advertising --- src/path-watcher.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/path-watcher.js b/src/path-watcher.js index 6e6801f8c..476582597 100644 --- a/src/path-watcher.js +++ b/src/path-watcher.js @@ -161,7 +161,16 @@ class NativeWatcher { const oldPath = path.join(event.directory, oldFileName) const newPath = newFileName && path.join(event.directory, newFileName) - return {oldPath, newPath, type} + const payload = {type} + + if (event.file) { + payload.path = path.join(event.directory, event.file) + } else { + payload.oldPath = path.join(event.directory, event.oldFile) + payload.path = path.join(event.directory, event.newFile) + } + + return payload })) } From 67a8ba2a046f6f6abe40589e554b1e746e6ba69c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 14:03:33 -0400 Subject: [PATCH 199/314] Adjust specs for the changed event shape --- spec/path-watcher-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/path-watcher-spec.js b/spec/path-watcher-spec.js index 74d7980aa..2b451fc8c 100644 --- a/spec/path-watcher-spec.js +++ b/spec/path-watcher-spec.js @@ -33,7 +33,7 @@ describe('watchPath', function () { return new Promise(resolve => { const sub = watcher.onDidChange(events => { for (const event of events) { - if (waiting.delete(event.oldPath)) { + if (waiting.delete(event.path)) { relevantEvents.push(event) } } From 697dfaf3b39688f60f1f863b5d51a1f73249228f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 14:13:44 -0400 Subject: [PATCH 200/314] Re-export watchPath --- exports/atom.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/exports/atom.js b/exports/atom.js index 9ad4f60c2..3611958bf 100644 --- a/exports/atom.js +++ b/exports/atom.js @@ -7,6 +7,7 @@ import BufferedNodeProcess from '../src/buffered-node-process' import BufferedProcess from '../src/buffered-process' import GitRepository from '../src/git-repository' import Notification from '../src/notification' +import watchPath from '../src/path-watcher' const atomExport = { BufferedNodeProcess, @@ -20,7 +21,8 @@ const atomExport = { Directory, Emitter, Disposable, - CompositeDisposable + CompositeDisposable, + watchPath } // Shell integration is required by both Squirrel and Settings-View From b3f327b0b334c05e72b3ae475211ff106f193bfc Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 14:38:58 -0400 Subject: [PATCH 201/314] Implement `atom.project.onDidChangeFiles` --- src/project.coffee | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/project.coffee b/src/project.coffee index bf497e1db..81bf1954a 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -4,6 +4,7 @@ _ = require 'underscore-plus' fs = require 'fs-plus' {Emitter, Disposable} = require 'event-kit' TextBuffer = require 'text-buffer' +watchPath = require('./path-watcher').default DefaultDirectoryProvider = require './default-directory-provider' Model = require './model' @@ -28,11 +29,13 @@ class Project extends Model @repositoryPromisesByPath = new Map() @repositoryProviders = [new GitRepositoryProvider(this, config)] @loadPromisesByPath = {} + @watchersByPath = {} @consumeServices(packageManager) destroyed: -> buffer.destroy() for buffer in @buffers.slice() repository?.destroy() for repository in @repositories.slice() + watcher.dispose() for _, watcher in @watchersByPath @rootDirectories = [] @repositories = [] @@ -114,6 +117,26 @@ class Project extends Model callback(buffer) for buffer in @getBuffers() @onDidAddBuffer callback + # Public: Invoke the given callback when any filesystem change occurs within an open + # project path. + # + # To watch paths outside of open projects, use the {watchPaths} function. + # + # * `callback` {Function} to be called with batches of filesystem events reported by + # the operating system. + # * `events` An {Array} of objects the describe filesystem events. + # * `type` {String} describing the filesystem action that occurred. One of `"created"`, + # `"modified"`, `"deleted"`, or `"renamed"`. + # * `path` {String} containing the absolute path to the filesystem entry + # that was acted upon. + # * `oldPath` For rename events, {String} containing the filesystem entry's + # former absolute path. + # + # Returns a {PathWatcher} that may be used like a {Disposable} to manage + # this event subscription. + onDidChangeFiles: (callback) -> + @emitter.on 'did-change-files', callback + ### Section: Accessing the git repository ### @@ -171,6 +194,7 @@ class Project extends Model repository?.destroy() for repository in @repositories @rootDirectories = [] @repositories = [] + watcher.dispose() for _, watcher in @watchersByPath @addPath(projectPath, emitEvent: false) for projectPath in projectPaths @@ -186,6 +210,11 @@ class Project extends Model return if existingDirectory.getPath() is directory.getPath() @rootDirectories.push(directory) + @watchersByPath[directory] = watchPath directory.getPath(), {}, (events) => + @emitter.emit 'did-change-files', events + + for root, watcher in @watchersByPath + watcher.dispose() unless @rootDirectoryies.includes root repo = null for provider in @repositoryProviders @@ -220,6 +249,7 @@ class Project extends Model [removedDirectory] = @rootDirectories.splice(indexToRemove, 1) [removedRepository] = @repositories.splice(indexToRemove, 1) removedRepository?.destroy() unless removedRepository in @repositories + @watchersByPath[projectPath]?.dispose() @emitter.emit "did-change-paths", @getPaths() true else From ee9ad53d91a9e8d274bbf51ef42ca83f5f4d9509 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 15:55:08 -0400 Subject: [PATCH 202/314] :fire: unused variables --- src/path-watcher.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/path-watcher.js b/src/path-watcher.js index 476582597..723ee8788 100644 --- a/src/path-watcher.js +++ b/src/path-watcher.js @@ -156,11 +156,6 @@ class NativeWatcher { onEvents (events) { this.emitter.emit('did-change', events.map(event => { const type = ACTION_MAP.get(event.action) || `unexpected (${event.action})` - const oldFileName = event.file || event.oldFile - const newFileName = event.newFile - const oldPath = path.join(event.directory, oldFileName) - const newPath = newFileName && path.join(event.directory, newFileName) - const payload = {type} if (event.file) { From 1285a89a4b0da95eff0e8d293252ec64671f40cc Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 15:55:28 -0400 Subject: [PATCH 203/314] Reset @watchersByPath on atom.project.setPaths --- src/project.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/project.coffee b/src/project.coffee index 81bf1954a..aceb89cb9 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -194,7 +194,9 @@ class Project extends Model repository?.destroy() for repository in @repositories @rootDirectories = [] @repositories = [] + watcher.dispose() for _, watcher in @watchersByPath + @watchersByPath = {} @addPath(projectPath, emitEvent: false) for projectPath in projectPaths From f005fcdca105e04e3fcb97f9c836956aaf12a98a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 15:55:39 -0400 Subject: [PATCH 204/314] Use directory.getPath() as the object key --- src/project.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/project.coffee b/src/project.coffee index aceb89cb9..22f6c2bf7 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -212,7 +212,7 @@ class Project extends Model return if existingDirectory.getPath() is directory.getPath() @rootDirectories.push(directory) - @watchersByPath[directory] = watchPath directory.getPath(), {}, (events) => + @watchersByPath[directory.getPath()] = watchPath directory.getPath(), {}, (events) => @emitter.emit 'did-change-files', events for root, watcher in @watchersByPath From 00346614a0d4b3b18e97638ee9bd406f96f5b11e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 15:58:38 -0400 Subject: [PATCH 205/314] Spec for onDidChangeFiles --- spec/project-spec.coffee | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index a1a1dc189..4e144fe58 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -548,6 +548,57 @@ describe "Project", -> atom.project.removePath(ftpURI) expect(atom.project.getPaths()).toEqual [] + describe ".onDidChangeFiles()", -> + sub = [] + events = [] + checkCallback = -> + + beforeEach -> + sub = atom.project.onDidChangeFiles (incoming) -> + events.push incoming... + checkCallback() + + afterEach -> + sub.dispose() + + waitForEvents = (paths) -> + remaining = new Set(fs.realpathSync(path) for path in paths) + new Promise (resolve, reject) -> + checkCallback = -> + remaining.delete(event.path) for event in events + resolve() if remaining.size is 0 + + expire = -> + checkCallback = -> + console.error "Paths not seen:", Array.from(remaining) + reject(new Error('Expired before all expected events were delivered.')) + + checkCallback() + setTimeout expire, 2000 + + it "reports filesystem changes within project paths", -> + dirOne = temp.mkdirSync('atom-spec-project') + fileOne = path.join(dirOne, 'file-one.txt') + fileTwo = path.join(dirOne, 'file-two.txt') + dirTwo = temp.mkdirSync('atom-spec-project') + fileThree = path.join(dirTwo, 'file-three.txt') + + atom.project.setPaths([dirOne]) + + waitsForPromise -> atom.project.watchersByPath[dirOne].getStartPromise() + + runs -> + expect(atom.project.watchersByPath[dirTwo]).toEqual undefined + + fs.writeFileSync fileThree, "three\n" + fs.writeFileSync fileTwo, "two\n" + fs.writeFileSync fileOne, "one\n" + + waitsForPromise -> waitForEvents [fileOne, fileTwo] + + runs -> + expect(events.some (event) -> event.path is fileThree).toBeFalsy() + describe ".onDidAddBuffer()", -> it "invokes the callback with added text buffers", -> buffers = [] From c7a47a9e89743f07947e73356908cf687088bfa5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 1 Aug 2017 16:40:54 -0400 Subject: [PATCH 206/314] Use module.exports to not break Joanna horribly --- exports/atom.js | 2 +- spec/native-watcher-registry-spec.js | 2 +- spec/path-watcher-spec.js | 2 +- src/native-watcher-registry.js | 8 ++++---- src/path-watcher.js | 21 ++++++++++----------- src/project.coffee | 2 +- 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/exports/atom.js b/exports/atom.js index 3611958bf..d7ca55909 100644 --- a/exports/atom.js +++ b/exports/atom.js @@ -7,7 +7,7 @@ import BufferedNodeProcess from '../src/buffered-node-process' import BufferedProcess from '../src/buffered-process' import GitRepository from '../src/git-repository' import Notification from '../src/notification' -import watchPath from '../src/path-watcher' +import {watchPath} from '../src/path-watcher' const atomExport = { BufferedNodeProcess, diff --git a/spec/native-watcher-registry-spec.js b/spec/native-watcher-registry-spec.js index 1663f3290..5cd4225ef 100644 --- a/spec/native-watcher-registry-spec.js +++ b/spec/native-watcher-registry-spec.js @@ -5,7 +5,7 @@ import {it, beforeEach} from './async-spec-helpers' import path from 'path' import {Emitter} from 'event-kit' -import NativeWatcherRegistry from '../src/native-watcher-registry' +import {NativeWatcherRegistry} from '../src/native-watcher-registry' function findRootDirectory () { let current = process.cwd() diff --git a/spec/path-watcher-spec.js b/spec/path-watcher-spec.js index 2b451fc8c..a805886e3 100644 --- a/spec/path-watcher-spec.js +++ b/spec/path-watcher-spec.js @@ -6,7 +6,7 @@ import fsCb from 'fs-plus' import path from 'path' import {CompositeDisposable} from 'event-kit' -import watchPath, {stopAllWatchers} from '../src/path-watcher' +import {watchPath, stopAllWatchers} from '../src/path-watcher' tempCb.track() diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index fa880b452..db3876d6b 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -1,6 +1,4 @@ -/** @babel */ - -import path from 'path' +const path = require('path') // Private: re-join the segments split from an absolute path to form another absolute path. function absolute(...parts) { @@ -403,7 +401,7 @@ class ChildrenResult { // 2. Subscribing to an existing {NativeWatcher} on a parent of a desired directory. // 3. Replacing multiple {NativeWatcher} instances on child directories with a single new {NativeWatcher} on the // parent. -export default class NativeWatcherRegistry { +class NativeWatcherRegistry { // Private: Instantiate an empty registry. // @@ -432,3 +430,5 @@ export default class NativeWatcherRegistry { }) } } + +module.exports = {NativeWatcherRegistry} diff --git a/src/path-watcher.js b/src/path-watcher.js index 723ee8788..4282a653f 100644 --- a/src/path-watcher.js +++ b/src/path-watcher.js @@ -1,12 +1,9 @@ -/** @babel */ +const fs = require('fs') +const path = require('path') -import fs from 'fs' -import path from 'path' - -import {Emitter, Disposable, CompositeDisposable} from 'event-kit' -import nsfw from 'nsfw' - -import NativeWatcherRegistry from './native-watcher-registry' +const {Emitter, Disposable, CompositeDisposable} = require('event-kit') +const nsfw = require('nsfw') +const {NativeWatcherRegistry} = require('./native-watcher-registry') // Private: Associate native watcher action type flags with descriptive String equivalents. const ACTION_MAP = new Map([ @@ -185,7 +182,7 @@ class NativeWatcher { // // Acts as a {Disposable}. // -export class PathWatcher { +class PathWatcher { // Private: Instantiate a new PathWatcher. Call {watchPath} instead. // @@ -439,12 +436,14 @@ class PathWatcherManager { // disposable.dispose() // ``` // -export default function watchPath (rootPath, options, eventCallback) { +function watchPath (rootPath, options, eventCallback) { return PathWatcherManager.instance().createWatcher(rootPath, options, eventCallback) } // Private: Return a Promise that resolves when all {NativeWatcher} instances associated with a FileSystemManager // have stopped listening. This is useful for `afterEach()` blocks in unit tests. -export function stopAllWatchers () { +function stopAllWatchers () { return PathWatcherManager.instance().stopAllWatchers() } + +module.exports = {watchPath, stopAllWatchers} diff --git a/src/project.coffee b/src/project.coffee index 22f6c2bf7..a28fc34a4 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -4,7 +4,7 @@ _ = require 'underscore-plus' fs = require 'fs-plus' {Emitter, Disposable} = require 'event-kit' TextBuffer = require 'text-buffer' -watchPath = require('./path-watcher').default +{watchPath} = require('./path-watcher') DefaultDirectoryProvider = require './default-directory-provider' Model = require './model' From ad7e206a78589179946dbb010fbe44964b90cfbb Mon Sep 17 00:00:00 2001 From: simurai Date: Wed, 2 Aug 2017 12:39:46 +0900 Subject: [PATCH 207/314] :arrow_up: one-dark/light-ui@v1.10.6 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1ed1c87fb..70941d7aa 100644 --- a/package.json +++ b/package.json @@ -78,8 +78,8 @@ "atom-light-ui": "0.46.0", "base16-tomorrow-dark-theme": "1.5.0", "base16-tomorrow-light-theme": "1.5.0", - "one-dark-ui": "1.10.5", - "one-light-ui": "1.10.5", + "one-dark-ui": "1.10.6", + "one-light-ui": "1.10.6", "one-dark-syntax": "1.8.0", "one-light-syntax": "1.8.0", "solarized-dark-syntax": "1.1.2", From 7244fb96708ec5228e8b335f633293988049f56c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 2 Aug 2017 08:18:25 -0400 Subject: [PATCH 208/314] Format script/test with standard --- script/test | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/script/test b/script/test index fe90160a8..d4d850f55 100755 --- a/script/test +++ b/script/test @@ -105,7 +105,6 @@ for (let packageName in CONFIG.appMetadata.packageDependencies) { const pkgJsonPath = path.join(repositoryPackagePath, 'package.json') const nodeModulesPath = path.join(repositoryPackagePath, 'node_modules') - const nodeModulesBackupPath = path.join(repositoryPackagePath, 'node_modules.bak') let finalize = () => null if (require(pkgJsonPath).atomTestRunner) { console.log(`Installing test runner dependencies for ${packageName}`.bold.green) @@ -152,12 +151,17 @@ function runBenchmarkTests (callback) { let testSuitesToRun = testSuitesForPlatform(process.platform) -function testSuitesForPlatform(platform) { - switch(platform) { - case 'darwin': return [runCoreMainProcessTests, runCoreRenderProcessTests, runBenchmarkTests].concat(packageTestSuites) - case 'win32': return (process.arch === 'x64') ? [runCoreMainProcessTests, runCoreRenderProcessTests] : [runCoreMainProcessTests] - case 'linux': return [runCoreMainProcessTests] - default: return [] +function testSuitesForPlatform (platform) { + switch (platform) { + case 'darwin': + return [runCoreMainProcessTests, runCoreRenderProcessTests, runBenchmarkTests].concat(packageTestSuites) + case 'win32': + return (process.arch === 'x64') ? [runCoreMainProcessTests, runCoreRenderProcessTests] : [runCoreMainProcessTests] + case 'linux': + return [runCoreMainProcessTests] + default: + console.log(`Unrecognized platform: ${platform}`) + return [] } } From 05a4f1f6fbb54f3f7e934d3ef1efefb7fb728ae0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 2 Aug 2017 08:29:10 -0400 Subject: [PATCH 209/314] :shirt: standard.js in script/test --- script/test | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/script/test b/script/test index fde922e94..dbb0f5c1f 100755 --- a/script/test +++ b/script/test @@ -90,7 +90,6 @@ for (let packageName in CONFIG.appMetadata.packageDependencies) { const pkgJsonPath = path.join(repositoryPackagePath, 'package.json') const nodeModulesPath = path.join(repositoryPackagePath, 'node_modules') - const nodeModulesBackupPath = path.join(repositoryPackagePath, 'node_modules.bak') let finalize = () => null if (require(pkgJsonPath).atomTestRunner) { console.log(`Installing test runner dependencies for ${packageName}`.bold.green) @@ -136,12 +135,16 @@ function runBenchmarkTests (callback) { let testSuitesToRun = testSuitesForPlatform(process.platform) -function testSuitesForPlatform(platform) { - switch(platform) { - case 'darwin': return [runCoreMainProcessTests, runCoreRenderProcessTests, runBenchmarkTests].concat(packageTestSuites) - case 'win32': return (process.arch === 'x64') ? [runCoreMainProcessTests, runCoreRenderProcessTests] : [runCoreMainProcessTests] - case 'linux': return [runCoreMainProcessTests] - default: return [] +function testSuitesForPlatform (platform) { + switch (platform) { + case 'darwin': + return [runCoreMainProcessTests, runCoreRenderProcessTests, runBenchmarkTests].concat(packageTestSuites) + case 'win32': + return (process.arch === 'x64') ? [runCoreMainProcessTests, runCoreRenderProcessTests] : [runCoreMainProcessTests] + case 'linux': + return [runCoreMainProcessTests] + default: + return [] } } From 320664a35939aeafa71c25b12925e9b687bbdca9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 2 Aug 2017 09:04:33 -0400 Subject: [PATCH 210/314] Remove an export I missed --- src/path-watcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path-watcher.js b/src/path-watcher.js index 4282a653f..17dab6d67 100644 --- a/src/path-watcher.js +++ b/src/path-watcher.js @@ -14,7 +14,7 @@ const ACTION_MAP = new Map([ ]) // Private: Possible states of a {NativeWatcher}. -export const WATCHER_STATE = { +const WATCHER_STATE = { STOPPED: Symbol('stopped'), STARTING: Symbol('starting'), RUNNING: Symbol('running'), From 318708bb421687442ce526d91ec4e8a08613c5af Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 2 Aug 2017 09:50:57 -0400 Subject: [PATCH 211/314] wip --- script/lib/generate-startup-snapshot.js | 6 +++++- src/native-watcher-registry.js | 2 ++ src/path-watcher.js | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index 9ea3abf93..3f504b22a 100644 --- a/script/lib/generate-startup-snapshot.js +++ b/script/lib/generate-startup-snapshot.js @@ -23,11 +23,12 @@ module.exports = function (packagedAppPath) { process.stdout.write(`Generating snapshot script at "${snapshotScriptPath}" (${++processedFiles})`) const relativePath = path.relative(baseDirPath, modulePath) - return ( + const result = ( modulePath.endsWith('.node') || coreModules.has(modulePath) || (relativePath.startsWith(path.join('..', 'src')) && relativePath.endsWith('-element.js')) || relativePath.startsWith(path.join('..', 'node_modules', 'dugite')) || + relativePath.startsWith(path.join('..', 'node_modules', 'nsfw')) || relativePath === path.join('..', 'exports', 'atom.js') || relativePath === path.join('..', 'src', 'electron-shims.js') || relativePath === path.join('..', 'src', 'safe-clipboard.js') || @@ -39,6 +40,7 @@ module.exports = function (packagedAppPath) { relativePath === path.join('..', 'node_modules', 'decompress-zip', 'lib', 'decompress-zip.js') || relativePath === path.join('..', 'node_modules', 'debug', 'node.js') || relativePath === path.join('..', 'node_modules', 'fs-extra', 'lib', 'index.js') || + relativePath === path.join('..', 'node_modules', 'github', 'node_modules', 'fs-extra', 'lib', 'index.js') || relativePath === path.join('..', 'node_modules', 'git-utils', 'lib', 'git.js') || relativePath === path.join('..', 'node_modules', 'glob', 'glob.js') || relativePath === path.join('..', 'node_modules', 'graceful-fs', 'graceful-fs.js') || @@ -69,6 +71,8 @@ module.exports = function (packagedAppPath) { relativePath === path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js') || relativePath === path.join('..', 'node_modules', 'tree-view', 'node_modules', 'minimatch', 'minimatch.js') ) + fs.appendFileSync('snapshot-files.txt', `${relativePath} = ${result}\n`) + return result } }).then((snapshotScript) => { fs.writeFileSync(snapshotScriptPath, snapshotScript) diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index db3876d6b..274b30cde 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -1,3 +1,5 @@ +/** @babel */ + const path = require('path') // Private: re-join the segments split from an absolute path to form another absolute path. diff --git a/src/path-watcher.js b/src/path-watcher.js index 17dab6d67..77eb87ecf 100644 --- a/src/path-watcher.js +++ b/src/path-watcher.js @@ -1,3 +1,5 @@ +/** @babel */ + const fs = require('fs') const path = require('path') From d57346f23ac118a51afb818bb380dacc75d6b347 Mon Sep 17 00:00:00 2001 From: Alan Yee Date: Wed, 2 Aug 2017 08:35:15 -0700 Subject: [PATCH 212/314] Update README.md Fix grammar error --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 16c5f9eda..c33244fbc 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Download the latest [Atom release](https://github.com/atom/atom/releases/latest) Atom will automatically update when a new release is available. -Using (Homebrew)[https://brew.sh]? Run to `brew cask install atom` install the latest version of Atom. +Using (Homebrew)[https://brew.sh]? Run `brew cask install atom` to install the latest version of Atom. ### Windows From 1f567137028dbef68ca22749219a31d6435abaa9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 2 Aug 2017 11:50:07 -0400 Subject: [PATCH 213/314] Un-exclude nsfw --- script/lib/generate-startup-snapshot.js | 1 - 1 file changed, 1 deletion(-) diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index 3f504b22a..6fe7b519c 100644 --- a/script/lib/generate-startup-snapshot.js +++ b/script/lib/generate-startup-snapshot.js @@ -28,7 +28,6 @@ module.exports = function (packagedAppPath) { coreModules.has(modulePath) || (relativePath.startsWith(path.join('..', 'src')) && relativePath.endsWith('-element.js')) || relativePath.startsWith(path.join('..', 'node_modules', 'dugite')) || - relativePath.startsWith(path.join('..', 'node_modules', 'nsfw')) || relativePath === path.join('..', 'exports', 'atom.js') || relativePath === path.join('..', 'src', 'electron-shims.js') || relativePath === path.join('..', 'src', 'safe-clipboard.js') || From 931b4e70554e64bbc2039de45b625af0ad1bf0f4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 2 Aug 2017 12:33:06 -0400 Subject: [PATCH 214/314] :shirt: lint lint lint --- src/native-watcher-registry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index 274b30cde..a779f78f5 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -3,7 +3,7 @@ const path = require('path') // Private: re-join the segments split from an absolute path to form another absolute path. -function absolute(...parts) { +function absolute (...parts) { const candidate = path.join(...parts) return path.isAbsolute(candidate) ? candidate : path.join(path.sep, candidate) } From 9b6eff2d81c63d0ed63217ecb65383e5de0752d8 Mon Sep 17 00:00:00 2001 From: Alan Yee Date: Wed, 2 Aug 2017 09:36:22 -0700 Subject: [PATCH 215/314] Update README.md Removing Homebrew install instructions --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index c33244fbc..c39634de5 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,6 @@ Download the latest [Atom release](https://github.com/atom/atom/releases/latest) Atom will automatically update when a new release is available. -Using (Homebrew)[https://brew.sh]? Run `brew cask install atom` to install the latest version of Atom. - ### Windows Download the latest [Atom installer](https://github.com/atom/atom/releases/latest). AtomSetup.exe is 32-bit, AtomSetup-x64.exe for 64-bit systems. From b9080bcec551c14073d30ff41ab8cf3c98420036 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 2 Aug 2017 12:59:38 -0400 Subject: [PATCH 216/314] Don't use "path" as a variable name --- spec/project-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 4e144fe58..75fc2caf8 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -562,7 +562,7 @@ describe "Project", -> sub.dispose() waitForEvents = (paths) -> - remaining = new Set(fs.realpathSync(path) for path in paths) + remaining = new Set(fs.realpathSync(p) for p in paths) new Promise (resolve, reject) -> checkCallback = -> remaining.delete(event.path) for event in events From 1e739aa891df66d65cd5208056a3e77083470e5e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 2 Aug 2017 14:55:12 -0400 Subject: [PATCH 217/314] Prepend a drive root to lookup paths on Windows --- spec/native-watcher-registry-spec.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/spec/native-watcher-registry-spec.js b/spec/native-watcher-registry-spec.js index 5cd4225ef..da361dbb7 100644 --- a/spec/native-watcher-registry-spec.js +++ b/spec/native-watcher-registry-spec.js @@ -20,11 +20,16 @@ function findRootDirectory () { } const ROOT = findRootDirectory() -function absolute(...parts) { +function absolute (...parts) { const candidate = path.join(...parts) return path.isAbsolute(candidate) ? candidate : path.join(ROOT, candidate) } +function prependRoot (...parts) { + const candidate = path.join(...parts) + return path.isAbsolute(candidate) ? parts : [ROOT].concat(parts) +} + class MockWatcher { constructor (normalizedPath) { this.normalizedPath = normalizedPath @@ -277,19 +282,19 @@ describe('NativeWatcherRegistry', function () { expect(childWatcher0.native).toBe(CHILD0) expect(childWatcher1.native).toBe(CHILD1) - expect(registry.tree.root.lookup(['parent']).when({ + expect(registry.tree.root.lookup(prependRoot('parent')).when({ parent: () => false, missing: () => false, children: () => true })).toBe(true) - expect(registry.tree.root.lookup(['parent', 'child0']).when({ + expect(registry.tree.root.lookup(prependRoot('parent', 'child0')).when({ parent: () => true, missing: () => false, children: () => false })).toBe(true) - expect(registry.tree.root.lookup(['parent', 'child1']).when({ + expect(registry.tree.root.lookup(prependRoot('parent', 'child1')).when({ parent: () => true, missing: () => false, children: () => false @@ -336,19 +341,19 @@ describe('NativeWatcherRegistry', function () { expect(childWatcher0.native).toBe(CHILD0) expect(childWatcher1.native).toBe(CHILD0) - expect(registry.tree.root.lookup(['parent']).when({ + expect(registry.tree.root.lookup(prependRoot('parent')).when({ parent: () => false, missing: () => false, children: () => true })).toBe(true) - expect(registry.tree.root.lookup(['parent', 'child0']).when({ + expect(registry.tree.root.lookup(prependRoot('parent', 'child0')).when({ parent: () => true, missing: () => false, children: () => false })).toBe(true) - expect(registry.tree.root.lookup(['parent', 'child0', 'child1']).when({ + expect(registry.tree.root.lookup(prependRoot('parent', 'child0', 'child1')).when({ parent: () => true, missing: () => false, children: () => false From 88c4d383fb2933c5444707fa2ce52e058054d458 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Wed, 2 Aug 2017 15:36:02 -0400 Subject: [PATCH 218/314] :arrow_up: Upgrade tree-view to 0.217.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 70941d7aa..18dca18a9 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "symbols-view": "0.117.0", "tabs": "0.106.2", "timecop": "0.36.0", - "tree-view": "0.217.5", + "tree-view": "0.217.6", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", "whitespace": "0.37.2", From 219eabfa50e4d9030860248ab49e3f504a59ef3e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 2 Aug 2017 16:29:36 -0400 Subject: [PATCH 219/314] Use jasmine-reporters 1.x --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c2c57e58e..bee7a8068 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "glob": "^7.1.1", "grim": "1.5.0", "jasmine-json": "~0.0", - "jasmine-reporters": "^2.2.1", + "jasmine-reporters": "1.1.0", "jasmine-tagged": "^1.1.4", "key-path-helpers": "^0.4.0", "less-cache": "1.1.0", From 977b337b4c6cfd4429ad34aec477b9cb60b8968c Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 2 Aug 2017 16:45:46 -0400 Subject: [PATCH 220/314] :arrow_up: language-php@0.41.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 70941d7aa..7911b7676 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "language-mustache": "0.14.1", "language-objective-c": "0.15.1", "language-perl": "0.37.0", - "language-php": "0.40.0", + "language-php": "0.41.0", "language-property-list": "0.9.1", "language-python": "0.45.4", "language-ruby": "0.71.2", From e90441303d29b486bc17238627b1bc0e18799908 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 2 Aug 2017 19:34:44 -0400 Subject: [PATCH 221/314] :fire: console.logs --- spec/jasmine-test-runner.coffee | 4 ---- spec/main-process/mocha-test-runner.js | 1 - 2 files changed, 5 deletions(-) diff --git a/spec/jasmine-test-runner.coffee b/spec/jasmine-test-runner.coffee index ba6246d8e..e6c594cef 100644 --- a/spec/jasmine-test-runner.coffee +++ b/spec/jasmine-test-runner.coffee @@ -9,11 +9,7 @@ module.exports = ({logFile, headless, testPaths, buildAtomEnvironment}) -> if process.env.TEST_JUNIT_XML_PATH require 'jasmine-reporters' - console.log "Producing JUnit XML output at #{process.env.TEST_JUNIT_XML_PATH}." jasmine.getEnv().addReporter new jasmine.JUnitXmlReporter(process.env.TEST_JUNIT_XML_PATH, true, true) - else - console.log "TEST_JUNIT_XML_PATH was falsy" - console.log require('util').inspect process.env # Allow document.title to be assigned in specs without screwing up spec window title documentTitle = null diff --git a/spec/main-process/mocha-test-runner.js b/spec/main-process/mocha-test-runner.js index d1634fd72..433727c56 100644 --- a/spec/main-process/mocha-test-runner.js +++ b/spec/main-process/mocha-test-runner.js @@ -12,7 +12,6 @@ export default function (testPaths) { } if (process.env.TEST_JUNIT_XML_PATH) { - console.log(`Mocha: Producing JUnit XML output at ${process.env.TEST_JUNIT_XML_PATH}.`) reporterOptions = { reporterEnabled: 'list, mocha-junit-reporter', mochaJunitReporterReporterOptions: { From e5139874d0f37fde5aba713367263de547ba69d6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 2 Aug 2017 20:08:20 -0400 Subject: [PATCH 222/314] :fire: dumping the test environment --- script/test | 2 -- 1 file changed, 2 deletions(-) diff --git a/script/test b/script/test index d4d850f55..2f22b1e0a 100755 --- a/script/test +++ b/script/test @@ -53,8 +53,6 @@ function runCoreMainProcessTests (callback) { ] const testEnv = Object.assign({}, prepareEnv('core-main-process'), {ATOM_GITHUB_INLINE_GIT_EXEC: 'true'}) - console.log(`test environment: ${require('util').inspect(testEnv)}`) - console.log('Executing core main process tests'.bold.green) const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit', env: testEnv}) cp.on('error', error => { callback(error) }) From 158418996230fcdf77c1d458162894d7d2b76ed4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 2 Aug 2017 16:06:52 -0600 Subject: [PATCH 223/314] Ensure custom decoration elements fill their container --- spec/text-editor-component-spec.js | 14 ++++++++++++++ src/text-editor-component.js | 15 ++++++++++++--- static/text-editor.less | 1 - 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index cb21f68a3..1ed7d6657 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1737,6 +1737,8 @@ describe('TextEditorComponent', () => { const marker3 = editor.markScreenRange([[9, 0], [12, 0]]) const decorationElement1 = document.createElement('div') const decorationElement2 = document.createElement('div') + // Packages may adopt this class name for decorations to be styled the same as line numbers + decorationElement2.className = 'line-number' const decoration1 = gutterA.decorateMarker(marker1, {class: 'a'}) const decoration2 = gutterA.decorateMarker(marker2, {class: 'b', item: decorationElement1}) @@ -1755,11 +1757,22 @@ describe('TextEditorComponent', () => { expect(decorationNode2.getBoundingClientRect().top).toBe(clientTopForLine(component, 6)) expect(decorationNode2.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 8)) expect(decorationNode2.firstChild).toBe(decorationElement1) + expect(decorationElement1.offsetHeight).toBe(decorationNode2.offsetHeight) + expect(decorationElement1.offsetWidth).toBe(decorationNode2.offsetWidth) expect(decorationNode3.className).toBe('decoration') expect(decorationNode3.getBoundingClientRect().top).toBe(clientTopForLine(component, 9)) expect(decorationNode3.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 12) + component.getLineHeight()) expect(decorationNode3.firstChild).toBe(decorationElement2) + expect(decorationElement2.offsetHeight).toBe(decorationNode3.offsetHeight) + expect(decorationElement2.offsetWidth).toBe(decorationNode3.offsetWidth) + + // Inline styled height is updated when line height changes + element.style.fontSize = parseInt(getComputedStyle(element).fontSize) + 10 + 'px' + TextEditor.didUpdateStyles() + await component.getNextUpdatePromise() + expect(decorationElement1.offsetHeight).toBe(decorationNode2.offsetHeight) + expect(decorationElement2.offsetHeight).toBe(decorationNode3.offsetHeight) decoration1.setProperties({type: 'gutter', gutterName: 'a', class: 'c', item: decorationElement1}) decoration2.setProperties({type: 'gutter', gutterName: 'a'}) @@ -1767,6 +1780,7 @@ describe('TextEditorComponent', () => { await component.getNextUpdatePromise() expect(decorationNode1.className).toBe('decoration c') expect(decorationNode1.firstChild).toBe(decorationElement1) + expect(decorationElement1.offsetHeight).toBe(decorationNode1.offsetHeight) expect(decorationNode2.className).toBe('decoration') expect(decorationNode2.firstChild).toBeNull() expect(gutterB.getElement().firstChild.children.length).toBe(0) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index bffa5db50..e403a1e8f 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -3263,7 +3263,10 @@ class CustomGutterDecorationComponent { this.element.style.top = top + 'px' this.element.style.height = height + 'px' if (className != null) this.element.className = className - if (element != null) this.element.appendChild(element) + if (element != null) { + this.element.appendChild(element) + element.style.height = height + 'px' + } } update (newProps) { @@ -3271,11 +3274,17 @@ class CustomGutterDecorationComponent { this.props = newProps if (newProps.top !== oldProps.top) this.element.style.top = newProps.top + 'px' - if (newProps.height !== oldProps.height) this.element.style.height = newProps.height + 'px' + if (newProps.height !== oldProps.height) { + this.element.style.height = newProps.height + 'px' + if (newProps.element) newProps.element.style.height = newProps.height + 'px' + } if (newProps.className !== oldProps.className) this.element.className = newProps.className || '' if (newProps.element !== oldProps.element) { if (this.element.firstChild) this.element.firstChild.remove() - if (newProps.element != null) this.element.appendChild(newProps.element) + if (newProps.element != null) { + this.element.appendChild(newProps.element) + newProps.element.style.height = newProps.height + 'px' + } } } } diff --git a/static/text-editor.less b/static/text-editor.less index 8917d8dd1..ac40ffe65 100644 --- a/static/text-editor.less +++ b/static/text-editor.less @@ -52,7 +52,6 @@ atom-text-editor { } .line-number { - width: min-content; padding-left: .5em; white-space: nowrap; opacity: 0.6; From b5bdf4acb9f1fbd1775d94cc4d696cc396fcef7e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 2 Aug 2017 21:03:29 -0600 Subject: [PATCH 224/314] Don't enable electron logging by default --- atom.sh | 7 +++---- resources/win/atom.cmd | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/atom.sh b/atom.sh index 6b0e94430..b36938bc5 100755 --- a/atom.sh +++ b/atom.sh @@ -31,6 +31,9 @@ while getopts ":wtfvh-:" opt; do foreground|benchmark|benchmark-test|test) EXPECT_OUTPUT=1 ;; + enable-electron-logging) + export ELECTRON_ENABLE_LOGGING=1 + ;; esac ;; w) @@ -50,10 +53,6 @@ if [ $REDIRECT_STDERR ]; then exec 2> /dev/null fi -if [ $EXPECT_OUTPUT ]; then - export ELECTRON_ENABLE_LOGGING=1 -fi - if [ $OS == 'Mac' ]; then if [ -L "$0" ]; then SCRIPT="$(readlink "$0")" diff --git a/resources/win/atom.cmd b/resources/win/atom.cmd index 43ec8ebe3..40f07b872 100644 --- a/resources/win/atom.cmd +++ b/resources/win/atom.cmd @@ -5,16 +5,17 @@ SET WAIT= SET PSARGS=%* FOR %%a IN (%*) DO ( - IF /I "%%a"=="-f" SET EXPECT_OUTPUT=YES - IF /I "%%a"=="--foreground" SET EXPECT_OUTPUT=YES - IF /I "%%a"=="-h" SET EXPECT_OUTPUT=YES - IF /I "%%a"=="--help" SET EXPECT_OUTPUT=YES - IF /I "%%a"=="-t" SET EXPECT_OUTPUT=YES - IF /I "%%a"=="--test" SET EXPECT_OUTPUT=YES - IF /I "%%a"=="--benchmark" SET EXPECT_OUTPUT=YES - IF /I "%%a"=="--benchmark-test" SET EXPECT_OUTPUT=YES - IF /I "%%a"=="-v" SET EXPECT_OUTPUT=YES - IF /I "%%a"=="--version" SET EXPECT_OUTPUT=YES + IF /I "%%a"=="-f" SET EXPECT_OUTPUT=YES + IF /I "%%a"=="--foreground" SET EXPECT_OUTPUT=YES + IF /I "%%a"=="-h" SET EXPECT_OUTPUT=YES + IF /I "%%a"=="--help" SET EXPECT_OUTPUT=YES + IF /I "%%a"=="-t" SET EXPECT_OUTPUT=YES + IF /I "%%a"=="--test" SET EXPECT_OUTPUT=YES + IF /I "%%a"=="--benchmark" SET EXPECT_OUTPUT=YES + IF /I "%%a"=="--benchmark-test" SET EXPECT_OUTPUT=YES + IF /I "%%a"=="-v" SET EXPECT_OUTPUT=YES + IF /I "%%a"=="--version" SET EXPECT_OUTPUT=YES + IF /I "%%a"=="--enable-electron-logging" SET ELECTRON_ENABLE_LOGGING=YES IF /I "%%a"=="-w" ( SET EXPECT_OUTPUT=YES SET WAIT=YES @@ -26,7 +27,6 @@ FOR %%a IN (%*) DO ( ) IF "%EXPECT_OUTPUT%"=="YES" ( - SET ELECTRON_ENABLE_LOGGING=YES IF "%WAIT%"=="YES" ( powershell -noexit "Start-Process -FilePath \"%~dp0\..\..\atom.exe\" -ArgumentList \"--pid=$pid $env:PSARGS\" ; wait-event" exit 0 From 55748bd2c4a4ea70d4b541eec51e6b5fb293e599 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 2 Aug 2017 21:12:02 -0600 Subject: [PATCH 225/314] Document enable-electron-logging option --- src/main-process/parse-command-line.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main-process/parse-command-line.js b/src/main-process/parse-command-line.js index 3f9f2523a..7531e609b 100644 --- a/src/main-process/parse-command-line.js +++ b/src/main-process/parse-command-line.js @@ -55,6 +55,7 @@ module.exports = function parseCommandLine (processArgs) { options.string('socket-path') options.string('user-data-dir') options.boolean('clear-window-state').describe('clear-window-state', 'Delete all Atom environment state.') + options.boolean('enable-electron-logging').describe('enable-electron-logging', 'Enable low-level logging messages from Electron.') const args = options.argv From b6a3c5c6d21ac5d5ca5e9337986b63f5b7da377c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 3 Aug 2017 08:51:44 -0400 Subject: [PATCH 226/314] Consistently split paths in test cases --- spec/native-watcher-registry-spec.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/spec/native-watcher-registry-spec.js b/spec/native-watcher-registry-spec.js index da361dbb7..bc657f496 100644 --- a/spec/native-watcher-registry-spec.js +++ b/spec/native-watcher-registry-spec.js @@ -25,9 +25,8 @@ function absolute (...parts) { return path.isAbsolute(candidate) ? candidate : path.join(ROOT, candidate) } -function prependRoot (...parts) { - const candidate = path.join(...parts) - return path.isAbsolute(candidate) ? parts : [ROOT].concat(parts) +function parts (fullPath) { + return fullPath.split(path.sep).filter(part => part.length > 0) } class MockWatcher { @@ -282,19 +281,19 @@ describe('NativeWatcherRegistry', function () { expect(childWatcher0.native).toBe(CHILD0) expect(childWatcher1.native).toBe(CHILD1) - expect(registry.tree.root.lookup(prependRoot('parent')).when({ + expect(registry.tree.root.lookup(parts(parentDir)).when({ parent: () => false, missing: () => false, children: () => true })).toBe(true) - expect(registry.tree.root.lookup(prependRoot('parent', 'child0')).when({ + expect(registry.tree.root.lookup(parts(childDir0)).when({ parent: () => true, missing: () => false, children: () => false })).toBe(true) - expect(registry.tree.root.lookup(prependRoot('parent', 'child1')).when({ + expect(registry.tree.root.lookup(parts(childDir1)).when({ parent: () => true, missing: () => false, children: () => false @@ -341,19 +340,19 @@ describe('NativeWatcherRegistry', function () { expect(childWatcher0.native).toBe(CHILD0) expect(childWatcher1.native).toBe(CHILD0) - expect(registry.tree.root.lookup(prependRoot('parent')).when({ + expect(registry.tree.root.lookup(parts(parentDir)).when({ parent: () => false, missing: () => false, children: () => true })).toBe(true) - expect(registry.tree.root.lookup(prependRoot('parent', 'child0')).when({ + expect(registry.tree.root.lookup(parts(childDir0)).when({ parent: () => true, missing: () => false, children: () => false })).toBe(true) - expect(registry.tree.root.lookup(prependRoot('parent', 'child0', 'child1')).when({ + expect(registry.tree.root.lookup(parts(childDir1)).when({ parent: () => true, missing: () => false, children: () => false From 3e3ab737483fed9175b3468666fa65c1d07ac454 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 3 Aug 2017 07:48:22 -0700 Subject: [PATCH 227/314] Use distinct names for spec directories --- spec/project-spec.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 75fc2caf8..6c849cab1 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -577,10 +577,10 @@ describe "Project", -> setTimeout expire, 2000 it "reports filesystem changes within project paths", -> - dirOne = temp.mkdirSync('atom-spec-project') + dirOne = temp.mkdirSync('atom-spec-project-one') fileOne = path.join(dirOne, 'file-one.txt') fileTwo = path.join(dirOne, 'file-two.txt') - dirTwo = temp.mkdirSync('atom-spec-project') + dirTwo = temp.mkdirSync('atom-spec-project-two') fileThree = path.join(dirTwo, 'file-three.txt') atom.project.setPaths([dirOne]) From 3deb26b6a08619f54a21e756392c39a66155647e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 3 Aug 2017 11:52:15 -0400 Subject: [PATCH 228/314] console.log debugging --- spec/project-spec.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 6c849cab1..16b88893f 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -583,12 +583,14 @@ describe "Project", -> dirTwo = temp.mkdirSync('atom-spec-project-two') fileThree = path.join(dirTwo, 'file-three.txt') + console.log "Setting project paths to #{dirOne}" atom.project.setPaths([dirOne]) waitsForPromise -> atom.project.watchersByPath[dirOne].getStartPromise() runs -> expect(atom.project.watchersByPath[dirTwo]).toEqual undefined + console.log "Watching #{dirOne} but not #{dirTwo}" fs.writeFileSync fileThree, "three\n" fs.writeFileSync fileTwo, "two\n" @@ -597,6 +599,7 @@ describe "Project", -> waitsForPromise -> waitForEvents [fileOne, fileTwo] runs -> + console.log "Events seen:\n#{require('util').inspect events}" expect(events.some (event) -> event.path is fileThree).toBeFalsy() describe ".onDidAddBuffer()", -> From 29810c6cd1924e08126bd18d759b80abfd1c199c Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 3 Aug 2017 12:24:56 -0400 Subject: [PATCH 229/314] Attempt to fix flaky test re: blinking cursor As shown in #15122, this test sometimes fails in CI with the following error: TextEditorComponent rendering it blinks cursors when the editor is focused and the cursors are not moving Expected '0' to be '1'. at it (C:\projects\atom\spec\text-editor-component-spec.js:414:49) Expected '0' to be '1'. at it (C:\projects\atom\spec\text-editor-component-spec.js:415:49) I *think* this might be a case of overspecification in the test's assertions. Prior to this commit, the test expected the blinking cursor to *start* in the visible state, and then transition to the invisible state. When we see the failure above, I suspect that the cursor has already transitioned from the visible state to the invisible state by the time the assertion runs. Since the test aims to verify that the cursor blinks, it seems like we should focus on the blinking, and not worry about the *initial* state of the cursor. This commit removes the assertions that verify the initial state of the cursor, and instead asserts that the cursor toggles between the visible and the invisible state. --- spec/text-editor-component-spec.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 1ed7d6657..b12ec23bb 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -411,26 +411,28 @@ describe('TextEditorComponent', () => { await component.getNextUpdatePromise() const [cursor1, cursor2] = element.querySelectorAll('.cursor') - expect(getComputedStyle(cursor1).opacity).toBe('1') - expect(getComputedStyle(cursor2).opacity).toBe('1') - - await conditionPromise(() => - getComputedStyle(cursor1).opacity === '0' && getComputedStyle(cursor2).opacity === '0' - ) - await conditionPromise(() => getComputedStyle(cursor1).opacity === '1' && getComputedStyle(cursor2).opacity === '1' ) - await conditionPromise(() => getComputedStyle(cursor1).opacity === '0' && getComputedStyle(cursor2).opacity === '0' ) + await conditionPromise(() => + getComputedStyle(cursor1).opacity === '1' && getComputedStyle(cursor2).opacity === '1' + ) editor.moveRight() await component.getNextUpdatePromise() - expect(getComputedStyle(cursor1).opacity).toBe('1') - expect(getComputedStyle(cursor2).opacity).toBe('1') + await conditionPromise(() => + getComputedStyle(cursor1).opacity === '0' && getComputedStyle(cursor2).opacity === '0' + ) + await conditionPromise(() => + getComputedStyle(cursor1).opacity === '1' && getComputedStyle(cursor2).opacity === '1' + ) + await conditionPromise(() => + getComputedStyle(cursor1).opacity === '0' && getComputedStyle(cursor2).opacity === '0' + ) }) it('gives cursors at the end of lines the width of an "x" character', async () => { From dedf5193cd9615366515055f6a2254fb9d7c0ccd Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 3 Aug 2017 13:11:09 -0400 Subject: [PATCH 230/314] Does stderr work there... ? --- spec/project-spec.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 16b88893f..47de4c11e 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -583,14 +583,14 @@ describe "Project", -> dirTwo = temp.mkdirSync('atom-spec-project-two') fileThree = path.join(dirTwo, 'file-three.txt') - console.log "Setting project paths to #{dirOne}" + process.stderr.write "Setting project paths to #{dirOne}\n" atom.project.setPaths([dirOne]) waitsForPromise -> atom.project.watchersByPath[dirOne].getStartPromise() runs -> expect(atom.project.watchersByPath[dirTwo]).toEqual undefined - console.log "Watching #{dirOne} but not #{dirTwo}" + process.stderr.write "Watching #{dirOne} but not #{dirTwo}\n" fs.writeFileSync fileThree, "three\n" fs.writeFileSync fileTwo, "two\n" @@ -599,7 +599,7 @@ describe "Project", -> waitsForPromise -> waitForEvents [fileOne, fileTwo] runs -> - console.log "Events seen:\n#{require('util').inspect events}" + process.stderr.write "Events seen:\n#{require('util').inspect events}\n" expect(events.some (event) -> event.path is fileThree).toBeFalsy() describe ".onDidAddBuffer()", -> From 18c5a31fbc9061b3f8fb5deff38262152196115b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 3 Aug 2017 11:53:36 -0600 Subject: [PATCH 231/314] Unset ELECTRON_ENABLE_LOGGING on Windows unless explicitly requested --- resources/win/atom.cmd | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/win/atom.cmd b/resources/win/atom.cmd index 40f07b872..07b9933cb 100644 --- a/resources/win/atom.cmd +++ b/resources/win/atom.cmd @@ -3,6 +3,7 @@ SET EXPECT_OUTPUT= SET WAIT= SET PSARGS=%* +SET ELECTRON_ENABLE_LOGGING= FOR %%a IN (%*) DO ( IF /I "%%a"=="-f" SET EXPECT_OUTPUT=YES From 654cb26819f419fa179a4348333935b5dd26ce3e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 3 Aug 2017 14:20:24 -0400 Subject: [PATCH 232/314] Only run render process tests on Windows for the moment --- script/test | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/test b/script/test index 2f22b1e0a..bf3a53361 100755 --- a/script/test +++ b/script/test @@ -154,7 +154,8 @@ function testSuitesForPlatform (platform) { case 'darwin': return [runCoreMainProcessTests, runCoreRenderProcessTests, runBenchmarkTests].concat(packageTestSuites) case 'win32': - return (process.arch === 'x64') ? [runCoreMainProcessTests, runCoreRenderProcessTests] : [runCoreMainProcessTests] + // return (process.arch === 'x64') ? [runCoreMainProcessTests, runCoreRenderProcessTests] : [runCoreMainProcessTests] + return (process.arch === 'x64') ? [runCoreRenderProcessTests] : [] case 'linux': return [runCoreMainProcessTests] default: From 263adde3776fe9f1aca58903be5d999f8219e5e1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 3 Aug 2017 14:22:54 -0400 Subject: [PATCH 233/314] Use async cleanup to avoid ENOTEMPTY on Windows --- spec/project-spec.coffee | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 47de4c11e..3d1ec0369 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -14,7 +14,13 @@ describe "Project", -> waits(1) afterEach -> - temp.cleanupSync() + waitsForPromise -> + new Promise (resolve, reject) -> + temp.cleanup err -> + if err? + reject(err) + else + resolve() describe "serialization", -> deserializedProject = null From bb91bb58e5646165ee88973c2ab405364e331522 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 3 Aug 2017 14:30:40 -0400 Subject: [PATCH 234/314] Okay fine let's do this the dumb way --- script/test | 10 +++++++++- spec/project-spec.coffee | 9 ++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/script/test b/script/test index bf3a53361..bbfda912b 100755 --- a/script/test +++ b/script/test @@ -69,7 +69,15 @@ function runCoreRenderProcessTests (callback) { console.log('Executing core render process tests'.bold.green) const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit', env: testEnv}) - cp.on('error', error => { callback(error) }) + cp.on('error', error => { + try { + const projectSpecLog = fs.readFileSync('project-spec.log', {encoding: 'utf8'}) + console.log(`project-spec log:\n${projectSpecLog}\n`) + } catch (e) { + console.error(`Unable to open log file:\n${e.stack}`) + } + callback(error) + }) cp.on('close', exitCode => { callback(null, exitCode) }) } diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 3d1ec0369..5dcaaeddd 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -6,6 +6,9 @@ path = require 'path' {Directory} = require 'pathwatcher' GitRepository = require '../src/git-repository' +logToFile = text -> + fs.appendFileSync 'project-spec.log', text + describe "Project", -> beforeEach -> atom.project.setPaths([atom.project.getDirectories()[0]?.resolve('dir')]) @@ -589,14 +592,14 @@ describe "Project", -> dirTwo = temp.mkdirSync('atom-spec-project-two') fileThree = path.join(dirTwo, 'file-three.txt') - process.stderr.write "Setting project paths to #{dirOne}\n" + logToFile "Setting project paths to #{dirOne}\n" atom.project.setPaths([dirOne]) waitsForPromise -> atom.project.watchersByPath[dirOne].getStartPromise() runs -> expect(atom.project.watchersByPath[dirTwo]).toEqual undefined - process.stderr.write "Watching #{dirOne} but not #{dirTwo}\n" + logToFile "Watching #{dirOne} but not #{dirTwo}\n" fs.writeFileSync fileThree, "three\n" fs.writeFileSync fileTwo, "two\n" @@ -605,7 +608,7 @@ describe "Project", -> waitsForPromise -> waitForEvents [fileOne, fileTwo] runs -> - process.stderr.write "Events seen:\n#{require('util').inspect events}\n" + logToFile "Events seen:\n#{require('util').inspect events}\n" expect(events.some (event) -> event.path is fileThree).toBeFalsy() describe ".onDidAddBuffer()", -> From 94c91c57b1879e3c200d17f884062f2af1953d6d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 3 Aug 2017 15:32:18 -0400 Subject: [PATCH 235/314] Explicitly put the logfile in ${HOME} --- script/test | 2 +- spec/project-spec.coffee | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/script/test b/script/test index bbfda912b..5aa3dd2b8 100755 --- a/script/test +++ b/script/test @@ -71,7 +71,7 @@ function runCoreRenderProcessTests (callback) { const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit', env: testEnv}) cp.on('error', error => { try { - const projectSpecLog = fs.readFileSync('project-spec.log', {encoding: 'utf8'}) + const projectSpecLog = fs.readFileSync(path.join(process.env.HOME, 'project-spec.log'), {encoding: 'utf8'}) console.log(`project-spec log:\n${projectSpecLog}\n`) } catch (e) { console.error(`Unable to open log file:\n${e.stack}`) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 5dcaaeddd..97f1f0417 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -6,8 +6,8 @@ path = require 'path' {Directory} = require 'pathwatcher' GitRepository = require '../src/git-repository' -logToFile = text -> - fs.appendFileSync 'project-spec.log', text +logToFile = (text) -> + fs.appendFileSync path.join(process.env.HOME, 'project-spec.log'), text describe "Project", -> beforeEach -> @@ -19,7 +19,7 @@ describe "Project", -> afterEach -> waitsForPromise -> new Promise (resolve, reject) -> - temp.cleanup err -> + temp.cleanup (err) -> if err? reject(err) else From 83f022b6189ed02d19ada6d3805b1a8ed9e29882 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 3 Aug 2017 11:48:13 -0400 Subject: [PATCH 236/314] Add a fileSystemWatcher config key to use as a feature flag --- src/config-schema.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/config-schema.js b/src/config-schema.js index 39f058555..fb0164766 100644 --- a/src/config-schema.js +++ b/src/config-schema.js @@ -308,6 +308,21 @@ const configSchema = { description: 'Warn before opening files larger than this number of megabytes.', type: 'number', default: 40 + }, + fileSystemWatcher: { + description: 'Choose the underlying implementation used to watch for filesystem changes. Emulating changes will miss any events caused by applications other than Atom, but may help prevent crashes or freezes.', + type: 'string', + default: 'native', + enum: [ + { + value: 'native', + description: 'Native operating system APIs' + }, + { + value: 'atom', + description: 'Emulated with Atom events' + } + ] } } }, From 418fe48bad161ba16883b1d37a53f87ade573629 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 3 Aug 2017 11:48:36 -0400 Subject: [PATCH 237/314] Emulate a "filesystem watcher" by subscribing to Atom events --- src/path-watcher.js | 198 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 174 insertions(+), 24 deletions(-) diff --git a/src/path-watcher.js b/src/path-watcher.js index 77eb87ecf..a12bb9944 100644 --- a/src/path-watcher.js +++ b/src/path-watcher.js @@ -23,6 +23,144 @@ const WATCHER_STATE = { STOPPING: Symbol('stopping') } +// Private: Emulate a "filesystem watcher" by subscribing to Atom events like buffers being saved. This will miss +// any changes made to files outside of Atom, but it also has no overhead. +class AtomBackend { + async start (rootPath, eventCallback, errorCallback) { + const getRealPath = givenPath => { + return new Promise(resolve => { + fs.realpath(givenPath, (err, resolvedPath) => { + err ? resolve(null) : resolve(resolvedPath) + }) + }) + } + + this.subs = new CompositeDisposable() + + this.subs.add(atom.workspace.observeTextEditors(async editor => { + let realPath = await getRealPath(editor.getPath()) + if (!realPath || !realPath.startsWith(rootPath)) { + return + } + + const announce = (type, oldPath) => { + const payload = {type, path: realPath} + if (oldPath) payload.oldPath = oldPath + eventCallback([payload]) + } + + const buffer = editor.getBuffer() + + this.subs.add(buffer.onDidConflict(() => announce('modified'))) + this.subs.add(buffer.onDidReload(() => announce('modified'))) + this.subs.add(buffer.onDidSave(event => { + if (event.path === realPath) { + announce('modified') + } else { + const oldPath = realPath + realPath = event.path + announce('renamed', oldPath) + } + })) + + this.subs.add(buffer.onDidDelete(() => announce('deleted'))) + + this.subs.add(buffer.onDidChangePath(newPath => { + if (newPath !== realPath) { + const oldPath = realPath + realPath = newPath + announce('renamed', oldPath) + } + })) + })) + + // Giant-ass brittle hack to hook files (and eventually directories) created from the TreeView. + const treeViewPackage = await atom.packages.getLoadedPackage('tree-view') + if (!treeViewPackage) return + await treeViewPackage.activationPromise + const treeViewModule = treeViewPackage.mainModule + if (!treeViewModule) return + const treeView = treeViewModule.getTreeViewInstance() + + const isOpenInEditor = async eventPath => { + const openPaths = await Promise.all( + atom.workspace.getTextEditors().map(editor => getRealPath(editor.getPath())) + ) + return openPaths.includes(eventPath) + } + + this.subs.add(treeView.onFileCreated(async event => { + const realPath = await getRealPath(event.path) + if (!realPath) return + + eventCallback([{type: 'added', path: realPath}]) + })) + + this.subs.add(treeView.onEntryDeleted(async event => { + const realPath = await getRealPath(event.path) + if (!realPath || isOpenInEditor(realPath)) return + + eventCallback([{type: 'deleted', path: realPath}]) + })) + + this.subs.add(treeView.onEntryMoved(async event => { + const [realNewPath, realOldPath] = await Promise.all([ + getRealPath(event.newPath), + getRealPath(event.initialPath) + ]) + if (!realNewPath || !realOldPath || isOpenInEditor(realNewPath) || isOpenInEditor(realOldPath)) return + + eventCallback([{type: 'renamed', path: realNewPath, oldPath: realOldPath}]) + })) + } + + async stop () { + this.subs && this.subs.dispose() + } +} + +// Private: Implement a native watcher by translating events from an NSFW watcher. +class NSFWBackend { + async start (rootPath, eventCallback, errorCallback) { + const handler = events => { + eventCallback(events.map(event => { + const type = ACTION_MAP.get(event.action) || `unexpected (${event.action})` + const payload = {type} + + if (event.file) { + payload.path = path.join(event.directory, event.file) + } else { + payload.oldPath = path.join(event.directory, event.oldFile) + payload.path = path.join(event.directory, event.newFile) + } + + return payload + })) + } + + this.watcher = await nsfw( + rootPath, + handler, + {debounceMS: 100, errorCallback} + ) + + await this.watcher.start() + } + + stop () { + return this.watcher.stop() + } +} + +// Private: Map configuration settings from the feature flag to backend implementations. +const BACKENDS = { + atom: AtomBackend, + native: NSFWBackend +} + +// Private: the backend implementation to fall back to if the config setting is invalid. +const DEFAULT_BACKEND = BACKENDS.nsfw + // Private: Interface with and normalize events from a native OS filesystem watcher. class NativeWatcher { @@ -32,9 +170,39 @@ class NativeWatcher { constructor (normalizedPath) { this.normalizedPath = normalizedPath this.emitter = new Emitter() + this.subs = new CompositeDisposable() - this.watcher = null + this.backend = null this.state = WATCHER_STATE.STOPPED + + this.onEvents = this.onEvents.bind(this) + this.onError = this.onError.bind(this) + + this.subs.add(atom.config.onDidChange('core.fileSystemWatcher', async () => { + if (this.state === WATCHER_STATE.STARTING) { + // Wait for this watcher to finish starting. + await new Promise(resolve => { + const sub = this.onDidStart(() => { + sub.dispose() + resolve() + }) + }) + } + + // Re-read the config setting in case it's changed again while we were waiting for the watcher + // to start. + const Backend = this.getCurrentBackend() + if (this.state === WATCHER_STATE.RUNNING && !(this.backend instanceof Backend)) { + await this.stop() + await this.start() + } + })) + } + + // Private: Read the `core.fileSystemWatcher` setting to determine the filesystem backend to use. + getCurrentBackend () { + const setting = atom.config.get('core.fileSystemWatcher') + return BACKENDS[setting] || DEFAULT_BACKEND } // Private: Begin watching for filesystem events. @@ -46,16 +214,10 @@ class NativeWatcher { } this.state = WATCHER_STATE.STARTING - this.watcher = await nsfw( - this.normalizedPath, - this.onEvents.bind(this), - { - debounceMS: 100, - errorCallback: this.onError.bind(this) - } - ) + const Backend = this.getCurrentBackend() - await this.watcher.start() + this.backend = new Backend() + await this.backend.start(this.normalizedPath, this.onEvents, this.onError) this.state = WATCHER_STATE.RUNNING this.emitter.emit('did-start') @@ -137,7 +299,7 @@ class NativeWatcher { this.state = WATCHER_STATE.STOPPING this.emitter.emit('will-stop') - await this.watcher.stop() + await this.backend.stop() this.state = WATCHER_STATE.STOPPED this.emitter.emit('did-stop') @@ -153,19 +315,7 @@ class NativeWatcher { // // * `events` An Array of filesystem events. onEvents (events) { - this.emitter.emit('did-change', events.map(event => { - const type = ACTION_MAP.get(event.action) || `unexpected (${event.action})` - const payload = {type} - - if (event.file) { - payload.path = path.join(event.directory, event.file) - } else { - payload.oldPath = path.join(event.directory, event.oldFile) - payload.path = path.join(event.directory, event.newFile) - } - - return payload - })) + this.emitter.emit('did-change', events) } // Private: Callback function invoked by the native watcher when an error occurs. From 15a055b6cd118baaa0f4f3cd01107500aa73e78b Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 3 Aug 2017 17:36:59 -0400 Subject: [PATCH 238/314] Revert some of the changes from 29810c6cd1 xref: https://github.com/atom/atom/pull/15154#discussion_r131270263 --- spec/text-editor-component-spec.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index b12ec23bb..481602871 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -424,15 +424,8 @@ describe('TextEditorComponent', () => { editor.moveRight() await component.getNextUpdatePromise() - await conditionPromise(() => - getComputedStyle(cursor1).opacity === '0' && getComputedStyle(cursor2).opacity === '0' - ) - await conditionPromise(() => - getComputedStyle(cursor1).opacity === '1' && getComputedStyle(cursor2).opacity === '1' - ) - await conditionPromise(() => - getComputedStyle(cursor1).opacity === '0' && getComputedStyle(cursor2).opacity === '0' - ) + expect(getComputedStyle(cursor1).opacity).toBe('1') + expect(getComputedStyle(cursor2).opacity).toBe('1') }) it('gives cursors at the end of lines the width of an "x" character', async () => { From 095f6da379168f7c8676f1f45fe0237a39984c91 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 3 Aug 2017 18:15:39 -0400 Subject: [PATCH 239/314] Default to the appveyor home dir --- script/test | 2 +- spec/project-spec.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/script/test b/script/test index 5aa3dd2b8..0146123a1 100755 --- a/script/test +++ b/script/test @@ -71,7 +71,7 @@ function runCoreRenderProcessTests (callback) { const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit', env: testEnv}) cp.on('error', error => { try { - const projectSpecLog = fs.readFileSync(path.join(process.env.HOME, 'project-spec.log'), {encoding: 'utf8'}) + const projectSpecLog = fs.readFileSync(path.join(process.env.HOME || 'C:\\Users\\appveyor', 'project-spec.log'), {encoding: 'utf8'}) console.log(`project-spec log:\n${projectSpecLog}\n`) } catch (e) { console.error(`Unable to open log file:\n${e.stack}`) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 97f1f0417..6e5a8c048 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -7,7 +7,7 @@ path = require 'path' GitRepository = require '../src/git-repository' logToFile = (text) -> - fs.appendFileSync path.join(process.env.HOME, 'project-spec.log'), text + fs.appendFileSync path.join(process.env.HOME || 'C:\\Users\\appveyor', 'project-spec.log'), text describe "Project", -> beforeEach -> From a84694fac114648b6e1c5b70730a2106b00eeabf Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 3 Aug 2017 18:21:11 -0400 Subject: [PATCH 240/314] Stop watchers in an afterEach block --- spec/project-spec.coffee | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 6e5a8c048..66c5417a1 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -4,6 +4,7 @@ Project = require '../src/project' fs = require 'fs-plus' path = require 'path' {Directory} = require 'pathwatcher' +{stopAllWatchers} = require '../src/path-watcher' GitRepository = require '../src/git-repository' logToFile = (text) -> @@ -17,13 +18,8 @@ describe "Project", -> waits(1) afterEach -> - waitsForPromise -> - new Promise (resolve, reject) -> - temp.cleanup (err) -> - if err? - reject(err) - else - resolve() + waitsForPromise -> stopAllWatchers() + runs -> temp.cleanupSync() describe "serialization", -> deserializedProject = null From 7b61d4f62f150ede30a57ed84ce93846d96d242a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 3 Aug 2017 18:21:24 -0400 Subject: [PATCH 241/314] Log a few more things --- spec/project-spec.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 66c5417a1..02cd31875 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -560,6 +560,7 @@ describe "Project", -> beforeEach -> sub = atom.project.onDidChangeFiles (incoming) -> + logToFile "Events received: #{require('util').inspect incoming}" events.push incoming... checkCallback() @@ -597,6 +598,7 @@ describe "Project", -> expect(atom.project.watchersByPath[dirTwo]).toEqual undefined logToFile "Watching #{dirOne} but not #{dirTwo}\n" + logToFile "Writing #{fileOne}, #{fileTwo}, #{fileThree}" fs.writeFileSync fileThree, "three\n" fs.writeFileSync fileTwo, "two\n" fs.writeFileSync fileOne, "one\n" From dc9cb76fa4b6dc10471191b2af6758e0f2c49f75 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 3 Aug 2017 19:07:52 -0400 Subject: [PATCH 242/314] tfw your diagnostic tests don't even run because of a linter error --- script/test | 2 +- spec/project-spec.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/script/test b/script/test index 0146123a1..f02d26214 100755 --- a/script/test +++ b/script/test @@ -71,7 +71,7 @@ function runCoreRenderProcessTests (callback) { const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit', env: testEnv}) cp.on('error', error => { try { - const projectSpecLog = fs.readFileSync(path.join(process.env.HOME || 'C:\\Users\\appveyor', 'project-spec.log'), {encoding: 'utf8'}) + const projectSpecLog = fs.readFileSync(path.join(process.env.HOME or 'C:\\Users\\appveyor', 'project-spec.log'), {encoding: 'utf8'}) console.log(`project-spec log:\n${projectSpecLog}\n`) } catch (e) { console.error(`Unable to open log file:\n${e.stack}`) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 02cd31875..91c902e2d 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -8,7 +8,7 @@ path = require 'path' GitRepository = require '../src/git-repository' logToFile = (text) -> - fs.appendFileSync path.join(process.env.HOME || 'C:\\Users\\appveyor', 'project-spec.log'), text + fs.appendFileSync path.join(process.env.HOME or 'C:\\Users\\appveyor', 'project-spec.log'), text describe "Project", -> beforeEach -> From 08a7fab4f917b53cff604543ef067ad1d5480547 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 3 Aug 2017 19:49:50 -0400 Subject: [PATCH 243/314] Grrr --- script/test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/test b/script/test index f02d26214..0146123a1 100755 --- a/script/test +++ b/script/test @@ -71,7 +71,7 @@ function runCoreRenderProcessTests (callback) { const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit', env: testEnv}) cp.on('error', error => { try { - const projectSpecLog = fs.readFileSync(path.join(process.env.HOME or 'C:\\Users\\appveyor', 'project-spec.log'), {encoding: 'utf8'}) + const projectSpecLog = fs.readFileSync(path.join(process.env.HOME || 'C:\\Users\\appveyor', 'project-spec.log'), {encoding: 'utf8'}) console.log(`project-spec log:\n${projectSpecLog}\n`) } catch (e) { console.error(`Unable to open log file:\n${e.stack}`) From 3b57d2a259fc18984d8bcae0323ecf28bc7fda9a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 3 Aug 2017 20:29:01 -0400 Subject: [PATCH 244/314] Let's see if we're still green without diagnostics! --- script/test | 3 +-- spec/project-spec.coffee | 8 -------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/script/test b/script/test index 0146123a1..b61b1aca4 100755 --- a/script/test +++ b/script/test @@ -162,8 +162,7 @@ function testSuitesForPlatform (platform) { case 'darwin': return [runCoreMainProcessTests, runCoreRenderProcessTests, runBenchmarkTests].concat(packageTestSuites) case 'win32': - // return (process.arch === 'x64') ? [runCoreMainProcessTests, runCoreRenderProcessTests] : [runCoreMainProcessTests] - return (process.arch === 'x64') ? [runCoreRenderProcessTests] : [] + return (process.arch === 'x64') ? [runCoreMainProcessTests, runCoreRenderProcessTests] : [runCoreMainProcessTests] case 'linux': return [runCoreMainProcessTests] default: diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 91c902e2d..aa074bbe2 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -7,9 +7,6 @@ path = require 'path' {stopAllWatchers} = require '../src/path-watcher' GitRepository = require '../src/git-repository' -logToFile = (text) -> - fs.appendFileSync path.join(process.env.HOME or 'C:\\Users\\appveyor', 'project-spec.log'), text - describe "Project", -> beforeEach -> atom.project.setPaths([atom.project.getDirectories()[0]?.resolve('dir')]) @@ -560,7 +557,6 @@ describe "Project", -> beforeEach -> sub = atom.project.onDidChangeFiles (incoming) -> - logToFile "Events received: #{require('util').inspect incoming}" events.push incoming... checkCallback() @@ -589,16 +585,13 @@ describe "Project", -> dirTwo = temp.mkdirSync('atom-spec-project-two') fileThree = path.join(dirTwo, 'file-three.txt') - logToFile "Setting project paths to #{dirOne}\n" atom.project.setPaths([dirOne]) waitsForPromise -> atom.project.watchersByPath[dirOne].getStartPromise() runs -> expect(atom.project.watchersByPath[dirTwo]).toEqual undefined - logToFile "Watching #{dirOne} but not #{dirTwo}\n" - logToFile "Writing #{fileOne}, #{fileTwo}, #{fileThree}" fs.writeFileSync fileThree, "three\n" fs.writeFileSync fileTwo, "two\n" fs.writeFileSync fileOne, "one\n" @@ -606,7 +599,6 @@ describe "Project", -> waitsForPromise -> waitForEvents [fileOne, fileTwo] runs -> - logToFile "Events seen:\n#{require('util').inspect events}\n" expect(events.some (event) -> event.path is fileThree).toBeFalsy() describe ".onDidAddBuffer()", -> From ffb3b0b4624ce6a52e609d5242a085d57334fc27 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 4 Aug 2017 10:37:01 -0400 Subject: [PATCH 245/314] Missed the logfile reporting --- script/test | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/script/test b/script/test index b61b1aca4..2f22b1e0a 100755 --- a/script/test +++ b/script/test @@ -69,15 +69,7 @@ function runCoreRenderProcessTests (callback) { console.log('Executing core render process tests'.bold.green) const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit', env: testEnv}) - cp.on('error', error => { - try { - const projectSpecLog = fs.readFileSync(path.join(process.env.HOME || 'C:\\Users\\appveyor', 'project-spec.log'), {encoding: 'utf8'}) - console.log(`project-spec log:\n${projectSpecLog}\n`) - } catch (e) { - console.error(`Unable to open log file:\n${e.stack}`) - } - callback(error) - }) + cp.on('error', error => { callback(error) }) cp.on('close', exitCode => { callback(null, exitCode) }) } From ea91723b36735b2ce042e4f0dbb26e166cd59b3c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 4 Aug 2017 10:37:41 -0400 Subject: [PATCH 246/314] Only deal with watcher stopping in the onDidChangeFiles spec --- spec/project-spec.coffee | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index aa074bbe2..059208cbd 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -14,10 +14,6 @@ describe "Project", -> # Wait for project's service consumers to be asynchronously added waits(1) - afterEach -> - waitsForPromise -> stopAllWatchers() - runs -> temp.cleanupSync() - describe "serialization", -> deserializedProject = null @@ -585,8 +581,10 @@ describe "Project", -> dirTwo = temp.mkdirSync('atom-spec-project-two') fileThree = path.join(dirTwo, 'file-three.txt') - atom.project.setPaths([dirOne]) + # Ensure that all preexisting watchers are stopped + waitsForPromise -> stopAllWatchers() + runs -> atom.project.setPaths([dirOne]) waitsForPromise -> atom.project.watchersByPath[dirOne].getStartPromise() runs -> From 662e2aaf06a170004ebffc7c3d4fa17546b4ae12 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 4 Aug 2017 12:55:25 -0400 Subject: [PATCH 247/314] Revisit a bunch of documentation. --- src/path-watcher.js | 42 +++++++++++++++++++++++++++++++++++------- src/project.coffee | 21 +++++++++++++++++---- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/path-watcher.js b/src/path-watcher.js index a12bb9944..bbe412aad 100644 --- a/src/path-watcher.js +++ b/src/path-watcher.js @@ -326,14 +326,44 @@ class NativeWatcher { } } -// Extended: Manage a subscription to filesystem events that occur beneath a root directory. Construct these by calling -// {watchPath}. +// Extended: Manage a subscription to filesystem events that occur beneath a root directory. Don't instantiate these +// directly. Instead, construct them by calling `watchPath`, or use them indirectly through {Project::onDidChangeFiles} +// instead if you only need to watch for events within active project directories. // -// Maybe PathWatchers may be backed by the same native watcher to conserve operation system resources. Native watchers are -// started when at least one subscription is registered, and stopped when all subscriptions are disposed. +// Multiple PathWatchers may be backed by a single native watcher to conserve operation system resources. Native +// watchers are started when at least one subscription is registered, and stopped when all subscriptions are disposed. // -// Acts as a {Disposable}. +// Compatible with the {Disposable} protocol. When disposed, no more events will be delivered. // +// Arguments accepted by `watchPath`: +// +// * `rootPath` {String} specifies the absolute path to the root of the filesystem content to watch. +// * `options` Control the watcher's behavior. Currently a placeholder. +// * `eventCallback` {Function} or other callable to be called each time a batch of filesystem events is observed. +// * `events` {Array} of objects that describe the events that have occurred. +// * `type` {String} describing the filesystem action that occurred. One of `"created"`, `"modified"`, `"deleted"`, +// or `"renamed"`. +// * `path` {String} containing the absolute path to the filesystem entry that was acted upon. +// * `oldPath` For rename events, {String} containing the filesystem entry's former absolute path. +// +// ```js +// const {watchPath} = require('atom') +// +// const disposable = watchPath('/var/log', {}, events => { +// console.log(`Received batch of ${events.length} events.`) +// for (const event of events) { +// console.log(`Event action: ${event.type}`) // "created", "modified", "deleted", "renamed" +// console.log(`Event path: ${event.path}`) // absolute path to the filesystem entry that was touched +// if (event.type === 'renamed') { +// console.log(`.. renamed from: ${event.oldPath}`) +// } +// } +// }) +// +// // Immediately stop receiving filesystem events. If this is the last watcher, asynchronously release any OS +// // resources required to subscribe to these events. +// disposable.dispose() +// ``` class PathWatcher { // Private: Instantiate a new PathWatcher. Call {watchPath} instead. @@ -557,8 +587,6 @@ class PathWatcherManager { // // * `rootPath` {String} specifies the absolute path to the root of the filesystem content to watch. // * `options` Control the watcher's behavior. -// * `recursive` If true, passing the path to a directory will recursively watch all changes beneath that -// directory. If false, only the file or directory itself will be watched. // * `eventCallback` {Function} or other callable to be called each time a batch of filesystem events is observed. // * `events` {Array} of objects that describe the events that have occurred. // * `type` {String} describing the filesystem action that occurred. One of `"created"`, `"modified"`, `"deleted"`, diff --git a/src/project.coffee b/src/project.coffee index a28fc34a4..cdd09fa1b 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -117,10 +117,24 @@ class Project extends Model callback(buffer) for buffer in @getBuffers() @onDidAddBuffer callback - # Public: Invoke the given callback when any filesystem change occurs within an open + # Public: Invoke a callback when a filesystem change occurs within any open # project path. # - # To watch paths outside of open projects, use the {watchPaths} function. + # ```js + # const disposable = atom.project.onDidChangeFiles(events => { + # for (const event of events) { + # console.log(`Event action: ${event.type}`) // "created", "modified", "deleted", "renamed" + # console.log(`Event path: ${event.path}`) // absolute path to the filesystem entry that was touched + # if (event.type === 'renamed') { + # console.log(`.. renamed from: ${event.oldPath}`) + # } + # } + # } + # + # disposable.dispose() + # ``` + # + # To watch paths outside of open projects, use the [watchPaths]{PathWatcher} function instead. # # * `callback` {Function} to be called with batches of filesystem events reported by # the operating system. @@ -132,8 +146,7 @@ class Project extends Model # * `oldPath` For rename events, {String} containing the filesystem entry's # former absolute path. # - # Returns a {PathWatcher} that may be used like a {Disposable} to manage - # this event subscription. + # Returns a {Disposable} to manage this event subscription. onDidChangeFiles: (callback) -> @emitter.on 'did-change-files', callback From 77b0cac28b143dfc3a1283f3e535d73237af0a09 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Fri, 4 Aug 2017 10:11:48 -0700 Subject: [PATCH 248/314] :arrow_up: github@0.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d5f609512..3e8169227 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "exception-reporting": "0.41.4", "find-and-replace": "0.209.5", "fuzzy-finder": "1.5.8", - "github": "0.3.9-0", + "github": "0.4.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", From 97ffe46247d906d38ca13016d42f45a980fece9b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 4 Aug 2017 14:08:29 -0400 Subject: [PATCH 249/314] Consistently use require('temp').track() --- spec/main-process/atom-application.test.js | 3 ++- spec/title-bar-spec.js | 2 +- src/main-process/start.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index d4a913859..1e5ae0a86 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -508,7 +508,8 @@ describe('AtomApplication', function () { } function makeTempDir (name) { - return fs.realpathSync(require('temp').mkdirSync(name)) + const temp = require('temp').track() + return fs.realpathSync(temp.mkdirSync(name)) } let channelIdCounter = 0 diff --git a/spec/title-bar-spec.js b/spec/title-bar-spec.js index c0cb806b5..b219a5819 100644 --- a/spec/title-bar-spec.js +++ b/spec/title-bar-spec.js @@ -1,5 +1,5 @@ const TitleBar = require('../src/title-bar') -const temp = require('temp') +const temp = require('temp').track() describe('TitleBar', () => { it('updates its title when document.title changes', () => { diff --git a/src/main-process/start.js b/src/main-process/start.js index fae78a07e..9670e67b6 100644 --- a/src/main-process/start.js +++ b/src/main-process/start.js @@ -1,7 +1,7 @@ const {app} = require('electron') const nslog = require('nslog') const path = require('path') -const temp = require('temp') +const temp = require('temp').track() const parseCommandLine = require('./parse-command-line') const startCrashReporter = require('../crash-reporter-start') const atomPaths = require('../atom-paths') From dc9fe2525514d4fbaadccc7b4e920ff84224a4fd Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 4 Aug 2017 14:18:43 -0400 Subject: [PATCH 250/314] Wrap temp.cleanupSync() calls in try-catch blocks --- spec/atom-environment-spec.coffee | 3 ++- spec/atom-paths-spec.js | 6 +++++- spec/babel-spec.coffee | 3 ++- spec/command-installer-spec.coffee | 3 ++- spec/compile-cache-spec.coffee | 3 ++- spec/default-directory-provider-spec.coffee | 3 ++- spec/git-repository-provider-spec.coffee | 3 ++- spec/grammars-spec.coffee | 3 ++- spec/main-process/file-recovery-service.test.js | 6 +++++- spec/module-cache-spec.coffee | 3 ++- spec/package-manager-spec.coffee | 3 ++- spec/squirrel-update-spec.coffee | 3 ++- spec/style-manager-spec.js | 6 +++++- spec/theme-manager-spec.coffee | 3 ++- spec/update-process-env-spec.js | 6 +++++- spec/workspace-element-spec.js | 8 +++++++- spec/workspace-spec.js | 8 +++++++- 17 files changed, 56 insertions(+), 17 deletions(-) diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 5d269a3bb..8a3e4e0fb 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -6,7 +6,8 @@ StorageFolder = require '../src/storage-folder' describe "AtomEnvironment", -> afterEach -> - temp.cleanupSync() + try + temp.cleanupSync() describe 'window sizing methods', -> describe '::getPosition and ::setPosition', -> diff --git a/spec/atom-paths-spec.js b/spec/atom-paths-spec.js index 3e2da4760..f4bbbf2b7 100644 --- a/spec/atom-paths-spec.js +++ b/spec/atom-paths-spec.js @@ -86,7 +86,11 @@ describe("AtomPaths", () => { afterEach(() => { delete process.env.ATOM_HOME fs.removeSync(electronUserDataPath) - temp.cleanupSync() + try { + temp.cleanupSync() + } catch (e) { + // Ignore + } app.setPath('userData', defaultElectronUserDataPath) }) diff --git a/spec/babel-spec.coffee b/spec/babel-spec.coffee index 070ad7a0b..400e5c03e 100644 --- a/spec/babel-spec.coffee +++ b/spec/babel-spec.coffee @@ -19,7 +19,8 @@ describe "Babel transpiler support", -> afterEach -> CompileCache.setCacheDirectory(originalCacheDir) - temp.cleanupSync() + try + temp.cleanupSync() describe 'when a .js file starts with /** @babel */;', -> it "transpiles it using babel", -> diff --git a/spec/command-installer-spec.coffee b/spec/command-installer-spec.coffee index f0994fc08..dfd25a1df 100644 --- a/spec/command-installer-spec.coffee +++ b/spec/command-installer-spec.coffee @@ -21,7 +21,8 @@ describe "CommandInstaller on #darwin", -> spyOn(CommandInstaller::, 'getInstallDirectory').andReturn(installationPath) afterEach -> - temp.cleanupSync() + try + temp.cleanupSync() it "shows an error dialog when installing commands interactively fails", -> appDelegate = jasmine.createSpyObj("appDelegate", ["confirm"]) diff --git a/spec/compile-cache-spec.coffee b/spec/compile-cache-spec.coffee index 13db6a055..084f87e70 100644 --- a/spec/compile-cache-spec.coffee +++ b/spec/compile-cache-spec.coffee @@ -23,7 +23,8 @@ describe 'CompileCache', -> afterEach -> CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME) CSON.setCacheDir(CompileCache.getCacheDirectory()) - temp.cleanupSync() + try + temp.cleanupSync() describe 'addPathToCache(filePath, atomHome)', -> describe 'when the given file is plain javascript', -> diff --git a/spec/default-directory-provider-spec.coffee b/spec/default-directory-provider-spec.coffee index bf23195cf..35e40618c 100644 --- a/spec/default-directory-provider-spec.coffee +++ b/spec/default-directory-provider-spec.coffee @@ -10,7 +10,8 @@ describe "DefaultDirectoryProvider", -> tmp = temp.mkdirSync('atom-spec-default-dir-provider') afterEach -> - temp.cleanupSync() + try + temp.cleanupSync() describe ".directoryForURISync(uri)", -> it "returns a Directory with a path that matches the uri", -> diff --git a/spec/git-repository-provider-spec.coffee b/spec/git-repository-provider-spec.coffee index 6c6a7b4b9..16ccf8938 100644 --- a/spec/git-repository-provider-spec.coffee +++ b/spec/git-repository-provider-spec.coffee @@ -12,7 +12,8 @@ describe "GitRepositoryProvider", -> provider = new GitRepositoryProvider(atom.project, atom.config, atom.confirm) afterEach -> - temp.cleanupSync() + try + temp.cleanupSync() describe ".repositoryForDirectory(directory)", -> describe "when specified a Directory with a Git repository", -> diff --git a/spec/grammars-spec.coffee b/spec/grammars-spec.coffee index 47198a124..7d4754397 100644 --- a/spec/grammars-spec.coffee +++ b/spec/grammars-spec.coffee @@ -24,7 +24,8 @@ describe "the `grammars` global", -> afterEach -> atom.packages.deactivatePackages() atom.packages.unloadPackages() - temp.cleanupSync() + try + temp.cleanupSync() describe ".selectGrammar(filePath)", -> it "always returns a grammar", -> diff --git a/spec/main-process/file-recovery-service.test.js b/spec/main-process/file-recovery-service.test.js index 862b7f428..618c30ab0 100644 --- a/spec/main-process/file-recovery-service.test.js +++ b/spec/main-process/file-recovery-service.test.js @@ -16,7 +16,11 @@ describe("FileRecoveryService", () => { }) afterEach(() => { - temp.cleanupSync() + try { + temp.cleanupSync() + } catch (e) { + // Ignore + } }) describe("when no crash happens during a save", () => { diff --git a/spec/module-cache-spec.coffee b/spec/module-cache-spec.coffee index 1627ec776..693fd6634 100644 --- a/spec/module-cache-spec.coffee +++ b/spec/module-cache-spec.coffee @@ -9,7 +9,8 @@ describe 'ModuleCache', -> spyOn(Module, '_findPath').andCallThrough() afterEach -> - temp.cleanupSync() + try + temp.cleanupSync() it 'resolves Electron module paths without hitting the filesystem', -> builtins = ModuleCache.cache.builtins diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 62cc067e9..2d448ac7c 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -17,7 +17,8 @@ describe "PackageManager", -> spyOn(ModuleCache, 'add') afterEach -> - temp.cleanupSync() + try + temp.cleanupSync() describe "::getApmPath()", -> it "returns the path to the apm command", -> diff --git a/spec/squirrel-update-spec.coffee b/spec/squirrel-update-spec.coffee index 2838be297..fe0fa7479 100644 --- a/spec/squirrel-update-spec.coffee +++ b/spec/squirrel-update-spec.coffee @@ -37,7 +37,8 @@ describe "Windows Squirrel Update", -> WinShell.folderBackgroundContextMenu = new FakeShellOption() afterEach -> - temp.cleanupSync() + try + temp.cleanupSync() it "quits the app on all squirrel events", -> app = quit: jasmine.createSpy('quit') diff --git a/spec/style-manager-spec.js b/spec/style-manager-spec.js index e6b8acae6..641c93709 100644 --- a/spec/style-manager-spec.js +++ b/spec/style-manager-spec.js @@ -15,7 +15,11 @@ describe('StyleManager', () => { }) afterEach(() => { - temp.cleanupSync() + try { + temp.cleanupSync() + } catch (e) { + // Do nothing + } }) describe('::addStyleSheet(source, params)', () => { diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index 42ed9be6d..5d2912f5b 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -9,7 +9,8 @@ describe "atom.themes", -> afterEach -> atom.themes.deactivateThemes() - temp.cleanupSync() + try + temp.cleanupSync() describe "theme getters and setters", -> beforeEach -> diff --git a/spec/update-process-env-spec.js b/spec/update-process-env-spec.js index f730ae632..e5a1cfd9c 100644 --- a/spec/update-process-env-spec.js +++ b/spec/update-process-env-spec.js @@ -28,7 +28,11 @@ describe('updateProcessEnv(launchEnv)', function () { } process.env = originalProcessEnv process.platform = originalProcessPlatform - temp.cleanupSync() + try { + temp.cleanupSync() + } catch (e) { + // Do nothing + } }) describe('when the launch environment appears to come from a shell', function () { diff --git a/spec/workspace-element-spec.js b/spec/workspace-element-spec.js index aa5430c88..2e37d9903 100644 --- a/spec/workspace-element-spec.js +++ b/spec/workspace-element-spec.js @@ -9,7 +9,13 @@ const {Disposable} = require('event-kit') const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') describe('WorkspaceElement', () => { - afterEach(() => { temp.cleanupSync() }) + afterEach(() => { + try { + temp.cleanupSync() + } catch (e) { + // Do nothing + } + }) describe('when the workspace element is focused', () => { it('transfers focus to the active pane', () => { diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index 489b45120..bdd5677c8 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -25,7 +25,13 @@ describe('Workspace', () => { waitsForPromise(() => atom.workspace.itemLocationStore.clear()) }) - afterEach(() => temp.cleanupSync()) + afterEach(() => { + try { + temp.cleanupSync() + } catch (e) { + // Do nothing + } + }) function simulateReload() { waitsForPromise(() => { From 50f02495c0995ba84245f6156df200dbfe2875b9 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 4 Aug 2017 14:28:28 -0400 Subject: [PATCH 251/314] Attempt to fix flaky test re: flashing highlight decorations Based on the assertion failures seen in https://github.com/atom/atom/issues/15158#issue-247808059, it seems that the flash for class 'c' sometimes ends before the flash for class 'd' happens. Prior to this change, we only flashed class 'c' for 100ms, and perhaps that isn't always enough time. In this commit, we increase the flash duration from 100ms to 1000ms, greatly increasing the likelihood that we're allowing enough time for the flash on class 'd' to take place before the flash for class 'c' ends. We also extract the scenario into its own test, so that 1) we can more clearly explain the scenario that these assertions are testing and 2) future intermittent test failures will be easier to isolate. --- spec/text-editor-component-spec.js | 42 +++++++++++++++--------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 481602871..ad631b855 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1443,27 +1443,6 @@ describe('TextEditorComponent', () => { expect(highlights[0].classList.contains('b')).toBe(false) expect(highlights[1].classList.contains('b')).toBe(false) - // Flash existing highlight - decoration.flash('c', 100) - await component.getNextUpdatePromise() - expect(highlights[0].classList.contains('c')).toBe(true) - expect(highlights[1].classList.contains('c')).toBe(true) - - // Add second flash class - decoration.flash('d', 100) - await component.getNextUpdatePromise() - expect(highlights[0].classList.contains('c')).toBe(true) - expect(highlights[1].classList.contains('c')).toBe(true) - expect(highlights[0].classList.contains('d')).toBe(true) - expect(highlights[1].classList.contains('d')).toBe(true) - - await conditionPromise(() => - !highlights[0].classList.contains('c') && - !highlights[1].classList.contains('c') && - !highlights[0].classList.contains('d') && - !highlights[1].classList.contains('d') - ) - // Flashing the same class again before the first flash completes // removes the flash class and adds it back on the next frame to ensure // CSS transitions apply to the second flash. @@ -1488,6 +1467,27 @@ describe('TextEditorComponent', () => { ) }) + it("flashing a highlight decoration doesn't unflash other highlight decorations", async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 3, height: 200}) + const marker = editor.markScreenRange([[2, 4], [3, 4]]) + const decoration = editor.decorateMarker(marker, {type: 'highlight', class: 'a'}) + + // Flash one class + decoration.flash('c', 1000) + await component.getNextUpdatePromise() + const highlights = element.querySelectorAll('.highlight.a') + expect(highlights[0].classList.contains('c')).toBe(true) + expect(highlights[1].classList.contains('c')).toBe(true) + + // Flash another class while the previously-flashed class is still highlighted + decoration.flash('d', 100) + await component.getNextUpdatePromise() + expect(highlights[0].classList.contains('c')).toBe(true) + expect(highlights[1].classList.contains('c')).toBe(true) + expect(highlights[0].classList.contains('d')).toBe(true) + expect(highlights[1].classList.contains('d')).toBe(true) + }) + it('supports layer decorations', async () => { const {component, element, editor} = buildComponent({rowsPerTile: 12}) const markerLayer = editor.addMarkerLayer() From 23100536378e54b6ebdea907c38e381301adf7fb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 4 Aug 2017 14:34:06 -0400 Subject: [PATCH 252/314] Reword Project.onDidChangeFiles documentation --- src/project.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/project.coffee b/src/project.coffee index cdd09fa1b..47dc291c7 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -117,7 +117,7 @@ class Project extends Model callback(buffer) for buffer in @getBuffers() @onDidAddBuffer callback - # Public: Invoke a callback when a filesystem change occurs within any open + # Extended: Invoke a callback when a filesystem change occurs within any open # project path. # # ```js @@ -134,11 +134,11 @@ class Project extends Model # disposable.dispose() # ``` # - # To watch paths outside of open projects, use the [watchPaths]{PathWatcher} function instead. + # To watch paths outside of open projects, use the `watchPaths` function instead; see {PathWatcher}. # # * `callback` {Function} to be called with batches of filesystem events reported by # the operating system. - # * `events` An {Array} of objects the describe filesystem events. + # * `events` An {Array} of objects that describe a batch of filesystem events. # * `type` {String} describing the filesystem action that occurred. One of `"created"`, # `"modified"`, `"deleted"`, or `"renamed"`. # * `path` {String} containing the absolute path to the filesystem entry From 1d73f40d20d54b3cb368ea111ffb4c87f5e2733e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 4 Aug 2017 16:23:16 -0400 Subject: [PATCH 253/314] :arrow_up: joanna --- script/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/package.json b/script/package.json index 350fc2b0e..52f0b6d55 100644 --- a/script/package.json +++ b/script/package.json @@ -15,7 +15,7 @@ "electron-winstaller": "2.6.2", "fs-extra": "0.30.0", "glob": "7.0.3", - "joanna": "0.0.8", + "joanna": "0.0.9", "klaw-sync": "^1.1.2", "legal-eagle": "0.14.0", "lodash.template": "4.4.0", From ca28f8ac48de02ee9c1f2cbe7c85ecd9401b1112 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 4 Aug 2017 16:30:43 -0400 Subject: [PATCH 254/314] Fussing with documentation --- src/path-watcher.js | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/path-watcher.js b/src/path-watcher.js index bbe412aad..f1da920e1 100644 --- a/src/path-watcher.js +++ b/src/path-watcher.js @@ -335,25 +335,18 @@ class NativeWatcher { // // Compatible with the {Disposable} protocol. When disposed, no more events will be delivered. // -// Arguments accepted by `watchPath`: -// -// * `rootPath` {String} specifies the absolute path to the root of the filesystem content to watch. -// * `options` Control the watcher's behavior. Currently a placeholder. -// * `eventCallback` {Function} or other callable to be called each time a batch of filesystem events is observed. -// * `events` {Array} of objects that describe the events that have occurred. -// * `type` {String} describing the filesystem action that occurred. One of `"created"`, `"modified"`, `"deleted"`, -// or `"renamed"`. -// * `path` {String} containing the absolute path to the filesystem entry that was acted upon. -// * `oldPath` For rename events, {String} containing the filesystem entry's former absolute path. -// // ```js // const {watchPath} = require('atom') // // const disposable = watchPath('/var/log', {}, events => { // console.log(`Received batch of ${events.length} events.`) // for (const event of events) { -// console.log(`Event action: ${event.type}`) // "created", "modified", "deleted", "renamed" -// console.log(`Event path: ${event.path}`) // absolute path to the filesystem entry that was touched +// // "created", "modified", "deleted", "renamed" +// console.log(`Event action: ${event.type}`) +// +// // absolute path to the filesystem entry that was touched +// console.log(`Event path: ${event.path}`) +// // if (event.type === 'renamed') { // console.log(`.. renamed from: ${event.oldPath}`) // } @@ -364,6 +357,18 @@ class NativeWatcher { // // resources required to subscribe to these events. // disposable.dispose() // ``` +// +// `watchPath` accepts the following arguments: +// +// `rootPath` {String} specifies the absolute path to the root of the filesystem content to watch. +// `options` Control the watcher's behavior. Currently a placeholder. +// `eventCallback` {Function} or other callable to be called each time a batch of filesystem events is observed. +// * `events` {Array} of objects that describe the events that have occurred. +// * `type` {String} describing the filesystem action that occurred. One of `"created"`, `"modified"`, `"deleted"`, +// or `"renamed"`. +// * `path` {String} containing the absolute path to the filesystem entry that was acted upon. +// * `oldPath` For rename events, {String} containing the filesystem entry's former absolute path. +// class PathWatcher { // Private: Instantiate a new PathWatcher. Call {watchPath} instead. @@ -440,8 +445,9 @@ class PathWatcher { // Private: Attach another {Function} to be called with each batch of filesystem events. See {watchPath} for the // spec of the callback's argument. // - // Returns a {Disposable} that will stop the underlying watcher when all callbacks mapped to it have been disposed. + // * `callback` {Function} to be called with each batch of filesystem events. // + // Returns a {Disposable} that will stop the underlying watcher when all callbacks mapped to it have been disposed. onDidChange (callback) { if (this.native) { const sub = this.native.onDidChange(events => this.onNativeEvents(events, callback)) @@ -464,8 +470,10 @@ class PathWatcher { // Extended: Invoke a {Function} when any errors related to this watcher are reported. // - // Returns a {Disposable}. + // * `callback` {Function} to be called when an error occurs. + // * `err` An {Error} describing the failure condition. // + // Returns a {Disposable}. onDidError (callback) { return this.emitter.on('did-error', callback) } From 122d0f8363ecc3588dc6a6187fd5cbb6a7e47f94 Mon Sep 17 00:00:00 2001 From: Lee Dohm <1038121+lee-dohm@users.noreply.github.com> Date: Fri, 4 Aug 2017 14:17:14 -0700 Subject: [PATCH 255/314] Add support doc --- SUPPORT.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 SUPPORT.md diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 000000000..d908b3fff --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,11 @@ +# Atom Support + +If you're looking for support for Atom there are a lot of options, check out: + +* User Documentation — [The Atom Flight Manual](http://flight-manual.atom.io) +* Developer Documentation — [Atom API Documentation](https://atom.io/docs/api/latest) +* FAQ — [The Atom FAQ on Discuss](https://discuss.atom.io/c/faq) +* Message Board — [Discuss, the official Atom and Electron message board](https://discuss.atom.io) +* Chat — [Join the Atom Slack team](http://atom-slack.herokuapp.com/) + +On Discuss and in the Atom Slack team, there are a bunch of helpful community members that should be willing to point you in the right direction. From ca6ab3fae08f3b933f709346ba5236f9bffdc238 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sun, 6 Aug 2017 18:35:25 -0400 Subject: [PATCH 256/314] :arrow_up: settings-view@0.251.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3e8169227..7267a8dc7 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "notifications": "0.67.2", "open-on-github": "1.2.1", "package-generator": "1.1.1", - "settings-view": "0.251.2", + "settings-view": "0.251.3", "snippets": "1.1.4", "spell-check": "0.72.0", "status-bar": "1.8.11", From 8f01eeb7af4b304ff369cd84b9b1b28105bdd2c8 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sun, 6 Aug 2017 18:36:37 -0400 Subject: [PATCH 257/314] :arrow_up: language-todo@0.29.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7267a8dc7..2102a8d4b 100644 --- a/package.json +++ b/package.json @@ -162,7 +162,7 @@ "language-source": "0.9.0", "language-sql": "0.25.8", "language-text": "0.7.3", - "language-todo": "0.29.1", + "language-todo": "0.29.2", "language-toml": "0.18.1", "language-xml": "0.35.2", "language-yaml": "0.30.1" From ee0e5cc6ebd254daa6437ed78cd635a62dcd4f78 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sun, 6 Aug 2017 18:37:15 -0400 Subject: [PATCH 258/314] :arrow_up: language-ruby@0.71.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2102a8d4b..fc856d02e 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,7 @@ "language-php": "0.41.0", "language-property-list": "0.9.1", "language-python": "0.45.4", - "language-ruby": "0.71.2", + "language-ruby": "0.71.3", "language-ruby-on-rails": "0.25.2", "language-sass": "0.60.0", "language-shellscript": "0.25.2", From 224af13e6487b1540dfe92d36da90302c9c86cfe Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sun, 6 Aug 2017 18:38:09 -0400 Subject: [PATCH 259/314] :arrow_up: language-sass@0.61.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fc856d02e..2e2244102 100644 --- a/package.json +++ b/package.json @@ -157,7 +157,7 @@ "language-python": "0.45.4", "language-ruby": "0.71.3", "language-ruby-on-rails": "0.25.2", - "language-sass": "0.60.0", + "language-sass": "0.61.0", "language-shellscript": "0.25.2", "language-source": "0.9.0", "language-sql": "0.25.8", From b4125022b6720d9c8dff57b2dcd068d7510aa305 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sun, 6 Aug 2017 20:47:31 -0400 Subject: [PATCH 260/314] :arrow_up: settings-view@0.251.4 To fix a flaky test --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2e2244102..968548615 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "notifications": "0.67.2", "open-on-github": "1.2.1", "package-generator": "1.1.1", - "settings-view": "0.251.3", + "settings-view": "0.251.4", "snippets": "1.1.4", "spell-check": "0.72.0", "status-bar": "1.8.11", From 1aa013ca7a4418498cfcb15b644d2142a0247a0c Mon Sep 17 00:00:00 2001 From: Sergey Sharov Date: Mon, 7 Aug 2017 09:46:34 +0300 Subject: [PATCH 261/314] Fixed links in keymap.cson --- dot-atom/keymap.cson | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dot-atom/keymap.cson b/dot-atom/keymap.cson index 01ac18cdc..bfbadea95 100644 --- a/dot-atom/keymap.cson +++ b/dot-atom/keymap.cson @@ -18,7 +18,7 @@ # 'ctrl-p': 'core:move-down' # # You can find more information about keymaps in these guides: -# * http://flight-manual.atom.io/using-atom/sections/basic-customization/#_customizing_keybindings +# * http://flight-manual.atom.io/using-atom/sections/basic-customization/#customizing-keybindings # * http://flight-manual.atom.io/behind-atom/sections/keymaps-in-depth/ # # If you're having trouble with your keybindings not working, try the @@ -29,4 +29,4 @@ # This file uses CoffeeScript Object Notation (CSON). # If you are unfamiliar with CSON, you can read more about it in the # Atom Flight Manual: -# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson +# http://flight-manual.atom.io/using-atom/sections/basic-customization/#configuring-with-cson From f623b03157f9fdd0a00e87e33224db2078e472fc Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 7 Aug 2017 09:31:50 -0400 Subject: [PATCH 262/314] Documentation touchups --- src/path-watcher.js | 38 ++++++++++++++++++++------------------ src/project.coffee | 8 ++++++-- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/path-watcher.js b/src/path-watcher.js index f1da920e1..0294b6ec3 100644 --- a/src/path-watcher.js +++ b/src/path-watcher.js @@ -326,14 +326,15 @@ class NativeWatcher { } } -// Extended: Manage a subscription to filesystem events that occur beneath a root directory. Don't instantiate these -// directly. Instead, construct them by calling `watchPath`, or use them indirectly through {Project::onDidChangeFiles} -// instead if you only need to watch for events within active project directories. +// Extended: Manage a subscription to filesystem events that occur beneath a root directory. Construct these by +// calling `watchPath`. To watch for events within active project directories, use {Project::onDidChangeFiles} +// instead. // -// Multiple PathWatchers may be backed by a single native watcher to conserve operation system resources. Native -// watchers are started when at least one subscription is registered, and stopped when all subscriptions are disposed. +// Multiple PathWatchers may be backed by a single native watcher to conserve operation system resources. // -// Compatible with the {Disposable} protocol. When disposed, no more events will be delivered. +// Call {::dispose} to stop receiving events and, if possible, release underlying resources. A PathWatcher may be +// added to a {CompositeDisposable} to manage its lifetime along with other {Disposable} resources like event +// subscriptions. // // ```js // const {watchPath} = require('atom') @@ -353,22 +354,22 @@ class NativeWatcher { // } // }) // -// // Immediately stop receiving filesystem events. If this is the last watcher, asynchronously release any OS -// // resources required to subscribe to these events. +// // Immediately stop receiving filesystem events. If this is the last +// // watcher, asynchronously release any OS resources required to +// // subscribe to these events. // disposable.dispose() // ``` // // `watchPath` accepts the following arguments: // // `rootPath` {String} specifies the absolute path to the root of the filesystem content to watch. -// `options` Control the watcher's behavior. Currently a placeholder. -// `eventCallback` {Function} or other callable to be called each time a batch of filesystem events is observed. -// * `events` {Array} of objects that describe the events that have occurred. -// * `type` {String} describing the filesystem action that occurred. One of `"created"`, `"modified"`, `"deleted"`, -// or `"renamed"`. -// * `path` {String} containing the absolute path to the filesystem entry that was acted upon. -// * `oldPath` For rename events, {String} containing the filesystem entry's former absolute path. // +// `options` Control the watcher's behavior. Currently a placeholder. +// +// `eventCallback` {Function} to be called each time a batch of filesystem events is observed. Each event object has +// the keys: `type`, a {String} describing the filesystem action that occurred, one of `"created"`, `"modified"`, +// `"deleted"`, or `"renamed"`; `path`, a {String} containing the absolute path to the filesystem entry that was acted +// upon; for rename events only, `oldPath`, a {String} containing the filesystem entry's former absolute path. class PathWatcher { // Private: Instantiate a new PathWatcher. Call {watchPath} instead. @@ -433,7 +434,8 @@ class PathWatcher { // const watcher = watchPath(ROOT, {}, events => {}) // await watcher.getStartPromise() // fs.writeFile(FILE, 'contents\n', err => { - // // The watcher is listening and the event should be received asyncronously + // // The watcher is listening and the event should be + // // received asyncronously // } // }) // }) @@ -527,8 +529,8 @@ class PathWatcher { } } - // Extended: Unsubscribe all subscribers from filesystem events. The native watcher resources may take some time to - // be cleaned up, but the watcher will stop broadcasting events immediately. + // Extended: Unsubscribe all subscribers from filesystem events. Native resources will be release asynchronously, + // but this watcher will stop broadcasting events immediately. dispose () { for (const sub of this.changeCallbacks.values()) { sub.dispose() diff --git a/src/project.coffee b/src/project.coffee index 47dc291c7..4564e37bb 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -123,8 +123,12 @@ class Project extends Model # ```js # const disposable = atom.project.onDidChangeFiles(events => { # for (const event of events) { - # console.log(`Event action: ${event.type}`) // "created", "modified", "deleted", "renamed" - # console.log(`Event path: ${event.path}`) // absolute path to the filesystem entry that was touched + # // "created", "modified", "deleted", or "renamed" + # console.log(`Event action: ${event.type}`) + # + # // absolute path to the filesystem entry that was touched + # console.log(`Event path: ${event.path}`) + # # if (event.type === 'renamed') { # console.log(`.. renamed from: ${event.oldPath}`) # } From f270402c6b4554c4067807e71bc3233a82d7a7e4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 7 Aug 2017 10:03:36 -0400 Subject: [PATCH 263/314] s/type/action/, s/changed/modified/, s/added/created/ --- src/path-watcher.js | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/path-watcher.js b/src/path-watcher.js index 0294b6ec3..add392d7e 100644 --- a/src/path-watcher.js +++ b/src/path-watcher.js @@ -7,10 +7,10 @@ const {Emitter, Disposable, CompositeDisposable} = require('event-kit') const nsfw = require('nsfw') const {NativeWatcherRegistry} = require('./native-watcher-registry') -// Private: Associate native watcher action type flags with descriptive String equivalents. +// Private: Associate native watcher action action flags with descriptive String equivalents. const ACTION_MAP = new Map([ - [nsfw.actions.MODIFIED, 'changed'], - [nsfw.actions.CREATED, 'added'], + [nsfw.actions.MODIFIED, 'modified'], + [nsfw.actions.CREATED, 'created'], [nsfw.actions.DELETED, 'deleted'], [nsfw.actions.RENAMED, 'renamed'] ]) @@ -43,8 +43,8 @@ class AtomBackend { return } - const announce = (type, oldPath) => { - const payload = {type, path: realPath} + const announce = (action, oldPath) => { + const payload = {action, path: realPath} if (oldPath) payload.oldPath = oldPath eventCallback([payload]) } @@ -93,14 +93,14 @@ class AtomBackend { const realPath = await getRealPath(event.path) if (!realPath) return - eventCallback([{type: 'added', path: realPath}]) + eventCallback([{action: 'added', path: realPath}]) })) this.subs.add(treeView.onEntryDeleted(async event => { const realPath = await getRealPath(event.path) if (!realPath || isOpenInEditor(realPath)) return - eventCallback([{type: 'deleted', path: realPath}]) + eventCallback([{action: 'deleted', path: realPath}]) })) this.subs.add(treeView.onEntryMoved(async event => { @@ -110,7 +110,7 @@ class AtomBackend { ]) if (!realNewPath || !realOldPath || isOpenInEditor(realNewPath) || isOpenInEditor(realOldPath)) return - eventCallback([{type: 'renamed', path: realNewPath, oldPath: realOldPath}]) + eventCallback([{action: 'renamed', path: realNewPath, oldPath: realOldPath}]) })) } @@ -124,8 +124,8 @@ class NSFWBackend { async start (rootPath, eventCallback, errorCallback) { const handler = events => { eventCallback(events.map(event => { - const type = ACTION_MAP.get(event.action) || `unexpected (${event.action})` - const payload = {type} + const action = ACTION_MAP.get(event.action) || `unexpected (${event.action})` + const payload = {action} if (event.file) { payload.path = path.join(event.directory, event.file) @@ -343,12 +343,12 @@ class NativeWatcher { // console.log(`Received batch of ${events.length} events.`) // for (const event of events) { // // "created", "modified", "deleted", "renamed" -// console.log(`Event action: ${event.type}`) +// console.log(`Event action: ${event.action}`) // // // absolute path to the filesystem entry that was touched // console.log(`Event path: ${event.path}`) // -// if (event.type === 'renamed') { +// if (event.action === 'renamed') { // console.log(`.. renamed from: ${event.oldPath}`) // } // } @@ -367,7 +367,7 @@ class NativeWatcher { // `options` Control the watcher's behavior. Currently a placeholder. // // `eventCallback` {Function} to be called each time a batch of filesystem events is observed. Each event object has -// the keys: `type`, a {String} describing the filesystem action that occurred, one of `"created"`, `"modified"`, +// the keys: `action`, a {String} describing the filesystem action that occurred, one of `"created"`, `"modified"`, // `"deleted"`, or `"renamed"`; `path`, a {String} containing the absolute path to the filesystem entry that was acted // upon; for rename events only, `oldPath`, a {String} containing the filesystem entry's former absolute path. class PathWatcher { @@ -599,8 +599,8 @@ class PathWatcherManager { // * `options` Control the watcher's behavior. // * `eventCallback` {Function} or other callable to be called each time a batch of filesystem events is observed. // * `events` {Array} of objects that describe the events that have occurred. -// * `type` {String} describing the filesystem action that occurred. One of `"created"`, `"modified"`, `"deleted"`, -// or `"renamed"`. +// * `action` {String} describing the filesystem action that occurred. One of `"created"`, `"modified"`, +// `"deleted"`, or `"renamed"`. // * `path` {String} containing the absolute path to the filesystem entry that was acted upon. // * `oldPath` For rename events, {String} containing the filesystem entry's former absolute path. // @@ -613,9 +613,11 @@ class PathWatcherManager { // const disposable = watchPath('/var/log', {}, events => { // console.log(`Received batch of ${events.length} events.`) // for (const event of events) { -// console.log(`Event action: ${event.type}`) // "created", "modified", "deleted", "renamed" -// console.log(`Event path: ${event.path}`) // absolute path to the filesystem entry that was touched -// if (event.type === 'renamed') { +// // "created", "modified", "deleted", "renamed" +// console.log(`Event action: ${event.action}`) +// // absolute path to the filesystem entry that was touched +// console.log(`Event path: ${event.path}`) +// if (event.action === 'renamed') { // console.log(`.. renamed from: ${event.oldPath}`) // } // } From c7bfbc181c727ac026c75e5c2477014e7f97dbab Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 7 Aug 2017 09:50:10 -0400 Subject: [PATCH 264/314] :bug: Fix atom/tabs#461 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Linux, when the user performs a middle-button mouse click, Chromium fires both a mouse-down event *and* a paste event. This commit teaches the TextEditorComponent to ignore the paste event. When the user performs a middle-mouse click on a tab, we get close the tab and attempt to prevent Chromium's default processing for the event. [1] This prevents Chromium's default processing for the *mouse down* event, but then Chromium also fires a *paste* event, and that event pastes the clipboard's current content into the newly-focused text editor. :scream_cat: Since Atom already has its own logic for handling pasting, we shouldn't (🤞) need to handle browser paste events. By ignoring the browser paste events on Linux, we fix atom/tabs#461. [1] https://github.com/atom/tabs/blob/ce1d92e0abb669155caa178bb71166b9f16f329a/lib/tab-bar-view.coffee#L416-L418 --- spec/text-editor-component-spec.js | 32 ++++++++++++++++++++++++++---- src/text-editor-component.js | 14 +++++++++++-- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index ad631b855..34d09865e 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -2687,18 +2687,42 @@ describe('TextEditorComponent', () => { expect(component.getScrollLeft()).toBe(maxScrollLeft) }) - it('positions the cursor on clicking the middle mouse button on Linux', async () => { - // The browser synthesizes the paste as a textInput event on mouseup - // so it is not possible to test it here. + it('pastes the previously selected text when clicking the middle mouse button on Linux', async () => { + spyOn(electron.ipcRenderer, 'send').andCallFake(function (eventName, selectedText) { + if (eventName === 'write-text-to-selection-clipboard') { + clipboard.writeText(selectedText, 'selection') + } + }) + const {component, editor} = buildComponent({platform: 'linux'}) + // Middle mouse pasting. editor.setSelectedBufferRange([[1, 6], [1, 10]]) + await conditionPromise(() => TextEditor.clipboard.read() === 'sort') component.didMouseDownOnContent({ button: 1, clientX: clientLeftForCharacter(component, 10, 0), clientY: clientTopForLine(component, 10) }) - expect(editor.getSelectedBufferRange()).toEqual([[10, 0], [10, 0]]) + expect(TextEditor.clipboard.read()).toBe('sort') + expect(editor.lineTextForBufferRow(10)).toBe('sort') + editor.undo() + + // Ensure left clicks don't interfere. + editor.setSelectedBufferRange([[1, 2], [1, 5]]) + await conditionPromise(() => TextEditor.clipboard.read() === 'var') + component.didMouseDownOnContent({ + button: 0, + detail: 1, + clientX: clientLeftForCharacter(component, 10, 0), + clientY: clientTopForLine(component, 10) + }) + component.didMouseDownOnContent({ + button: 1, + clientX: clientLeftForCharacter(component, 10, 0), + clientY: clientTopForLine(component, 10) + }) + expect(editor.lineTextForBufferRow(10)).toBe('var') }) }) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index e403a1e8f..a910e1828 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -5,6 +5,7 @@ const {Point, Range} = require('text-buffer') const LineTopIndex = require('line-top-index') const TextEditor = require('./text-editor') const {isPairedCharacter} = require('./text-utils') +const clipboard = require('./safe-clipboard') const electron = require('electron') const $ = etch.dom @@ -641,6 +642,7 @@ class TextEditorComponent { didBlurHiddenInput: this.didBlurHiddenInput, didFocusHiddenInput: this.didFocusHiddenInput, didTextInput: this.didTextInput, + didPaste: this.didPaste, didKeydown: this.didKeydown, didKeyup: this.didKeyup, didKeypress: this.didKeypress, @@ -1549,6 +1551,11 @@ class TextEditorComponent { } } + didPaste (event) { + // TODO Explain the motivation for this logic + if ((this.props.platform || process.platform) === 'linux') event.preventDefault() + } + didTextInput (event) { if (!this.isInputEnabled()) return @@ -1654,8 +1661,10 @@ class TextEditorComponent { // textInput event with the contents of the selection clipboard will be // dispatched by the browser automatically on mouseup. if (platform === 'linux' && button === 1) { + const selection = clipboard.readText('selection') const screenPosition = this.screenPositionForMouseEvent(event) model.setCursorScreenPosition(screenPosition, {autoscroll: false}) + model.insertText(selection) return } @@ -3356,8 +3365,8 @@ class CursorsAndInputComponent { renderHiddenInput () { const { lineHeight, hiddenInputPosition, didBlurHiddenInput, didFocusHiddenInput, - didTextInput, didKeydown, didKeyup, didKeypress, didCompositionStart, - didCompositionUpdate, didCompositionEnd + didPaste, didTextInput, didKeydown, didKeyup, didKeypress, + didCompositionStart, didCompositionUpdate, didCompositionEnd } = this.props let top, left @@ -3376,6 +3385,7 @@ class CursorsAndInputComponent { on: { blur: didBlurHiddenInput, focus: didFocusHiddenInput, + paste: didPaste, textInput: didTextInput, keydown: didKeydown, keyup: didKeyup, From fc7ecb76d1bea7d7516a81fc19682b7a86f35cc2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 7 Aug 2017 11:13:03 -0400 Subject: [PATCH 265/314] :burn: double word --- src/path-watcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path-watcher.js b/src/path-watcher.js index add392d7e..001b17818 100644 --- a/src/path-watcher.js +++ b/src/path-watcher.js @@ -7,7 +7,7 @@ const {Emitter, Disposable, CompositeDisposable} = require('event-kit') const nsfw = require('nsfw') const {NativeWatcherRegistry} = require('./native-watcher-registry') -// Private: Associate native watcher action action flags with descriptive String equivalents. +// Private: Associate native watcher action flags with descriptive String equivalents. const ACTION_MAP = new Map([ [nsfw.actions.MODIFIED, 'modified'], [nsfw.actions.CREATED, 'created'], From 47420761f524429b649b67e12ab6708308554393 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 7 Aug 2017 11:32:57 -0400 Subject: [PATCH 266/314] =?UTF-8?q?=E2=9C=85=20Add=20basic=20test=20for=20?= =?UTF-8?q?TextEditorComponent::didPaste(event)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/text-editor-component-spec.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 34d09865e..3b2f2072a 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -3016,6 +3016,17 @@ describe('TextEditorComponent', () => { }) }) + describe('paste event', () => { + it("prevents the browser's default processing for the event on Linux", () => { + const {component} = buildComponent({platform: 'linux'}) + const event = { preventDefault: () => {} } + spyOn(event, 'preventDefault') + + component.didPaste(event) + expect(event.preventDefault).toHaveBeenCalled() + }) + }) + describe('keyboard input', () => { it('handles inserted accented characters via the press-and-hold menu on macOS correctly', () => { const {editor, component, element} = buildComponent({text: ''}) From b9ace6a5b66ae7f2311f56ec6468e24035d9789b Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 7 Aug 2017 12:31:04 -0400 Subject: [PATCH 267/314] :art: --- src/text-editor-component.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index a910e1828..3417fc929 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -70,6 +70,7 @@ class TextEditorComponent { this.updateSync = this.updateSync.bind(this) this.didBlurHiddenInput = this.didBlurHiddenInput.bind(this) this.didFocusHiddenInput = this.didFocusHiddenInput.bind(this) + this.didPaste = this.didPaste.bind(this) this.didTextInput = this.didTextInput.bind(this) this.didKeydown = this.didKeydown.bind(this) this.didKeyup = this.didKeyup.bind(this) @@ -1553,7 +1554,7 @@ class TextEditorComponent { didPaste (event) { // TODO Explain the motivation for this logic - if ((this.props.platform || process.platform) === 'linux') event.preventDefault() + if (this.getPlatform() === 'linux') event.preventDefault() } didTextInput (event) { From 3dc2e6199098c7a2defc53da7a3138c43862f154 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 7 Aug 2017 12:43:13 -0400 Subject: [PATCH 268/314] Add comment explaining motivation for didPaste implemenation --- src/text-editor-component.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 3417fc929..881aaad81 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1553,7 +1553,12 @@ class TextEditorComponent { } didPaste (event) { - // TODO Explain the motivation for this logic + // On Linux, Chromium translates a middle-button mouse click into a + // mousedown event *and* a paste event. Since Atom supports the middle mouse + // click as a way of closing a tab, we only want the mousedown event, not + // the paste event. And since we don't use the `paste` event for any + // behavior in Atom, we can no-op the event to eliminate this issue. + // See https://github.com/atom/atom/pull/15183#issue-248432413. if (this.getPlatform() === 'linux') event.preventDefault() } From 6fb6ac70a2bcdb264257cd0ee6b079160f591d71 Mon Sep 17 00:00:00 2001 From: Ian Olsen Date: Mon, 7 Aug 2017 10:21:20 -0700 Subject: [PATCH 269/314] 1.21.0-dev --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 968548615..de32f7622 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.20.0-dev", + "version": "1.21.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/main-process/main.js", "repository": { From 7b70b177fa4243db456f41bdc65a1435a61aea99 Mon Sep 17 00:00:00 2001 From: bene Date: Mon, 7 Aug 2017 22:05:12 +0200 Subject: [PATCH 270/314] :arrow_up: atom-keymap@8.2.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de32f7622..c7a7c6d01 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dependencies": { "@atom/source-map-support": "^0.3.4", "async": "0.2.6", - "atom-keymap": "8.2.2", + "atom-keymap": "8.2.3", "atom-select-list": "^0.1.0", "atom-ui": "0.4.1", "babel-core": "5.8.38", From d920d20c2f30bf30c2d12567eb0d9141530a6090 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 8 Aug 2017 12:42:01 -0400 Subject: [PATCH 271/314] :burn: diagnostic code --- script/lib/generate-startup-snapshot.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index 6fe7b519c..471bd1201 100644 --- a/script/lib/generate-startup-snapshot.js +++ b/script/lib/generate-startup-snapshot.js @@ -23,7 +23,7 @@ module.exports = function (packagedAppPath) { process.stdout.write(`Generating snapshot script at "${snapshotScriptPath}" (${++processedFiles})`) const relativePath = path.relative(baseDirPath, modulePath) - const result = ( + return ( modulePath.endsWith('.node') || coreModules.has(modulePath) || (relativePath.startsWith(path.join('..', 'src')) && relativePath.endsWith('-element.js')) || @@ -70,8 +70,6 @@ module.exports = function (packagedAppPath) { relativePath === path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js') || relativePath === path.join('..', 'node_modules', 'tree-view', 'node_modules', 'minimatch', 'minimatch.js') ) - fs.appendFileSync('snapshot-files.txt', `${relativePath} = ${result}\n`) - return result } }).then((snapshotScript) => { fs.writeFileSync(snapshotScriptPath, snapshotScript) From 4c4e5fa3a14a1d0a7e8613c7335d6568f775b528 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 8 Aug 2017 10:37:55 -0700 Subject: [PATCH 272/314] Reliability of Windows Installer process on appveyor.yml --- appveyor.yml | 6 +----- script/create-installer.cmd | 6 ++++++ 2 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 script/create-installer.cmd diff --git a/appveyor.yml b/appveyor.yml index 32a67e18e..6a8d3ac91 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -41,11 +41,7 @@ test_script: - script\test.cmd after_test: - - IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] ( - IF NOT EXIST C:\sqtemp MKDIR C:\sqtemp - SET SQUIRREL_TEMP=C:\sqtemp - script\build.cmd --existing-binaries --code-sign --create-windows-installer - ) + - IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] ( script\create-installer.cmd ) deploy: off artifacts: diff --git a/script/create-installer.cmd b/script/create-installer.cmd new file mode 100644 index 000000000..0354f0bac --- /dev/null +++ b/script/create-installer.cmd @@ -0,0 +1,6 @@ +@ECHO OFF +IF NOT EXIST C:\sqtemp MKDIR C:\sqtemp +SET SQUIRREL_TEMP=C:\sqtemp +del script\package-lock.json /q +del apm\package-lock.json /q +script\build.cmd --existing-binaries --code-sign --create-windows-installer From 2bb7746d3edd0551742101dc43f5efa95c1de765 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 8 Aug 2017 14:42:41 -0400 Subject: [PATCH 273/314] :arrow_up: github --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 06710e579..4db9c61ab 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "exception-reporting": "0.41.4", "find-and-replace": "0.209.5", "fuzzy-finder": "1.5.8", - "github": "0.4.0", + "github": "0.4.1", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", From 20fa0714b3d080a7d151a1bfe3ca9ffc555180b0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 8 Aug 2017 15:31:39 -0400 Subject: [PATCH 274/314] Revert ":arrow_up: github" This reverts commit 2bb7746d3edd0551742101dc43f5efa95c1de765. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4db9c61ab..06710e579 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "exception-reporting": "0.41.4", "find-and-replace": "0.209.5", "fuzzy-finder": "1.5.8", - "github": "0.4.1", + "github": "0.4.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", From ff608ab524c182d95d09b0f9c3618450f8f6b103 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 8 Aug 2017 15:32:24 -0400 Subject: [PATCH 275/314] :arrow_up: github --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 06710e579..4db9c61ab 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "exception-reporting": "0.41.4", "find-and-replace": "0.209.5", "fuzzy-finder": "1.5.8", - "github": "0.4.0", + "github": "0.4.1", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", From 276fcb9e9693538bf55bdc8ebacde15b9f0484c1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 8 Aug 2017 16:52:38 -0400 Subject: [PATCH 276/314] :arrow_up: electron-link --- script/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/package.json b/script/package.json index 52f0b6d55..454d561aa 100644 --- a/script/package.json +++ b/script/package.json @@ -9,7 +9,7 @@ "csslint": "1.0.2", "donna": "1.0.16", "electron-chromedriver": "~1.6", - "electron-link": "0.1.0", + "electron-link": "0.1.1", "electron-mksnapshot": "~1.6", "electron-packager": "7.3.0", "electron-winstaller": "2.6.2", From 35a7ae6d2e3fd6766211c6fdc63cba0c1dbef937 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 8 Aug 2017 17:36:23 -0400 Subject: [PATCH 277/314] :arrow_up: github --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4db9c61ab..8e3981529 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "exception-reporting": "0.41.4", "find-and-replace": "0.209.5", "fuzzy-finder": "1.5.8", - "github": "0.4.1", + "github": "0.4.2", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", From 98296bf95027f2a9668e9d078ffcf1ce25cf1c88 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 8 Aug 2017 21:29:22 -0400 Subject: [PATCH 278/314] :arrow_up: language-java@0.27.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 181cbfa12..79dda7061 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "language-go": "0.44.2", "language-html": "0.47.3", "language-hyperlink": "0.16.2", - "language-java": "0.27.2", + "language-java": "0.27.3", "language-javascript": "0.127.1", "language-json": "0.19.1", "language-less": "0.33.0", From 9fe67290c5abf3e2aaefebe356133f7741e2ac33 Mon Sep 17 00:00:00 2001 From: Nontawat Numor Date: Wed, 9 Aug 2017 12:16:19 +0700 Subject: [PATCH 279/314] Update document with electron version :tea: --- docs/native-profiling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/native-profiling.md b/docs/native-profiling.md index 58a164982..afac6b4ab 100644 --- a/docs/native-profiling.md +++ b/docs/native-profiling.md @@ -6,7 +6,7 @@ * Open the dev tools with `alt-cmd-i` * Evaluate `process.versions.electron` in the console. * Based on this version, download the appropriate Electron symbols from the [releases](https://github.com/atom/electron/releases) page. - * The file name should look like `electron-v0.X.Y-darwin-x64-dsym.zip`. + * The file name should look like `electron-v1.X.Y-darwin-x64-dsym.zip`. * Decompress these symbols in your `~/Downloads` directory. * Now create a time profile in Instruments. * Open `Instruments.app`. From a09b634e1bc56c37056f8c949c6ab18116aec3c4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 9 Aug 2017 09:18:41 -0400 Subject: [PATCH 280/314] Separate tests and installer creation on AppVeyor --- appveyor.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 6a8d3ac91..4ed91e643 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,9 +19,13 @@ environment: global: ATOM_DEV_RESOURCE_PATH: c:\projects\atom TEST_JUNIT_XML_ROOT: c:\projects\junit-test-results + NODE_VERSION: 6.9.4 matrix: - - NODE_VERSION: 6.9.4 + - TASK: test + BUILD_ARG: + - TASK: installer + BUILD_ARG: --create-windows-installer matrix: fast_finish: true @@ -34,14 +38,16 @@ install: build_script: - CD %APPVEYOR_BUILD_FOLDER% - - script\build.cmd --code-sign --compress-artifacts + - script\build.cmd --code-sign --compress-artifacts %BUILD_ARG% test_script: - - script\lint.cmd - - script\test.cmd - -after_test: - - IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] ( script\create-installer.cmd ) + - | + IF [%TASK]==[test] ( + script\lint.cmd + script\test.cmd + ) ELSE ( + ECHO + ) deploy: off artifacts: From 1233bbccb00be78387a1cbcfe412cc84ba8a967b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 9 Aug 2017 09:20:52 -0400 Subject: [PATCH 281/314] Missing % --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 4ed91e643..ae5f97d7b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -42,7 +42,7 @@ build_script: test_script: - | - IF [%TASK]==[test] ( + IF [%TASK%]==[test] ( script\lint.cmd script\test.cmd ) ELSE ( From 2d186820cf05ea74f200eebea056ccc9be35d0b0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 9 Aug 2017 09:28:37 -0400 Subject: [PATCH 282/314] The matrix will be slightly prettier this way <_< >_> --- appveyor.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index ae5f97d7b..c366f7afa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,9 +23,7 @@ environment: matrix: - TASK: test - BUILD_ARG: - TASK: installer - BUILD_ARG: --create-windows-installer matrix: fast_finish: true @@ -38,6 +36,12 @@ install: build_script: - CD %APPVEYOR_BUILD_FOLDER% + - | + IF [%TASK%]==[installer] ( + SET BUILD_ARG=--create-windows-installer + ) ELSE ( + SET BUILD_ARG= + ) - script\build.cmd --code-sign --compress-artifacts %BUILD_ARG% test_script: From 3a9872e8091d5dfc9cb832feee10628b939395c8 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 9 Aug 2017 10:02:24 -0400 Subject: [PATCH 283/314] YAML syntax tweaks --- appveyor.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index c366f7afa..7bf8be73e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -36,8 +36,7 @@ install: build_script: - CD %APPVEYOR_BUILD_FOLDER% - - | - IF [%TASK%]==[installer] ( + - IF [%TASK%]==[installer] ( SET BUILD_ARG=--create-windows-installer ) ELSE ( SET BUILD_ARG= @@ -45,12 +44,11 @@ build_script: - script\build.cmd --code-sign --compress-artifacts %BUILD_ARG% test_script: - - | - IF [%TASK%]==[test] ( - script\lint.cmd + - IF [%TASK%]==[test] ( + script\lint.cmd && script\test.cmd ) ELSE ( - ECHO + ECHO Skipping tests ) deploy: off From fbd4b688dd611b1f828f3ea0135e8d7737bfa76d Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 9 Aug 2017 11:17:48 -0400 Subject: [PATCH 284/314] :arrow_up: language-javascript@0.127.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 79dda7061..6f07b00d9 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "language-html": "0.47.3", "language-hyperlink": "0.16.2", "language-java": "0.27.3", - "language-javascript": "0.127.1", + "language-javascript": "0.127.2", "language-json": "0.19.1", "language-less": "0.33.0", "language-make": "0.22.3", From 4acd52a978011d8d84df8fa25e0bb533da6a19b4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 9 Aug 2017 12:18:07 -0400 Subject: [PATCH 285/314] Skip installer build on non-release branches --- appveyor.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 7bf8be73e..f0880c10b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -37,9 +37,14 @@ install: build_script: - CD %APPVEYOR_BUILD_FOLDER% - IF [%TASK%]==[installer] ( - SET BUILD_ARG=--create-windows-installer + IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] ( + SET BUILD_ARG=--create-windows-installer + ) ELSE ( + ECHO "Skipping installer build on non-release branch" + ) ) ELSE ( - SET BUILD_ARG= + SET BUILD_ARG= && + ECHO "Skipping installer build on non-installer build matrix row" ) - script\build.cmd --code-sign --compress-artifacts %BUILD_ARG% From 2088143a9050feb2ece9f70deea2e2cb431f8864 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 9 Aug 2017 11:49:21 -0400 Subject: [PATCH 286/314] Fix specs --- spec/tokenized-buffer-spec.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index 5b1863b35..07e7e80e6 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -184,7 +184,7 @@ describe "TokenizedBuffer", -> it "schedules the invalidated lines to be tokenized in the background", -> buffer.insert([5, 30], '/* */') buffer.setTextInRange([[2, 0], [3, 0]], '/*') - expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.js'] + expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js'] expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js'] advanceClock() @@ -214,7 +214,7 @@ describe "TokenizedBuffer", -> it "schedules the invalidated lines to be tokenized in the background", -> buffer.insert([5, 30], '/* */') buffer.insert([2, 0], '/*\nabcde\nabcder') - expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.js'] + expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js'] expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual ['source.js'] @@ -596,10 +596,10 @@ describe "TokenizedBuffer", -> {position: Point(0, 9), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []} {position: Point(0, 10), closeTags: [], openTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"]} {position: Point(0, 11), closeTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"], openTags: []} - {position: Point(0, 12), closeTags: [], openTags: ["syntax--comment syntax--block syntax--js", "syntax--punctuation syntax--definition syntax--comment syntax--js"]} - {position: Point(0, 14), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js"], openTags: []} - {position: Point(1, 5), closeTags: [], openTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js"]} - {position: Point(1, 7), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js", "syntax--comment syntax--block syntax--js"], openTags: ["syntax--storage syntax--type syntax--var syntax--js"]} + {position: Point(0, 12), closeTags: [], openTags: ["syntax--comment syntax--block syntax--js", "syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js"]} + {position: Point(0, 14), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js"], openTags: []} + {position: Point(1, 5), closeTags: [], openTags: ["syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js"]} + {position: Point(1, 7), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js", "syntax--comment syntax--block syntax--js"], openTags: ["syntax--storage syntax--type syntax--var syntax--js"]} {position: Point(1, 10), closeTags: ["syntax--storage syntax--type syntax--var syntax--js"], openTags: []} {position: Point(1, 15), closeTags: [], openTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"]} {position: Point(1, 16), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []} From 0db8c9c3ece5932dab5ca9e533d3a474905d5df2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 9 Aug 2017 13:01:40 -0400 Subject: [PATCH 287/314] Don't build on non-release installer rows either --- appveyor.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index f0880c10b..0f566f936 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -38,15 +38,20 @@ build_script: - CD %APPVEYOR_BUILD_FOLDER% - IF [%TASK%]==[installer] ( IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] ( - SET BUILD_ARG=--create-windows-installer + SET BUILD_ARG=--create-windows-installer && + SET BUILD=yes ) ELSE ( - ECHO "Skipping installer build on non-release branch" + ECHO "Skipping installer and Atom build on non-release branch" && + SET BUILD=no ) ) ELSE ( SET BUILD_ARG= && + SET BUILD=yes && ECHO "Skipping installer build on non-installer build matrix row" ) - - script\build.cmd --code-sign --compress-artifacts %BUILD_ARG% + - IF [%BUILD]==[yes] ( + script\build.cmd --code-sign --compress-artifacts %BUILD_ARG% + ) test_script: - IF [%TASK%]==[test] ( From ee12c1c7d2c032da13048852cdd6654460a9591f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 9 Aug 2017 13:10:26 -0400 Subject: [PATCH 288/314] Why does my brain always edit out the second % --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 0f566f936..8df08bc01 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -49,7 +49,7 @@ build_script: SET BUILD=yes && ECHO "Skipping installer build on non-installer build matrix row" ) - - IF [%BUILD]==[yes] ( + - IF [%BUILD%]==[yes] ( script\build.cmd --code-sign --compress-artifacts %BUILD_ARG% ) From ee0df014bc95167282f1e3317ffc5e138bffac77 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 9 Aug 2017 13:15:34 -0400 Subject: [PATCH 289/314] Less variable magic --- appveyor.yml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 8df08bc01..ee95a5d5e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -38,19 +38,13 @@ build_script: - CD %APPVEYOR_BUILD_FOLDER% - IF [%TASK%]==[installer] ( IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] ( - SET BUILD_ARG=--create-windows-installer && - SET BUILD=yes + script\build.cmd --code-sign --compress-artifacts --create-windows-installer ) ELSE ( - ECHO "Skipping installer and Atom build on non-release branch" && - SET BUILD=no + ECHO Skipping installer and Atom build on non-release branch ) ) ELSE ( - SET BUILD_ARG= && - SET BUILD=yes && - ECHO "Skipping installer build on non-installer build matrix row" - ) - - IF [%BUILD%]==[yes] ( - script\build.cmd --code-sign --compress-artifacts %BUILD_ARG% + ECHO Skipping installer build on non-installer build matrix row && + script\build.cmd --code-sign --compress-artifacts ) test_script: @@ -58,7 +52,7 @@ test_script: script\lint.cmd && script\test.cmd ) ELSE ( - ECHO Skipping tests + ECHO Skipping tests on installer build matrix row ) deploy: off From 2fa2feacafa95a82e363e8b036f2ad95f08d5227 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 9 Aug 2017 22:52:39 -0400 Subject: [PATCH 290/314] Multiline is important, don't forget to set it --- src/workspace.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workspace.js b/src/workspace.js index 3bf112461..ad37630eb 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -1949,7 +1949,7 @@ module.exports = class Workspace extends Model { } if (!outOfProcessFinished.length) { - let flags = 'g' + let flags = 'gm' // set multiline flag so ^ and $ match start/end of line, not file if (regex.ignoreCase) { flags += 'i' } const task = Task.once( From 95b216f2345589aa36ea6801babb27bd6526a226 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 9 Aug 2017 23:15:33 -0400 Subject: [PATCH 291/314] Add multiline spec --- spec/workspace-spec.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index bdd5677c8..add555f28 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -2394,6 +2394,22 @@ i = /test/; #FIXME\ expect(results[0].replacements).toBe(6) }) }) + + it('uses the multiline flag when searching', () => { + const filePath = path.join(projectDir, 'sample.js') + fs.copyFileSync(path.join(fixturesDir, 'sample.js'), filePath) + + const results = [] + waitsForPromise(() => + atom.workspace.replace(/;$/gi, 'items', [filePath], result => results.push(result)) + ) + + runs(() => { + expect(results).toHaveLength(1) + expect(results[0].filePath).toBe(filePath) + expect(results[0].replacements).toBe(8) + }) + }) }) describe('when a buffer is already open', () => { From 9fb4f0d9cdee00b9f21ffb44f308cbc8bb660a5b Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 9 Aug 2017 16:04:49 -0700 Subject: [PATCH 292/314] Create shorter temp path for Squirrel --- appveyor.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index ee95a5d5e..5195d81cc 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -36,6 +36,8 @@ install: build_script: - CD %APPVEYOR_BUILD_FOLDER% + - IF NOT EXIST C:\tmp MKDIR C:\tmp + - SET SQUIRREL_TEMP=C:\tmp - IF [%TASK%]==[installer] ( IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] ( script\build.cmd --code-sign --compress-artifacts --create-windows-installer From 00d27befe8719b4ab40041a353dfe62c148c559b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 10 Aug 2017 17:25:50 +0200 Subject: [PATCH 293/314] Create, update and destroy highlights manually Etch's reconciliation routine causes elements to be sometimes re-ordered. In order to move an element, however, Etch needs to first detach it from the DOM and then re-append it at the right location. This behavior is unacceptable for highlight decorations because it could re-start CSS animations on a certain highlight decoration when a completely different one is added or removed. Even though we are still interested in restructuring etch's reconciliation logic to prevent unwanted re-orderings, with this commit we are switching to a custom routine to create/update/remove highlight decorations that prevents unnecessary moves and, as a result, fixes the undesired behavior described above. --- spec/text-editor-component-spec.js | 21 +++++++ src/text-editor-component.js | 88 +++++++++++++++++++++--------- 2 files changed, 82 insertions(+), 27 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 3b2f2072a..2c13b48af 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1524,6 +1524,27 @@ describe('TextEditorComponent', () => { await setScrollTop(component, component.getLineHeight() * 3) expect(element.querySelectorAll('.highlight.a').length).toBe(0) }) + + it('does not move existing highlights when adding or removing other highlight decorations (regression)', async () => { + const {component, element, editor} = buildComponent() + + const marker1 = editor.markScreenRange([[1, 6], [1, 10]]) + editor.decorateMarker(marker1, {type: 'highlight', class: 'a'}) + await component.getNextUpdatePromise() + const marker1Region = element.querySelector('.highlight.a') + expect(Array.from(marker1Region.parentElement.children).indexOf(marker1Region)).toBe(0) + + const marker2 = editor.markScreenRange([[1, 2], [1, 4]]) + editor.decorateMarker(marker2, {type: 'highlight', class: 'b'}) + await component.getNextUpdatePromise() + const marker2Region = element.querySelector('.highlight.b') + expect(Array.from(marker1Region.parentElement.children).indexOf(marker1Region)).toBe(0) + expect(Array.from(marker2Region.parentElement.children).indexOf(marker2Region)).toBe(1) + + marker2.destroy() + await component.getNextUpdatePromise() + expect(Array.from(marker1Region.parentElement.children).indexOf(marker1Region)).toBe(0) + }) }) describe('overlay decorations', () => { diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 881aaad81..970ee29c1 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -3417,8 +3417,10 @@ class CursorsAndInputComponent { class LinesTileComponent { constructor (props) { + this.highlightComponentsByKey = new Map() this.props = props etch.initialize(this) + this.updateHighlights() this.createLines() this.updateBlockDecorations({}, props) } @@ -3432,13 +3434,22 @@ class LinesTileComponent { this.updateLines(oldProps, newProps) this.updateBlockDecorations(oldProps, newProps) } + this.updateHighlights() } } destroy () { + this.highlightComponentsByKey.forEach((highlightComponent) => { + highlightComponent.destroy() + }) + this.highlightComponentsByKey.clear() + for (let i = 0; i < this.lineComponents.length; i++) { this.lineComponents[i].destroy() } + this.lineComponents.length = 0 + + return etch.destroy(this) } render () { @@ -3456,34 +3467,12 @@ class LinesTileComponent { backgroundColor: 'inherit' } }, - this.renderHighlights() - // Lines and block decorations will be manually inserted here for efficiency - ) - } - - renderHighlights () { - const {top, lineHeight, highlightDecorations} = this.props - - let children = null - if (highlightDecorations) { - const decorationCount = highlightDecorations.length - children = new Array(decorationCount) - for (let i = 0; i < decorationCount; i++) { - const highlightProps = Object.assign( - {parentTileTop: top, lineHeight}, - highlightDecorations[i] - ) - children[i] = $(HighlightComponent, highlightProps) - highlightDecorations[i].flashRequested = false - } - } - - return $.div( - { + $.div({ + ref: 'highlights', className: 'highlights', - style: {contain: 'layout'} - }, - children + style: {layout: 'contain'} + }) + // Lines and block decorations will be manually inserted here for efficiency ) } @@ -3676,6 +3665,40 @@ class LinesTileComponent { } } + updateHighlights () { + const {top, lineHeight, highlightDecorations} = this.props + + const visibleHighlightDecorations = new Set() + if (highlightDecorations) { + for (let i = 0; i < highlightDecorations.length; i++) { + const highlightDecoration = highlightDecorations[i] + + const highlightProps = Object.assign( + {parentTileTop: top, lineHeight}, + highlightDecorations[i] + ) + let highlightComponent = this.highlightComponentsByKey.get(highlightDecoration.key) + if (highlightComponent) { + highlightComponent.update(highlightProps) + } else { + highlightComponent = new HighlightComponent(highlightProps) + this.refs.highlights.appendChild(highlightComponent.element) + this.highlightComponentsByKey.set(highlightDecoration.key, highlightComponent) + } + + highlightDecorations[i].flashRequested = false + visibleHighlightDecorations.add(highlightDecoration.key) + } + } + + this.highlightComponentsByKey.forEach((highlightComponent, key) => { + if (!visibleHighlightDecorations.has(key)) { + highlightComponent.destroy() + this.highlightComponentsByKey.delete(key) + } + }) + } + shouldUpdate (newProps) { const oldProps = this.props if (oldProps.top !== newProps.top) return true @@ -3876,6 +3899,17 @@ class HighlightComponent { if (this.props.flashRequested) this.performFlash() } + destroy () { + if (this.timeoutsByClassName) { + this.timeoutsByClassName.forEach((timeout) => { + window.clearTimeout(timeout) + }) + this.timeoutsByClassName.clear() + } + + return etch.destroy(this) + } + update (newProps) { this.props = newProps etch.updateSync(this) From 8963cf495517bddf197441f54deb112167d8b209 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 10 Aug 2017 13:24:46 -0400 Subject: [PATCH 294/314] Only use multiline if the flag is passed in --- spec/workspace-spec.js | 4 ++-- src/workspace.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index add555f28..476a4ba5b 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -2395,13 +2395,13 @@ i = /test/; #FIXME\ }) }) - it('uses the multiline flag when searching', () => { + it('does not discard the multiline flag', () => { const filePath = path.join(projectDir, 'sample.js') fs.copyFileSync(path.join(fixturesDir, 'sample.js'), filePath) const results = [] waitsForPromise(() => - atom.workspace.replace(/;$/gi, 'items', [filePath], result => results.push(result)) + atom.workspace.replace(/;$/gmi, 'items', [filePath], result => results.push(result)) ) runs(() => { diff --git a/src/workspace.js b/src/workspace.js index ad37630eb..17c6b2a8b 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -1949,7 +1949,8 @@ module.exports = class Workspace extends Model { } if (!outOfProcessFinished.length) { - let flags = 'gm' // set multiline flag so ^ and $ match start/end of line, not file + let flags = 'g' + if (regex.multiline) { flags += 'm' } if (regex.ignoreCase) { flags += 'i' } const task = Task.once( From 7f8f184e969478189b1d6f759cf17fcb76da6779 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 10 Aug 2017 12:55:41 -0600 Subject: [PATCH 295/314] Shim rowsPerPage property on Editor instances Several packages were relying on a raw property rather than the getter method. This isn't really supported, but may as well keep them working. --- src/text-editor.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 6962bf10a..39abd05a0 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3527,6 +3527,10 @@ class TextEditor extends Model else 1 + Object.defineProperty(@prototype, 'rowsPerPage', { + get: -> @getRowsPerPage() + }) + ### Section: Config ### From b14c4a32c98fffbdcd7859b7536bc2b07e5b6926 Mon Sep 17 00:00:00 2001 From: Linus Eriksson Date: Fri, 11 Aug 2017 21:26:31 +0200 Subject: [PATCH 296/314] :arrow_up: bracket-matcher@0.87.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6f07b00d9..8bcb93f7e 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "autosave": "0.24.3", "background-tips": "0.27.1", "bookmarks": "0.44.4", - "bracket-matcher": "0.87.0", + "bracket-matcher": "0.87.3", "command-palette": "0.40.4", "dalek": "0.2.1", "deprecation-cop": "0.56.7", From 5a8b197db19f9328063f0aa8aaecc725eb620be8 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Fri, 11 Aug 2017 13:24:34 -0700 Subject: [PATCH 297/314] Ensure Pane.destroyItem always returns a promise Fixes #15157 --- spec/pane-spec.js | 25 ++++++++++++++++--------- src/pane.coffee | 5 ++++- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/spec/pane-spec.js b/spec/pane-spec.js index c36abbf6a..68e93c38f 100644 --- a/spec/pane-spec.js +++ b/spec/pane-spec.js @@ -551,10 +551,11 @@ describe('Pane', () => { itemURI = 'test' confirm.andReturn(0) - await pane.destroyItem(item1) + const success = await pane.destroyItem(item1) expect(item1.save).toHaveBeenCalled() expect(pane.getItems().includes(item1)).toBe(false) expect(item1.isDestroyed()).toBe(true) + expect(success).toBe(true) }) }) @@ -565,11 +566,12 @@ describe('Pane', () => { showSaveDialog.andReturn('/selected/path') confirm.andReturn(0) - await pane.destroyItem(item1) + const success = await pane.destroyItem(item1) expect(showSaveDialog).toHaveBeenCalled() expect(item1.saveAs).toHaveBeenCalledWith('/selected/path') expect(pane.getItems().includes(item1)).toBe(false) expect(item1.isDestroyed()).toBe(true) + expect(success).toBe(true) }) }) }) @@ -578,10 +580,11 @@ describe('Pane', () => { it('removes and destroys the item without saving it', async () => { confirm.andReturn(2) - await pane.destroyItem(item1) + const success = await pane.destroyItem(item1) expect(item1.save).not.toHaveBeenCalled() expect(pane.getItems().includes(item1)).toBe(false) expect(item1.isDestroyed()).toBe(true) + expect(success).toBe(true); }) }) @@ -589,19 +592,21 @@ describe('Pane', () => { it('does not save, remove, or destroy the item', async () => { confirm.andReturn(1) - await pane.destroyItem(item1) + const success = await pane.destroyItem(item1) expect(item1.save).not.toHaveBeenCalled() expect(pane.getItems().includes(item1)).toBe(true) expect(item1.isDestroyed()).toBe(false) + expect(success).toBe(false) }) }) describe('when force=true', () => { it('destroys the item immediately', async () => { - await pane.destroyItem(item1, true) + const success = await pane.destroyItem(item1, true) expect(item1.save).not.toHaveBeenCalled() expect(pane.getItems().includes(item1)).toBe(false) expect(item1.isDestroyed()).toBe(true) + expect(success).toBe(true) }) }) }) @@ -630,18 +635,20 @@ describe('Pane', () => { }) describe('when passed a permanent dock item', () => { - it("doesn't destroy the item", () => { + it("doesn't destroy the item", async () => { spyOn(item1, 'isPermanentDockItem').andReturn(true) - pane.destroyItem(item1) + const success = await pane.destroyItem(item1) expect(pane.getItems().includes(item1)).toBe(true) expect(item1.isDestroyed()).toBe(false) + expect(success).toBe(false); }) - it('destroy the item if force=true', () => { + it('destroy the item if force=true', async () => { spyOn(item1, 'isPermanentDockItem').andReturn(true) - pane.destroyItem(item1, true) + const success = await pane.destroyItem(item1, true) expect(pane.getItems().includes(item1)).toBe(false) expect(item1.isDestroyed()).toBe(true) + expect(success).toBe(true) }) }) }) diff --git a/src/pane.coffee b/src/pane.coffee index 9c0440e0a..23a60306b 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -621,12 +621,15 @@ class Pane destroyItem: (item, force) -> index = @items.indexOf(item) if index isnt -1 - return false if not force and @getContainer()?.getLocation() isnt 'center' and item.isPermanentDockItem?() + if not force and @getContainer()?.getLocation() isnt 'center' and item.isPermanentDockItem?() + return Promise.resolve(false) + @emitter.emit 'will-destroy-item', {item, index} @container?.willDestroyPaneItem({item, index, pane: this}) if force or not item?.shouldPromptToSave?() @removeItem(item, false) item.destroy?() + Promise.resolve(true) else @promptToSaveItem(item).then (result) => if result From 8667cfdd13b1f28e01358e5399ffe19b0e1cbadd Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 11 Aug 2017 15:39:42 -0600 Subject: [PATCH 298/314] Work around incorrect data on compositionupdate events in Chrome 56 --- src/text-editor-component.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 970ee29c1..0c80ef52c 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1651,7 +1651,14 @@ class TextEditorComponent { } didCompositionUpdate (event) { - this.props.model.insertText(event.data, {select: true}) + if (parseInt(process.versions.chrome) === 56) { + process.nextTick(() => { + const previewText = this.refs.cursorsAndInput.refs.hiddenInput.value + this.props.model.insertText(previewText, {select: true}) + }) + } else { + this.props.model.insertText(event.data, {select: true}) + } } didCompositionEnd (event) { From 54a6f0d29faf886a24a50287cd386171054d324d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 11 Aug 2017 15:57:46 -0600 Subject: [PATCH 299/314] Clear hidden input compositionstart on Chrome 56 We use the value of the hidden input to display a preview of the composition, but it might already contain spaces from previous keystrokes, since we don't call preventDefault when spaces are inserted. --- src/text-editor-component.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 0c80ef52c..79105f868 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1644,6 +1644,10 @@ class TextEditorComponent { // 4. compositionend fired // 5. textInput fired; event.data == the completion string didCompositionStart () { + if (parseInt(process.versions.chrome) === 56) { + this.getHiddenInput().value = '' + } + this.compositionCheckpoint = this.props.model.createCheckpoint() if (this.accentedCharacterMenuIsOpen) { this.props.model.selectLeft() @@ -1653,7 +1657,7 @@ class TextEditorComponent { didCompositionUpdate (event) { if (parseInt(process.versions.chrome) === 56) { process.nextTick(() => { - const previewText = this.refs.cursorsAndInput.refs.hiddenInput.value + const previewText = this.getHiddenInput().value this.props.model.insertText(previewText, {select: true}) }) } else { @@ -2815,6 +2819,10 @@ class TextEditorComponent { return this.props.inputEnabled != null ? this.props.inputEnabled : true } + getHiddenInput () { + return this.refs.cursorsAndInput.refs.hiddenInput + } + getPlatform () { return this.props.platform || process.platform } From 2e048826fb565c87725809a5d722049e57d1f7aa Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Fri, 11 Aug 2017 15:58:55 -0700 Subject: [PATCH 300/314] :arrow_up: apm --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index ba8d06f1d..7117147f1 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.18.3" + "atom-package-manager": "1.18.4" } } From 3e0d79005012382569acfb1480cde79d47337032 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Fri, 11 Aug 2017 17:01:34 -0700 Subject: [PATCH 301/314] Remove language-typescript deprecation. New package soon. --- script/deprecated-packages.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/script/deprecated-packages.json b/script/deprecated-packages.json index 12638967e..dc97e3734 100644 --- a/script/deprecated-packages.json +++ b/script/deprecated-packages.json @@ -875,10 +875,6 @@ "hasDeprecations": true, "latestHasDeprecations": false }, - "language-typescript": { - "hasAlternative": true, - "alternative": "atom-typescript" - }, "laravel-facades": { "version": "<=1.0.0", "hasDeprecations": true, From ca183dd6938dd2a1724f7cdc65e7f4ec91afaffe Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 12 Aug 2017 10:06:56 +0200 Subject: [PATCH 302/314] Don't insert IME preview on next tick if composition has already ended --- src/text-editor-component.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 79105f868..a1867a931 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1657,8 +1657,10 @@ class TextEditorComponent { didCompositionUpdate (event) { if (parseInt(process.versions.chrome) === 56) { process.nextTick(() => { - const previewText = this.getHiddenInput().value - this.props.model.insertText(previewText, {select: true}) + if (this.compositionCheckpoint) { + const previewText = this.getHiddenInput().value + this.props.model.insertText(previewText, {select: true}) + } }) } else { this.props.model.insertText(event.data, {select: true}) From fc1327eb22265d2ae37db25cf9e9f8107e3b67fb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 12 Aug 2017 12:28:32 +0200 Subject: [PATCH 303/314] Fix measuring lines in presence of pending autoscroll requests Calling `pixelPositionForScreenPosition` was sometimes throwing an error indicating that the requested position was not rendered and that, as such, could not be measured. This was caused by trying to measure a line that was visible at the moment of the call while also having a pending autoscroll request that would cause that line to go off-screen. Due to how the code was structured, we would mistakenly detect that line as visible, autoscroll to a different location, re-render a different region of the buffer and then try to measure the now invisible line. This commit fixes this issue by restructuring and simplifying the logic for rendering extra lines in order to measure them. Now, every line for which a measurement has been requested is stored in a `linesToMeasure` map. During the first phase of the update process (after honoring autoscroll requests), we detect which of these lines are currently visible and if they're not, store them into the `extraRenderedScreenLines` map, which is then used to render lines that are invisible but need to be measured. --- spec/text-editor-component-spec.js | 19 ++++++++ src/text-editor-component.js | 78 +++++++++++++++--------------- 2 files changed, 59 insertions(+), 38 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 2c13b48af..9dcd2c32c 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -3399,6 +3399,15 @@ describe('TextEditorComponent', () => { expect(top).toBe(clientTopForLine(referenceComponent, 12) - referenceContentRect.top) expect(left).toBe(clientLeftForCharacter(referenceComponent, 12, 1) - referenceContentRect.left) } + + // Measuring a currently rendered line while an autoscroll that causes + // that line to go off-screen is in progress. + { + editor.setCursorScreenPosition([10, 0]) + const {top, left} = component.pixelPositionForScreenPosition({row: 3, column: 5}) + expect(top).toBe(clientTopForLine(referenceComponent, 3) - referenceContentRect.top) + expect(left).toBe(clientLeftForCharacter(referenceComponent, 3, 5) - referenceContentRect.left) + } }) it('does not get the component into an inconsistent state when the model has unflushed changes (regression)', async () => { @@ -3445,6 +3454,16 @@ describe('TextEditorComponent', () => { pixelPosition.left += component.getBaseCharacterWidth() / 3 expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual([12, 1]) } + + // Measuring a currently rendered line while an autoscroll that causes + // that line to go off-screen is in progress. + { + const pixelPosition = referenceComponent.pixelPositionForScreenPosition({row: 3, column: 4}) + pixelPosition.top += component.getLineHeight() / 3 + pixelPosition.left += component.getBaseCharacterWidth() / 3 + editor.setCursorBufferPosition([10, 0]) + expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual([3, 4]) + } }) }) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 970ee29c1..53e8d4882 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -110,8 +110,8 @@ class TextEditorComponent { this.cursorsBlinking = false this.cursorsBlinkedOff = false this.nextUpdateOnlyBlinksCursors = null - this.extraLinesToMeasure = null - this.extraRenderedScreenLines = null + this.linesToMeasure = new Map() + this.extraRenderedScreenLines = new Map() this.horizontalPositionsToMeasure = new Map() // Keys are rows with positions we want to measure, values are arrays of columns to measure this.horizontalPixelPositionsByScreenLineId = new Map() // Values are maps from column to horiontal pixel positions this.blockDecorationsToMeasure = new Set() @@ -355,6 +355,7 @@ class TextEditorComponent { this.queryLineNumbersToRender() this.queryGuttersToRender() this.queryDecorationsToRender() + this.queryExtraScreenLinesToRender() this.shouldRenderDummyScrollbars = !this.remeasureScrollbars etch.updateSync(this) this.updateClassList() @@ -369,8 +370,6 @@ class TextEditorComponent { } const wasHorizontalScrollbarVisible = this.isHorizontalScrollbarVisible() - this.extraRenderedScreenLines = this.extraLinesToMeasure - this.extraLinesToMeasure = null this.measureLongestLineWidth() this.measureHorizontalPositions() this.updateAbsolutePositionedDecorations() @@ -606,21 +605,19 @@ class TextEditorComponent { }) } - if (this.extraLinesToMeasure) { - this.extraLinesToMeasure.forEach((screenLine, screenRow) => { - if (screenRow < startRow || screenRow >= endRow) { - tileNodes.push($(LineComponent, { - key: 'extra-' + screenLine.id, - screenLine, - screenRow, - displayLayer, - nodePool: this.lineNodesPool, - lineNodesByScreenLineId, - textNodesByScreenLineId - })) - } - }) - } + this.extraRenderedScreenLines.forEach((screenLine, screenRow) => { + if (screenRow < startRow || screenRow >= endRow) { + tileNodes.push($(LineComponent, { + key: 'extra-' + screenLine.id, + screenLine, + screenRow, + displayLayer, + nodePool: this.lineNodesPool, + lineNodesByScreenLineId, + textNodesByScreenLineId + })) + } + }) return $.div({ key: 'lineTiles', @@ -830,12 +827,22 @@ class TextEditorComponent { const longestLineRow = model.getApproximateLongestScreenRow() const longestLine = model.screenLineForScreenRow(longestLineRow) if (longestLine !== this.previousLongestLine) { - this.requestExtraLineToMeasure(longestLineRow, longestLine) + this.requestLineToMeasure(longestLineRow, longestLine) this.longestLineToMeasure = longestLine this.previousLongestLine = longestLine } } + queryExtraScreenLinesToRender () { + this.extraRenderedScreenLines.clear() + this.linesToMeasure.forEach((screenLine, row) => { + if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) { + this.extraRenderedScreenLines.set(row, screenLine) + } + }) + this.linesToMeasure.clear() + } + queryLineNumbersToRender () { const {model} = this.props if (!model.isLineNumberGutterVisible()) return @@ -906,7 +913,7 @@ class TextEditorComponent { renderedScreenLineForRow (row) { return ( this.renderedScreenLines[row - this.getRenderedStartRow()] || - (this.extraRenderedScreenLines ? this.extraRenderedScreenLines.get(row) : null) + this.extraRenderedScreenLines.get(row) ) } @@ -2125,29 +2132,24 @@ class TextEditorComponent { } } - requestExtraLineToMeasure (row, screenLine) { - if (!this.extraLinesToMeasure) this.extraLinesToMeasure = new Map() - this.extraLinesToMeasure.set(row, screenLine) + requestLineToMeasure (row, screenLine) { + this.linesToMeasure.set(row, screenLine) } requestHorizontalMeasurement (row, column) { if (column === 0) return - if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) { - const screenLine = this.props.model.screenLineForScreenRow(row) - if (screenLine) { - this.requestExtraLineToMeasure(row, screenLine) - } else { - return - } - } + const screenLine = this.props.model.screenLineForScreenRow(row) + if (screenLine) { + this.requestLineToMeasure(row, screenLine) - let columns = this.horizontalPositionsToMeasure.get(row) - if (columns == null) { - columns = [] - this.horizontalPositionsToMeasure.set(row, columns) + let columns = this.horizontalPositionsToMeasure.get(row) + if (columns == null) { + columns = [] + this.horizontalPositionsToMeasure.set(row, columns) + } + columns.push(column) } - columns.push(column) } measureHorizontalPositions () { @@ -2260,7 +2262,7 @@ class TextEditorComponent { let screenLine = this.renderedScreenLineForRow(row) if (!screenLine) { - this.requestExtraLineToMeasure(row, model.screenLineForScreenRow(row)) + this.requestLineToMeasure(row, model.screenLineForScreenRow(row)) this.updateSyncBeforeMeasuringContent() this.measureContentDuringUpdateSync() screenLine = this.renderedScreenLineForRow(row) From b4f029e9f0885789f63df3ec24c58466f38c77e2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 12 Aug 2017 10:11:14 +0200 Subject: [PATCH 304/314] Test both Chrome 56 and other Chrome versions IME behavior --- spec/text-editor-component-spec.js | 598 ++++++++++++++++++++--------- src/text-editor-component.js | 8 +- 2 files changed, 419 insertions(+), 187 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 2c13b48af..c52cb53a8 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -3049,198 +3049,421 @@ describe('TextEditorComponent', () => { }) describe('keyboard input', () => { - it('handles inserted accented characters via the press-and-hold menu on macOS correctly', () => { - const {editor, component, element} = buildComponent({text: ''}) - editor.insertText('x') - editor.setCursorBufferPosition([0, 1]) + describe('on Chrome 56', () => { + it('handles inserted accented characters via the press-and-hold menu on macOS correctly', async () => { + const {editor, component, element} = buildComponent({text: '', chromeVersion: 56}) + editor.insertText('x') + editor.setCursorBufferPosition([0, 1]) - // Simulate holding the A key to open the press-and-hold menu, - // then closing it via ESC. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeydown({code: 'KeyA'}) - component.didKeydown({code: 'KeyA'}) - component.didKeyup({code: 'KeyA'}) - component.didKeydown({code: 'Escape'}) - component.didKeyup({code: 'Escape'}) - expect(editor.getText()).toBe('xa') - // Ensure another "a" can be typed correctly. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeyup({code: 'KeyA'}) - expect(editor.getText()).toBe('xaa') - editor.undo() - expect(editor.getText()).toBe('x') + // Simulate holding the A key to open the press-and-hold menu, + // then closing it via ESC. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'Escape'}) + component.didKeyup({code: 'Escape'}) + expect(editor.getText()).toBe('xa') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xaa') + editor.undo() + expect(editor.getText()).toBe('x') - // Simulate holding the A key to open the press-and-hold menu, - // then selecting an alternative by typing a number. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeydown({code: 'KeyA'}) - component.didKeydown({code: 'KeyA'}) - component.didKeyup({code: 'KeyA'}) - component.didKeydown({code: 'Digit2'}) - component.didKeyup({code: 'Digit2'}) - component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) - expect(editor.getText()).toBe('xá') - // Ensure another "a" can be typed correctly. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeyup({code: 'KeyA'}) - expect(editor.getText()).toBe('xáa') - editor.undo() - expect(editor.getText()).toBe('x') + // Simulate holding the A key to open the press-and-hold menu, + // then selecting an alternative by typing a number. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'Digit2'}) + component.didKeyup({code: 'Digit2'}) + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') - // Simulate holding the A key to open the press-and-hold menu, - // then selecting an alternative by clicking on it. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeydown({code: 'KeyA'}) - component.didKeydown({code: 'KeyA'}) - component.didKeyup({code: 'KeyA'}) - component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) - expect(editor.getText()).toBe('xá') - // Ensure another "a" can be typed correctly. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeyup({code: 'KeyA'}) - expect(editor.getText()).toBe('xáa') - editor.undo() - expect(editor.getText()).toBe('x') + // Simulate holding the A key to open the press-and-hold menu, + // then selecting an alternative by clicking on it. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') - // Simulate holding the A key to open the press-and-hold menu, - // cycling through the alternatives with the arrows, then selecting one of them with Enter. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeydown({code: 'KeyA'}) - component.didKeydown({code: 'KeyA'}) - component.didKeyup({code: 'KeyA'}) - component.didKeydown({code: 'ArrowRight'}) - component.didCompositionStart({data: ''}) - component.didCompositionUpdate({data: 'à'}) - component.didKeyup({code: 'ArrowRight'}) - expect(editor.getText()).toBe('xà') - component.didKeydown({code: 'ArrowRight'}) - component.didCompositionUpdate({data: 'á'}) - component.didKeyup({code: 'ArrowRight'}) - expect(editor.getText()).toBe('xá') - component.didKeydown({code: 'Enter'}) - component.didCompositionUpdate({data: 'á'}) - component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) - component.didCompositionEnd({data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput}) - component.didKeyup({code: 'Enter'}) - expect(editor.getText()).toBe('xá') - // Ensure another "a" can be typed correctly. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeyup({code: 'KeyA'}) - expect(editor.getText()).toBe('xáa') - editor.undo() - expect(editor.getText()).toBe('x') + // Simulate holding the A key to open the press-and-hold menu, + // cycling through the alternatives with the arrows, then selecting one of them with Enter. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.getHiddenInput().value = 'à' + component.didKeyup({code: 'ArrowRight'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.getHiddenInput().value = 'á' + component.didKeyup({code: 'ArrowRight'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xá') + component.didKeydown({code: 'Enter'}) + component.didCompositionUpdate({data: 'á'}) + component.getHiddenInput().value = 'á' + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'á', target: component.getHiddenInput()}) + component.didKeyup({code: 'Enter'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xá') - // Simulate holding the A key to open the press-and-hold menu, - // cycling through the alternatives with the arrows, then closing it via ESC. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeydown({code: 'KeyA'}) - component.didKeydown({code: 'KeyA'}) - component.didKeyup({code: 'KeyA'}) - component.didKeydown({code: 'ArrowRight'}) - component.didCompositionStart({data: ''}) - component.didCompositionUpdate({data: 'à'}) - component.didKeyup({code: 'ArrowRight'}) - expect(editor.getText()).toBe('xà') - component.didKeydown({code: 'ArrowRight'}) - component.didCompositionUpdate({data: 'á'}) - component.didKeyup({code: 'ArrowRight'}) - expect(editor.getText()).toBe('xá') - component.didKeydown({code: 'Escape'}) - component.didCompositionUpdate({data: 'a'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput}) - component.didKeyup({code: 'Escape'}) - expect(editor.getText()).toBe('xa') - // Ensure another "a" can be typed correctly. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeyup({code: 'KeyA'}) - expect(editor.getText()).toBe('xaa') - editor.undo() - expect(editor.getText()).toBe('x') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') - // Simulate pressing the O key and holding the A key to open the press-and-hold menu right before releasing the O key, - // cycling through the alternatives with the arrows, then closing it via ESC. - component.didKeydown({code: 'KeyO'}) - component.didKeypress({code: 'KeyO'}) - component.didTextInput({data: 'o', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeyup({code: 'KeyO'}) - component.didKeydown({code: 'KeyA'}) - component.didKeydown({code: 'KeyA'}) - component.didKeydown({code: 'ArrowRight'}) - component.didCompositionStart({data: ''}) - component.didCompositionUpdate({data: 'à'}) - component.didKeyup({code: 'ArrowRight'}) - expect(editor.getText()).toBe('xoà') - component.didKeydown({code: 'ArrowRight'}) - component.didCompositionUpdate({data: 'á'}) - component.didKeyup({code: 'ArrowRight'}) - expect(editor.getText()).toBe('xoá') - component.didKeydown({code: 'Escape'}) - component.didCompositionUpdate({data: 'a'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput}) - component.didKeyup({code: 'Escape'}) - expect(editor.getText()).toBe('xoa') - // Ensure another "a" can be typed correctly. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeyup({code: 'KeyA'}) - editor.undo() - expect(editor.getText()).toBe('x') + // Simulate holding the A key to open the press-and-hold menu, + // cycling through the alternatives with the arrows, then closing it via ESC. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.getHiddenInput().value = 'à' + component.didKeyup({code: 'ArrowRight'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.getHiddenInput().value = 'á' + component.didKeyup({code: 'ArrowRight'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xá') + component.didKeydown({code: 'Escape'}) + component.didCompositionUpdate({data: 'a'}) + component.getHiddenInput().value = 'a' + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput}) + component.didKeyup({code: 'Escape'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xa') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xaa') + editor.undo() + expect(editor.getText()).toBe('x') - // Simulate holding the A key to open the press-and-hold menu, - // cycling through the alternatives with the arrows, then closing it by changing focus. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeydown({code: 'KeyA'}) - component.didKeydown({code: 'KeyA'}) - component.didKeyup({code: 'KeyA'}) - component.didKeydown({code: 'ArrowRight'}) - component.didCompositionStart({data: ''}) - component.didCompositionUpdate({data: 'à'}) - component.didKeyup({code: 'ArrowRight'}) - expect(editor.getText()).toBe('xà') - component.didKeydown({code: 'ArrowRight'}) - component.didCompositionUpdate({data: 'á'}) - component.didKeyup({code: 'ArrowRight'}) - expect(editor.getText()).toBe('xá') - component.didCompositionUpdate({data: 'á'}) - component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) - component.didCompositionEnd({data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput}) - expect(editor.getText()).toBe('xá') - // Ensure another "a" can be typed correctly. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeyup({code: 'KeyA'}) - expect(editor.getText()).toBe('xáa') - editor.undo() - expect(editor.getText()).toBe('x') + // Simulate pressing the O key and holding the A key to open the press-and-hold menu right before releasing the O key, + // cycling through the alternatives with the arrows, then closing it via ESC. + component.didKeydown({code: 'KeyO'}) + component.didKeypress({code: 'KeyO'}) + component.didTextInput({data: 'o', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyO'}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.getHiddenInput().value = 'à' + component.didKeyup({code: 'ArrowRight'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xoà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.getHiddenInput().value = 'á' + component.didKeyup({code: 'ArrowRight'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xoá') + component.didKeydown({code: 'Escape'}) + component.didCompositionUpdate({data: 'a'}) + component.getHiddenInput().value = 'a' + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput}) + component.didKeyup({code: 'Escape'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xoa') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate holding the A key to open the press-and-hold menu, + // cycling through the alternatives with the arrows, then closing it by changing focus. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.getHiddenInput().value = 'à' + component.didKeyup({code: 'ArrowRight'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.getHiddenInput().value = 'á' + component.didKeyup({code: 'ArrowRight'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xá') + component.didCompositionUpdate({data: 'á'}) + component.getHiddenInput().value = 'á' + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput}) + await getNextTickPromise() + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') + }) + }) + + describe('on other versions of Chrome', () => { + it('handles inserted accented characters via the press-and-hold menu on macOS correctly', () => { + const {editor, component, element} = buildComponent({text: '', chromeVersion: 57}) + editor.insertText('x') + editor.setCursorBufferPosition([0, 1]) + + // Simulate holding the A key to open the press-and-hold menu, + // then closing it via ESC. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'Escape'}) + component.didKeyup({code: 'Escape'}) + expect(editor.getText()).toBe('xa') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xaa') + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate holding the A key to open the press-and-hold menu, + // then selecting an alternative by typing a number. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'Digit2'}) + component.didKeyup({code: 'Digit2'}) + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate holding the A key to open the press-and-hold menu, + // then selecting an alternative by clicking on it. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate holding the A key to open the press-and-hold menu, + // cycling through the alternatives with the arrows, then selecting one of them with Enter. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xá') + component.didKeydown({code: 'Enter'}) + component.didCompositionUpdate({data: 'á'}) + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput}) + component.didKeyup({code: 'Enter'}) + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate holding the A key to open the press-and-hold menu, + // cycling through the alternatives with the arrows, then closing it via ESC. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xá') + component.didKeydown({code: 'Escape'}) + component.didCompositionUpdate({data: 'a'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput}) + component.didKeyup({code: 'Escape'}) + expect(editor.getText()).toBe('xa') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xaa') + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate pressing the O key and holding the A key to open the press-and-hold menu right before releasing the O key, + // cycling through the alternatives with the arrows, then closing it via ESC. + component.didKeydown({code: 'KeyO'}) + component.didKeypress({code: 'KeyO'}) + component.didTextInput({data: 'o', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyO'}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xoà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xoá') + component.didKeydown({code: 'Escape'}) + component.didCompositionUpdate({data: 'a'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput}) + component.didKeyup({code: 'Escape'}) + expect(editor.getText()).toBe('xoa') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate holding the A key to open the press-and-hold menu, + // cycling through the alternatives with the arrows, then closing it by changing focus. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xá') + component.didCompositionUpdate({data: 'á'}) + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput}) + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') + }) }) }) @@ -3546,6 +3769,7 @@ function buildComponent (params = {}) { rowsPerTile: params.rowsPerTile, updatedSynchronously: params.updatedSynchronously || false, platform: params.platform, + chromeVersion: params.chromeVersion, mouseWheelScrollSensitivity: params.mouseWheelScrollSensitivity }) const {element} = component @@ -3679,3 +3903,7 @@ function getElementHeight (element) { bottomRuler.remove() return height } + +function getNextTickPromise () { + return new Promise((resolve) => process.nextTick(resolve)) +} diff --git a/src/text-editor-component.js b/src/text-editor-component.js index a1867a931..d315df360 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1644,7 +1644,7 @@ class TextEditorComponent { // 4. compositionend fired // 5. textInput fired; event.data == the completion string didCompositionStart () { - if (parseInt(process.versions.chrome) === 56) { + if (this.getChromeVersion() === 56) { this.getHiddenInput().value = '' } @@ -1655,7 +1655,7 @@ class TextEditorComponent { } didCompositionUpdate (event) { - if (parseInt(process.versions.chrome) === 56) { + if (this.getChromeVersion() === 56) { process.nextTick(() => { if (this.compositionCheckpoint) { const previewText = this.getHiddenInput().value @@ -2828,6 +2828,10 @@ class TextEditorComponent { getPlatform () { return this.props.platform || process.platform } + + getChromeVersion () { + return this.props.chromeVersion || parseInt(process.versions.chrome) + } } class DummyScrollbarComponent { From 964f209c404e5aa6e62dbbd7c0f70a92605b318f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 12 Aug 2017 15:33:20 +0200 Subject: [PATCH 305/314] Don't throw an error when setting an incredibly small `lineHeight` Instead, if the measured line height equals 0, default it to 1 so that the editor component doesn't start computing `NaN` or `Infinity` values due to e.g. dividing by 0. We should probably consider sanitizing line heights smaller than a certain threshold, but that's non trivial because line height is expressed as a multiplier of the font size. Also, users may style the `line-height` property via CSS, which may still throw errors when using small values. --- spec/text-editor-component-spec.js | 38 ++++++++++++++++++++++++++++++ src/text-editor-component.js | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 2c13b48af..b2f94b097 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -119,6 +119,44 @@ describe('TextEditorComponent', () => { } }) + it('re-renders lines when their height changes', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 3, autoHeight: false}) + element.style.height = 4 * component.measurements.lineHeight + 'px' + await component.getNextUpdatePromise() + expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(9) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(9) + + element.style.lineHeight = '2.0' + TextEditor.didUpdateStyles() + await component.getNextUpdatePromise() + expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(6) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(6) + + element.style.lineHeight = '0.7' + TextEditor.didUpdateStyles() + await component.getNextUpdatePromise() + expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(12) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(12) + + element.style.lineHeight = '0.05' + TextEditor.didUpdateStyles() + await component.getNextUpdatePromise() + expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(13) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(13) + + element.style.lineHeight = '0' + TextEditor.didUpdateStyles() + await component.getNextUpdatePromise() + expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(13) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(13) + + element.style.lineHeight = '1' + TextEditor.didUpdateStyles() + await component.getNextUpdatePromise() + expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(9) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(9) + }) + it('makes the content at least as tall as the scroll container client height', async () => { const {component, element, editor} = buildComponent({text: 'a', height: 100}) expect(component.refs.content.offsetHeight).toBe(100) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 970ee29c1..f4401ad8c 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -2046,7 +2046,7 @@ class TextEditorComponent { } measureCharacterDimensions () { - this.measurements.lineHeight = this.refs.characterMeasurementLine.getBoundingClientRect().height + this.measurements.lineHeight = Math.max(1, this.refs.characterMeasurementLine.getBoundingClientRect().height) this.measurements.baseCharacterWidth = this.refs.normalWidthCharacterSpan.getBoundingClientRect().width this.measurements.doubleWidthCharacterWidth = this.refs.doubleWidthCharacterSpan.getBoundingClientRect().width this.measurements.halfWidthCharacterWidth = this.refs.halfWidthCharacterSpan.getBoundingClientRect().width From 8cbfcf6e2b6f18026818517fbb445e5080fe54d5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 12 Aug 2017 15:50:35 +0200 Subject: [PATCH 306/314] Use default cursor on dummy scrollbars and make them 15px wide/tall --- src/text-editor-component.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 970ee29c1..0b1570a24 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -2851,20 +2851,22 @@ class DummyScrollbarComponent { outerStyle.bottom = 0 outerStyle.left = 0 outerStyle.right = right + 'px' - outerStyle.height = '20px' + outerStyle.height = '15px' outerStyle.overflowY = 'hidden' outerStyle.overflowX = this.props.forceScrollbarVisible ? 'scroll' : 'auto' - innerStyle.height = '20px' + outerStyle.cursor = 'default' + innerStyle.height = '15px' innerStyle.width = (this.props.scrollWidth || 0) + 'px' } else { let bottom = (this.props.horizontalScrollbarHeight || 0) outerStyle.right = 0 outerStyle.top = 0 outerStyle.bottom = bottom + 'px' - outerStyle.width = '20px' + outerStyle.width = '15px' outerStyle.overflowX = 'hidden' outerStyle.overflowY = this.props.forceScrollbarVisible ? 'scroll' : 'auto' - innerStyle.width = '20px' + outerStyle.cursor = 'default' + innerStyle.width = '15px' innerStyle.height = (this.props.scrollHeight || 0) + 'px' } From 744b96df4601c16ebf86c456c9952872a44bd2c2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 11 Aug 2017 16:41:39 -0600 Subject: [PATCH 307/314] Suppress composition events default prevented on previous keydown This seems like a browser bug. --- src/text-editor-component.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 5764502ca..2f5817778 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1570,6 +1570,10 @@ class TextEditorComponent { } didTextInput (event) { + // Workaround for Chromium not preventing composition events when + // preventDefault is called on the keydown event that precipitated them. + if (this.lastKeydown && this.lastKeydown.defaultPrevented) return + if (!this.isInputEnabled()) return event.stopPropagation() @@ -1626,7 +1630,6 @@ class TextEditorComponent { didKeypress (event) { this.lastKeydownBeforeKeypress = this.lastKeydown - this.lastKeydown = null // This cancels the accented character behavior if we type a key normally // with the menu open. @@ -1636,7 +1639,6 @@ class TextEditorComponent { didKeyup (event) { if (this.lastKeydownBeforeKeypress && this.lastKeydownBeforeKeypress.code === event.code) { this.lastKeydownBeforeKeypress = null - this.lastKeydown = null } } @@ -1662,6 +1664,10 @@ class TextEditorComponent { } didCompositionUpdate (event) { + // Workaround for Chromium not preventing composition events when + // preventDefault is called on the keydown event that precipitated them. + if (this.lastKeydown && this.lastKeydown.defaultPrevented) return + if (this.getChromeVersion() === 56) { process.nextTick(() => { if (this.compositionCheckpoint) { From f82ff9c0d125d475909961ec44bb54cd962357c3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 12 Aug 2017 18:09:22 -0600 Subject: [PATCH 308/314] :arrow_up: language-css --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8bcb93f7e..6949a69d1 100644 --- a/package.json +++ b/package.json @@ -139,7 +139,7 @@ "language-clojure": "0.22.4", "language-coffee-script": "0.48.9", "language-csharp": "0.14.2", - "language-css": "0.42.3", + "language-css": "0.42.4", "language-gfm": "0.90.0", "language-git": "0.19.1", "language-go": "0.44.2", From 38978da0fadff72140de6b32e016a0e306b39a43 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 13 Aug 2017 14:03:58 -0600 Subject: [PATCH 309/314] Explicitly compare compositionCheckpoint against null, since it can be 0 --- src/text-editor-component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 2f5817778..403f24bbd 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1670,7 +1670,7 @@ class TextEditorComponent { if (this.getChromeVersion() === 56) { process.nextTick(() => { - if (this.compositionCheckpoint) { + if (this.compositionCheckpoint != null) { const previewText = this.getHiddenInput().value this.props.model.insertText(previewText, {select: true}) } From 91b7c14281e88929afa6cdb36f339c20c39c9590 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 14 Aug 2017 12:21:19 +0200 Subject: [PATCH 310/314] Prompt user only once when quitting/restarting and canceling save dialog I think this slipped through during the refactoring performed in dc32018. With this commit we are fixing the regression and adding a new main process regression test to exercise this behavior. --- spec/main-process/atom-application.test.js | 64 +++++++++++++++++----- src/main-process/atom-application.coffee | 13 ++++- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 1e5ae0a86..80e99adc0 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -14,10 +14,11 @@ const ATOM_RESOURCE_PATH = path.resolve(__dirname, '..', '..') describe('AtomApplication', function () { this.timeout(60 * 1000) - let originalAppQuit, originalAtomHome, atomApplicationsToDestroy + let originalAppQuit, originalShowMessageBox, originalAtomHome, atomApplicationsToDestroy beforeEach(function () { originalAppQuit = electron.app.quit + originalShowMessageBox = electron.dialog mockElectronAppQuit() originalAtomHome = process.env.ATOM_HOME process.env.ATOM_HOME = makeTempDir('atom-home') @@ -39,6 +40,7 @@ describe('AtomApplication', function () { } await clearElectronSession() electron.app.quit = originalAppQuit + electron.dialog.showMessageBox = originalShowMessageBox }) describe('launch', function () { @@ -462,20 +464,42 @@ describe('AtomApplication', function () { }) }) - describe('before quitting', function () { - it('waits until all the windows have saved their state and then quits', async function () { - const dirAPath = makeTempDir("a") - const dirBPath = makeTempDir("b") - const atomApplication = buildAtomApplication() - const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath, 'file-a')])) - await focusWindow(window1) - const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath, 'file-b')])) - await focusWindow(window2) - electron.app.quit() - assert(!electron.app.hasQuitted()) - await Promise.all([window1.lastPrepareToUnloadPromise, window2.lastPrepareToUnloadPromise]) - assert(electron.app.hasQuitted()) + it('waits until all the windows have saved their state before quitting', async function () { + const dirAPath = makeTempDir("a") + const dirBPath = makeTempDir("b") + const atomApplication = buildAtomApplication() + const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath, 'file-a')])) + await focusWindow(window1) + const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath, 'file-b')])) + await focusWindow(window2) + electron.app.quit() + assert(!electron.app.hasQuitted()) + await Promise.all([window1.lastPrepareToUnloadPromise, window2.lastPrepareToUnloadPromise]) + assert(electron.app.hasQuitted()) + }) + + it.only('prevents quitting if user cancels when prompted to save an item', async () => { + const atomApplication = buildAtomApplication() + const window1 = atomApplication.launch(parseCommandLine([])) + const window2 = atomApplication.launch(parseCommandLine([])) + await Promise.all([window1.loadedPromise, window2.loadedPromise]) + await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { + atom.workspace.getActiveTextEditor().insertText('unsaved text') + sendBackToMainProcess() }) + + // Choosing "Cancel" + mockElectronShowMessageBox({choice: 1}) + electron.app.quit() + await atomApplication.lastBeforeQuitPromise + assert(!electron.app.hasQuitted()) + assert.equal(electron.app.quit.callCount, 1) // Ensure choosing "Cancel" doesn't try to quit the electron app more than once (regression) + + // Choosing "Don't save" + mockElectronShowMessageBox({choice: 2}) + electron.app.quit() + await atomApplication.lastBeforeQuitPromise + assert(electron.app.hasQuitted()) }) function buildAtomApplication () { @@ -496,6 +520,12 @@ describe('AtomApplication', function () { function mockElectronAppQuit () { let quitted = false electron.app.quit = function () { + if (electron.app.quit.callCount) { + electron.app.quit.callCount++ + } else { + electron.app.quit.callCount = 1 + } + let shouldQuit = true electron.app.emit('before-quit', {preventDefault: () => { shouldQuit = false }}) if (shouldQuit) { @@ -507,6 +537,12 @@ describe('AtomApplication', function () { } } + function mockElectronShowMessageBox ({choice}) { + electron.dialog.showMessageBox = function () { + return choice + } + } + function makeTempDir (name) { const temp = require('temp').track() return fs.realpathSync(temp.mkdirSync(name)) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 8e8fb8b55..dcc7c6513 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -269,10 +269,19 @@ class AtomApplication @openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md')) @disposable.add ipcHelpers.on app, 'before-quit', (event) => - unless @quitting + resolveBeforeQuitPromise = null + @lastBeforeQuitPromise = new Promise((resolve) -> resolveBeforeQuitPromise = resolve) + if @quitting + resolveBeforeQuitPromise() + else event.preventDefault() @quitting = true - Promise.all(@windows.map((window) -> window.prepareToUnload())).then(-> app.quit()) + windowUnloadPromises = @windows.map((window) -> window.prepareToUnload()) + Promise.all(windowUnloadPromises).then((windowUnloadedResults) -> + didUnloadAllWindows = windowUnloadedResults.every((didUnloadWindow) -> didUnloadWindow) + app.quit() if didUnloadAllWindows + resolveBeforeQuitPromise() + ) @disposable.add ipcHelpers.on app, 'will-quit', => @killAllProcesses() From e50a73b033395b3a1b7b95f5f2c136a6abf89f73 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 14 Aug 2017 12:28:19 +0200 Subject: [PATCH 311/314] Fix tests --- 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 80e99adc0..62fae82b3 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -18,7 +18,7 @@ describe('AtomApplication', function () { beforeEach(function () { originalAppQuit = electron.app.quit - originalShowMessageBox = electron.dialog + originalShowMessageBox = electron.dialog.showMessageBox mockElectronAppQuit() originalAtomHome = process.env.ATOM_HOME process.env.ATOM_HOME = makeTempDir('atom-home') @@ -478,7 +478,7 @@ describe('AtomApplication', function () { assert(electron.app.hasQuitted()) }) - it.only('prevents quitting if user cancels when prompted to save an item', async () => { + it('prevents quitting if user cancels when prompted to save an item', async () => { const atomApplication = buildAtomApplication() const window1 = atomApplication.launch(parseCommandLine([])) const window2 = atomApplication.launch(parseCommandLine([])) From eb1eeb3fde80d812e528df41fe34a5b0f9afdd9f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 14 Aug 2017 15:43:48 +0200 Subject: [PATCH 312/314] Ignore clicks on block decorations Previously, clicking on a block decoration to interact with it would cause the editor to scroll to the line next to it. This is inconvenient, especially if the decoration was designed to be interactive and contained buttons or links. If the decoration was close to the bottom of the screen, clicking on a button inside of it would make the editor scroll down and abort the click. This behavior regressed during the editor rendering layer rewrite and with this commit we are restoring the original behavior by simply ignoring clicks that land on block decorations. --- spec/text-editor-component-spec.js | 33 ++++++++++++++++++++++++++++++ src/text-editor-component.js | 12 +++++++++++ 2 files changed, 45 insertions(+) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index a4631b541..4c0108b33 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -2153,6 +2153,39 @@ describe('TextEditorComponent', () => { expect(component.refs.blockDecorationMeasurementArea.offsetWidth).toBe(component.getScrollWidth()) }) + it('does not change the cursor position when clicking on a block decoration', async () => { + const {editor, component} = buildComponent() + + const decorationElement = document.createElement('div') + decorationElement.textContent = 'Parent' + const childElement = document.createElement('div') + childElement.textContent = 'Child' + decorationElement.appendChild(childElement) + const marker = editor.markScreenPosition([4, 0]) + editor.decorateMarker(marker, {type: 'block', item: decorationElement}) + await component.getNextUpdatePromise() + + const decorationElementClientRect = decorationElement.getBoundingClientRect() + component.didMouseDownOnContent({ + target: decorationElement, + detail: 1, + button: 0, + clientX: decorationElementClientRect.left, + clientY: decorationElementClientRect.top + }) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + + const childElementClientRect = childElement.getBoundingClientRect() + component.didMouseDownOnContent({ + target: childElement, + detail: 1, + button: 0, + clientX: childElementClientRect.left, + clientY: childElementClientRect.top + }) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + }) + function createBlockDecorationAtScreenRow(editor, screenRow, {height, margin, position}) { const marker = editor.markScreenPosition([screenRow, 0], {invalidate: 'never'}) const item = document.createElement('div') diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 403f24bbd..e409001a8 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1689,6 +1689,18 @@ class TextEditorComponent { const {target, button, detail, ctrlKey, shiftKey, metaKey} = event const platform = this.getPlatform() + // Ignore clicks on block decorations. + if (target) { + let element = target + while (element && element !== this.element) { + if (this.blockDecorationsByElement.has(element)) { + return + } + + element = element.parentElement + } + } + // On Linux, position the cursor on middle mouse button click. A // textInput event with the contents of the selection clipboard will be // dispatched by the browser automatically on mouseup. From 433c514673670500eba609fb4e0ca7cc49900aa5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 14 Aug 2017 11:53:02 -0600 Subject: [PATCH 313/314] Always render hidden input This avoids mysterious timing issues in which the editor gets a 'focus' event in a state where `isVisible` returns false. If we always render the hidden input, we can always focus it. Signed-off-by: Antonio Scandurra --- spec/text-editor-element-spec.js | 16 ++++++++++++++++ src/text-editor-component.js | 7 +++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/spec/text-editor-element-spec.js b/spec/text-editor-element-spec.js index 684d160a8..6b8844b17 100644 --- a/spec/text-editor-element-spec.js +++ b/spec/text-editor-element-spec.js @@ -203,6 +203,22 @@ describe('TextEditorElement', () => { expect(document.activeElement).toBe(element.querySelector('input')) }) }) + + describe('if focused when invisible due to a zero height and width', () => { + it('focuses the hidden input and does not throw an exception', () => { + const parentElement = document.createElement('div') + parentElement.style.position = 'absolute' + parentElement.style.width = '0px' + parentElement.style.height = '0px' + + const element = buildTextEditorElement({attach: false}) + parentElement.appendChild(element) + jasmineContent.appendChild(parentElement) + + element.focus() + expect(document.activeElement).toBe(element.component.getHiddenInput()) + }) + }) }) describe('::setModel', () => { diff --git a/src/text-editor-component.js b/src/text-editor-component.js index e409001a8..2370774e3 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -552,6 +552,7 @@ class TextEditorComponent { ] } else { children = [ + this.renderCursorsAndInput(), this.renderBlockDecorationMeasurementArea(), this.renderCharacterMeasurementLine() ] @@ -1449,16 +1450,14 @@ class TextEditorComponent { this.scheduleUpdate() } - const {hiddenInput} = this.refs.cursorsAndInput.refs - hiddenInput.focus() + this.getHiddenInput().focus() } // Called by TextEditorElement so that this function is always the first // listener to be fired, even if other listeners are bound before creating // the component. didBlur (event) { - const {cursorsAndInput} = this.refs - if (cursorsAndInput && event.relatedTarget === cursorsAndInput.refs.hiddenInput) { + if (event.relatedTarget === this.getHiddenInput()) { event.stopImmediatePropagation() } } From 708c9b4e22f03261a6e873a342dc4a26b0102bdf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 14 Aug 2017 15:35:48 -0600 Subject: [PATCH 314/314] :arrow_up: text-buffer Fixes #15190 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6949a69d1..30b09d9ab 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.0.8", + "text-buffer": "13.0.9", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1",