From 3bee3633757761b7052a7d7a0eb5c7df7a79d1ca Mon Sep 17 00:00:00 2001 From: Morten Piibeleht Date: Tue, 18 Jul 2017 10:40:24 +1200 Subject: [PATCH 001/176] Allow independent Atom instances By having an $ATOM_HOME-dependent part in the socket name, Atom instances that have different homes will run in independent processes. Fixes the current behaviour where starting Atom with a new $ATOM_HOME "opens" an Atom window with settings and packages from the original $ATOM_HOME. Useful for IDEs. --- src/main-process/atom-application.coffee | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index dcc7c6513..13bee7407 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -8,6 +8,7 @@ FileRecoveryService = require './file-recovery-service' ipcHelpers = require '../ipc-helpers' {BrowserWindow, Menu, app, dialog, ipcMain, shell, screen} = require 'electron' {CompositeDisposable, Disposable} = require 'event-kit' +crypto = require 'crypto' fs = require 'fs-plus' path = require 'path' os = require 'os' @@ -33,11 +34,16 @@ class AtomApplication # Public: The entry point into the Atom application. @open: (options) -> unless options.socketPath? + username = if process.platform is 'win32' then process.env.USERNAME else process.env.USER + # Lowercasing the ATOM_HOME to make sure that we don't get multiple sockets + # on case-insensitive filesystems due to arbitrary case differences in paths. + atomHomeUnique = path.resolve(process.env.ATOM_HOME).toLowerCase() + hash = crypto.createHash('sha1').update(username).update('|').update(atomHomeUnique) + atomInstanceDigest = hash.digest('hex').substring(0, 32) if process.platform is 'win32' - userNameSafe = new Buffer(process.env.USERNAME).toString('base64') - options.socketPath = "\\\\.\\pipe\\atom-#{options.version}-#{userNameSafe}-#{process.arch}-sock" + options.socketPath = "\\\\.\\pipe\\atom-#{options.version}-#{process.arch}-#{atomInstanceDigest}-sock" else - options.socketPath = path.join(os.tmpdir(), "atom-#{options.version}-#{process.env.USER}.sock") + options.socketPath = path.join(os.tmpdir(), "atom-#{options.version}-#{process.arch}-#{atomInstanceDigest}.sock") # FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely # take a few seconds to trigger 'error' event, it could be a bug of node From c629a1aac48252b319ce50348939312b098b066e Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Thu, 2 Nov 2017 10:43:56 -0500 Subject: [PATCH 002/176] Make notifications.clear public and emit event --- src/notification-manager.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/notification-manager.js b/src/notification-manager.js index df5e5fb42..3d15419d5 100644 --- a/src/notification-manager.js +++ b/src/notification-manager.js @@ -199,8 +199,10 @@ class NotificationManager { /* Section: Managing Notifications */ - + + // Public: Clear all the notifications. clear () { this.notifications = [] + this.emitter.emit('did-clear-notifications') } } From a565988c6f8aca6cfb4ebf6bd4902aa6ae5796c3 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Thu, 2 Nov 2017 10:59:04 -0500 Subject: [PATCH 003/176] add event --- src/notification-manager.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/notification-manager.js b/src/notification-manager.js index 3d15419d5..cd2a06e9e 100644 --- a/src/notification-manager.js +++ b/src/notification-manager.js @@ -27,6 +27,15 @@ class NotificationManager { return this.emitter.on('did-add-notification', callback) } + // Public: Invoke the given callback after the notifications have been cleared. + // + // * `callback` {Function} to be called after the notifications are cleared. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidClearNotifications (callback) { + return this.emitter.on('did-clear-notifications', callback) + } + /* Section: Adding Notifications */ From 076308ab7343cc907722e7094a7402e51c6c6128 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Thu, 16 Nov 2017 00:10:13 -0600 Subject: [PATCH 004/176] fix linting --- src/notification-manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notification-manager.js b/src/notification-manager.js index cd2a06e9e..a0ae139d3 100644 --- a/src/notification-manager.js +++ b/src/notification-manager.js @@ -208,7 +208,7 @@ class NotificationManager { /* Section: Managing Notifications */ - + // Public: Clear all the notifications. clear () { this.notifications = [] From fe4df885d650724e5f11ccbc024c518a6d34d0fd Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Thu, 16 Nov 2017 00:29:24 -0600 Subject: [PATCH 005/176] add tests --- spec/notification-manager-spec.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/spec/notification-manager-spec.js b/spec/notification-manager-spec.js index 3f6a20b67..b62569ee6 100644 --- a/spec/notification-manager-spec.js +++ b/spec/notification-manager-spec.js @@ -66,4 +66,27 @@ describe('NotificationManager', () => { expect(notification.getType()).toBe('success') }) }) + + describe('clearing notifications', function () { + it('clears the notifications when ::clear has been called', function(){ + manager.addSuccess('success') + expect(manager.getNotifications().length).toBe(1) + manager.clear() + expect(manager.getNotifications().length).toBe(0) + }) + + describe('adding events', () => { + let addSpy + + beforeEach(() => { + addSpy = jasmine.createSpy() + manager.onDidClearNotifications(addSpy) + }) + + it('emits an event when the notifications have been cleared', () => { + manager.clear() + expect(addSpy).toHaveBeenCalled() + }) + }) + }) }) From b54655e41d2d57c42456e67d1f3aa421c2b6bf0f Mon Sep 17 00:00:00 2001 From: Ian Olsen Date: Mon, 6 Nov 2017 14:46:51 -0800 Subject: [PATCH 006/176] :arrow_up: electron@1.7.9 --- package.json | 2 +- script/package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 15dc32972..7451e7e58 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "url": "https://github.com/atom/atom/issues" }, "license": "MIT", - "electronVersion": "1.6.15", + "electronVersion": "1.7.9", "dependencies": { "@atom/nsfw": "^1.0.18", "@atom/source-map-support": "^0.3.4", diff --git a/script/package.json b/script/package.json index 4cf1bfb8c..3f335417a 100644 --- a/script/package.json +++ b/script/package.json @@ -8,9 +8,9 @@ "colors": "1.1.2", "csslint": "1.0.2", "donna": "1.0.16", - "electron-chromedriver": "~1.6", + "electron-chromedriver": "~1.7", "electron-link": "0.1.2", - "electron-mksnapshot": "~1.6", + "electron-mksnapshot": "~1.7", "electron-packager": "7.3.0", "electron-winstaller": "2.6.3", "fs-admin": "^0.1.5", From 909caa2a59d96059180ba1c78dd08ae87b2e67d3 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 28 Nov 2017 20:01:31 -0800 Subject: [PATCH 007/176] Add 'readonly' attribute to element --- spec/text-editor-element-spec.js | 14 ++++++++++++++ src/text-editor-component.js | 14 ++++++++++---- src/text-editor-element.js | 6 +++++- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/spec/text-editor-element-spec.js b/spec/text-editor-element-spec.js index 7cdd374a1..02c3c0ba0 100644 --- a/spec/text-editor-element-spec.js +++ b/spec/text-editor-element-spec.js @@ -70,6 +70,20 @@ describe('TextEditorElement', () => { expect(element.getModel().isLineNumberGutterVisible()).toBe(false) }) + it("honors the 'readonly' attribute", async function() { + console.log('set attribute'); + jasmineContent.innerHTML = "" + const element = jasmineContent.firstChild + + expect(element.getComponent().isInputEnabled()).toBe(false) + + element.removeAttribute('readonly') + expect(element.getComponent().isInputEnabled()).toBe(true) + + element.setAttribute('readonly', true) + expect(element.getComponent().isInputEnabled()).toBe(false) + }) + it('honors the text content', () => { jasmineContent.innerHTML = 'testing' const element = jasmineContent.firstChild diff --git a/src/text-editor-component.js b/src/text-editor-component.js index da6ec452d..c6135087c 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -55,6 +55,8 @@ class TextEditorComponent { constructor (props) { this.props = props + this.setInputEnabled(!this.props.readonly) + if (!props.model) { props.model = new TextEditor({mini: props.mini}) } @@ -460,9 +462,13 @@ class TextEditorComponent { } } - let attributes = null + let attributes = {} if (model.isMini()) { - attributes = {mini: ''} + attributes.mini = '' + } + + if (!this.isInputEnabled()) { + attributes.readonly = '' } const dataset = {encoding: model.getEncoding()} @@ -819,7 +825,7 @@ class TextEditorComponent { const oldClassList = this.classList const newClassList = ['editor'] - if (this.focused) newClassList.push('is-focused') + if (this.focused && this.isInputEnabled()) newClassList.push('is-focused') if (model.isMini()) newClassList.push('mini') for (var i = 0; i < model.selections.length; i++) { if (!model.selections[i].isEmpty()) { @@ -2966,7 +2972,7 @@ class TextEditorComponent { } isInputEnabled (inputEnabled) { - return this.props.inputEnabled != null ? this.props.inputEnabled : true + return this.props.inputEnabled; } getHiddenInput () { diff --git a/src/text-editor-element.js b/src/text-editor-element.js index d56c5596b..83ef5da36 100644 --- a/src/text-editor-element.js +++ b/src/text-editor-element.js @@ -59,6 +59,9 @@ class TextEditorElement extends HTMLElement { case 'gutter-hidden': this.getModel().update({lineNumberGutterVisible: newValue == null}) break + case 'readonly': + this.getComponent().setInputEnabled(newValue == null) + break } } } @@ -275,7 +278,8 @@ class TextEditorElement extends HTMLElement { this.component = new TextEditorComponent({ element: this, mini: this.hasAttribute('mini'), - updatedSynchronously: this.updatedSynchronously + updatedSynchronously: this.updatedSynchronously, + readonly: this.hasAttribute('readonly') }) this.updateModelFromAttributes() } From aeb9af63dfba1c475b0e6b7f74d8ed4c04355fa2 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 28 Nov 2017 21:14:40 -0800 Subject: [PATCH 008/176] Store readOnly state on the TextEditor model --- src/text-editor-component.js | 8 +++----- src/text-editor-element.js | 4 ++-- src/text-editor.js | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index c6135087c..4557469dc 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -55,10 +55,8 @@ class TextEditorComponent { constructor (props) { this.props = props - this.setInputEnabled(!this.props.readonly) - if (!props.model) { - props.model = new TextEditor({mini: props.mini}) + props.model = new TextEditor({mini: props.mini, readOnly: props.readOnly}) } this.props.model.component = this @@ -2968,11 +2966,11 @@ class TextEditorComponent { } setInputEnabled (inputEnabled) { - this.props.inputEnabled = inputEnabled + this.props.model.update({readOnly: !inputEnabled}) } isInputEnabled (inputEnabled) { - return this.props.inputEnabled; + return !this.props.model.isReadOnly(); } getHiddenInput () { diff --git a/src/text-editor-element.js b/src/text-editor-element.js index 83ef5da36..7218b7f05 100644 --- a/src/text-editor-element.js +++ b/src/text-editor-element.js @@ -60,7 +60,7 @@ class TextEditorElement extends HTMLElement { this.getModel().update({lineNumberGutterVisible: newValue == null}) break case 'readonly': - this.getComponent().setInputEnabled(newValue == null) + this.getModel().update({readOnly: newValue != null}) break } } @@ -279,7 +279,7 @@ class TextEditorElement extends HTMLElement { element: this, mini: this.hasAttribute('mini'), updatedSynchronously: this.updatedSynchronously, - readonly: this.hasAttribute('readonly') + readOnly: this.hasAttribute('readonly') }) this.updateModelFromAttributes() } diff --git a/src/text-editor.js b/src/text-editor.js index a0b9d19a0..aef6ffc1a 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -121,6 +121,7 @@ class TextEditor { this.decorationManager = params.decorationManager this.selectionsMarkerLayer = params.selectionsMarkerLayer this.mini = (params.mini != null) ? params.mini : false + this.readOnly = (params.readOnly != null) ? params.readOnly : false this.placeholderText = params.placeholderText this.showLineNumbers = params.showLineNumbers this.largeFileMode = params.largeFileMode @@ -404,6 +405,15 @@ class TextEditor { } break + case 'readOnly': + if (value !== this.readOnly) { + this.readOnly = value + if (this.component != null) { + this.component.scheduleUpdate() + } + } + break + case 'placeholderText': if (value !== this.placeholderText) { this.placeholderText = value @@ -538,6 +548,7 @@ class TextEditor { softWrapAtPreferredLineLength: this.softWrapAtPreferredLineLength, preferredLineLength: this.preferredLineLength, mini: this.mini, + readOnly: this.readOnly, editorWidthInChars: this.editorWidthInChars, width: this.width, largeFileMode: this.largeFileMode, @@ -968,6 +979,12 @@ class TextEditor { isMini () { return this.mini } + setReadOnly (readOnly) { + this.update({readOnly}) + } + + isReadOnly () { return this.readOnly } + onDidChangeMini (callback) { return this.emitter.on('did-change-mini', callback) } From a993742f7fbe8028b4ee154465db35e810549538 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 28 Nov 2017 22:06:33 -0800 Subject: [PATCH 009/176] :shirt: :fire: semicolon --- 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 4557469dc..08af5ada1 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -2970,7 +2970,7 @@ class TextEditorComponent { } isInputEnabled (inputEnabled) { - return !this.props.model.isReadOnly(); + return !this.props.model.isReadOnly() } getHiddenInput () { From 0a9437bef2d8014a94b637ddaf4c8e0e12276f73 Mon Sep 17 00:00:00 2001 From: Xavier Fontes Date: Wed, 29 Nov 2017 15:38:47 +0000 Subject: [PATCH 010/176] :bug: Add event handler for window resizing. Added event handler to solve issues about saving the window dimensions --- src/window-event-handler.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/window-event-handler.js b/src/window-event-handler.js index 6d380819b..da735294e 100644 --- a/src/window-event-handler.js +++ b/src/window-event-handler.js @@ -9,6 +9,7 @@ class WindowEventHandler { this.handleFocusNext = this.handleFocusNext.bind(this) this.handleFocusPrevious = this.handleFocusPrevious.bind(this) this.handleWindowBlur = this.handleWindowBlur.bind(this) + this.handleWindowResize = this.handleWindowResize.bind(this) this.handleEnterFullScreen = this.handleEnterFullScreen.bind(this) this.handleLeaveFullScreen = this.handleLeaveFullScreen.bind(this) this.handleWindowBeforeunload = this.handleWindowBeforeunload.bind(this) @@ -51,6 +52,7 @@ class WindowEventHandler { this.addEventListener(this.window, 'beforeunload', this.handleWindowBeforeunload) this.addEventListener(this.window, 'focus', this.handleWindowFocus) this.addEventListener(this.window, 'blur', this.handleWindowBlur) + this.addEventListener(this.window, 'resize', this.handleWindowResize) this.addEventListener(this.document, 'keyup', this.handleDocumentKeyEvent) this.addEventListener(this.document, 'keydown', this.handleDocumentKeyEvent) @@ -189,6 +191,10 @@ class WindowEventHandler { this.atomEnvironment.storeWindowDimensions() } + handleWindowResize () { + this.atomEnvironment.storeWindowDimensions() + } + handleEnterFullScreen () { this.document.body.classList.add('fullscreen') } From 03ac8d715e3ed95ecd578846a5d9ed58f2bf0a6c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 29 Nov 2017 17:01:20 -0800 Subject: [PATCH 011/176] :arrow_up: language-javascript for new tree-sitter version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc3313577..1fc94780f 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,7 @@ "language-html": "0.48.3", "language-hyperlink": "0.16.3", "language-java": "0.27.6", - "language-javascript": "0.127.7", + "language-javascript": "0.128.0-0", "language-json": "0.19.1", "language-less": "0.34.1", "language-make": "0.22.3", From 5c1a49fccf291b229cee1c76faf52e590249784d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 29 Nov 2017 17:14:11 -0800 Subject: [PATCH 012/176] Add initial TreeSitterLanguageMode implementation Much of this is from the tree-sitter-syntax package. Also, add a dependency on the tree-sitter module. --- package.json | 1 + spec/syntax-scope-map-spec.js | 77 ++++++ src/syntax-scope-map.js | 178 +++++++++++++ src/tree-sitter-grammar.js | 74 ++++++ src/tree-sitter-language-mode.js | 416 +++++++++++++++++++++++++++++++ 5 files changed, 746 insertions(+) create mode 100644 spec/syntax-scope-map-spec.js create mode 100644 src/syntax-scope-map.js create mode 100644 src/tree-sitter-grammar.js create mode 100644 src/tree-sitter-language-mode.js diff --git a/package.json b/package.json index 1fc94780f..91cf950b4 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "sinon": "1.17.4", "temp": "^0.8.3", "text-buffer": "13.9.2", + "tree-sitter": "0.7.4", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", diff --git a/spec/syntax-scope-map-spec.js b/spec/syntax-scope-map-spec.js new file mode 100644 index 000000000..61b1bdc7d --- /dev/null +++ b/spec/syntax-scope-map-spec.js @@ -0,0 +1,77 @@ +const SyntaxScopeMap = require('../src/syntax-scope-map') + +describe('SyntaxScopeMap', () => { + it('can match immediate child selectors', () => { + const map = new SyntaxScopeMap({ + 'a > b > c': 'x', + 'b > c': 'y', + 'c': 'z' + }) + + expect(map.get(['a', 'b', 'c'], [0, 0, 0])).toBe('x') + expect(map.get(['d', 'b', 'c'], [0, 0, 0])).toBe('y') + expect(map.get(['d', 'e', 'c'], [0, 0, 0])).toBe('z') + expect(map.get(['e', 'c'], [0, 0, 0])).toBe('z') + expect(map.get(['c'], [0, 0, 0])).toBe('z') + expect(map.get(['d'], [0, 0, 0])).toBe(undefined) + }) + + it('can match :nth-child pseudo-selectors on leaves', () => { + const map = new SyntaxScopeMap({ + 'a > b': 'w', + 'a > b:nth-child(1)': 'x', + 'b': 'y', + 'b:nth-child(2)': 'z' + }) + + expect(map.get(['a', 'b'], [0, 0])).toBe('w') + expect(map.get(['a', 'b'], [0, 1])).toBe('x') + expect(map.get(['a', 'b'], [0, 2])).toBe('w') + expect(map.get(['b'], [0])).toBe('y') + expect(map.get(['b'], [1])).toBe('y') + expect(map.get(['b'], [2])).toBe('z') + }) + + it('can match :nth-child pseudo-selectors on interior nodes', () => { + const map = new SyntaxScopeMap({ + 'b:nth-child(1) > c': 'w', + 'a > b > c': 'x', + 'a > b:nth-child(2) > c': 'y' + }) + + expect(map.get(['b', 'c'], [0, 0])).toBe(undefined) + expect(map.get(['b', 'c'], [1, 0])).toBe('w') + expect(map.get(['a', 'b', 'c'], [1, 0, 0])).toBe('x') + expect(map.get(['a', 'b', 'c'], [1, 2, 0])).toBe('y') + }) + + it('allows anonymous tokens to be referred to by their string value', () => { + const map = new SyntaxScopeMap({ + '"b"': 'w', + 'a > "b"': 'x', + 'a > "b":nth-child(1)': 'y' + }) + + expect(map.get(['b'], [0], true)).toBe(undefined) + expect(map.get(['b'], [0], false)).toBe('w') + expect(map.get(['a', 'b'], [0, 0], false)).toBe('x') + expect(map.get(['a', 'b'], [0, 1], false)).toBe('y') + }) + + it('supports the wildcard selector', () => { + const map = new SyntaxScopeMap({ + '*': 'w', + 'a > *': 'x', + 'a > *:nth-child(1)': 'y', + 'a > *:nth-child(1) > b': 'z' + }) + + expect(map.get(['b'], [0])).toBe('w') + expect(map.get(['c'], [0])).toBe('w') + expect(map.get(['a', 'b'], [0, 0])).toBe('x') + expect(map.get(['a', 'b'], [0, 1])).toBe('y') + expect(map.get(['a', 'c'], [0, 1])).toBe('y') + expect(map.get(['a', 'c', 'b'], [0, 1, 1])).toBe('z') + expect(map.get(['a', 'c', 'b'], [0, 2, 1])).toBe('w') + }) +}) diff --git a/src/syntax-scope-map.js b/src/syntax-scope-map.js new file mode 100644 index 000000000..e000fb647 --- /dev/null +++ b/src/syntax-scope-map.js @@ -0,0 +1,178 @@ +const parser = require('postcss-selector-parser') + +module.exports = +class SyntaxScopeMap { + constructor (scopeNamesBySelector) { + this.namedScopeTable = {} + this.anonymousScopeTable = {} + for (let selector in scopeNamesBySelector) { + this.addSelector(selector, scopeNamesBySelector[selector]) + } + setTableDefaults(this.namedScopeTable) + setTableDefaults(this.anonymousScopeTable) + } + + addSelector (selector, scopeName) { + parser((parseResult) => { + for (let selectorNode of parseResult.nodes) { + let currentTable = null + let currentIndexValue = null + + for (let i = selectorNode.nodes.length - 1; i >= 0; i--) { + const termNode = selectorNode.nodes[i] + + switch (termNode.type) { + case 'tag': + if (!currentTable) currentTable = this.namedScopeTable + if (!currentTable[termNode.value]) currentTable[termNode.value] = {} + currentTable = currentTable[termNode.value] + if (currentIndexValue != null) { + if (!currentTable.indices) currentTable.indices = {} + if (!currentTable.indices[currentIndexValue]) currentTable.indices[currentIndexValue] = {} + currentTable = currentTable.indices[currentIndexValue] + currentIndexValue = null + } + break + + case 'string': + if (!currentTable) currentTable = this.anonymousScopeTable + const value = termNode.value.slice(1, -1) + if (!currentTable[value]) currentTable[value] = {} + currentTable = currentTable[value] + if (currentIndexValue != null) { + if (!currentTable.indices) currentTable.indices = {} + if (!currentTable.indices[currentIndexValue]) currentTable.indices[currentIndexValue] = {} + currentTable = currentTable.indices[currentIndexValue] + currentIndexValue = null + } + break + + case 'universal': + if (currentTable) { + if (!currentTable['*']) currentTable['*'] = {} + currentTable = currentTable['*'] + } else { + if (!this.namedScopeTable['*']) { + this.namedScopeTable['*'] = this.anonymousScopeTable['*'] = {} + } + currentTable = this.namedScopeTable['*'] + } + if (currentIndexValue != null) { + if (!currentTable.indices) currentTable.indices = {} + if (!currentTable.indices[currentIndexValue]) currentTable.indices[currentIndexValue] = {} + currentTable = currentTable.indices[currentIndexValue] + currentIndexValue = null + } + break + + case 'combinator': + if (currentIndexValue != null) { + rejectSelector(selector) + } + + if (termNode.value === '>') { + if (!currentTable.parents) currentTable.parents = {} + currentTable = currentTable.parents + } else { + rejectSelector(selector) + } + break + + case 'pseudo': + if (termNode.value === ':nth-child') { + currentIndexValue = termNode.nodes[0].nodes[0].value + } else { + rejectSelector(selector) + } + break + + default: + rejectSelector(selector) + } + } + + currentTable.scopeName = scopeName + } + }).process(selector) + } + + get (nodeTypes, childIndices, leafIsNamed = true) { + let result + let i = nodeTypes.length - 1 + let currentTable = leafIsNamed + ? this.namedScopeTable[nodeTypes[i]] + : this.anonymousScopeTable[nodeTypes[i]] + + if (!currentTable) currentTable = this.namedScopeTable['*'] + + while (currentTable) { + if (currentTable.indices && currentTable.indices[childIndices[i]]) { + currentTable = currentTable.indices[childIndices[i]] + } + + if (currentTable.scopeName) { + result = currentTable.scopeName + } + + if (i === 0) break + i-- + currentTable = currentTable.parents && ( + currentTable.parents[nodeTypes[i]] || + currentTable.parents['*'] + ) + } + + return result + } +} + +function setTableDefaults (table) { + const defaultTypeTable = table['*'] + + for (let type in table) { + let typeTable = table[type] + if (typeTable === defaultTypeTable) continue + + if (defaultTypeTable) { + mergeTable(typeTable, defaultTypeTable) + } + + if (typeTable.parents) { + setTableDefaults(typeTable.parents) + } + + for (let key in typeTable.indices) { + const indexTable = typeTable.indices[key] + mergeTable(indexTable, typeTable, false) + if (indexTable.parents) { + setTableDefaults(indexTable.parents) + } + } + } +} + +function mergeTable (table, defaultTable, mergeIndices = true) { + if (mergeIndices && defaultTable.indices) { + if (!table.indices) table.indices = {} + for (let key in defaultTable.indices) { + if (!table.indices[key]) table.indices[key] = {} + mergeTable(table.indices[key], defaultTable.indices[key]) + } + } + + if (defaultTable.parents) { + if (!table.parents) table.parents = {} + for (let key in defaultTable.parents) { + if (!table.parents[key]) table.parents[key] = {} + mergeTable(table.parents[key], defaultTable.parents[key]) + } + } + + if (defaultTable.scopeName && !table.scopeName) { + table.scopeName = defaultTable.scopeName + } +} + +function rejectSelector (selector) { + throw new TypeError(`Unsupported selector '${selector}'`) +} diff --git a/src/tree-sitter-grammar.js b/src/tree-sitter-grammar.js new file mode 100644 index 000000000..141e2da5f --- /dev/null +++ b/src/tree-sitter-grammar.js @@ -0,0 +1,74 @@ +const path = require('path') +const SyntaxScopeMap = require('./syntax-scope-map') +const Module = require('module') +const {OnigRegExp} = require('oniguruma') + +module.exports = +class TreeSitterGrammar { + constructor (registry, filePath, params) { + this.registry = registry + this.id = params.id + this.name = params.name + + this.foldConfig = params.folds || {} + if (!this.foldConfig.delimiters) this.foldConfig.delimiters = [] + if (!this.foldConfig.tokens) this.foldConfig.tokens = [] + + this.commentStrings = { + commentStartString: params.comments && params.comments.start, + commentEndString: params.comments && params.comments.end + } + + const scopeSelectors = {} + for (const key of Object.keys(params.scopes)) { + scopeSelectors[key] = params.scopes[key] + .split('.') + .map(s => `syntax--${s}`) + .join(' ') + } + + this.scopeMap = new SyntaxScopeMap(scopeSelectors) + this.fileTypes = params.fileTypes + + // TODO - When we upgrade to a new enough version of node, use `require.resolve` + // with the new `paths` option instead of this private API. + const languageModulePath = Module._resolveFilename(params.parser, { + id: filePath, + filename: filePath, + paths: Module._nodeModulePaths(path.dirname(filePath)) + }) + + this.languageModule = require(languageModulePath) + this.firstLineRegex = new OnigRegExp(params.firstLineMatch) + this.scopesById = new Map() + this.idsByScope = {} + this.nextScopeId = 256 + 1 + this.registration = null + } + + idForScope (scope) { + let id = this.idsByScope[scope] + if (!id) { + id = this.nextScopeId += 2 + this.idsByScope[scope] = id + this.scopesById.set(id, scope) + } + return id + } + + classNameForScopeId (id) { + return this.scopesById.get(id) + } + + get scopeName () { + return this.id + } + + activate () { + this.registration = this.registry.addGrammar(this) + } + + deactivate () { + if (this.registration) this.registration.dispose() + } +} diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js new file mode 100644 index 000000000..7d77a99fd --- /dev/null +++ b/src/tree-sitter-language-mode.js @@ -0,0 +1,416 @@ +const {Document} = require('tree-sitter') +const {Point, Range, Emitter} = require('atom') +const ScopeDescriptor = require('./scope-descriptor') +const TokenizedLine = require('./tokenized-line') + +let nextId = 0 + +module.exports = +class TreeSitterLanguageMode { + constructor ({buffer, grammar, config}) { + this.id = nextId++ + this.buffer = buffer + this.grammar = grammar + this.config = config + this.document = new Document() + this.document.setInput(new TreeSitterTextBufferInput(buffer)) + this.document.setLanguage(grammar.languageModule) + this.document.parse() + this.rootScopeDescriptor = new ScopeDescriptor({scopes: [this.grammar.id]}) + this.emitter = new Emitter() + } + + getLanguageId () { + return this.grammar.id + } + + bufferDidChange ({oldRange, newRange, oldText, newText}) { + this.document.edit({ + startIndex: this.buffer.characterIndexForPosition(oldRange.start), + lengthRemoved: oldText.length, + lengthAdded: newText.length, + startPosition: oldRange.start, + extentRemoved: oldRange.getExtent(), + extentAdded: newRange.getExtent() + }) + } + + /* + * Section - Highlighting + */ + + buildHighlightIterator () { + const invalidatedRanges = this.document.parse() + for (let i = 0, n = invalidatedRanges.length; i < n; i++) { + this.emitter.emit('did-change-highlighting', invalidatedRanges[i]) + } + return new TreeSitterHighlightIterator(this) + } + + onDidChangeHighlighting (callback) { + return this.emitter.on('did-change-hightlighting', callback) + } + + classNameForScopeId (scopeId) { + return this.grammar.classNameForScopeId(scopeId) + } + + /* + * Section - Commenting + */ + + commentStringsForPosition () { + return this.grammar.commentStrings + } + + isRowCommented () { + return false + } + + /* + * Section - Indentation + */ + + suggestedIndentForLineAtBufferRow (row, line, tabLength) { + return this.suggestedIndentForBufferRow(row, tabLength) + } + + suggestedIndentForBufferRow (row, tabLength, options) { + let precedingRow + if (!options || options.skipBlankLines !== false) { + precedingRow = this.buffer.previousNonBlankRow(row) + if (precedingRow == null) return 0 + } else { + precedingRow = row - 1 + if (precedingRow < 0) return 0 + } + + return this.indentLevelForLine(this.buffer.lineForRow(precedingRow), tabLength) + } + + suggestedIndentForEditedBufferRow (row) { + return null + } + + indentLevelForLine (line, tabLength = tabLength) { + let indentLength = 0 + for (let i = 0, {length} = line; i < length; i++) { + const char = line[i] + if (char === '\t') { + indentLength += tabLength - (indentLength % tabLength) + } else if (char === ' ') { + indentLength++ + } else { + break + } + } + return indentLength / tabLength + } + + /* + * Section - Folding + */ + + isFoldableAtRow (row) { + return this.getFoldableRangeContainingPoint(Point(row, Infinity), false) != null + } + + getFoldableRanges () { + return this.getFoldableRangesAtIndentLevel(null) + } + + getFoldableRangesAtIndentLevel (goalLevel) { + let result = [] + let stack = [{node: this.document.rootNode, level: 0}] + while (stack.length > 0) { + const {node, level} = stack.pop() + const startRow = node.startPosition.row + const endRow = node.endPosition.row + + let childLevel = level + const range = this.getFoldableRangeForNode(node) + if (range) { + if (goalLevel == null || level === goalLevel) { + let updatedExistingRange = false + for (let i = 0, {length} = result; i < length; i++) { + if (result[i].start.row === range.start.row && + result[i].end.row === range.end.row) { + result[i] = range + updatedExistingRange = true + } + } + if (!updatedExistingRange) result.push(range) + } + childLevel++ + } + + for (let children = node.namedChildren, i = 0, {length} = children; i < length; i++) { + const child = children[i] + const childStartRow = child.startPosition.row + const childEndRow = child.endPosition.row + if (childEndRow > childStartRow) { + if (childStartRow === startRow && childEndRow === endRow) { + stack.push({node: child, level: level}) + } else if (childLevel <= goalLevel || goalLevel == null) { + stack.push({node: child, level: childLevel}) + } + } + } + } + + return result.sort((a, b) => a.start.row - b.start.row) + } + + getFoldableRangeContainingPoint (point, allowPreviousRows = true) { + let node = this.document.rootNode.descendantForPosition(this.buffer.clipPosition(point)) + while (node) { + if (!allowPreviousRows && node.startPosition.row < point.row) break + if (node.endPosition.row > point.row) { + const range = this.getFoldableRangeForNode(node) + if (range) return range + } + node = node.parent + } + } + + getFoldableRangeForNode (node) { + const {firstChild} = node + if (firstChild) { + const {lastChild} = node + + for (let i = 0, n = this.grammar.foldConfig.delimiters.length; i < n; i++) { + const entry = this.grammar.foldConfig.delimiters[i] + if (firstChild.type === entry[0] && lastChild.type === entry[1]) { + let childPrecedingFold = firstChild + + const options = entry[2] + if (options) { + const {children} = node + let childIndexPrecedingFold = options.afterChildCount || 0 + if (options.afterType) { + for (let i = childIndexPrecedingFold, n = children.length; i < n; i++) { + if (children[i].type === options.afterType) { + childIndexPrecedingFold = i + break + } + } + } + childPrecedingFold = children[childIndexPrecedingFold] + } + + let granchildPrecedingFold = childPrecedingFold.lastChild + if (granchildPrecedingFold) { + return Range(granchildPrecedingFold.endPosition, lastChild.startPosition) + } else { + return Range(childPrecedingFold.endPosition, lastChild.startPosition) + } + } + } + } else { + for (let i = 0, n = this.grammar.foldConfig.tokens.length; i < n; i++) { + const foldableToken = this.grammar.foldConfig.tokens[i] + if (node.type === foldableToken[0]) { + const start = node.startPosition + const end = node.endPosition + start.column += foldableToken[1] + end.column -= foldableToken[2] + return Range(start, end) + } + } + } + } + + /* + * Section - Backward compatibility shims + */ + + tokenizedLineForRow (row) { + return new TokenizedLine({ + openScopes: [], + text: this.buffer.lineForRow(row), + tags: [], + ruleStack: [], + lineEnding: this.buffer.lineEndingForRow(row), + tokenIterator: null, + grammar: this.grammar + }) + } + + scopeDescriptorForPosition (point) { + return this.rootScopeDescriptor + } + + getGrammar () { + return this.grammar + } +} + +class TreeSitterHighlightIterator { + constructor (layer, document) { + this.layer = layer + this.closeTags = null + this.openTags = null + this.containingNodeTypes = null + this.containingNodeChildIndices = null + this.currentNode = null + this.currentChildIndex = null + } + + seek (targetPosition) { + const containingTags = [] + + this.closeTags = [] + this.openTags = [] + this.containingNodeTypes = [] + this.containingNodeChildIndices = [] + this.currentPosition = targetPosition + this.currentIndex = this.layer.buffer.characterIndexForPosition(targetPosition) + + let currentNode = this.layer.document.rootNode + let currentChildIndex = null + while (currentNode) { + this.currentNode = currentNode + this.containingNodeTypes.push(currentNode.type) + this.containingNodeChildIndices.push(currentChildIndex) + + const scopeName = this.currentScopeName() + if (scopeName) { + const id = this.layer.grammar.idForScope(scopeName) + if (this.currentIndex === currentNode.startIndex) { + this.openTags.push(id) + } else { + containingTags.push(id) + } + } + + const {children} = currentNode + currentNode = null + for (let i = 0, childCount = children.length; i < childCount; i++) { + const child = children[i] + if (child.endIndex > this.currentIndex) { + currentNode = child + currentChildIndex = i + break + } + } + } + + return containingTags + } + + moveToSuccessor () { + this.closeTags = [] + this.openTags = [] + + if (!this.currentNode) { + this.currentPosition = {row: Infinity, column: Infinity} + return false + } + + do { + if (this.currentIndex < this.currentNode.endIndex) { + while (true) { + this.pushCloseTag() + const nextSibling = this.currentNode.nextSibling + if (nextSibling) { + if (this.currentNode.endIndex === nextSibling.startIndex) { + this.currentNode = nextSibling + this.currentChildIndex++ + this.currentIndex = nextSibling.startIndex + this.currentPosition = nextSibling.startPosition + this.pushOpenTag() + this.descendLeft() + } else { + this.currentIndex = this.currentNode.endIndex + this.currentPosition = this.currentNode.endPosition + } + break + } else { + this.currentIndex = this.currentNode.endIndex + this.currentPosition = this.currentNode.endPosition + this.currentNode = this.currentNode.parent + this.currentChildIndex = last(this.containingNodeChildIndices) + if (!this.currentNode) break + } + } + } else { + if ((this.currentNode = this.currentNode.nextSibling)) { + this.currentChildIndex++ + this.currentPosition = this.currentNode.startPosition + this.currentIndex = this.currentNode.startIndex + this.pushOpenTag() + this.descendLeft() + } + } + } while (this.closeTags.length === 0 && this.openTags.length === 0 && this.currentNode) + + return true + } + + getPosition () { + return this.currentPosition + } + + getCloseScopeIds () { + return this.closeTags.slice() + } + + getOpenScopeIds () { + return this.openTags.slice() + } + + // Private methods + + descendLeft () { + let child + while ((child = this.currentNode.firstChild)) { + this.currentNode = child + this.currentChildIndex = 0 + this.pushOpenTag() + } + } + + currentScopeName () { + return this.layer.grammar.scopeMap.get( + this.containingNodeTypes, + this.containingNodeChildIndices, + this.currentNode.isNamed + ) + } + + pushCloseTag () { + const scopeName = this.currentScopeName() + if (scopeName) this.closeTags.push(this.layer.grammar.idForScope(scopeName)) + this.containingNodeTypes.pop() + this.containingNodeChildIndices.pop() + } + + pushOpenTag () { + this.containingNodeTypes.push(this.currentNode.type) + this.containingNodeChildIndices.push(this.currentChildIndex) + const scopeName = this.currentScopeName() + if (scopeName) this.openTags.push(this.layer.grammar.idForScope(scopeName)) + } +} + +class TreeSitterTextBufferInput { + constructor (buffer) { + this.buffer = buffer + this.seek(0) + } + + seek (characterIndex) { + this.position = this.buffer.positionForCharacterIndex(characterIndex) + } + + read () { + const endPosition = this.buffer.clipPosition(this.position.traverse({row: 1000, column: 0})) + const text = this.buffer.getTextInRange([this.position, endPosition]) + this.position = endPosition + return text + } +} + +function last (array) { + return array[array.length - 1] +} From 9762685106d161edf4a8df711278da47c170405f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 29 Nov 2017 17:22:35 -0800 Subject: [PATCH 013/176] Start work on loading tree-sitter grammars in GrammarRegistry --- .../grammars/fake-parser.js | 1 + .../grammars/some-language.cson | 14 ++++ spec/grammar-registry-spec.js | 6 +- spec/package-manager-spec.js | 7 ++ spec/spec-helper.coffee | 3 +- src/grammar-registry.js | 69 +++++++++++++++---- src/tree-sitter-grammar.js | 2 +- 7 files changed, 84 insertions(+), 18 deletions(-) create mode 100644 spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/fake-parser.js create mode 100644 spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/some-language.cson diff --git a/spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/fake-parser.js b/spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/fake-parser.js new file mode 100644 index 000000000..028ee5135 --- /dev/null +++ b/spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/fake-parser.js @@ -0,0 +1 @@ +exports.isFakeTreeSitterParser = true diff --git a/spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/some-language.cson b/spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/some-language.cson new file mode 100644 index 000000000..5eb473456 --- /dev/null +++ b/spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/some-language.cson @@ -0,0 +1,14 @@ +name: 'Some Language' + +id: 'some-language' + +type: 'tree-sitter' + +parser: './fake-parser' + +fileTypes: [ + 'somelang' +] + +scopes: + 'class > identifier': 'entity.name.type.class' diff --git a/spec/grammar-registry-spec.js b/spec/grammar-registry-spec.js index c51ea03b9..3fc5a6056 100644 --- a/spec/grammar-registry-spec.js +++ b/spec/grammar-registry-spec.js @@ -13,8 +13,8 @@ describe('GrammarRegistry', () => { grammarRegistry = new GrammarRegistry({config: atom.config}) }) - describe('.assignLanguageMode(buffer, languageName)', () => { - it('assigns to the buffer a language mode with the given language name', async () => { + describe('.assignLanguageMode(buffer, languageId)', () => { + it('assigns to the buffer a language mode with the given language id', async () => { grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson')) grammarRegistry.loadGrammarSync(require.resolve('language-css/grammars/css.cson')) @@ -34,7 +34,7 @@ describe('GrammarRegistry', () => { expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css') }) - describe('when no languageName is passed', () => { + describe('when no languageId is passed', () => { it('makes the buffer use the null grammar', () => { grammarRegistry.loadGrammarSync(require.resolve('language-css/grammars/css.cson')) diff --git a/spec/package-manager-spec.js b/spec/package-manager-spec.js index 0b26bf839..b1ecf834d 100644 --- a/spec/package-manager-spec.js +++ b/spec/package-manager-spec.js @@ -1030,6 +1030,13 @@ describe('PackageManager', () => { expect(atom.grammars.selectGrammar('a.alot').name).toBe('Alot') expect(atom.grammars.selectGrammar('a.alittle').name).toBe('Alittle') }) + + it('loads any tree-sitter grammars defined in the package', async () => { + await atom.packages.activatePackage('package-with-tree-sitter-grammar') + const grammar = atom.grammars.selectGrammar('test.somelang') + expect(grammar.name).toBe('Some Language') + expect(grammar.languageModule.isFakeTreeSitterParser).toBe(true) + }) }) describe('scoped-property loading', () => { diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 44319ba52..3bbc78018 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -111,7 +111,8 @@ beforeEach -> new CompositeDisposable( @emitter.on("did-tokenize", callback), @onDidChangeGrammar => - if @buffer.getLanguageMode().tokenizeInBackground.originalValue + languageMode = @buffer.getLanguageMode() + if languageMode.tokenizeInBackground?.originalValue callback() ) diff --git a/src/grammar-registry.js b/src/grammar-registry.js index db86958fd..9aa7f1ca6 100644 --- a/src/grammar-registry.js +++ b/src/grammar-registry.js @@ -1,8 +1,11 @@ const _ = require('underscore-plus') const Grim = require('grim') +const CSON = require('season') const FirstMate = require('first-mate') const {Disposable, CompositeDisposable} = require('event-kit') const TextMateLanguageMode = require('./text-mate-language-mode') +const TreeSitterLanguageMode = require('./tree-sitter-language-mode') +const TreeSitterGrammar = require('./tree-sitter-grammar') const Token = require('./token') const fs = require('fs-plus') const {Point, Range} = require('text-buffer') @@ -24,6 +27,7 @@ class GrammarRegistry { clear () { this.textmateRegistry.clear() + this.treeSitterGrammarsById = {} if (this.subscriptions) this.subscriptions.dispose() this.subscriptions = new CompositeDisposable() this.languageOverridesByBufferId = new Map() @@ -112,7 +116,7 @@ class GrammarRegistry { let grammar = null if (languageId != null) { - grammar = this.textmateRegistry.grammarForScopeName(languageId) + grammar = this.grammarForId(languageId) if (!grammar) return false this.languageOverridesByBufferId.set(buffer.id, languageId) } else { @@ -146,7 +150,11 @@ class GrammarRegistry { } languageModeForGrammarAndBuffer (grammar, buffer) { - return new TextMateLanguageMode({grammar, buffer, config: this.config}) + if (grammar instanceof TreeSitterGrammar) { + return new TreeSitterLanguageMode({grammar, buffer, config: this.config}) + } else { + return new TextMateLanguageMode({grammar, buffer, config: this.config}) + } } // Extended: Select a grammar for the given file path and file contents. @@ -165,25 +173,25 @@ class GrammarRegistry { selectGrammarWithScore (filePath, fileContents) { let bestMatch = null let highestScore = -Infinity - for (let grammar of this.textmateRegistry.grammars) { + this.forEachGrammar(grammar => { const score = this.getGrammarScore(grammar, filePath, fileContents) - if ((score > highestScore) || (bestMatch == null)) { + if (score > highestScore || bestMatch == null) { bestMatch = grammar highestScore = score } - } + }) return {grammar: bestMatch, score: highestScore} } // Extended: Returns a {Number} representing how well the grammar matches the // `filePath` and `contents`. getGrammarScore (grammar, filePath, contents) { - if ((contents == null) && fs.isFileSync(filePath)) { + if (contents == null && fs.isFileSync(filePath)) { contents = fs.readFileSync(filePath, 'utf8') } let score = this.getGrammarPathScore(grammar, filePath) - if ((score > 0) && !grammar.bundledPackage) { + if (score > 0 && !grammar.bundledPackage) { score += 0.125 } if (this.grammarMatchesContents(grammar, contents)) { @@ -193,7 +201,7 @@ class GrammarRegistry { } getGrammarPathScore (grammar, filePath) { - if (!filePath) { return -1 } + if (!filePath) return -1 if (process.platform === 'win32') { filePath = filePath.replace(/\\/g, '/') } const pathComponents = filePath.toLowerCase().split(PATH_SPLIT_REGEX) @@ -225,7 +233,7 @@ class GrammarRegistry { } grammarMatchesContents (grammar, contents) { - if ((contents == null) || (grammar.firstLineRegex == null)) { return false } + if (contents == null || grammar.firstLineRegex == null) return false let escaped = false let numberOfNewlinesInRegex = 0 @@ -246,6 +254,20 @@ class GrammarRegistry { return grammar.firstLineRegex.testSync(lines.slice(0, numberOfNewlinesInRegex + 1).join('\n')) } + forEachGrammar (callback) { + this.textmateRegistry.grammars.forEach(callback) + for (let grammarId in this.treeSitterGrammarsById) { + callback(this.treeSitterGrammarsById[grammarId]) + } + } + + grammarForId (languageId) { + return ( + this.textmateRegistry.grammarForScopeName(languageId) || + this.treeSitterGrammarsById[languageId] + ) + } + // Deprecated: Get the grammar override for the given file path. // // * `filePath` A {String} file path. @@ -352,7 +374,13 @@ class GrammarRegistry { } addGrammar (grammar) { - return this.textmateRegistry.addGrammar(grammar) + if (grammar instanceof TreeSitterGrammar) { + this.treeSitterGrammarsById[grammar.id] = grammar + this.grammarAddedOrUpdated(grammar) + return new Disposable(() => delete this.treeSitterGrammarsById[grammar.id]) + } else { + return this.textmateRegistry.addGrammar(grammar) + } } removeGrammar (grammar) { @@ -391,7 +419,15 @@ class GrammarRegistry { // // Returns undefined. readGrammar (grammarPath, callback) { - return this.textmateRegistry.readGrammar(grammarPath, callback) + if (!callback) callback = () => {} + CSON.readFile(grammarPath, (error, params = {}) => { + if (error) return callback(error) + try { + callback(null, this.createGrammar(grammarPath, params)) + } catch (error) { + callback(error) + } + }) } // Extended: Read a grammar synchronously but don't add it to the registry. @@ -400,11 +436,18 @@ class GrammarRegistry { // // Returns a {Grammar}. readGrammarSync (grammarPath) { - return this.textmateRegistry.readGrammarSync(grammarPath) + return this.createGrammar(grammarPath, CSON.readFileSync(grammarPath) || {}) } createGrammar (grammarPath, params) { - return this.textmateRegistry.createGrammar(grammarPath, params) + if (params.type === 'tree-sitter') { + return new TreeSitterGrammar(this, grammarPath, params) + } else { + if (typeof params.scopeName !== 'string' || params.scopeName.length === 0) { + throw new Error(`Grammar missing required scopeName property: ${grammarPath}`) + } + return this.textmateRegistry.createGrammar(grammarPath, params) + } } // Extended: Get all the grammars in this registry. diff --git a/src/tree-sitter-grammar.js b/src/tree-sitter-grammar.js index 141e2da5f..6117f8732 100644 --- a/src/tree-sitter-grammar.js +++ b/src/tree-sitter-grammar.js @@ -39,7 +39,7 @@ class TreeSitterGrammar { }) this.languageModule = require(languageModulePath) - this.firstLineRegex = new OnigRegExp(params.firstLineMatch) + this.firstLineRegex = params.firstLineMatch && new OnigRegExp(params.firstLineMatch) this.scopesById = new Map() this.idsByScope = {} this.nextScopeId = 256 + 1 From 28edfb5b0ae2d995f089ab0454a3300fb0e8fe76 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 29 Nov 2017 17:34:08 -0800 Subject: [PATCH 014/176] Exclude tree-sitter's main JS file from the startup snapshot --- script/lib/generate-startup-snapshot.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index 333acdc0a..fd2d049c7 100644 --- a/script/lib/generate-startup-snapshot.js +++ b/script/lib/generate-startup-snapshot.js @@ -57,7 +57,8 @@ module.exports = function (packagedAppPath) { relativePath === path.join('..', 'node_modules', 'spelling-manager', 'node_modules', 'natural', 'lib', 'natural', 'index.js') || relativePath === path.join('..', 'node_modules', 'tar', 'tar.js') || relativePath === path.join('..', 'node_modules', 'temp', 'lib', 'temp.js') || - relativePath === path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js') + relativePath === path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js') || + relativePath === path.join('..', 'node_modules', 'tree-sitter', 'index.js') ) } }).then((snapshotScript) => { From 894ce56821a2f6d2427aadab88dba8910249ea84 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Nov 2017 09:32:18 -0800 Subject: [PATCH 015/176] Don't use JS as an example in removeGrammar test --- spec/grammar-registry-spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/grammar-registry-spec.js b/spec/grammar-registry-spec.js index 3fc5a6056..43fc63d71 100644 --- a/spec/grammar-registry-spec.js +++ b/spec/grammar-registry-spec.js @@ -339,10 +339,10 @@ describe('GrammarRegistry', () => { describe('.removeGrammar(grammar)', () => { it("removes the grammar, so it won't be returned by selectGrammar", async () => { - await atom.packages.activatePackage('language-javascript') - const grammar = atom.grammars.selectGrammar('foo.js') + await atom.packages.activatePackage('language-css') + const grammar = atom.grammars.selectGrammar('foo.css') atom.grammars.removeGrammar(grammar) - expect(atom.grammars.selectGrammar('foo.js').name).not.toBe(grammar.name) + expect(atom.grammars.selectGrammar('foo.css').name).not.toBe(grammar.name) }) }) From 273d708a487408516f011a6db0a7c8c7ccba21f8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Nov 2017 10:58:26 -0800 Subject: [PATCH 016/176] Add preference for using Tree-sitter parsers --- spec/grammar-registry-spec.js | 73 +++++++++++++++++++++++++++++++- src/config-schema.js | 5 +++ src/grammar-registry.js | 49 ++++++++++++++++++--- src/text-editor.js | 10 +++++ src/tree-sitter-language-mode.js | 4 ++ 5 files changed, 135 insertions(+), 6 deletions(-) diff --git a/spec/grammar-registry-spec.js b/spec/grammar-registry-spec.js index 43fc63d71..4066af24d 100644 --- a/spec/grammar-registry-spec.js +++ b/spec/grammar-registry-spec.js @@ -5,6 +5,8 @@ const fs = require('fs-plus') const temp = require('temp').track() const TextBuffer = require('text-buffer') const GrammarRegistry = require('../src/grammar-registry') +const TreeSitterGrammar = require('../src/tree-sitter-grammar') +const FirstMate = require('first-mate') describe('GrammarRegistry', () => { let grammarRegistry @@ -48,6 +50,30 @@ describe('GrammarRegistry', () => { }) }) + describe('.grammarForId(languageId)', () => { + it('converts the language id to a text-mate language id when `core.useTreeSitterParsers` is false', () => { + atom.config.set('core.useTreeSitterParsers', false) + + grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson')) + grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson')) + + const grammar = grammarRegistry.grammarForId('javascript') + expect(grammar instanceof FirstMate.Grammar).toBe(true) + expect(grammar.scopeName).toBe('source.js') + }) + + it('converts the language id to a tree-sitter language id when `core.useTreeSitterParsers` is true', () => { + atom.config.set('core.useTreeSitterParsers', true) + + grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson')) + grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson')) + + const grammar = grammarRegistry.grammarForId('source.js') + expect(grammar instanceof TreeSitterGrammar).toBe(true) + expect(grammar.id).toBe('javascript') + }) + }) + describe('.autoAssignLanguageMode(buffer)', () => { it('assigns to the buffer a language mode based on the best available grammar', () => { grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson')) @@ -78,7 +104,9 @@ describe('GrammarRegistry', () => { expect(buffer.getLanguageMode().getLanguageId()).toBe('source.c') }) - it('updates the buffer\'s grammar when a more appropriate grammar is added for its path', async () => { + it('updates the buffer\'s grammar when a more appropriate text-mate grammar is added for its path', async () => { + atom.config.set('core.useTreeSitterParsers', false) + const buffer = new TextBuffer() expect(buffer.getLanguageMode().getLanguageId()).toBe(null) @@ -87,6 +115,25 @@ describe('GrammarRegistry', () => { grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson')) expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js') + + grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson')) + expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js') + }) + + it('updates the buffer\'s grammar when a more appropriate tree-sitter grammar is added for its path', async () => { + atom.config.set('core.useTreeSitterParsers', true) + + const buffer = new TextBuffer() + expect(buffer.getLanguageMode().getLanguageId()).toBe(null) + + buffer.setPath('test.js') + grammarRegistry.maintainLanguageMode(buffer) + + grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson')) + expect(buffer.getLanguageMode().getLanguageId()).toBe('javascript') + + grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson')) + expect(buffer.getLanguageMode().getLanguageId()).toBe('javascript') }) it('can be overridden by calling .assignLanguageMode', () => { @@ -335,6 +382,30 @@ describe('GrammarRegistry', () => { await atom.packages.activatePackage('language-javascript') expect(atom.grammars.selectGrammar('foo.rb', '#!/usr/bin/env node').scopeName).toBe('source.ruby') }) + + describe('tree-sitter vs text-mate', () => { + it('favors a text-mate grammar over a tree-sitter grammar when `core.useTreeSitterParsers` is false', () => { + atom.config.set('core.useTreeSitterParsers', false) + + grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson')) + grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson')) + + const grammar = grammarRegistry.selectGrammar('test.js') + expect(grammar.scopeName).toBe('source.js') + expect(grammar instanceof FirstMate.Grammar).toBe(true) + }) + + it('favors a tree-sitter grammar over a text-mate grammar when `core.useTreeSitterParsers` is true', () => { + atom.config.set('core.useTreeSitterParsers', true) + + grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson')) + grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson')) + + const grammar = grammarRegistry.selectGrammar('test.js') + expect(grammar.id).toBe('javascript') + expect(grammar instanceof TreeSitterGrammar).toBe(true) + }) + }) }) describe('.removeGrammar(grammar)', () => { diff --git a/src/config-schema.js b/src/config-schema.js index 2ff68be86..18dc3d774 100644 --- a/src/config-schema.js +++ b/src/config-schema.js @@ -342,6 +342,11 @@ const configSchema = { description: 'Emulated with Atom events' } ] + }, + useTreeSitterParsers: { + type: 'boolean', + default: false, + description: 'Use the new Tree-sitter parsing system for supported languages' } } }, diff --git a/src/grammar-registry.js b/src/grammar-registry.js index 9aa7f1ca6..6dbb248e7 100644 --- a/src/grammar-registry.js +++ b/src/grammar-registry.js @@ -10,9 +10,15 @@ const Token = require('./token') const fs = require('fs-plus') const {Point, Range} = require('text-buffer') +const GRAMMAR_TYPE_BONUS = 1000 const GRAMMAR_SELECTION_RANGE = Range(Point.ZERO, Point(10, 0)).freeze() const PATH_SPLIT_REGEX = new RegExp('[/.]') +const LANGUAGE_ID_MAP = [ + ['source.js', 'javascript'], + ['source.ts', 'typescript'] +] + // Extended: This class holds the grammars used for tokenizing. // // An instance of this class is always available as the `atom.grammars` global. @@ -113,6 +119,7 @@ class GrammarRegistry { // found. assignLanguageMode (buffer, languageId) { if (buffer.getBuffer) buffer = buffer.getBuffer() + languageId = this.normalizeLanguageId(languageId) let grammar = null if (languageId != null) { @@ -197,6 +204,11 @@ class GrammarRegistry { if (this.grammarMatchesContents(grammar, contents)) { score += 0.25 } + + if (score > 0 && this.isGrammarPreferredType(grammar)) { + score += GRAMMAR_TYPE_BONUS + } + return score } @@ -250,6 +262,7 @@ class GrammarRegistry { escaped = false } } + const lines = contents.split('\n') return grammar.firstLineRegex.testSync(lines.slice(0, numberOfNewlinesInRegex + 1).join('\n')) } @@ -262,6 +275,8 @@ class GrammarRegistry { } grammarForId (languageId) { + languageId = this.normalizeLanguageId(languageId) + return ( this.textmateRegistry.grammarForScopeName(languageId) || this.treeSitterGrammarsById[languageId] @@ -306,6 +321,8 @@ class GrammarRegistry { } grammarAddedOrUpdated (grammar) { + if (grammar.scopeName && !grammar.id) grammar.id = grammar.scopeName + this.grammarScoresByBuffer.forEach((score, buffer) => { const languageMode = buffer.getLanguageMode() if (grammar.injectionSelector) { @@ -317,8 +334,8 @@ class GrammarRegistry { const languageOverride = this.languageOverridesByBufferId.get(buffer.id) - if ((grammar.scopeName === buffer.getLanguageMode().getLanguageId() || - grammar.scopeName === languageOverride)) { + if ((grammar.id === buffer.getLanguageMode().getLanguageId() || + grammar.id === languageOverride)) { buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer)) } else if (!languageOverride) { const score = this.getGrammarScore( @@ -370,7 +387,7 @@ class GrammarRegistry { } grammarForScopeName (scopeName) { - return this.textmateRegistry.grammarForScopeName(scopeName) + return this.grammarForId(scopeName) } addGrammar (grammar) { @@ -398,7 +415,11 @@ class GrammarRegistry { // * `error` An {Error}, may be null. // * `grammar` A {Grammar} or null if an error occured. loadGrammar (grammarPath, callback) { - return this.textmateRegistry.loadGrammar(grammarPath, callback) + this.readGrammar(grammarPath, (error, grammar) => { + if (error) return callback(error) + this.addGrammar(grammar) + callback(grammar) + }) } // Extended: Read a grammar synchronously and add it to this registry. @@ -407,7 +428,9 @@ class GrammarRegistry { // // Returns a {Grammar}. loadGrammarSync (grammarPath) { - return this.textmateRegistry.loadGrammarSync(grammarPath) + const grammar = this.readGrammarSync(grammarPath) + this.addGrammar(grammar) + return grammar } // Extended: Read a grammar asynchronously but don't add it to the registry. @@ -460,4 +483,20 @@ class GrammarRegistry { scopeForId (id) { return this.textmateRegistry.scopeForId(id) } + + isGrammarPreferredType (grammar) { + return this.config.get('core.useTreeSitterParsers') + ? grammar instanceof TreeSitterGrammar + : grammar instanceof FirstMate.Grammar + } + + normalizeLanguageId (languageId) { + if (this.config.get('core.useTreeSitterParsers')) { + const row = LANGUAGE_ID_MAP.find(entry => entry[0] === languageId) + return row ? row[1] : languageId + } else { + const row = LANGUAGE_ID_MAP.find(entry => entry[1] === languageId) + return row ? row[0] : languageId + } + } } diff --git a/src/text-editor.js b/src/text-editor.js index bcd9c19d3..016d076b0 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -3053,6 +3053,16 @@ class TextEditor { return this.expandSelectionsBackward(selection => selection.selectToBeginningOfPreviousParagraph()) } + selectLargerSyntaxNode () { + const languageMode = this.buffer.getLanguageMode() + if (!languageMode.getRangeForSyntaxNodeContainingRange) return + + this.expandSelectionsForward(selection => { + const range = languageMode.getRangeForSyntaxNodeContainingRange(selection.getBufferRange()) + if (range) selection.setBufferRange(range) + }) + } + // Extended: Select the range of the given marker if it is valid. // // * `marker` A {DisplayMarker} diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 7d77a99fd..0d2e36af6 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -240,6 +240,10 @@ class TreeSitterLanguageMode { return this.rootScopeDescriptor } + hasTokenForSelector (scopeSelector) { + return false + } + getGrammar () { return this.grammar } From 203c38ca452cc23751a6714563bbeb80cd320681 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Nov 2017 15:17:14 -0800 Subject: [PATCH 017/176] Add select-{larger,smaller}-syntax-node commands --- keymaps/darwin.cson | 2 ++ src/register-default-commands.coffee | 2 ++ src/text-editor.js | 24 ++++++++++++++++++++++-- src/tree-sitter-language-mode.js | 14 ++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson index 7161a8478..d5cc7b7da 100644 --- a/keymaps/darwin.cson +++ b/keymaps/darwin.cson @@ -161,6 +161,8 @@ 'ctrl-alt-shift-right': 'editor:select-to-next-subword-boundary' 'ctrl-alt-backspace': 'editor:delete-to-beginning-of-subword' 'ctrl-alt-delete': 'editor:delete-to-end-of-subword' + 'ctrl-alt-up': 'editor:select-larger-syntax-node' + 'ctrl-alt-down': 'editor:select-smaller-syntax-node' 'atom-workspace atom-text-editor:not([mini])': # Atom specific diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index 0bacfbb8e..a367e6188 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -160,6 +160,8 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage 'editor:select-to-previous-subword-boundary': -> @selectToPreviousSubwordBoundary() 'editor:select-to-first-character-of-line': -> @selectToFirstCharacterOfLine() 'editor:select-line': -> @selectLinesContainingCursors() + 'editor:select-larger-syntax-node': -> @selectLargerSyntaxNode() + 'editor:select-smaller-syntax-node': -> @selectSmallerSyntaxNode() }), false ) diff --git a/src/text-editor.js b/src/text-editor.js index 016d076b0..b3d0e592a 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -3053,13 +3053,33 @@ class TextEditor { return this.expandSelectionsBackward(selection => selection.selectToBeginningOfPreviousParagraph()) } + // Extended: For each selection, select the syntax node that contains + // that selection. selectLargerSyntaxNode () { const languageMode = this.buffer.getLanguageMode() if (!languageMode.getRangeForSyntaxNodeContainingRange) return this.expandSelectionsForward(selection => { - const range = languageMode.getRangeForSyntaxNodeContainingRange(selection.getBufferRange()) - if (range) selection.setBufferRange(range) + const currentRange = selection.getBufferRange() + const newRange = languageMode.getRangeForSyntaxNodeContainingRange(currentRange) + if (newRange) { + if (!selection._rangeStack) selection._rangeStack = [] + selection._rangeStack.push(currentRange) + selection.setBufferRange(newRange) + } + }) + } + + // Extended: Undo the effect a preceding call to {::selectLargerSyntaxNode}. + selectSmallerSyntaxNode () { + this.expandSelectionsForward(selection => { + if (selection._rangeStack) { + const lastRange = selection._rangeStack[selection._rangeStack.length - 1] + if (lastRange && selection.getBufferRange().containsRange(lastRange)) { + selection._rangeStack.length-- + selection.setBufferRange(lastRange) + } + } }) } diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 0d2e36af6..aa2c50a18 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -220,6 +220,20 @@ class TreeSitterLanguageMode { } } + /* + * Syntax Tree APIs + */ + + getRangeForSyntaxNodeContainingRange (range) { + const startIndex = this.buffer.characterIndexForPosition(range.start) + const endIndex = this.buffer.characterIndexForPosition(range.end) + let node = this.document.rootNode.descendantForIndex(startIndex, endIndex - 1) + while (node && node.startIndex === startIndex && node.endIndex === endIndex) { + node = node.parent + } + if (node) return new Range(node.startPosition, node.endPosition) + } + /* * Section - Backward compatibility shims */ From 7665c34496b2f1b49ca947be95c863e8d96f8768 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Nov 2017 17:13:30 -0800 Subject: [PATCH 018/176] Start on TreeSitterLanguageMode spec --- spec/tree-sitter-language-mode-spec.js | 61 ++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 spec/tree-sitter-language-mode-spec.js diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js new file mode 100644 index 000000000..501cdef71 --- /dev/null +++ b/spec/tree-sitter-language-mode-spec.js @@ -0,0 +1,61 @@ +const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') + +const dedent = require('dedent') +const TextBuffer = require('text-buffer') +const TextEditor = require('../src/text-editor') +const TreeSitterGrammar = require('../src/tree-sitter-grammar') +const TreeSitterLanguageMode = require('../src/tree-sitter-language-mode') + +const jsGrammarPath = require.resolve('language-javascript/grammars/tree-sitter-javascript.cson') + +describe('TreeSitterLanguageMode', () => { + let editor, buffer + + beforeEach(async () => { + editor = await atom.workspace.open('') + buffer = editor.getBuffer() + atom.config.set('core.useTreeSitterParsers', true) + }) + + describe('highlighting', () => { + it('applies the most specific scope mapping to each token in the syntax tree', () => { + grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { + parser: 'tree-sitter-javascript', + scopes: { + 'program': 'source', + 'call_expression > identifier': 'function', + 'property_identifier': 'property', + 'call_expression > member_expression > property_identifier': 'method' + } + }) + + buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar})) + buffer.setText('aa.bbb = cc(d.eee());') + expect(getTokens(editor).slice(0, 1)).toEqual([[ + {text: 'aa.', scopes: ['source']}, + {text: 'bbb', scopes: ['source', 'property']}, + {text: ' = ', scopes: ['source']}, + {text: 'cc', scopes: ['source', 'function']}, + {text: '(d.', scopes: ['source']}, + {text: 'eee', scopes: ['source', 'method']}, + {text: '());', scopes: ['source']} + ]]) + }) + }) +}) + +function getTokens (editor) { + const result = [] + for (let row = 0, lastRow = editor.getLastScreenRow(); row <= lastRow; row++) { + result.push( + editor.tokensForScreenRow(row).map(({text, scopes}) => ({ + text, + scopes: scopes.map(scope => scope + .split(' ') + .map(className => className.slice('syntax--'.length)) + .join(' ')) + })) + ) + } + return result +} From bda50585c4b92ec38fe5944b6a45947f2b75d440 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 1 Dec 2017 14:58:09 -0800 Subject: [PATCH 019/176] Make TreeSitterHighlightIterator stop in between tokens when needed --- src/tree-sitter-language-mode.js | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index aa2c50a18..46da8f4f8 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -286,10 +286,12 @@ class TreeSitterHighlightIterator { let currentNode = this.layer.document.rootNode let currentChildIndex = null + let precedesCurrentNode = false while (currentNode) { this.currentNode = currentNode this.containingNodeTypes.push(currentNode.type) this.containingNodeChildIndices.push(currentChildIndex) + if (precedesCurrentNode) break const scopeName = this.currentScopeName() if (scopeName) { @@ -308,6 +310,7 @@ class TreeSitterHighlightIterator { if (child.endIndex > this.currentIndex) { currentNode = child currentChildIndex = i + if (child.startIndex > this.currentIndex) precedesCurrentNode = true break } } @@ -326,33 +329,35 @@ class TreeSitterHighlightIterator { } do { - if (this.currentIndex < this.currentNode.endIndex) { + if (this.currentIndex < this.currentNode.startIndex) { + this.currentIndex = this.currentNode.startIndex + this.currentPosition = this.currentNode.startPosition + this.pushOpenTag() + this.descendLeft() + } else if (this.currentIndex < this.currentNode.endIndex) { while (true) { this.pushCloseTag() - const nextSibling = this.currentNode.nextSibling + this.currentIndex = this.currentNode.endIndex + this.currentPosition = this.currentNode.endPosition + + const {nextSibling} = this.currentNode if (nextSibling) { - if (this.currentNode.endIndex === nextSibling.startIndex) { - this.currentNode = nextSibling - this.currentChildIndex++ - this.currentIndex = nextSibling.startIndex - this.currentPosition = nextSibling.startPosition + this.currentNode = nextSibling + this.currentChildIndex++ + if (this.currentIndex === nextSibling.startIndex) { this.pushOpenTag() this.descendLeft() - } else { - this.currentIndex = this.currentNode.endIndex - this.currentPosition = this.currentNode.endPosition } break } else { - this.currentIndex = this.currentNode.endIndex - this.currentPosition = this.currentNode.endPosition this.currentNode = this.currentNode.parent this.currentChildIndex = last(this.containingNodeChildIndices) if (!this.currentNode) break } } } else { - if ((this.currentNode = this.currentNode.nextSibling)) { + this.currentNode = this.currentNode.nextSibling + if (this.currentNode) { this.currentChildIndex++ this.currentPosition = this.currentNode.startPosition this.currentIndex = this.currentNode.startIndex From d893fb25a8b863d92a35f5a8b156415d8d66e250 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 1 Dec 2017 16:18:25 -0800 Subject: [PATCH 020/176] :art: TreeSitterLanguageMode --- src/tree-sitter-language-mode.js | 74 ++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 46da8f4f8..63fc2a85a 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -266,62 +266,80 @@ class TreeSitterLanguageMode { class TreeSitterHighlightIterator { constructor (layer, document) { this.layer = layer - this.closeTags = null - this.openTags = null - this.containingNodeTypes = null - this.containingNodeChildIndices = null + + // Conceptually, the iterator represents a single position in the text. It stores this + // position both as a character index and as a `Point`. This position corresponds to a + // leaf node of the syntax tree, which either contains or follows the iterator's + // textual position. The `currentNode` property represents that leaf node, and + // `currentChildIndex` represents the child index of that leaf node within its parent. + this.currentIndex = null + this.currentPosition = null this.currentNode = null this.currentChildIndex = null + + // In order to determine which selectors match its current node, the iterator maintains + // a list of the current node's ancestors. Because the selectors can use the `:nth-child` + // pseudo-class, each node's child index is also stored. + this.containingNodeTypes = [] + this.containingNodeChildIndices = [] + + // At any given position, the iterator exposes the list of class names that should be + // *ended* at its current position and the list of class names that should be *started* + // at its current position. + this.closeTags = [] + this.openTags = [] } seek (targetPosition) { const containingTags = [] - this.closeTags = [] - this.openTags = [] - this.containingNodeTypes = [] - this.containingNodeChildIndices = [] + this.closeTags.length = 0 + this.openTags.length = 0 + this.containingNodeTypes.length = 0 + this.containingNodeChildIndices.length = 0 this.currentPosition = targetPosition this.currentIndex = this.layer.buffer.characterIndexForPosition(targetPosition) - let currentNode = this.layer.document.rootNode - let currentChildIndex = null - let precedesCurrentNode = false - while (currentNode) { - this.currentNode = currentNode - this.containingNodeTypes.push(currentNode.type) - this.containingNodeChildIndices.push(currentChildIndex) - if (precedesCurrentNode) break + var node = this.layer.document.rootNode + var childIndex = -1 + var done = false + var nodeContainsTarget = true + do { + this.currentNode = node + this.currentChildIndex = childIndex + this.containingNodeTypes.push(node.type) + this.containingNodeChildIndices.push(childIndex) + if (!nodeContainsTarget) break const scopeName = this.currentScopeName() if (scopeName) { const id = this.layer.grammar.idForScope(scopeName) - if (this.currentIndex === currentNode.startIndex) { + if (this.currentIndex === node.startIndex) { this.openTags.push(id) } else { containingTags.push(id) } } - const {children} = currentNode - currentNode = null - for (let i = 0, childCount = children.length; i < childCount; i++) { + done = true + for (var i = 0, {children} = node, childCount = children.length; i < childCount; i++) { const child = children[i] if (child.endIndex > this.currentIndex) { - currentNode = child - currentChildIndex = i - if (child.startIndex > this.currentIndex) precedesCurrentNode = true + node = child + childIndex = i + done = false + if (child.startIndex > this.currentIndex) nodeContainsTarget = false break } } - } + } while (!done) return containingTags } moveToSuccessor () { - this.closeTags = [] - this.openTags = [] + this.closeTags.length = 0 + this.openTags.length = 0 if (!this.currentNode) { this.currentPosition = {row: Infinity, column: Infinity} @@ -336,9 +354,9 @@ class TreeSitterHighlightIterator { this.descendLeft() } else if (this.currentIndex < this.currentNode.endIndex) { while (true) { - this.pushCloseTag() this.currentIndex = this.currentNode.endIndex this.currentPosition = this.currentNode.endPosition + this.pushCloseTag() const {nextSibling} = this.currentNode if (nextSibling) { @@ -386,7 +404,7 @@ class TreeSitterHighlightIterator { descendLeft () { let child - while ((child = this.currentNode.firstChild)) { + while ((child = this.currentNode.firstChild) && this.currentIndex === child.startIndex) { this.currentNode = child this.currentChildIndex = 0 this.pushOpenTag() From 6282cd639a0534eea7728032ce086c0859ff3337 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 4 Dec 2017 10:18:38 -0800 Subject: [PATCH 021/176] Add tree-sitter highlighting test with nested scopes --- spec/tree-sitter-language-mode-spec.js | 49 ++++++++++++++++++++------ 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js index 501cdef71..c7b1d1f08 100644 --- a/spec/tree-sitter-language-mode-spec.js +++ b/spec/tree-sitter-language-mode-spec.js @@ -14,12 +14,11 @@ describe('TreeSitterLanguageMode', () => { beforeEach(async () => { editor = await atom.workspace.open('') buffer = editor.getBuffer() - atom.config.set('core.useTreeSitterParsers', true) }) describe('highlighting', () => { - it('applies the most specific scope mapping to each token in the syntax tree', () => { - grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { + it('applies the most specific scope mapping to each node in the syntax tree', () => { + const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { parser: 'tree-sitter-javascript', scopes: { 'program': 'source', @@ -31,7 +30,7 @@ describe('TreeSitterLanguageMode', () => { buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar})) buffer.setText('aa.bbb = cc(d.eee());') - expect(getTokens(editor).slice(0, 1)).toEqual([[ + expectTokensToEqual(editor, [ {text: 'aa.', scopes: ['source']}, {text: 'bbb', scopes: ['source', 'property']}, {text: ' = ', scopes: ['source']}, @@ -39,16 +38,42 @@ describe('TreeSitterLanguageMode', () => { {text: '(d.', scopes: ['source']}, {text: 'eee', scopes: ['source', 'method']}, {text: '());', scopes: ['source']} - ]]) + ]) + }) + + it('can start or end multiple scopes at the same position', () => { + const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { + parser: 'tree-sitter-javascript', + scopes: { + 'program': 'source', + 'call_expression': 'call', + 'member_expression': 'member', + 'identifier': 'variable', + '"("': 'open-paren', + '")"': 'close-paren', + } + }) + + buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar})) + buffer.setText('a = bb.ccc();') + expectTokensToEqual(editor, [ + {text: 'a', scopes: ['source', 'variable']}, + {text: ' = ', scopes: ['source']}, + {text: 'bb', scopes: ['source', 'call', 'member', 'variable']}, + {text: '.ccc', scopes: ['source', 'call', 'member']}, + {text: '(', scopes: ['source', 'call', 'open-paren']}, + {text: ')', scopes: ['source', 'call', 'close-paren']}, + {text: ';', scopes: ['source']} + ]) }) }) }) -function getTokens (editor) { - const result = [] +function expectTokensToEqual (editor, expectedTokens) { + const tokens = [] for (let row = 0, lastRow = editor.getLastScreenRow(); row <= lastRow; row++) { - result.push( - editor.tokensForScreenRow(row).map(({text, scopes}) => ({ + tokens.push( + ...editor.tokensForScreenRow(row).map(({text, scopes}) => ({ text, scopes: scopes.map(scope => scope .split(' ') @@ -57,5 +82,9 @@ function getTokens (editor) { })) ) } - return result + + expect(tokens.length).toEqual(expectedTokens.length) + for (let i = 0; i < tokens.length; i++) { + expect(tokens[i]).toEqual(expectedTokens[i], `Token ${i}`) + } } From 4e38b61a5e57ac4f709d7b0fb49ee354c83e6075 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 4 Dec 2017 11:02:24 -0800 Subject: [PATCH 022/176] Optimize TreeSitterLanguageMode.isFoldableAtRow --- spec/tree-sitter-language-mode-spec.js | 61 ++++++++++++++++++++++++++ src/tree-sitter-language-mode.js | 12 ++--- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js index c7b1d1f08..79ca654c7 100644 --- a/spec/tree-sitter-language-mode-spec.js +++ b/spec/tree-sitter-language-mode-spec.js @@ -67,8 +67,69 @@ describe('TreeSitterLanguageMode', () => { ]) }) }) + + describe('folding', () => { + beforeEach(() => { + editor.displayLayer.reset({foldCharacter: '…'}) + }) + + it('folds nodes that start and end with specified tokens and span multiple lines', () => { + const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { + parser: 'tree-sitter-javascript', + scopes: {'program': 'source'}, + folds: { + delimiters: [ + ['{', '}'], + ['(', ')'] + ] + } + }) + + buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar})) + buffer.setText(dedent ` + module.exports = + class A { + getB (c, + d, + e) { + return this.b + } + } + `) + + editor.screenLineForScreenRow(0) + + expect(editor.isFoldableAtBufferRow(0)).toBe(false) + expect(editor.isFoldableAtBufferRow(1)).toBe(true) + expect(editor.isFoldableAtBufferRow(2)).toBe(true) + expect(editor.isFoldableAtBufferRow(3)).toBe(false) + expect(editor.isFoldableAtBufferRow(4)).toBe(true) + + editor.foldBufferRow(2) + expect(getDisplayText(editor)).toBe(dedent ` + module.exports = + class A { + getB (…) { + return this.b + } + } + `) + + editor.foldBufferRow(4) + expect(getDisplayText(editor)).toBe(dedent ` + module.exports = + class A { + getB (…) {…} + } + `) + }) + }) }) +function getDisplayText (editor) { + return editor.displayLayer.getText() +} + function expectTokensToEqual (editor, expectedTokens) { const tokens = [] for (let row = 0, lastRow = editor.getLastScreenRow(); row <= lastRow; row++) { diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 63fc2a85a..4c3df538a 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -112,7 +112,7 @@ class TreeSitterLanguageMode { */ isFoldableAtRow (row) { - return this.getFoldableRangeContainingPoint(Point(row, Infinity), false) != null + return this.getFoldableRangeContainingPoint(Point(row, Infinity), 0, true) != null } getFoldableRanges () { @@ -161,19 +161,19 @@ class TreeSitterLanguageMode { return result.sort((a, b) => a.start.row - b.start.row) } - getFoldableRangeContainingPoint (point, allowPreviousRows = true) { + getFoldableRangeContainingPoint (point, tabLength, existenceOnly = false) { let node = this.document.rootNode.descendantForPosition(this.buffer.clipPosition(point)) while (node) { - if (!allowPreviousRows && node.startPosition.row < point.row) break + if (existenceOnly && node.startPosition.row < point.row) break if (node.endPosition.row > point.row) { - const range = this.getFoldableRangeForNode(node) + const range = this.getFoldableRangeForNode(node, existenceOnly) if (range) return range } node = node.parent } } - getFoldableRangeForNode (node) { + getFoldableRangeForNode (node, existenceOnly) { const {firstChild} = node if (firstChild) { const {lastChild} = node @@ -181,6 +181,7 @@ class TreeSitterLanguageMode { for (let i = 0, n = this.grammar.foldConfig.delimiters.length; i < n; i++) { const entry = this.grammar.foldConfig.delimiters[i] if (firstChild.type === entry[0] && lastChild.type === entry[1]) { + if (existenceOnly) return true let childPrecedingFold = firstChild const options = entry[2] @@ -210,6 +211,7 @@ class TreeSitterLanguageMode { for (let i = 0, n = this.grammar.foldConfig.tokens.length; i < n; i++) { const foldableToken = this.grammar.foldConfig.tokens[i] if (node.type === foldableToken[0]) { + if (existenceOnly) return true const start = node.startPosition const end = node.endPosition start.column += foldableToken[1] From 98e11673aa37728b00503540dec3cea0c4fe7306 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 4 Dec 2017 11:40:44 -0800 Subject: [PATCH 023/176] Tweak TreeSitterLanguageMode folding configuration --- spec/tree-sitter-language-mode-spec.js | 47 +++++++++++++++++++++++++- src/tree-sitter-grammar.js | 7 ++-- src/tree-sitter-language-mode.js | 21 +++++------- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js index 79ca654c7..93937f4b4 100644 --- a/spec/tree-sitter-language-mode-spec.js +++ b/spec/tree-sitter-language-mode-spec.js @@ -73,7 +73,7 @@ describe('TreeSitterLanguageMode', () => { editor.displayLayer.reset({foldCharacter: '…'}) }) - it('folds nodes that start and end with specified tokens and span multiple lines', () => { + it('can fold nodes that start and end with specified tokens and span multiple lines', () => { const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { parser: 'tree-sitter-javascript', scopes: {'program': 'source'}, @@ -123,6 +123,51 @@ describe('TreeSitterLanguageMode', () => { } `) }) + + it('can fold specified types of multi-line nodes', () => { + const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { + parser: 'tree-sitter-javascript', + scopes: {'program': 'source'}, + folds: { + nodes: [ + 'template_string', + 'comment' + ] + } + }) + + buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar})) + buffer.setText(dedent ` + /** + * Important + */ + const x = \`one + two + three\` + `) + + editor.screenLineForScreenRow(0) + + expect(editor.isFoldableAtBufferRow(0)).toBe(true) + expect(editor.isFoldableAtBufferRow(1)).toBe(false) + expect(editor.isFoldableAtBufferRow(2)).toBe(false) + expect(editor.isFoldableAtBufferRow(3)).toBe(true) + expect(editor.isFoldableAtBufferRow(4)).toBe(false) + + editor.foldBufferRow(0) + expect(getDisplayText(editor)).toBe(dedent ` + /**… */ + const x = \`one + two + three\` + `) + + editor.foldBufferRow(3) + expect(getDisplayText(editor)).toBe(dedent ` + /**… */ + const x = \`one… three\` + `) + }) }) }) diff --git a/src/tree-sitter-grammar.js b/src/tree-sitter-grammar.js index 6117f8732..d7d36a0a7 100644 --- a/src/tree-sitter-grammar.js +++ b/src/tree-sitter-grammar.js @@ -10,9 +10,10 @@ class TreeSitterGrammar { this.id = params.id this.name = params.name - this.foldConfig = params.folds || {} - if (!this.foldConfig.delimiters) this.foldConfig.delimiters = [] - if (!this.foldConfig.tokens) this.foldConfig.tokens = [] + this.foldConfig = { + delimiters: params.folds && params.folds.delimiters || [], + nodes: new Set(params.folds && params.folds.nodes || []) + } this.commentStrings = { commentStartString: params.comments && params.comments.start, diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 4c3df538a..8d4049a51 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -207,18 +207,15 @@ class TreeSitterLanguageMode { } } } - } else { - for (let i = 0, n = this.grammar.foldConfig.tokens.length; i < n; i++) { - const foldableToken = this.grammar.foldConfig.tokens[i] - if (node.type === foldableToken[0]) { - if (existenceOnly) return true - const start = node.startPosition - const end = node.endPosition - start.column += foldableToken[1] - end.column -= foldableToken[2] - return Range(start, end) - } - } + } + + if (this.grammar.foldConfig.nodes.has(node.type)) { + if (existenceOnly) return true + const start = node.startPosition + const end = node.endPosition + start.column = Infinity + end.column = 0 + return Range(start, end) } } From d4dd08da2244102121a275d97df556ff9cba85ed Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Mon, 4 Dec 2017 10:28:22 -0700 Subject: [PATCH 024/176] Use buildIndentString method instead of hard-coding spaces --- src/text-editor.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/text-editor.js b/src/text-editor.js index bcd9c19d3..851718946 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -4522,8 +4522,7 @@ class TextEditor { ? minBlankIndentLevel : 0 - const tabLength = this.getTabLength() - const indentString = ' '.repeat(tabLength * minIndentLevel) + const indentString = this.buildIndentString(minIndentLevel) for (let row = start; row <= end; row++) { const line = this.buffer.lineForRow(row) if (NON_WHITESPACE_REGEXP.test(line)) { From 8a1c7619f3063d2935824f6d78721009adbccd24 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 4 Dec 2017 12:07:05 -0800 Subject: [PATCH 025/176] Add test for .select{Larger,Smaller}SyntaxNode --- spec/tree-sitter-language-mode-spec.js | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js index 93937f4b4..b05147631 100644 --- a/spec/tree-sitter-language-mode-spec.js +++ b/spec/tree-sitter-language-mode-spec.js @@ -169,6 +169,48 @@ describe('TreeSitterLanguageMode', () => { `) }) }) + + describe('TextEditor.selectLargerSyntaxNode and .selectSmallerSyntaxNode', () => { + it('expands and contract the selection based on the syntax tree', () => { + const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { + parser: 'tree-sitter-javascript', + scopes: {'program': 'source'} + }) + + buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar})) + buffer.setText(dedent ` + function a (b, c, d) { + eee.f() + g() + } + `) + + editor.screenLineForScreenRow(0) + + editor.setCursorBufferPosition([1, 3]) + editor.selectLargerSyntaxNode() + expect(editor.getSelectedText()).toBe('eee') + editor.selectLargerSyntaxNode() + expect(editor.getSelectedText()).toBe('eee.f') + editor.selectLargerSyntaxNode() + expect(editor.getSelectedText()).toBe('eee.f()') + editor.selectLargerSyntaxNode() + expect(editor.getSelectedText()).toBe('{\n eee.f()\n g()\n}') + editor.selectLargerSyntaxNode() + expect(editor.getSelectedText()).toBe('function a (b, c, d) {\n eee.f()\n g()\n}') + + editor.selectSmallerSyntaxNode() + expect(editor.getSelectedText()).toBe('{\n eee.f()\n g()\n}') + editor.selectSmallerSyntaxNode() + expect(editor.getSelectedText()).toBe('eee.f()') + editor.selectSmallerSyntaxNode() + expect(editor.getSelectedText()).toBe('eee.f') + editor.selectSmallerSyntaxNode() + expect(editor.getSelectedText()).toBe('eee') + editor.selectSmallerSyntaxNode() + expect(editor.getSelectedBufferRange()).toEqual([[1, 3], [1, 3]]) + }) + }) }) function getDisplayText (editor) { From 70e773fa7b2e5b9944088722326a8a0ea7dae0fd Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 4 Dec 2017 13:12:08 -0800 Subject: [PATCH 026/176] Ensure read-only text editors are not considered 'modified'. Also clear read-only flag on successful save. --- spec/text-editor-spec.js | 17 +++++++++++++++++ src/text-editor.js | 8 +++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index 198cf1c43..f15a66e30 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -85,6 +85,23 @@ describe('TextEditor', () => { }) }) + describe('when the editor is readonly', () => { + it('overrides TextBuffer.isModified to return false', async () => { + const editor = await atom.workspace.open(null, {readOnly: true}) + editor.setText('I am altering the buffer, pray I do not alter it any further') + expect(editor.isModified()).toBe(false) + editor.setReadOnly(false) + expect(editor.isModified()).toBe(true) + }) + it('clears the readonly status when saved', async () => { + const editor = await atom.workspace.open(null, {readOnly: true}) + editor.setText('I am altering the buffer, pray I do not alter it any further') + expect(editor.isReadOnly()).toBe(true) + await editor.saveAs(temp.openSync('was-readonly').path) + expect(editor.isReadOnly()).toBe(false) + }) + }) + describe('when the editor is constructed with the largeFileMode option set to true', () => { it("loads the editor but doesn't tokenize", async () => { editor = await atom.workspace.openTextFile('sample.js', {largeFileMode: true}) diff --git a/src/text-editor.js b/src/text-editor.js index aef6ffc1a..0b14e45b6 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -411,6 +411,7 @@ class TextEditor { if (this.component != null) { this.component.scheduleUpdate() } + this.buffer.emitModifiedStatusChanged(this.isModified()) } break @@ -575,6 +576,11 @@ class TextEditor { this.disposables.add(this.buffer.onDidChangeModified(() => { if (!this.hasTerminatedPendingState && this.buffer.isModified()) this.terminatePendingState() })) + this.disposables.add(this.buffer.onDidSave(() => { + if (this.isReadOnly()) { + this.setReadOnly(false) + } + })) } terminatePendingState () { @@ -1126,7 +1132,7 @@ class TextEditor { setEncoding (encoding) { this.buffer.setEncoding(encoding) } // Essential: Returns {Boolean} `true` if this editor has been modified. - isModified () { return this.buffer.isModified() } + isModified () { return this.isReadOnly() ? false : this.buffer.isModified() } // Essential: Returns {Boolean} `true` if this editor has no content. isEmpty () { return this.buffer.isEmpty() } From abc0d3c534743897c02a8e434e2ccabde9026c07 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 4 Dec 2017 13:13:03 -0800 Subject: [PATCH 027/176] Remove stray console log --- spec/text-editor-element-spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/text-editor-element-spec.js b/spec/text-editor-element-spec.js index 02c3c0ba0..f298ee3a4 100644 --- a/spec/text-editor-element-spec.js +++ b/spec/text-editor-element-spec.js @@ -71,7 +71,6 @@ describe('TextEditorElement', () => { }) it("honors the 'readonly' attribute", async function() { - console.log('set attribute'); jasmineContent.innerHTML = "" const element = jasmineContent.firstChild From 376919772bdbf7788ad004ab79bb61016e15ba06 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 5 Dec 2017 00:47:03 +0100 Subject: [PATCH 028/176] Spy on HistoryManager prototype To prevent other instances of HistoryManager from messing up the project history --- spec/spec-helper.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 44319ba52..9a219e650 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -9,6 +9,7 @@ pathwatcher = require 'pathwatcher' FindParentDir = require 'find-parent-dir' {CompositeDisposable} = require 'event-kit' +{HistoryManager} = require '../src/history-manager' TextEditor = require '../src/text-editor' TextEditorElement = require '../src/text-editor-element' TextMateLanguageMode = require '../src/text-mate-language-mode' @@ -63,7 +64,7 @@ else beforeEach -> # Do not clobber recent project history - spyOn(atom.history, 'saveState').andReturn(Promise.resolve()) + spyOn(HistoryManager::, 'saveState').andReturn(Promise.resolve()) atom.project.setPaths([specProjectPath]) From 4f86d60f7bfce22804a4e76a6eafe28f2cbbed1b Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 5 Dec 2017 01:22:13 +0100 Subject: [PATCH 029/176] Update HistoryManager spec to mock the state store --- spec/history-manager-spec.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/spec/history-manager-spec.js b/spec/history-manager-spec.js index 13a3192fb..054e2cce5 100644 --- a/spec/history-manager-spec.js +++ b/spec/history-manager-spec.js @@ -1,19 +1,14 @@ -/** @babel */ +const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') +const {Emitter, Disposable, CompositeDisposable} = require('event-kit') -import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers' -import {Emitter, Disposable, CompositeDisposable} from 'event-kit' - -import {HistoryManager, HistoryProject} from '../src/history-manager' -import StateStore from '../src/state-store' +const {HistoryManager, HistoryProject} = require('../src/history-manager') +const StateStore = require('../src/state-store') describe("HistoryManager", () => { let historyManager, commandRegistry, project, stateStore let commandDisposable, projectDisposable beforeEach(async () => { - // Do not clobber recent project history - spyOn(atom.applicationDelegate, 'didChangeHistoryManager') - commandDisposable = jasmine.createSpyObj('Disposable', ['dispose']) commandRegistry = jasmine.createSpyObj('CommandRegistry', ['add']) commandRegistry.add.andReturn(commandDisposable) @@ -185,11 +180,22 @@ describe("HistoryManager", () => { }) }) - describe("saveState" ,() => { + describe("saveState", () => { + let savedProjects + beforeEach(() => { + jasmine.unspy(historyManager, 'saveState') + + spyOn(historyManager.stateStore, 'save').andCallFake((name, {projects}) => { + savedProjects = {projects} + return Promise.resolve() + }) + }) + it("saves the state", async () => { await historyManager.addProject(["/save/state"]) await historyManager.saveState() const historyManager2 = new HistoryManager({stateStore, project, commands: commandRegistry}) + spyOn(historyManager2.stateStore, 'load').andCallFake(name => Promise.resolve(savedProjects)) await historyManager2.loadState() expect(historyManager2.getProjects()[0].paths).toEqual(['/save/state']) }) From 7b53a4f498f68df7be51281f4579ca862203f33f Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 5 Dec 2017 11:33:25 +0100 Subject: [PATCH 030/176] Don't require HistoryManager in spec-helper --- spec/spec-helper.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 9a219e650..5600a2b8d 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -9,7 +9,6 @@ pathwatcher = require 'pathwatcher' FindParentDir = require 'find-parent-dir' {CompositeDisposable} = require 'event-kit' -{HistoryManager} = require '../src/history-manager' TextEditor = require '../src/text-editor' TextEditorElement = require '../src/text-editor-element' TextMateLanguageMode = require '../src/text-mate-language-mode' @@ -64,7 +63,7 @@ else beforeEach -> # Do not clobber recent project history - spyOn(HistoryManager::, 'saveState').andReturn(Promise.resolve()) + spyOn(Object.getPrototypeOf(atom.history), 'saveState').andReturn(Promise.resolve()) atom.project.setPaths([specProjectPath]) From 6852fe8ee5225e5f559bcb17e11630ea2f0563f6 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 5 Dec 2017 11:34:11 +0100 Subject: [PATCH 031/176] :art: --- spec/history-manager-spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/history-manager-spec.js b/spec/history-manager-spec.js index 054e2cce5..1f28948d4 100644 --- a/spec/history-manager-spec.js +++ b/spec/history-manager-spec.js @@ -181,12 +181,12 @@ describe("HistoryManager", () => { }) describe("saveState", () => { - let savedProjects + let savedHistory beforeEach(() => { jasmine.unspy(historyManager, 'saveState') - spyOn(historyManager.stateStore, 'save').andCallFake((name, {projects}) => { - savedProjects = {projects} + spyOn(historyManager.stateStore, 'save').andCallFake((name, history) => { + savedHistory = history return Promise.resolve() }) }) @@ -195,7 +195,7 @@ describe("HistoryManager", () => { await historyManager.addProject(["/save/state"]) await historyManager.saveState() const historyManager2 = new HistoryManager({stateStore, project, commands: commandRegistry}) - spyOn(historyManager2.stateStore, 'load').andCallFake(name => Promise.resolve(savedProjects)) + spyOn(historyManager2.stateStore, 'load').andCallFake(name => Promise.resolve(savedHistory)) await historyManager2.loadState() expect(historyManager2.getProjects()[0].paths).toEqual(['/save/state']) }) From 266a40d914cc8dfee0a8ed5e59c6232d0482ff78 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 5 Dec 2017 11:37:04 +0100 Subject: [PATCH 032/176] :memo: --- spec/history-manager-spec.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/history-manager-spec.js b/spec/history-manager-spec.js index 1f28948d4..cc2a20058 100644 --- a/spec/history-manager-spec.js +++ b/spec/history-manager-spec.js @@ -183,6 +183,10 @@ describe("HistoryManager", () => { describe("saveState", () => { let savedHistory beforeEach(() => { + // historyManager.saveState is spied on globally to prevent specs from + // modifying the shared project history. Since these tests depend on + // saveState, we unspy it but in turn spy on the state store instead + // so that no data is actually stored to it. jasmine.unspy(historyManager, 'saveState') spyOn(historyManager.stateStore, 'save').andCallFake((name, history) => { From a475baf4b5ea3ab6e16dd40c897a56b9746eb5f5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2017 12:39:52 -0800 Subject: [PATCH 033/176] Rework fold API for tree-sitter grammars --- spec/tree-sitter-language-mode-spec.js | 88 +++++++++++++++++---- src/tree-sitter-grammar.js | 7 +- src/tree-sitter-language-mode.js | 103 ++++++++++++++++--------- 3 files changed, 140 insertions(+), 58 deletions(-) diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js index b05147631..1cc9afc94 100644 --- a/spec/tree-sitter-language-mode-spec.js +++ b/spec/tree-sitter-language-mode-spec.js @@ -76,13 +76,16 @@ describe('TreeSitterLanguageMode', () => { it('can fold nodes that start and end with specified tokens and span multiple lines', () => { const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { parser: 'tree-sitter-javascript', - scopes: {'program': 'source'}, - folds: { - delimiters: [ - ['{', '}'], - ['(', ')'] - ] - } + folds: [ + { + start: {type: '{', index: 0}, + end: {type: '}', index: -1} + }, + { + start: {type: '(', index: 0}, + end: {type: ')', index: -1} + } + ] }) buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar})) @@ -92,7 +95,7 @@ describe('TreeSitterLanguageMode', () => { getB (c, d, e) { - return this.b + return this.f(g) } } `) @@ -110,7 +113,7 @@ describe('TreeSitterLanguageMode', () => { module.exports = class A { getB (…) { - return this.b + return this.f(g) } } `) @@ -124,16 +127,69 @@ describe('TreeSitterLanguageMode', () => { `) }) + it('can fold nodes that start and end with specified tokens and span multiple lines', () => { + const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { + parser: 'tree-sitter-javascript', + folds: [ + { + type: 'jsx_element', + start: {index: 0, type: 'jsx_opening_element'}, + end: {index: -1, type: 'jsx_closing_element'} + }, + { + type: 'jsx_self_closing_element', + start: {index: 1}, + end: {type: '/', index: -2} + }, + ] + }) + + buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar})) + buffer.setText(dedent ` + const element1 = + + const element2 = + hello + world + + `) + + editor.screenLineForScreenRow(0) + + expect(editor.isFoldableAtBufferRow(0)).toBe(true) + expect(editor.isFoldableAtBufferRow(1)).toBe(false) + expect(editor.isFoldableAtBufferRow(2)).toBe(false) + expect(editor.isFoldableAtBufferRow(3)).toBe(false) + expect(editor.isFoldableAtBufferRow(4)).toBe(true) + expect(editor.isFoldableAtBufferRow(5)).toBe(false) + + editor.foldBufferRow(0) + expect(getDisplayText(editor)).toBe(dedent ` + const element1 = + + const element2 = + hello + world + + `) + + editor.foldBufferRow(4) + expect(getDisplayText(editor)).toBe(dedent ` + const element1 = + + const element2 = + `) + }) + it('can fold specified types of multi-line nodes', () => { const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { parser: 'tree-sitter-javascript', - scopes: {'program': 'source'}, - folds: { - nodes: [ - 'template_string', - 'comment' - ] - } + folds: [ + {type: 'template_string'}, + {type: 'comment'} + ] }) buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar})) diff --git a/src/tree-sitter-grammar.js b/src/tree-sitter-grammar.js index d7d36a0a7..3448d0cd1 100644 --- a/src/tree-sitter-grammar.js +++ b/src/tree-sitter-grammar.js @@ -10,10 +10,7 @@ class TreeSitterGrammar { this.id = params.id this.name = params.name - this.foldConfig = { - delimiters: params.folds && params.folds.delimiters || [], - nodes: new Set(params.folds && params.folds.nodes || []) - } + this.folds = params.folds || [] this.commentStrings = { commentStartString: params.comments && params.comments.start, @@ -21,7 +18,7 @@ class TreeSitterGrammar { } const scopeSelectors = {} - for (const key of Object.keys(params.scopes)) { + for (const key in params.scopes || {}) { scopeSelectors[key] = params.scopes[key] .split('.') .map(s => `syntax--${s}`) diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 8d4049a51..ff7d6c096 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -18,6 +18,7 @@ class TreeSitterLanguageMode { this.document.parse() this.rootScopeDescriptor = new ScopeDescriptor({scopes: [this.grammar.id]}) this.emitter = new Emitter() + this.isFoldableCache = [] } getLanguageId () { @@ -25,6 +26,7 @@ class TreeSitterLanguageMode { } bufferDidChange ({oldRange, newRange, oldText, newText}) { + this.isFoldableCache.length = 0 this.document.edit({ startIndex: this.buffer.characterIndexForPosition(oldRange.start), lengthRemoved: oldText.length, @@ -112,7 +114,10 @@ class TreeSitterLanguageMode { */ isFoldableAtRow (row) { - return this.getFoldableRangeContainingPoint(Point(row, Infinity), 0, true) != null + if (this.isFoldableCache[row] != null) return this.isFoldableCache[row] + const result = this.getFoldableRangeContainingPoint(Point(row, Infinity), 0, true) != null + this.isFoldableCache[row] = result + return result } getFoldableRanges () { @@ -174,48 +179,72 @@ class TreeSitterLanguageMode { } getFoldableRangeForNode (node, existenceOnly) { - const {firstChild} = node - if (firstChild) { - const {lastChild} = node + const {children, type: nodeType} = node + const childCount = children.length + let childTypes - for (let i = 0, n = this.grammar.foldConfig.delimiters.length; i < n; i++) { - const entry = this.grammar.foldConfig.delimiters[i] - if (firstChild.type === entry[0] && lastChild.type === entry[1]) { - if (existenceOnly) return true - let childPrecedingFold = firstChild + for (var i = 0, {length} = this.grammar.folds; i < length; i++) { + const foldEntry = this.grammar.folds[i] - const options = entry[2] - if (options) { - const {children} = node - let childIndexPrecedingFold = options.afterChildCount || 0 - if (options.afterType) { - for (let i = childIndexPrecedingFold, n = children.length; i < n; i++) { - if (children[i].type === options.afterType) { - childIndexPrecedingFold = i - break - } - } - } - childPrecedingFold = children[childIndexPrecedingFold] - } - - let granchildPrecedingFold = childPrecedingFold.lastChild - if (granchildPrecedingFold) { - return Range(granchildPrecedingFold.endPosition, lastChild.startPosition) - } else { - return Range(childPrecedingFold.endPosition, lastChild.startPosition) - } + if (foldEntry.type) { + if (typeof foldEntry.type === 'string') { + if (foldEntry.type !== nodeType) continue + } else { + if (!foldEntry.type.includes(nodeType)) continue + } + } + + let childBeforeFold + const startEntry = foldEntry.start + if (startEntry) { + if (startEntry.index != null) { + childBeforeFold = children[startEntry.index] + if (!childBeforeFold) continue + if (startEntry.type && startEntry.type !== childBeforeFold.type) continue + } else { + if (!childTypes) childTypes = children.map(child => child.type) + let index = childTypes.indexOf(startEntry.type) + if (index === -1) continue + childBeforeFold = children[index] + } + } + + let childAfterFold + const endEntry = foldEntry.end + if (endEntry) { + if (endEntry.index != null) { + const index = endEntry.index < 0 ? childCount + endEntry.index : endEntry.index + childAfterFold = children[index] + if (!childAfterFold) continue + if (endEntry.type && endEntry.type !== childAfterFold.type) continue + } else { + if (!childTypes) childTypes = children.map(child => child.type) + let index = childTypes.lastIndexOf(endEntry.type) + if (index === -1) continue + childAfterFold = children[index] } } - } - if (this.grammar.foldConfig.nodes.has(node.type)) { if (existenceOnly) return true - const start = node.startPosition - const end = node.endPosition - start.column = Infinity - end.column = 0 - return Range(start, end) + + let start, end + if (childBeforeFold) { + start = childBeforeFold.endPosition + } else { + start = new Point(node.startPosition.row, Infinity) + } + if (childAfterFold) { + end = childAfterFold.startPosition + } else { + const {endPosition} = node + if (endPosition.column === 0) { + end = Point(endPosition.row - 1, Infinity) + } else { + end = Point(endPosition.row, 0) + } + } + + return new Range(start, end) } } From 69214dc26af85c5ed2369e374c6db485e48bc773 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 5 Dec 2017 23:11:45 +0100 Subject: [PATCH 034/176] On clicks, only move cursor if there is one cursor without a selection --- src/text-editor-component.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index da6ec452d..a43616122 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1758,11 +1758,12 @@ class TextEditorComponent { const screenPosition = this.screenPositionForMouseEvent(event) - // All clicks should set the cursor position, but only left-clicks should - // have additional logic. - // On macOS, ctrl-click brings up the context menu so also handle that case. if (button !== 0 || (platform === 'darwin' && ctrlKey)) { - model.setCursorScreenPosition(screenPosition, {autoscroll: false}) + // Set cursor position only if there is one cursor with no selection + const ranges = model.getSelectedBufferRanges() + if (ranges.length === 1 && ranges[0].isEmpty()) { + model.setCursorScreenPosition(screenPosition, {autoscroll: false}) + } // On Linux, pasting happens on middle click. A textInput event with the // contents of the selection clipboard will be dispatched by the browser From db392502e4544ed7f01c4992087bba53dcc08e3e Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 5 Dec 2017 23:38:21 +0100 Subject: [PATCH 035/176] Always move the cursor on middle-click --- spec/text-editor-component-spec.js | 180 ++++++++++++++++++++--------- src/text-editor-component.js | 5 +- 2 files changed, 126 insertions(+), 59 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 47ba4ca63..578f6ec62 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -2840,83 +2840,149 @@ describe('TextEditorComponent', () => { describe('mouse input', () => { describe('on the lines', () => { - it('positions the cursor on single-click or when middle/right-clicking', async () => { - for (const button of [0, 1, 2]) { + describe('when there is only one cursor and no selection', () => { + it('positions the cursor on single-click or when middle/right-clicking', async () => { + for (const button of [0, 1, 2]) { + const {component, element, editor} = buildComponent() + const {lineHeight} = component.measurements + + editor.setCursorScreenPosition([Infinity, Infinity], {autoscroll: false}) + component.didMouseDownOnContent({ + detail: 1, + button, + clientX: clientLeftForCharacter(component, 0, 0) - 1, + clientY: clientTopForLine(component, 0) - 1 + }) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + + const maxRow = editor.getLastScreenRow() + editor.setCursorScreenPosition([Infinity, Infinity], {autoscroll: false}) + component.didMouseDownOnContent({ + detail: 1, + button, + clientX: clientLeftForCharacter(component, maxRow, editor.lineLengthForScreenRow(maxRow)) + 1, + clientY: clientTopForLine(component, maxRow) + 1 + }) + expect(editor.getCursorScreenPosition()).toEqual([maxRow, editor.lineLengthForScreenRow(maxRow)]) + + component.didMouseDownOnContent({ + detail: 1, + button, + clientX: clientLeftForCharacter(component, 0, editor.lineLengthForScreenRow(0)) + 1, + clientY: clientTopForLine(component, 0) + lineHeight / 2 + }) + expect(editor.getCursorScreenPosition()).toEqual([0, editor.lineLengthForScreenRow(0)]) + + component.didMouseDownOnContent({ + detail: 1, + button, + clientX: (clientLeftForCharacter(component, 3, 0) + clientLeftForCharacter(component, 3, 1)) / 2, + clientY: clientTopForLine(component, 1) + lineHeight / 2 + }) + expect(editor.getCursorScreenPosition()).toEqual([1, 0]) + + component.didMouseDownOnContent({ + detail: 1, + button, + clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 15)) / 2, + clientY: clientTopForLine(component, 3) + lineHeight / 2 + }) + expect(editor.getCursorScreenPosition()).toEqual([3, 14]) + + component.didMouseDownOnContent({ + detail: 1, + button, + clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 15)) / 2 + 1, + clientY: clientTopForLine(component, 3) + lineHeight / 2 + }) + expect(editor.getCursorScreenPosition()).toEqual([3, 15]) + + editor.getBuffer().setTextInRange([[3, 14], [3, 15]], '🐣') + await component.getNextUpdatePromise() + + component.didMouseDownOnContent({ + detail: 1, + button, + clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 16)) / 2, + clientY: clientTopForLine(component, 3) + lineHeight / 2 + }) + expect(editor.getCursorScreenPosition()).toEqual([3, 14]) + + component.didMouseDownOnContent({ + detail: 1, + button, + clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 16)) / 2 + 1, + clientY: clientTopForLine(component, 3) + lineHeight / 2 + }) + expect(editor.getCursorScreenPosition()).toEqual([3, 16]) + + expect(editor.testAutoscrollRequests).toEqual([]) + } + }) + }) + + describe('when there is more than one cursor', () => { + it('does not move the cursor when right-clicking', async () => { const {component, element, editor} = buildComponent() const {lineHeight} = component.measurements - editor.setCursorScreenPosition([Infinity, Infinity], {autoscroll: false}) + editor.setCursorScreenPosition([5, 17], {autoscroll: false}) + editor.addCursorAtScreenPosition([2, 4]) component.didMouseDownOnContent({ detail: 1, - button, + button: 2, clientX: clientLeftForCharacter(component, 0, 0) - 1, clientY: clientTopForLine(component, 0) - 1 }) - expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + expect(editor.getCursorScreenPositions()).toEqual([Point.fromObject([5, 17]), Point.fromObject([2, 4])]) + }) - const maxRow = editor.getLastScreenRow() - editor.setCursorScreenPosition([Infinity, Infinity], {autoscroll: false}) + it('does move the cursor when middle-clicking', async () => { + const {component, element, editor} = buildComponent() + const {lineHeight} = component.measurements + + editor.setCursorScreenPosition([5, 17], {autoscroll: false}) + editor.addCursorAtScreenPosition([2, 4]) component.didMouseDownOnContent({ detail: 1, - button, - clientX: clientLeftForCharacter(component, maxRow, editor.lineLengthForScreenRow(maxRow)) + 1, - clientY: clientTopForLine(component, maxRow) + 1 + button: 1, + clientX: clientLeftForCharacter(component, 0, 0) - 1, + clientY: clientTopForLine(component, 0) - 1 }) - expect(editor.getCursorScreenPosition()).toEqual([maxRow, editor.lineLengthForScreenRow(maxRow)]) + expect(editor.getCursorScreenPositions()).toEqual([Point.fromObject([0, 0])]) + }) + }) + describe('when there are non-empty selections', () => { + it('does not move the cursor when right-clicking', async () => { + const {component, element, editor} = buildComponent() + const {lineHeight} = component.measurements + + editor.setCursorScreenPosition([5, 17], {autoscroll: false}) + editor.selectRight(3) component.didMouseDownOnContent({ detail: 1, - button, - clientX: clientLeftForCharacter(component, 0, editor.lineLengthForScreenRow(0)) + 1, - clientY: clientTopForLine(component, 0) + lineHeight / 2 + button: 2, + clientX: clientLeftForCharacter(component, 0, 0) - 1, + clientY: clientTopForLine(component, 0) - 1 }) - expect(editor.getCursorScreenPosition()).toEqual([0, editor.lineLengthForScreenRow(0)]) + expect(editor.getSelectedScreenRange()).toEqual([[5, 17], [5, 20]]) + }) + it('does move the cursor when middle-clicking', async () => { + const {component, element, editor} = buildComponent() + const {lineHeight} = component.measurements + + editor.setCursorScreenPosition([5, 17], {autoscroll: false}) + editor.selectRight(3) component.didMouseDownOnContent({ detail: 1, - button, - clientX: (clientLeftForCharacter(component, 3, 0) + clientLeftForCharacter(component, 3, 1)) / 2, - clientY: clientTopForLine(component, 1) + lineHeight / 2 + button: 1, + clientX: clientLeftForCharacter(component, 0, 0) - 1, + clientY: clientTopForLine(component, 0) - 1 }) - expect(editor.getCursorScreenPosition()).toEqual([1, 0]) - - component.didMouseDownOnContent({ - detail: 1, - button, - clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 15)) / 2, - clientY: clientTopForLine(component, 3) + lineHeight / 2 - }) - expect(editor.getCursorScreenPosition()).toEqual([3, 14]) - - component.didMouseDownOnContent({ - detail: 1, - button, - clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 15)) / 2 + 1, - clientY: clientTopForLine(component, 3) + lineHeight / 2 - }) - expect(editor.getCursorScreenPosition()).toEqual([3, 15]) - - editor.getBuffer().setTextInRange([[3, 14], [3, 15]], '🐣') - await component.getNextUpdatePromise() - - component.didMouseDownOnContent({ - detail: 1, - button, - clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 16)) / 2, - clientY: clientTopForLine(component, 3) + lineHeight / 2 - }) - expect(editor.getCursorScreenPosition()).toEqual([3, 14]) - - component.didMouseDownOnContent({ - detail: 1, - button, - clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 16)) / 2 + 1, - clientY: clientTopForLine(component, 3) + lineHeight / 2 - }) - expect(editor.getCursorScreenPosition()).toEqual([3, 16]) - - expect(editor.testAutoscrollRequests).toEqual([]) - } + expect(editor.getSelectedScreenRange()).toEqual([[0, 0], [0, 0]]) + }) }) describe('when the input is for the primary mouse button', () => { diff --git a/src/text-editor-component.js b/src/text-editor-component.js index a43616122..06bbe535f 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1759,9 +1759,10 @@ class TextEditorComponent { const screenPosition = this.screenPositionForMouseEvent(event) if (button !== 0 || (platform === 'darwin' && ctrlKey)) { - // Set cursor position only if there is one cursor with no selection + // Always set cursor position on middle-click + // Only set cursor position on right-click if there is one cursor with no selection const ranges = model.getSelectedBufferRanges() - if (ranges.length === 1 && ranges[0].isEmpty()) { + if (button === 1 || (ranges.length === 1 && ranges[0].isEmpty())) { model.setCursorScreenPosition(screenPosition, {autoscroll: false}) } From f3715779e5d00266147776ffda9b51f6aedbb45f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2017 16:26:24 -0800 Subject: [PATCH 036/176] Support contentRegExp field on grammars, to match more than one line Signed-off-by: Nathan Sobo --- spec/grammar-registry-spec.js | 27 +++++++++++++++++ src/grammar-registry.js | 57 ++++++++++++++++++++--------------- src/tree-sitter-grammar.js | 3 +- 3 files changed, 60 insertions(+), 27 deletions(-) diff --git a/spec/grammar-registry-spec.js b/spec/grammar-registry-spec.js index 4066af24d..7b8f6f1b2 100644 --- a/spec/grammar-registry-spec.js +++ b/spec/grammar-registry-spec.js @@ -1,5 +1,6 @@ const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') +const dedent = require('dedent') const path = require('path') const fs = require('fs-plus') const temp = require('temp').track() @@ -273,6 +274,32 @@ describe('GrammarRegistry', () => { expect(atom.grammars.selectGrammar('/hu.git/config').name).toBe('Null Grammar') }) + describe('when the grammar has a contentRegExp field', () => { + it('favors grammars whose contentRegExp matches a prefix of the file\'s content', () => { + atom.grammars.addGrammar({ + id: 'javascript-1', + fileTypes: ['js'] + }) + atom.grammars.addGrammar({ + id: 'flow-javascript', + contentRegExp: new RegExp('//.*@flow'), + fileTypes: ['js'] + }) + atom.grammars.addGrammar({ + id: 'javascript-2', + fileTypes: ['js'] + }) + + const selectedGrammar = atom.grammars.selectGrammar('test.js', dedent` + // Copyright EvilCorp + // @flow + + module.exports = function () { return 1 + 1 } + `) + expect(selectedGrammar.id).toBe('flow-javascript') + }) + }) + it("uses the filePath's shebang line if the grammar cannot be determined by the extension or basename", async () => { await atom.packages.activatePackage('language-javascript') await atom.packages.activatePackage('language-ruby') diff --git a/src/grammar-registry.js b/src/grammar-registry.js index 6dbb248e7..6722e097b 100644 --- a/src/grammar-registry.js +++ b/src/grammar-registry.js @@ -11,7 +11,6 @@ const fs = require('fs-plus') const {Point, Range} = require('text-buffer') const GRAMMAR_TYPE_BONUS = 1000 -const GRAMMAR_SELECTION_RANGE = Range(Point.ZERO, Point(10, 0)).freeze() const PATH_SPLIT_REGEX = new RegExp('[/.]') const LANGUAGE_ID_MAP = [ @@ -147,7 +146,7 @@ class GrammarRegistry { autoAssignLanguageMode (buffer) { const result = this.selectGrammarWithScore( buffer.getPath(), - buffer.getTextInRange(GRAMMAR_SELECTION_RANGE) + getGrammarSelectionContent(buffer) ) this.languageOverridesByBufferId.delete(buffer.id) this.grammarScoresByBuffer.set(buffer, result.score) @@ -245,26 +244,32 @@ class GrammarRegistry { } grammarMatchesContents (grammar, contents) { - if (contents == null || grammar.firstLineRegex == null) return false + if (contents == null) return false - let escaped = false - let numberOfNewlinesInRegex = 0 - for (let character of grammar.firstLineRegex.source) { - switch (character) { - case '\\': - escaped = !escaped - break - case 'n': - if (escaped) { numberOfNewlinesInRegex++ } - escaped = false - break - default: - escaped = false + if (grammar.contentRegExp) { // TreeSitter grammars + return grammar.contentRegExp.test(contents) + } else if (grammar.firstLineRegex) { // FirstMate grammars + let escaped = false + let numberOfNewlinesInRegex = 0 + for (let character of grammar.firstLineRegex.source) { + switch (character) { + case '\\': + escaped = !escaped + break + case 'n': + if (escaped) { numberOfNewlinesInRegex++ } + escaped = false + break + default: + escaped = false + } } - } - const lines = contents.split('\n') - return grammar.firstLineRegex.testSync(lines.slice(0, numberOfNewlinesInRegex + 1).join('\n')) + const lines = contents.split('\n') + return grammar.firstLineRegex.testSync(lines.slice(0, numberOfNewlinesInRegex + 1).join('\n')) + } else { + return false + } } forEachGrammar (callback) { @@ -338,12 +343,7 @@ class GrammarRegistry { grammar.id === languageOverride)) { buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer)) } else if (!languageOverride) { - const score = this.getGrammarScore( - grammar, - buffer.getPath(), - buffer.getTextInRange(GRAMMAR_SELECTION_RANGE) - ) - + const score = this.getGrammarScore(grammar, buffer.getPath(), getGrammarSelectionContent(buffer)) const currentScore = this.grammarScoresByBuffer.get(buffer) if (currentScore == null || score > currentScore) { buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer)) @@ -500,3 +500,10 @@ class GrammarRegistry { } } } + +function getGrammarSelectionContent (buffer) { + return buffer.getTextInRange(Range( + Point(0, 0), + buffer.positionForCharacterIndex(1024) + )) +} diff --git a/src/tree-sitter-grammar.js b/src/tree-sitter-grammar.js index 3448d0cd1..b36505a0b 100644 --- a/src/tree-sitter-grammar.js +++ b/src/tree-sitter-grammar.js @@ -1,7 +1,6 @@ const path = require('path') const SyntaxScopeMap = require('./syntax-scope-map') const Module = require('module') -const {OnigRegExp} = require('oniguruma') module.exports = class TreeSitterGrammar { @@ -9,6 +8,7 @@ class TreeSitterGrammar { this.registry = registry this.id = params.id this.name = params.name + if (params.contentRegExp) this.contentRegExp = new RegExp(params.contentRegExp) this.folds = params.folds || [] @@ -37,7 +37,6 @@ class TreeSitterGrammar { }) this.languageModule = require(languageModulePath) - this.firstLineRegex = params.firstLineMatch && new OnigRegExp(params.firstLineMatch) this.scopesById = new Map() this.idsByScope = {} this.nextScopeId = 256 + 1 From 77fd29647a6f756a668268551fd11bc88600be60 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2017 17:01:49 -0800 Subject: [PATCH 037/176] Cache foldability more intelligently Signed-off-by: Nathan Sobo --- src/tree-sitter-language-mode.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index ff7d6c096..166816d0d 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -26,7 +26,10 @@ class TreeSitterLanguageMode { } bufferDidChange ({oldRange, newRange, oldText, newText}) { - this.isFoldableCache.length = 0 + const startRow = oldRange.start.row + const oldEndRow = oldRange.end.row + const newEndRow = newRange.end.row + this.isFoldableCache.splice(startRow, oldEndRow - startRow, ...new Array(newEndRow - startRow)) this.document.edit({ startIndex: this.buffer.characterIndexForPosition(oldRange.start), lengthRemoved: oldText.length, @@ -44,7 +47,13 @@ class TreeSitterLanguageMode { buildHighlightIterator () { const invalidatedRanges = this.document.parse() for (let i = 0, n = invalidatedRanges.length; i < n; i++) { - this.emitter.emit('did-change-highlighting', invalidatedRanges[i]) + const range = invalidatedRanges[i] + const startRow = range.start.row + const endRow = range.end.row + for (let row = startRow; row < endRow; row++) { + this.isFoldableCache[row] = undefined + } + this.emitter.emit('did-change-highlighting', range) } return new TreeSitterHighlightIterator(this) } From 815b445d2e78c7f0a04fcd65dd8e924d08ca8883 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2017 17:58:39 -0800 Subject: [PATCH 038/176] :arrow_up: language packages --- package.json | 12 ++++++------ src/grammar-registry.js | 7 ++++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 91cf950b4..4c9fa8389 100644 --- a/package.json +++ b/package.json @@ -137,18 +137,18 @@ "welcome": "0.36.6", "whitespace": "0.37.5", "wrap-guide": "0.40.3", - "language-c": "0.58.1", + "language-c": "0.59.0-1", "language-clojure": "0.22.5", "language-coffee-script": "0.49.3", "language-csharp": "0.14.3", "language-css": "0.42.8", "language-gfm": "0.90.2", "language-git": "0.19.1", - "language-go": "0.44.3", + "language-go": "0.45.0-2", "language-html": "0.48.3", "language-hyperlink": "0.16.3", "language-java": "0.27.6", - "language-javascript": "0.128.0-0", + "language-javascript": "0.128.0-1", "language-json": "0.19.1", "language-less": "0.34.1", "language-make": "0.22.3", @@ -157,17 +157,17 @@ "language-perl": "0.38.1", "language-php": "0.42.2", "language-property-list": "0.9.1", - "language-python": "0.45.5", + "language-python": "0.46.0-0", "language-ruby": "0.71.4", "language-ruby-on-rails": "0.25.2", "language-sass": "0.61.3", - "language-shellscript": "0.25.4", + "language-shellscript": "0.26.0-0", "language-source": "0.9.0", "language-sql": "0.25.8", "language-text": "0.7.3", "language-todo": "0.29.3", "language-toml": "0.18.1", - "language-typescript": "0.2.3", + "language-typescript": "0.3.0-0", "language-xml": "0.35.2", "language-yaml": "0.31.1" }, diff --git a/src/grammar-registry.js b/src/grammar-registry.js index 6722e097b..dd11171ba 100644 --- a/src/grammar-registry.js +++ b/src/grammar-registry.js @@ -15,7 +15,12 @@ const PATH_SPLIT_REGEX = new RegExp('[/.]') const LANGUAGE_ID_MAP = [ ['source.js', 'javascript'], - ['source.ts', 'typescript'] + ['source.ts', 'typescript'], + ['source.c', 'c'], + ['source.cpp', 'cpp'], + ['source.go', 'go'], + ['source.python', 'python'], + ['source.sh', 'bash'] ] // Extended: This class holds the grammars used for tokenizing. From 3f775b550510ec2483c3fc8220dd5acf54f87449 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Dec 2017 11:09:44 -0800 Subject: [PATCH 039/176] Fix folding of internal nodes when fold end isn't specified --- spec/tree-sitter-language-mode-spec.js | 157 ++++++++++++++++++++++--- src/tree-sitter-language-mode.js | 44 ++++--- 2 files changed, 163 insertions(+), 38 deletions(-) diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js index 1cc9afc94..426291e5f 100644 --- a/spec/tree-sitter-language-mode-spec.js +++ b/spec/tree-sitter-language-mode-spec.js @@ -6,6 +6,8 @@ const TextEditor = require('../src/text-editor') const TreeSitterGrammar = require('../src/tree-sitter-grammar') const TreeSitterLanguageMode = require('../src/tree-sitter-language-mode') +const cGrammarPath = require.resolve('language-c/grammars/tree-sitter-c.cson') +const pythonGrammarPath = require.resolve('language-python/grammars/tree-sitter-python.cson') const jsGrammarPath = require.resolve('language-javascript/grammars/tree-sitter-javascript.cson') describe('TreeSitterLanguageMode', () => { @@ -73,7 +75,7 @@ describe('TreeSitterLanguageMode', () => { editor.displayLayer.reset({foldCharacter: '…'}) }) - it('can fold nodes that start and end with specified tokens and span multiple lines', () => { + it('can fold nodes that start and end with specified tokens', () => { const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { parser: 'tree-sitter-javascript', folds: [ @@ -107,6 +109,7 @@ describe('TreeSitterLanguageMode', () => { expect(editor.isFoldableAtBufferRow(2)).toBe(true) expect(editor.isFoldableAtBufferRow(3)).toBe(false) expect(editor.isFoldableAtBufferRow(4)).toBe(true) + expect(editor.isFoldableAtBufferRow(5)).toBe(false) editor.foldBufferRow(2) expect(getDisplayText(editor)).toBe(dedent ` @@ -127,20 +130,24 @@ describe('TreeSitterLanguageMode', () => { `) }) - it('can fold nodes that start and end with specified tokens and span multiple lines', () => { + it('can fold nodes of specified types', () => { const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { parser: 'tree-sitter-javascript', folds: [ + // Start the fold after the first child (the opening tag) and end it at the last child + // (the closing tag). { type: 'jsx_element', - start: {index: 0, type: 'jsx_opening_element'}, - end: {index: -1, type: 'jsx_closing_element'} + start: {index: 0}, + end: {index: -1} }, + + // End the fold at the *second* to last child of the self-closing tag: the `/`. { type: 'jsx_self_closing_element', start: {index: 1}, - end: {type: '/', index: -2} - }, + end: {index: -2} + } ] }) @@ -183,11 +190,12 @@ describe('TreeSitterLanguageMode', () => { `) }) - it('can fold specified types of multi-line nodes', () => { + it('can fold entire nodes when no start or end parameters are specified', () => { const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { parser: 'tree-sitter-javascript', folds: [ - {type: 'template_string'}, + // By default, for a node with no children, folds are started at the *end* of the first + // line of a node, and ended at the *beginning* of the last line. {type: 'comment'} ] }) @@ -197,9 +205,9 @@ describe('TreeSitterLanguageMode', () => { /** * Important */ - const x = \`one - two - three\` + const x = 1 /* + Also important + */ `) editor.screenLineForScreenRow(0) @@ -213,17 +221,136 @@ describe('TreeSitterLanguageMode', () => { editor.foldBufferRow(0) expect(getDisplayText(editor)).toBe(dedent ` /**… */ - const x = \`one - two - three\` + const x = 1 /* + Also important + */ `) editor.foldBufferRow(3) expect(getDisplayText(editor)).toBe(dedent ` /**… */ - const x = \`one… three\` + const x = 1 /*…*/ `) }) + + it('tries each folding strategy for a given node in the order specified', () => { + const grammar = new TreeSitterGrammar(atom.grammars, cGrammarPath, { + parser: 'tree-sitter-c', + folds: [ + // If the #ifdef has an `#else` clause, then end the fold there. + { + type: 'preproc_ifdef', + start: {index: 1}, + end: {type: 'preproc_else'} + }, + + // Otherwise, end the fold at the last child - the `#endif`. + { + type: 'preproc_ifdef', + start: {index: 1}, + end: {index: -1} + }, + + // When folding an `#else` clause, the fold extends to the end of the clause. + { + type: 'preproc_else', + start: {index: 0} + } + ] + }) + + buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar})) + + buffer.setText(dedent ` + #ifndef FOO_H_ + #define FOO_H_ + + #ifdef _WIN32 + + #include + const char *path_separator = "\\"; + + #else + + #include + const char *path_separator = "/"; + + #endif + + #endif + `) + + editor.screenLineForScreenRow(0) + + editor.foldBufferRow(3) + expect(getDisplayText(editor)).toBe(dedent ` + #ifndef FOO_H_ + #define FOO_H_ + + #ifdef _WIN32…#else + + #include + const char *path_separator = "/"; + + #endif + + #endif + `) + + editor.foldBufferRow(8) + expect(getDisplayText(editor)).toBe(dedent ` + #ifndef FOO_H_ + #define FOO_H_ + + #ifdef _WIN32…#else… + + #endif + + #endif + `) + + editor.foldBufferRow(0) + expect(getDisplayText(editor)).toBe(dedent ` + #ifndef FOO_H_…#endif + `) + }) + + describe('when folding a node that ends with a line break', () => { + it('ends the fold at the end of the previous line', () => { + const grammar = new TreeSitterGrammar(atom.grammars, pythonGrammarPath, { + parser: 'tree-sitter-python', + folds: [ + { + type: 'function_definition', + start: {type: ':'} + } + ] + }) + + buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar})) + + buffer.setText(dedent ` + def ab(): + print 'a' + print 'b' + + def cd(): + print 'c' + print 'd' + `) + + editor.screenLineForScreenRow(0) + + editor.foldBufferRow(0) + expect(getDisplayText(editor)).toBe(dedent ` + def ab():… + + def cd(): + print 'c' + print 'd' + `) + }) + }) }) describe('TextEditor.selectLargerSyntaxNode and .selectSmallerSyntaxNode', () => { diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 166816d0d..f47d89db7 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -203,57 +203,55 @@ class TreeSitterLanguageMode { } } - let childBeforeFold + let foldStart const startEntry = foldEntry.start if (startEntry) { if (startEntry.index != null) { - childBeforeFold = children[startEntry.index] - if (!childBeforeFold) continue - if (startEntry.type && startEntry.type !== childBeforeFold.type) continue + const child = children[startEntry.index] + if (!child || (startEntry.type && startEntry.type !== child.type)) continue + foldStart = child.endPosition } else { if (!childTypes) childTypes = children.map(child => child.type) - let index = childTypes.indexOf(startEntry.type) + const index = childTypes.indexOf(startEntry.type) if (index === -1) continue - childBeforeFold = children[index] + foldStart = children[index].endPosition } } - let childAfterFold + let foldEnd const endEntry = foldEntry.end if (endEntry) { if (endEntry.index != null) { const index = endEntry.index < 0 ? childCount + endEntry.index : endEntry.index - childAfterFold = children[index] - if (!childAfterFold) continue - if (endEntry.type && endEntry.type !== childAfterFold.type) continue + const child = children[index] + if (!child || (endEntry.type && endEntry.type !== child.type)) continue + foldEnd = child.startPosition } else { if (!childTypes) childTypes = children.map(child => child.type) - let index = childTypes.lastIndexOf(endEntry.type) + const index = childTypes.lastIndexOf(endEntry.type) if (index === -1) continue - childAfterFold = children[index] + foldEnd = children[index].startPosition } } if (existenceOnly) return true - let start, end - if (childBeforeFold) { - start = childBeforeFold.endPosition - } else { - start = new Point(node.startPosition.row, Infinity) + if (!foldStart) { + foldStart = new Point(node.startPosition.row, Infinity) } - if (childAfterFold) { - end = childAfterFold.startPosition - } else { + + if (!foldEnd) { const {endPosition} = node if (endPosition.column === 0) { - end = Point(endPosition.row - 1, Infinity) + foldEnd = Point(endPosition.row - 1, Infinity) + } else if (childCount > 0) { + foldEnd = endPosition } else { - end = Point(endPosition.row, 0) + foldEnd = Point(endPosition.row, 0) } } - return new Range(start, end) + return new Range(foldStart, foldEnd) } } From 4c6abd3b7a8fde0eacda3f620f8a48ec8bc58058 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Dec 2017 14:16:25 -0800 Subject: [PATCH 040/176] :arrow_up: language-javascript, language-typescript --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4c9fa8389..fa50740de 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ "language-html": "0.48.3", "language-hyperlink": "0.16.3", "language-java": "0.27.6", - "language-javascript": "0.128.0-1", + "language-javascript": "0.128.0-2", "language-json": "0.19.1", "language-less": "0.34.1", "language-make": "0.22.3", @@ -167,7 +167,7 @@ "language-text": "0.7.3", "language-todo": "0.29.3", "language-toml": "0.18.1", - "language-typescript": "0.3.0-0", + "language-typescript": "0.3.0-1", "language-xml": "0.35.2", "language-yaml": "0.31.1" }, From 59f9417606eb67cef28bfd515a489a8b4b0b308c Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 7 Dec 2017 02:54:32 +0100 Subject: [PATCH 041/176] :arrow_up: language-html@0.48.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2af021a4f..19449c50b 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "language-gfm": "0.90.2", "language-git": "0.19.1", "language-go": "0.44.3", - "language-html": "0.48.3", + "language-html": "0.48.4", "language-hyperlink": "0.16.3", "language-java": "0.27.6", "language-javascript": "0.127.7", From 474af30e7b42c1955aae1a90670d6a443e44ccaa Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 7 Dec 2017 03:00:55 +0100 Subject: [PATCH 042/176] :arrow_up: bracket-matcher@0.88.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 19449c50b..5a3f40e0c 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "autosave": "0.24.6", "background-tips": "0.27.1", "bookmarks": "0.45.0", - "bracket-matcher": "0.88.0", + "bracket-matcher": "0.88.1", "command-palette": "0.43.0", "dalek": "0.2.1", "deprecation-cop": "0.56.9", From f82a555fb8e2b2fcd880d0cc36556016f1cf52ab Mon Sep 17 00:00:00 2001 From: simurai Date: Thu, 7 Dec 2017 16:34:33 +0900 Subject: [PATCH 043/176] :arrow_up: Update various themes --- package.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 5a3f40e0c..c16c3084d 100644 --- a/package.json +++ b/package.json @@ -77,18 +77,18 @@ "yargs": "^3.23.0" }, "packageDependencies": { - "atom-dark-syntax": "0.28.0", - "atom-dark-ui": "0.53.0", + "atom-dark-syntax": "0.29.0", + "atom-dark-ui": "0.53.1", "atom-light-syntax": "0.29.0", - "atom-light-ui": "0.46.0", + "atom-light-ui": "0.46.1", "base16-tomorrow-dark-theme": "1.5.0", "base16-tomorrow-light-theme": "1.5.0", - "one-dark-ui": "1.10.8", - "one-light-ui": "1.10.8", - "one-dark-syntax": "1.8.0", - "one-light-syntax": "1.8.0", - "solarized-dark-syntax": "1.1.2", - "solarized-light-syntax": "1.1.2", + "one-dark-ui": "1.10.9", + "one-light-ui": "1.10.9", + "one-dark-syntax": "1.8.1", + "one-light-syntax": "1.8.1", + "solarized-dark-syntax": "1.1.3", + "solarized-light-syntax": "1.1.3", "about": "1.7.8", "archive-view": "0.64.1", "autocomplete-atom-api": "0.10.5", From 67d5e45bacddd039842eea57ff7d62aa71829122 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 7 Dec 2017 11:56:24 +0100 Subject: [PATCH 044/176] :arrow_up: language-ruby-on-rails@0.25.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c16c3084d..f1c1bda92 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,7 @@ "language-property-list": "0.9.1", "language-python": "0.45.5", "language-ruby": "0.71.4", - "language-ruby-on-rails": "0.25.2", + "language-ruby-on-rails": "0.25.3", "language-sass": "0.61.3", "language-shellscript": "0.25.4", "language-source": "0.9.0", From 55cd0fe7358621fbb8592f9ad9a8ee0239b082ac Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 7 Dec 2017 11:57:16 +0100 Subject: [PATCH 045/176] :arrow_up: language-python@0.45.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f1c1bda92..aa1b00425 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "language-perl": "0.38.1", "language-php": "0.42.2", "language-property-list": "0.9.1", - "language-python": "0.45.5", + "language-python": "0.45.6", "language-ruby": "0.71.4", "language-ruby-on-rails": "0.25.3", "language-sass": "0.61.3", From c3a9da2da02b0f8e7e48720a92e00cd7dbd7d930 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 7 Dec 2017 11:57:50 +0100 Subject: [PATCH 046/176] :arrow_up: language-go@0.44.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aa1b00425..aa29489d5 100644 --- a/package.json +++ b/package.json @@ -143,7 +143,7 @@ "language-css": "0.42.8", "language-gfm": "0.90.2", "language-git": "0.19.1", - "language-go": "0.44.3", + "language-go": "0.44.4", "language-html": "0.48.4", "language-hyperlink": "0.16.3", "language-java": "0.27.6", From f3f11e40fc36237ce2403fa21983881a23d4bb9a Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 7 Dec 2017 11:58:28 +0100 Subject: [PATCH 047/176] :arrow_up: language-sql@0.25.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aa29489d5..fcc368170 100644 --- a/package.json +++ b/package.json @@ -162,7 +162,7 @@ "language-sass": "0.61.3", "language-shellscript": "0.25.4", "language-source": "0.9.0", - "language-sql": "0.25.8", + "language-sql": "0.25.9", "language-text": "0.7.3", "language-todo": "0.29.3", "language-toml": "0.18.1", From 5b2631ee6f0f68f8e66bce043f1baf92f351779f Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 7 Dec 2017 11:59:18 +0100 Subject: [PATCH 048/176] :arrow_up: language-gfm@0.90.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fcc368170..5e4090f2d 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "language-coffee-script": "0.49.3", "language-csharp": "0.14.3", "language-css": "0.42.8", - "language-gfm": "0.90.2", + "language-gfm": "0.90.3", "language-git": "0.19.1", "language-go": "0.44.4", "language-html": "0.48.4", From 96662e0b7ed30a7e9b9d06e85e6b53a1c605e63d Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 7 Dec 2017 12:05:14 +0100 Subject: [PATCH 049/176] :arrow_up: language-php@0.43.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5e4090f2d..d48b309c9 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,7 @@ "language-mustache": "0.14.4", "language-objective-c": "0.15.1", "language-perl": "0.38.1", - "language-php": "0.42.2", + "language-php": "0.43.0", "language-property-list": "0.9.1", "language-python": "0.45.6", "language-ruby": "0.71.4", From c513ff1fc9b02d28fb62180ba1f19e167bd37582 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 7 Dec 2017 12:24:28 +0100 Subject: [PATCH 050/176] :arrow_up: autocomplete-css@0.17.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c16c3084d..a2f907b0b 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "about": "1.7.8", "archive-view": "0.64.1", "autocomplete-atom-api": "0.10.5", - "autocomplete-css": "0.17.4", + "autocomplete-css": "0.17.5", "autocomplete-html": "0.8.3", "autocomplete-plus": "2.39.0", "autocomplete-snippets": "1.11.2", From b61ebbb88c688072d72eb5e82e85ecc09f4ba46b Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 7 Dec 2017 12:34:25 +0100 Subject: [PATCH 051/176] :arrow_up: autocomplete-html@0.8.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a2f907b0b..8147ac5e3 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "archive-view": "0.64.1", "autocomplete-atom-api": "0.10.5", "autocomplete-css": "0.17.5", - "autocomplete-html": "0.8.3", + "autocomplete-html": "0.8.4", "autocomplete-plus": "2.39.0", "autocomplete-snippets": "1.11.2", "autoflow": "0.29.0", From 1eefff021ed3c3766aac39a1412aa80fd5c85362 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 7 Dec 2017 12:37:03 +0100 Subject: [PATCH 052/176] :arrow_up: atom-autocomplete-api@0.10.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8147ac5e3..a27265631 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "solarized-light-syntax": "1.1.3", "about": "1.7.8", "archive-view": "0.64.1", - "autocomplete-atom-api": "0.10.5", + "autocomplete-atom-api": "0.10.6", "autocomplete-css": "0.17.5", "autocomplete-html": "0.8.4", "autocomplete-plus": "2.39.0", From c9aa65559e2bff49eb35ef872574c624401ebdca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Here=C3=B1=C3=BA?= Date: Thu, 7 Dec 2017 11:10:08 -0300 Subject: [PATCH 053/176] Typos on #83 #84 --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 693e7358c..dceaecddb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,7 +65,7 @@ Atom is intentionally very modular. Nearly every non-editor UI element you inter ![atom-packages](https://cloud.githubusercontent.com/assets/69169/10472281/84fc9792-71d3-11e5-9fd1-19da717df079.png) -To get a sense for the packages that are bundled with Atom, you can go to Settings > Packages within Atom and take a look at the Core Packages section. +To get a sense for the packages that are bundled with Atom, you can go to `Settings` > `Packages` within Atom and take a look at the Core Packages section. Here's a list of the big ones: @@ -80,8 +80,8 @@ Here's a list of the big ones: * [autocomplete-plus](https://github.com/atom/autocomplete-plus) - autocompletions shown while typing. Some languages have additional packages for autocompletion functionality, such as [autocomplete-html](https://github.com/atom/autocomplete-html). * [git-diff](https://github.com/atom/git-diff) - Git change indicators shown in the editor's gutter. * [language-javascript](https://github.com/atom/language-javascript) - all bundled languages are packages too, and each one has a separate package `language-[name]`. Use these for feedback on syntax highlighting issues that only appear for a specific language. -* [one-dark-ui](https://github.com/atom/one-dark-ui) - the default UI styling for anything but the text editor. UI theme packages (i.e. packages with a `-ui` suffix) provide only styling and it's possible that a bundled package is responsible for a UI issue. There are other other bundled UI themes, such as [one-light-ui](https://github.com/atom/one-light-ui). -* [one-dark-syntax](https://github.com/atom/one-dark-syntax) - the default syntax highlighting styles applied for all languages. There are other other bundled syntax themes, such as [solarized-dark-syntax](https://github.com/atom/solarized-dark-syntax). You should use these packages for reporting issues that appear in many languages, but disappear if you change to another syntax theme. +* [one-dark-ui](https://github.com/atom/one-dark-ui) - the default UI styling for anything but the text editor. UI theme packages (i.e. packages with a `-ui` suffix) provide only styling and it's possible that a bundled package is responsible for a UI issue. There are other bundled UI themes, such as [one-light-ui](https://github.com/atom/one-light-ui). +* [one-dark-syntax](https://github.com/atom/one-dark-syntax) - the default syntax highlighting styles applied for all languages. There are other bundled syntax themes, such as [solarized-dark-syntax](https://github.com/atom/solarized-dark-syntax). You should use these packages for reporting issues that appear in many languages, but disappear if you change to another syntax theme. * [apm](https://github.com/atom/apm) - the `apm` command line tool (Atom Package Manager). You should use this repository for any contributions related to the `apm` tool and to publishing packages. * [atom.io](https://github.com/atom/atom.io) - the repository for feedback on the [Atom.io website](https://atom.io) and the [Atom.io package API](https://github.com/atom/atom/blob/master/docs/apm-rest-api.md) used by [apm](https://github.com/atom/apm). From b7258bdc8f414b991609f4da731d6a0a9f9e2218 Mon Sep 17 00:00:00 2001 From: Hubot Date: Thu, 7 Dec 2017 08:04:55 -0800 Subject: [PATCH 054/176] 1.25.0-dev --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9f782eb53..803944c87 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.24.0-dev", + "version": "1.25.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/main-process/main.js", "repository": { From 2369219c875d334f7de81a5137ccee4393740187 Mon Sep 17 00:00:00 2001 From: GilTeixeira Date: Thu, 7 Dec 2017 16:53:38 +0000 Subject: [PATCH 055/176] Changed atom safe mode theme to One Dark. --- src/theme-manager.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/theme-manager.js b/src/theme-manager.js index a23305c92..68a5eb45a 100644 --- a/src/theme-manager.js +++ b/src/theme-manager.js @@ -136,12 +136,12 @@ class ThemeManager { ] themeNames = _.intersection(themeNames, builtInThemeNames) if (themeNames.length === 0) { - themeNames = ['atom-dark-syntax', 'atom-dark-ui'] + themeNames = ['one-dark-syntax', 'one-dark-ui'] } else if (themeNames.length === 1) { if (_.endsWith(themeNames[0], '-ui')) { - themeNames.unshift('atom-dark-syntax') + themeNames.unshift('one-dark-syntax') } else { - themeNames.push('atom-dark-ui') + themeNames.push('one-dark-ui') } } } From be9e4696e64c726736845d2ccad4e6703b963ed7 Mon Sep 17 00:00:00 2001 From: GilTeixeira Date: Thu, 7 Dec 2017 18:58:52 +0000 Subject: [PATCH 056/176] Updated theme manager specs. --- spec/theme-manager-spec.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/theme-manager-spec.js b/spec/theme-manager-spec.js index f4ed3b9f5..9d1d3a3cc 100644 --- a/spec/theme-manager-spec.js +++ b/spec/theme-manager-spec.js @@ -424,12 +424,12 @@ h2 { waitsForPromise(() => atom.themes.activateThemes()) }) - it('uses the default dark UI and syntax themes and logs a warning', function () { + it('uses the default one-dark UI and syntax themes and logs a warning', function () { const activeThemeNames = atom.themes.getActiveThemeNames() expect(console.warn.callCount).toBe(2) expect(activeThemeNames.length).toBe(2) - expect(activeThemeNames).toContain('atom-dark-ui') - expect(activeThemeNames).toContain('atom-dark-syntax') + expect(activeThemeNames).toContain('one-dark-ui') + expect(activeThemeNames).toContain('one-dark-syntax') }) }) @@ -459,8 +459,8 @@ h2 { it('uses the default dark UI and syntax themes', function () { const activeThemeNames = atom.themes.getActiveThemeNames() expect(activeThemeNames.length).toBe(2) - expect(activeThemeNames).toContain('atom-dark-ui') - expect(activeThemeNames).toContain('atom-dark-syntax') + expect(activeThemeNames).toContain('one-dark-ui') + expect(activeThemeNames).toContain('one-dark-syntax') }) }) @@ -471,10 +471,10 @@ h2 { waitsForPromise(() => atom.themes.activateThemes()) }) - it('uses the default dark UI theme', function () { + it('uses the default one-dark UI theme', function () { const activeThemeNames = atom.themes.getActiveThemeNames() expect(activeThemeNames.length).toBe(2) - expect(activeThemeNames).toContain('atom-dark-ui') + expect(activeThemeNames).toContain('one-dark-ui') expect(activeThemeNames).toContain('atom-light-syntax') }) }) @@ -486,11 +486,11 @@ h2 { waitsForPromise(() => atom.themes.activateThemes()) }) - it('uses the default dark syntax theme', function () { + it('uses the default one-dark syntax theme', function () { const activeThemeNames = atom.themes.getActiveThemeNames() expect(activeThemeNames.length).toBe(2) expect(activeThemeNames).toContain('atom-light-ui') - expect(activeThemeNames).toContain('atom-dark-syntax') + expect(activeThemeNames).toContain('one-dark-syntax') }) }) }) From 264de98d927aba90bae0323b65e0631ea8da1d6d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 7 Dec 2017 11:54:03 -0800 Subject: [PATCH 057/176] :arrow_up: tree-sitter --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5fbf1dbf7..339c5313d 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "sinon": "1.17.4", "temp": "^0.8.3", "text-buffer": "13.9.2", - "tree-sitter": "0.7.4", + "tree-sitter": "0.7.5", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From d1caf26ab5094bf6ae33a43d95eb1a2495d19595 Mon Sep 17 00:00:00 2001 From: ungb Date: Thu, 7 Dec 2017 15:25:26 -0800 Subject: [PATCH 058/176] Add test for storing window dimension on close. --- spec/window-event-handler-spec.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/window-event-handler-spec.js b/spec/window-event-handler-spec.js index a03e168fa..074fb1db3 100644 --- a/spec/window-event-handler-spec.js +++ b/spec/window-event-handler-spec.js @@ -51,8 +51,16 @@ describe('WindowEventHandler', () => { window.dispatchEvent(new CustomEvent('window:close')) expect(atom.close).toHaveBeenCalled() }) + + it ('saves the window state', () => { + spyOn(atom, 'storeWindowDimensions') + window.dispatchEvent(new CustomEvent('window:close')) + expect(atom.storeWindowDimensions).toHaveBeenCalled() + }) ) + + describe('when a link is clicked', () => it('opens the http/https links in an external application', () => { const {shell} = require('electron') From 77f021a24fe13d101257c69746f952586f39ae8b Mon Sep 17 00:00:00 2001 From: Bryant Ung Date: Thu, 7 Dec 2017 15:26:15 -0800 Subject: [PATCH 059/176] Remove unneeded newline --- spec/window-event-handler-spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/window-event-handler-spec.js b/spec/window-event-handler-spec.js index 074fb1db3..cdb9c2015 100644 --- a/spec/window-event-handler-spec.js +++ b/spec/window-event-handler-spec.js @@ -59,8 +59,6 @@ describe('WindowEventHandler', () => { }) ) - - describe('when a link is clicked', () => it('opens the http/https links in an external application', () => { const {shell} = require('electron') From 136dc86584a04f42b69b01685af8ec8c8403ca88 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 7 Dec 2017 15:29:11 -0800 Subject: [PATCH 060/176] Leave muli-character fold end tokens on their own line Signed-off-by: Nathan Sobo --- spec/tree-sitter-language-mode-spec.js | 33 ++++++++++++++++++++++---- src/tree-sitter-language-mode.js | 16 +++++++++---- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js index 426291e5f..0eeeb8b93 100644 --- a/spec/tree-sitter-language-mode-spec.js +++ b/spec/tree-sitter-language-mode-spec.js @@ -186,7 +186,8 @@ describe('TreeSitterLanguageMode', () => { expect(getDisplayText(editor)).toBe(dedent ` const element1 = - const element2 = + const element2 = … + `) }) @@ -239,10 +240,15 @@ describe('TreeSitterLanguageMode', () => { folds: [ // If the #ifdef has an `#else` clause, then end the fold there. { - type: 'preproc_ifdef', + type: ['preproc_ifdef', 'preproc_elif'], start: {index: 1}, end: {type: 'preproc_else'} }, + { + type: ['preproc_ifdef', 'preproc_elif'], + start: {index: 1}, + end: {type: 'preproc_elif'} + }, // Otherwise, end the fold at the last child - the `#endif`. { @@ -270,6 +276,11 @@ describe('TreeSitterLanguageMode', () => { #include const char *path_separator = "\\"; + #elif defined MACOS + + #include + const char *path_separator = "/"; + #else #include @@ -287,7 +298,13 @@ describe('TreeSitterLanguageMode', () => { #ifndef FOO_H_ #define FOO_H_ - #ifdef _WIN32…#else + #ifdef _WIN32… + #elif defined MACOS + + #include + const char *path_separator = "/"; + + #else #include const char *path_separator = "/"; @@ -302,7 +319,12 @@ describe('TreeSitterLanguageMode', () => { #ifndef FOO_H_ #define FOO_H_ - #ifdef _WIN32…#else… + #ifdef _WIN32… + #elif defined MACOS… + #else + + #include + const char *path_separator = "/"; #endif @@ -311,7 +333,8 @@ describe('TreeSitterLanguageMode', () => { editor.foldBufferRow(0) expect(getDisplayText(editor)).toBe(dedent ` - #ifndef FOO_H_…#endif + #ifndef FOO_H_… + #endif `) }) diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index f47d89db7..a76043638 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -221,16 +221,22 @@ class TreeSitterLanguageMode { let foldEnd const endEntry = foldEntry.end if (endEntry) { + let foldEndNode if (endEntry.index != null) { const index = endEntry.index < 0 ? childCount + endEntry.index : endEntry.index - const child = children[index] - if (!child || (endEntry.type && endEntry.type !== child.type)) continue - foldEnd = child.startPosition + foldEndNode = children[index] + if (!foldEndNode || (endEntry.type && endEntry.type !== foldEndNode.type)) continue } else { - if (!childTypes) childTypes = children.map(child => child.type) + if (!childTypes) childTypes = children.map(foldEndNode => foldEndNode.type) const index = childTypes.lastIndexOf(endEntry.type) if (index === -1) continue - foldEnd = children[index].startPosition + foldEndNode = children[index] + } + + if (foldEndNode.endIndex - foldEndNode.startIndex > 1 && foldEndNode.startPosition.row > foldStart.row) { + foldEnd = new Point(foldEndNode.startPosition.row - 1, Infinity) + } else { + foldEnd = foldEndNode.startPosition } } From f712de65d0c2ea2a6c8cc2fefd2efdde8d5910a3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 7 Dec 2017 15:30:48 -0800 Subject: [PATCH 061/176] Fix nesting level calculation for children of partially-folded nodes Signed-off-by: Nathan Sobo --- spec/tree-sitter-language-mode-spec.js | 14 ++++++++++++++ src/text-editor.js | 2 +- src/tree-sitter-language-mode.js | 22 ++++++++++++---------- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js index 0eeeb8b93..fe9ec239b 100644 --- a/spec/tree-sitter-language-mode-spec.js +++ b/spec/tree-sitter-language-mode-spec.js @@ -336,6 +336,20 @@ describe('TreeSitterLanguageMode', () => { #ifndef FOO_H_… #endif `) + + editor.foldAllAtIndentLevel(1) + expect(getDisplayText(editor)).toBe(dedent ` + #ifndef FOO_H_ + #define FOO_H_ + + #ifdef _WIN32… + #elif defined MACOS… + #else… + + #endif + + #endif + `) }) describe('when folding a node that ends with a line break', () => { diff --git a/src/text-editor.js b/src/text-editor.js index e24476b2d..08d07aa71 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -3891,7 +3891,7 @@ class TextEditor { // Extended: Fold all foldable lines at the given indent level. // - // * `level` A {Number}. + // * `level` A {Number} starting at 0. foldAllAtIndentLevel (level) { const languageMode = this.buffer.getLanguageMode() const foldableRanges = ( diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index a76043638..6eec047c3 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -138,10 +138,7 @@ class TreeSitterLanguageMode { let stack = [{node: this.document.rootNode, level: 0}] while (stack.length > 0) { const {node, level} = stack.pop() - const startRow = node.startPosition.row - const endRow = node.endPosition.row - let childLevel = level const range = this.getFoldableRangeForNode(node) if (range) { if (goalLevel == null || level === goalLevel) { @@ -155,18 +152,23 @@ class TreeSitterLanguageMode { } if (!updatedExistingRange) result.push(range) } - childLevel++ } + const parentStartRow = node.startPosition.row + const parentEndRow = node.endPosition.row for (let children = node.namedChildren, i = 0, {length} = children; i < length; i++) { const child = children[i] - const childStartRow = child.startPosition.row - const childEndRow = child.endPosition.row - if (childEndRow > childStartRow) { - if (childStartRow === startRow && childEndRow === endRow) { + const {startPosition: childStart, endPosition: childEnd} = child + if (childEnd.row > childStart.row) { + if (childStart.row === parentStartRow && childEnd.row === parentEndRow) { stack.push({node: child, level: level}) - } else if (childLevel <= goalLevel || goalLevel == null) { - stack.push({node: child, level: childLevel}) + } else { + const childLevel = range.containsPoint(childStart) && range.containsPoint(childEnd) + ? level + 1 + : level + if (childLevel <= goalLevel || goalLevel == null) { + stack.push({node: child, level: childLevel}) + } } } } From a7a53f4158cbd302210a10b57617e7e084539776 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 7 Dec 2017 17:08:47 -0800 Subject: [PATCH 062/176] Allow multiple child types to be specified as fold start or end --- spec/tree-sitter-language-mode-spec.js | 7 +------ src/tree-sitter-language-mode.js | 11 ++++++++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js index fe9ec239b..5ecc73308 100644 --- a/spec/tree-sitter-language-mode-spec.js +++ b/spec/tree-sitter-language-mode-spec.js @@ -242,12 +242,7 @@ describe('TreeSitterLanguageMode', () => { { type: ['preproc_ifdef', 'preproc_elif'], start: {index: 1}, - end: {type: 'preproc_else'} - }, - { - type: ['preproc_ifdef', 'preproc_elif'], - start: {index: 1}, - end: {type: 'preproc_elif'} + end: {type: ['preproc_else', 'preproc_elif']} }, // Otherwise, end the fold at the last child - the `#endif`. diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 6eec047c3..9f88a71ec 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -148,6 +148,7 @@ class TreeSitterLanguageMode { result[i].end.row === range.end.row) { result[i] = range updatedExistingRange = true + break } } if (!updatedExistingRange) result.push(range) @@ -163,7 +164,7 @@ class TreeSitterLanguageMode { if (childStart.row === parentStartRow && childEnd.row === parentEndRow) { stack.push({node: child, level: level}) } else { - const childLevel = range.containsPoint(childStart) && range.containsPoint(childEnd) + const childLevel = range && range.containsPoint(childStart) && range.containsPoint(childEnd) ? level + 1 : level if (childLevel <= goalLevel || goalLevel == null) { @@ -214,7 +215,9 @@ class TreeSitterLanguageMode { foldStart = child.endPosition } else { if (!childTypes) childTypes = children.map(child => child.type) - const index = childTypes.indexOf(startEntry.type) + const index = typeof startEntry.type === 'string' + ? childTypes.indexOf(startEntry.type) + : childTypes.findIndex(type => startEntry.type.includes(type)) if (index === -1) continue foldStart = children[index].endPosition } @@ -230,7 +233,9 @@ class TreeSitterLanguageMode { if (!foldEndNode || (endEntry.type && endEntry.type !== foldEndNode.type)) continue } else { if (!childTypes) childTypes = children.map(foldEndNode => foldEndNode.type) - const index = childTypes.lastIndexOf(endEntry.type) + const index = typeof endEntry.type === 'string' + ? childTypes.indexOf(endEntry.type) + : childTypes.findIndex(type => endEntry.type.includes(type)) if (index === -1) continue foldEndNode = children[index] } From 3d11c1726428766bf9e5b5ec292125950d123f72 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 7 Dec 2017 17:42:52 -0800 Subject: [PATCH 063/176] Fix exception in getFoldableRangeForNode --- src/tree-sitter-language-mode.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 9f88a71ec..33656cf35 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -221,6 +221,8 @@ class TreeSitterLanguageMode { if (index === -1) continue foldStart = children[index].endPosition } + } else { + foldStart = new Point(node.startPosition.row, Infinity) } let foldEnd @@ -245,15 +247,7 @@ class TreeSitterLanguageMode { } else { foldEnd = foldEndNode.startPosition } - } - - if (existenceOnly) return true - - if (!foldStart) { - foldStart = new Point(node.startPosition.row, Infinity) - } - - if (!foldEnd) { + } else { const {endPosition} = node if (endPosition.column === 0) { foldEnd = Point(endPosition.row - 1, Infinity) @@ -264,7 +258,7 @@ class TreeSitterLanguageMode { } } - return new Range(foldStart, foldEnd) + return existenceOnly ? true : new Range(foldStart, foldEnd) } } From 5128538b484aee8c3b6a6e0790710c09b5d7a164 Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 8 Dec 2017 15:00:27 +0900 Subject: [PATCH 064/176] :arrow_up: bracket-matcher@v0.88.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 803944c87..334d04b6b 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "autosave": "0.24.6", "background-tips": "0.27.1", "bookmarks": "0.45.0", - "bracket-matcher": "0.88.1", + "bracket-matcher": "0.88.2", "command-palette": "0.43.0", "dalek": "0.2.1", "deprecation-cop": "0.56.9", From 649acd08d56c87a9593c2df239a29e680149b358 Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 8 Dec 2017 15:55:00 +0900 Subject: [PATCH 065/176] :arrow_up: One and Solarized syntax themes Adds stronger gutter selection --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 334d04b6b..4ce86ac35 100644 --- a/package.json +++ b/package.json @@ -85,10 +85,10 @@ "base16-tomorrow-light-theme": "1.5.0", "one-dark-ui": "1.10.9", "one-light-ui": "1.10.9", - "one-dark-syntax": "1.8.1", - "one-light-syntax": "1.8.1", - "solarized-dark-syntax": "1.1.3", - "solarized-light-syntax": "1.1.3", + "one-dark-syntax": "1.8.2", + "one-light-syntax": "1.8.2", + "solarized-dark-syntax": "1.1.4", + "solarized-light-syntax": "1.1.4", "about": "1.7.8", "archive-view": "0.64.1", "autocomplete-atom-api": "0.10.6", From e2f01ef7c621050682781c946ad5a32e07184ec0 Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 8 Dec 2017 16:22:38 +0900 Subject: [PATCH 066/176] :arrow_up: settings-view@v0.253.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4ce86ac35..e7cf99195 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "notifications": "0.70.2", "open-on-github": "1.3.1", "package-generator": "1.3.0", - "settings-view": "0.253.0", + "settings-view": "0.253.1", "snippets": "1.1.9", "spell-check": "0.72.3", "status-bar": "1.8.15", From d28166b1e4dbb3875fb8b340c2d494285d88faac Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Fri, 8 Dec 2017 16:15:51 -0600 Subject: [PATCH 067/176] code cleanup --- spec/notification-manager-spec.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/notification-manager-spec.js b/spec/notification-manager-spec.js index b62569ee6..a3ba63905 100644 --- a/spec/notification-manager-spec.js +++ b/spec/notification-manager-spec.js @@ -68,7 +68,7 @@ describe('NotificationManager', () => { }) describe('clearing notifications', function () { - it('clears the notifications when ::clear has been called', function(){ + it('clears the notifications when ::clear has been called', () => { manager.addSuccess('success') expect(manager.getNotifications().length).toBe(1) manager.clear() @@ -76,16 +76,16 @@ describe('NotificationManager', () => { }) describe('adding events', () => { - let addSpy + let clearSpy beforeEach(() => { - addSpy = jasmine.createSpy() - manager.onDidClearNotifications(addSpy) + clearSpy = jasmine.createSpy() + manager.onDidClearNotifications(clearSpy) }) it('emits an event when the notifications have been cleared', () => { manager.clear() - expect(addSpy).toHaveBeenCalled() + expect(clearSpy).toHaveBeenCalled() }) }) }) From b89bfa26c33465d386fa819bd31b3b6cfa1952f6 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Fri, 8 Dec 2017 16:18:06 -0600 Subject: [PATCH 068/176] more cleanup --- spec/notification-manager-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/notification-manager-spec.js b/spec/notification-manager-spec.js index a3ba63905..3a8544d4e 100644 --- a/spec/notification-manager-spec.js +++ b/spec/notification-manager-spec.js @@ -67,7 +67,7 @@ describe('NotificationManager', () => { }) }) - describe('clearing notifications', function () { + describe('clearing notifications', () => { it('clears the notifications when ::clear has been called', () => { manager.addSuccess('success') expect(manager.getNotifications().length).toBe(1) From 06207e0d0e6bc4a0fd49cf9732eda66c6ea1e023 Mon Sep 17 00:00:00 2001 From: Bryant Ung Date: Fri, 8 Dec 2017 14:47:07 -0800 Subject: [PATCH 069/176] remove test to see if build passes --- spec/window-event-handler-spec.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/spec/window-event-handler-spec.js b/spec/window-event-handler-spec.js index cdb9c2015..09f3e1fc3 100644 --- a/spec/window-event-handler-spec.js +++ b/spec/window-event-handler-spec.js @@ -51,12 +51,13 @@ describe('WindowEventHandler', () => { window.dispatchEvent(new CustomEvent('window:close')) expect(atom.close).toHaveBeenCalled() }) - - it ('saves the window state', () => { - spyOn(atom, 'storeWindowDimensions') - window.dispatchEvent(new CustomEvent('window:close')) - expect(atom.storeWindowDimensions).toHaveBeenCalled() - }) + +// TODO: add this back, commenting out to see if build passes. +// it ('saves the window state', () => { +// spyOn(atom, 'storeWindowDimensions') +// window.dispatchEvent(new CustomEvent('window:close')) +// expect(atom.storeWindowDimensions).toHaveBeenCalled() +// }) ) describe('when a link is clicked', () => From 2349d28e5e243e43e81dabee68b7c773697085a5 Mon Sep 17 00:00:00 2001 From: Bryant Ung Date: Fri, 8 Dec 2017 14:57:27 -0800 Subject: [PATCH 070/176] update spec for windiws:close event --- spec/window-event-handler-spec.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/spec/window-event-handler-spec.js b/spec/window-event-handler-spec.js index 09f3e1fc3..a7513221c 100644 --- a/spec/window-event-handler-spec.js +++ b/spec/window-event-handler-spec.js @@ -48,16 +48,12 @@ describe('WindowEventHandler', () => { describe('window:close event', () => it('closes the window', () => { spyOn(atom, 'close') + spyOn(atom, 'storeWindowDimensions') window.dispatchEvent(new CustomEvent('window:close')) expect(atom.close).toHaveBeenCalled() + expect(atom.storeWindowDimensions).toHaveBeenCalled() }) - -// TODO: add this back, commenting out to see if build passes. -// it ('saves the window state', () => { -// spyOn(atom, 'storeWindowDimensions') -// window.dispatchEvent(new CustomEvent('window:close')) -// expect(atom.storeWindowDimensions).toHaveBeenCalled() -// }) + ) describe('when a link is clicked', () => From be3551cd18cd62eda0537bded282c2e23397ca23 Mon Sep 17 00:00:00 2001 From: Bryant Ung Date: Fri, 8 Dec 2017 16:50:40 -0800 Subject: [PATCH 071/176] Remove failing test --- spec/window-event-handler-spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/window-event-handler-spec.js b/spec/window-event-handler-spec.js index a7513221c..71c50d2c7 100644 --- a/spec/window-event-handler-spec.js +++ b/spec/window-event-handler-spec.js @@ -48,10 +48,8 @@ describe('WindowEventHandler', () => { describe('window:close event', () => it('closes the window', () => { spyOn(atom, 'close') - spyOn(atom, 'storeWindowDimensions') window.dispatchEvent(new CustomEvent('window:close')) expect(atom.close).toHaveBeenCalled() - expect(atom.storeWindowDimensions).toHaveBeenCalled() }) ) From d96193d61a43f789713a3b87fe986bad396d6b17 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 8 Dec 2017 17:37:52 -0800 Subject: [PATCH 072/176] :arrow_up: github@0.9.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e7cf99195..c9fa72ab0 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "exception-reporting": "0.42.0", "find-and-replace": "0.215.0", "fuzzy-finder": "1.7.3", - "github": "0.8.3", + "github": "0.9.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.8", From 9c7e06cef75ab99b9ecdd64e2f8a31f77beca083 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Fri, 8 Dec 2017 20:22:48 -0700 Subject: [PATCH 073/176] :arrow_up: git-utils@5.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c9fa72ab0..d906bff2f 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "fs-plus": "^3.0.1", "fstream": "0.1.24", "fuzzaldrin": "^2.1", - "git-utils": "5.1.0", + "git-utils": "5.2.0", "glob": "^7.1.1", "grim": "1.5.0", "jasmine-json": "~0.0", From 0fcef713f0b117b66837bf5763451d4488d1d217 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Fri, 8 Dec 2017 20:24:10 -0700 Subject: [PATCH 074/176] :arrow_up: autocomplete-plus@2.39.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c9fa72ab0..7474c3752 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.6", "autocomplete-css": "0.17.5", "autocomplete-html": "0.8.4", - "autocomplete-plus": "2.39.0", + "autocomplete-plus": "2.39.1", "autocomplete-snippets": "1.11.2", "autoflow": "0.29.0", "autosave": "0.24.6", From 9a8e01449d83b0a38e15e6a8d015e88f84d1c192 Mon Sep 17 00:00:00 2001 From: simurai Date: Sat, 9 Dec 2017 17:22:18 +0900 Subject: [PATCH 075/176] :arrow_up: one-dark/light-ui@v1.10.10 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 25175d898..5371e3744 100644 --- a/package.json +++ b/package.json @@ -83,8 +83,8 @@ "atom-light-ui": "0.46.1", "base16-tomorrow-dark-theme": "1.5.0", "base16-tomorrow-light-theme": "1.5.0", - "one-dark-ui": "1.10.9", - "one-light-ui": "1.10.9", + "one-dark-ui": "1.10.10", + "one-light-ui": "1.10.10", "one-dark-syntax": "1.8.2", "one-light-syntax": "1.8.2", "solarized-dark-syntax": "1.1.4", From 3039ac1f43e080b0fadc39d2829ddf1c251c661e Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 9 Dec 2017 16:46:52 +0100 Subject: [PATCH 076/176] :arrow_up: archive-view@0.64.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5371e3744..a8130b379 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "solarized-dark-syntax": "1.1.4", "solarized-light-syntax": "1.1.4", "about": "1.7.8", - "archive-view": "0.64.1", + "archive-view": "0.64.2", "autocomplete-atom-api": "0.10.6", "autocomplete-css": "0.17.5", "autocomplete-html": "0.8.4", From efade9be094c9f816a14d263ae2953ba50a14b04 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sun, 10 Dec 2017 17:23:26 +0100 Subject: [PATCH 077/176] :arrow_up: atom-select-list@0.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a8130b379..4912ef84a 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@atom/source-map-support": "^0.3.4", "async": "0.2.6", "atom-keymap": "8.2.8", - "atom-select-list": "^0.1.0", + "atom-select-list": "^0.7.0", "atom-ui": "0.4.1", "babel-core": "5.8.38", "cached-run-in-this-context": "0.4.1", From 97bbf1be59ef56ce5ec6d3c35a592af54b79347e Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sun, 10 Dec 2017 17:29:08 +0100 Subject: [PATCH 078/176] :arrow_up: update-package-dependencies@0.13.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a8130b379..af49943a9 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "tabs": "0.109.1", "timecop": "0.36.2", "tree-view": "0.221.3", - "update-package-dependencies": "0.13.0", + "update-package-dependencies": "0.13.1", "welcome": "0.36.6", "whitespace": "0.37.5", "wrap-guide": "0.40.3", From 59da908fc213c8fb34c24248066523e3c42df752 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 11 Dec 2017 00:38:49 +0100 Subject: [PATCH 079/176] :arrow_up: symbols-view@0.118.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4912ef84a..328ef1fa1 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "spell-check": "0.72.3", "status-bar": "1.8.15", "styleguide": "0.49.9", - "symbols-view": "0.118.1", + "symbols-view": "0.118.2", "tabs": "0.109.1", "timecop": "0.36.2", "tree-view": "0.221.3", From 4debb280c833793811f6b02af34355cbdafeaa4c Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 11 Dec 2017 00:39:42 +0100 Subject: [PATCH 080/176] :arrow_up: bookmarks@0.45.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 328ef1fa1..08b62626c 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "autoflow": "0.29.0", "autosave": "0.24.6", "background-tips": "0.27.1", - "bookmarks": "0.45.0", + "bookmarks": "0.45.1", "bracket-matcher": "0.88.2", "command-palette": "0.43.0", "dalek": "0.2.1", From 689b05a81c81b8507191e0bd6020d0507562752d Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 11 Dec 2017 00:40:28 +0100 Subject: [PATCH 081/176] :arrow_up: snippets@1.1.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 08b62626c..7c5084bdb 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "open-on-github": "1.3.1", "package-generator": "1.3.0", "settings-view": "0.253.1", - "snippets": "1.1.9", + "snippets": "1.1.10", "spell-check": "0.72.3", "status-bar": "1.8.15", "styleguide": "0.49.9", From 2d0ad55d659e156774acb41b1a7c662c946f91cb Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 11 Dec 2017 00:41:10 +0100 Subject: [PATCH 082/176] :arrow_up: styleguide@0.49.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7c5084bdb..6e568ed2d 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "snippets": "1.1.10", "spell-check": "0.72.3", "status-bar": "1.8.15", - "styleguide": "0.49.9", + "styleguide": "0.49.10", "symbols-view": "0.118.2", "tabs": "0.109.1", "timecop": "0.36.2", From 0ef2ffa82a2ef142891903bae8ce6bf67ba6b52e Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 11 Dec 2017 00:41:49 +0100 Subject: [PATCH 083/176] :arrow_up: encoding-selector@0.23.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6e568ed2d..79dd05e3f 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "dalek": "0.2.1", "deprecation-cop": "0.56.9", "dev-live-reload": "0.48.1", - "encoding-selector": "0.23.7", + "encoding-selector": "0.23.8", "exception-reporting": "0.42.0", "find-and-replace": "0.215.0", "fuzzy-finder": "1.7.3", From 74743fa2fcb2c7685ee25cca197610deb4b9dd68 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 11 Dec 2017 00:42:27 +0100 Subject: [PATCH 084/176] :arrow_up: git-diff@1.3.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 79dd05e3f..f79feb51d 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "find-and-replace": "0.215.0", "fuzzy-finder": "1.7.3", "github": "0.9.0", - "git-diff": "1.3.6", + "git-diff": "1.3.7", "go-to-line": "0.32.1", "grammar-selector": "0.49.8", "image-view": "0.62.4", From c5314b68a314fd4ff4c56c128cf020c40ef7e61d Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 11 Dec 2017 00:43:07 +0100 Subject: [PATCH 085/176] :arrow_up: grammar-selector@0.49.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f79feb51d..bbb4e46b2 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "github": "0.9.0", "git-diff": "1.3.7", "go-to-line": "0.32.1", - "grammar-selector": "0.49.8", + "grammar-selector": "0.49.9", "image-view": "0.62.4", "incompatible-packages": "0.27.3", "keybinding-resolver": "0.38.1", From 61ee571845d8a8b002e7e804edd5b41cec2758c0 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 11 Dec 2017 00:43:46 +0100 Subject: [PATCH 086/176] :arrow_up: line-ending-selector@0.7.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bbb4e46b2..e07549b01 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "image-view": "0.62.4", "incompatible-packages": "0.27.3", "keybinding-resolver": "0.38.1", - "line-ending-selector": "0.7.4", + "line-ending-selector": "0.7.5", "link": "0.31.4", "markdown-preview": "0.159.18", "metrics": "1.2.6", From 863a20d1745fd4bd11cf9c73959d9aa6bdc52c55 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 11 Dec 2017 00:44:31 +0100 Subject: [PATCH 087/176] :arrow_up: fuzzy-finder@1.7.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e07549b01..a44292ce6 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "encoding-selector": "0.23.8", "exception-reporting": "0.42.0", "find-and-replace": "0.215.0", - "fuzzy-finder": "1.7.3", + "fuzzy-finder": "1.7.4", "github": "0.9.0", "git-diff": "1.3.7", "go-to-line": "0.32.1", From 91aeb785d0aab2d9749b5020a9712a3db14c0195 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 11 Dec 2017 00:45:11 +0100 Subject: [PATCH 088/176] :arrow_up: spell-check@0.72.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a44292ce6..61962ca65 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "package-generator": "1.3.0", "settings-view": "0.253.1", "snippets": "1.1.10", - "spell-check": "0.72.3", + "spell-check": "0.72.4", "status-bar": "1.8.15", "styleguide": "0.49.10", "symbols-view": "0.118.2", From a10863110e7c66ace332c83d0501d3061319e7db Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 11 Dec 2017 22:43:45 +0100 Subject: [PATCH 089/176] :arrow_up: snippets@1.1.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0f4b1e1aa..3cafa0d59 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "open-on-github": "1.3.1", "package-generator": "1.3.0", "settings-view": "0.253.1", - "snippets": "1.1.10", + "snippets": "1.1.11", "spell-check": "0.72.4", "status-bar": "1.8.15", "styleguide": "0.49.10", From fb73df67b943710f1c499d800920035f7fec5321 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 11 Dec 2017 23:05:22 +0100 Subject: [PATCH 090/176] :arrow_up: settings-view@0.253.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3cafa0d59..2708a38f1 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "notifications": "0.70.2", "open-on-github": "1.3.1", "package-generator": "1.3.0", - "settings-view": "0.253.1", + "settings-view": "0.253.2", "snippets": "1.1.11", "spell-check": "0.72.4", "status-bar": "1.8.15", From ca53cf9ec1aed43edc05681029a23d379791586c Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 13 Dec 2017 09:17:30 -0800 Subject: [PATCH 091/176] Remove the texteditor isModified/isReadOnly handling intended for unsavable edit buffers as causes issues with vim-mode-plus --- spec/text-editor-spec.js | 17 ----------------- src/text-editor.js | 8 +------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index 41a14f76a..89af72137 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -86,23 +86,6 @@ describe('TextEditor', () => { }) }) - describe('when the editor is readonly', () => { - it('overrides TextBuffer.isModified to return false', async () => { - const editor = await atom.workspace.open(null, {readOnly: true}) - editor.setText('I am altering the buffer, pray I do not alter it any further') - expect(editor.isModified()).toBe(false) - editor.setReadOnly(false) - expect(editor.isModified()).toBe(true) - }) - it('clears the readonly status when saved', async () => { - const editor = await atom.workspace.open(null, {readOnly: true}) - editor.setText('I am altering the buffer, pray I do not alter it any further') - expect(editor.isReadOnly()).toBe(true) - await editor.saveAs(temp.openSync('was-readonly').path) - expect(editor.isReadOnly()).toBe(false) - }) - }) - describe('.copy()', () => { it('returns a different editor with the same initial state', () => { expect(editor.getAutoHeight()).toBeFalsy() diff --git a/src/text-editor.js b/src/text-editor.js index c214ec0f6..4daca5d49 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -407,7 +407,6 @@ class TextEditor { if (this.component != null) { this.component.scheduleUpdate() } - this.buffer.emitModifiedStatusChanged(this.isModified()) } break @@ -568,11 +567,6 @@ class TextEditor { this.disposables.add(this.buffer.onDidChangeModified(() => { if (!this.hasTerminatedPendingState && this.buffer.isModified()) this.terminatePendingState() })) - this.disposables.add(this.buffer.onDidSave(() => { - if (this.isReadOnly()) { - this.setReadOnly(false) - } - })) } terminatePendingState () { @@ -1129,7 +1123,7 @@ class TextEditor { setEncoding (encoding) { this.buffer.setEncoding(encoding) } // Essential: Returns {Boolean} `true` if this editor has been modified. - isModified () { return this.isReadOnly() ? false : this.buffer.isModified() } + isModified () { return this.buffer.isModified() } // Essential: Returns {Boolean} `true` if this editor has no content. isEmpty () { return this.buffer.isEmpty() } From 6c89853cfd6259414f2b08827850dd99f3ef202f Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 13 Dec 2017 11:30:39 -0800 Subject: [PATCH 092/176] Try adding python 2.x back - newer Travis images dropped it --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 62040612a..7a5b3bd0b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -57,6 +57,7 @@ addons: - fakeroot - git - libsecret-1-dev + - python - rpm - libx11-dev - libxkbfile-dev From d25760586a923adabcb155d27c504b51dde8c88d Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 13 Dec 2017 12:52:32 -0800 Subject: [PATCH 093/176] Try alternate way of getting right python --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7a5b3bd0b..e127aa499 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,8 @@ +language: python + +python: + - "2.7.13" + git: depth: 10 @@ -57,7 +62,6 @@ addons: - fakeroot - git - libsecret-1-dev - - python - rpm - libx11-dev - libxkbfile-dev From 2d6750cae323efd5284eeee5199a53075c577445 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 14 Dec 2017 11:56:23 +0100 Subject: [PATCH 094/176] Remove input enabled check for 'is-focused' class vim-mode-plus relies on this behavior --- 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 08af5ada1..263556ee0 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -823,7 +823,7 @@ class TextEditorComponent { const oldClassList = this.classList const newClassList = ['editor'] - if (this.focused && this.isInputEnabled()) newClassList.push('is-focused') + if (this.focused) newClassList.push('is-focused') if (model.isMini()) newClassList.push('mini') for (var i = 0; i < model.selections.length; i++) { if (!model.selections[i].isEmpty()) { From c6818c94d5de9fe4e6fd0638d77a2810a782f838 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 14 Dec 2017 11:57:01 +0100 Subject: [PATCH 095/176] :arrow_up: github@0.9.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2708a38f1..7a229d4c5 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "exception-reporting": "0.42.0", "find-and-replace": "0.215.0", "fuzzy-finder": "1.7.4", - "github": "0.9.0", + "github": "0.9.1", "git-diff": "1.3.7", "go-to-line": "0.32.1", "grammar-selector": "0.49.9", From bba2c474c47c17ebf90c2f0feac674a1b562a3d0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 14 Dec 2017 15:31:27 -0800 Subject: [PATCH 096/176] :arrow_up: autoflow --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7a229d4c5..2a9374db3 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "autocomplete-html": "0.8.4", "autocomplete-plus": "2.39.1", "autocomplete-snippets": "1.11.2", - "autoflow": "0.29.0", + "autoflow": "0.29.1", "autosave": "0.24.6", "background-tips": "0.27.1", "bookmarks": "0.45.1", From e09ee1c1fa8ac10dbec10a0fc47253bb51c7bbd7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 15 Dec 2017 09:44:45 -0800 Subject: [PATCH 097/176] Fix error in TreeSitterHighlightIterator.seek --- spec/tree-sitter-language-mode-spec.js | 63 ++++++++++++++++++++------ src/tree-sitter-language-mode.js | 2 +- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js index 5ecc73308..91070710f 100644 --- a/spec/tree-sitter-language-mode-spec.js +++ b/spec/tree-sitter-language-mode-spec.js @@ -32,7 +32,7 @@ describe('TreeSitterLanguageMode', () => { buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar})) buffer.setText('aa.bbb = cc(d.eee());') - expectTokensToEqual(editor, [ + expectTokensToEqual(editor, [[ {text: 'aa.', scopes: ['source']}, {text: 'bbb', scopes: ['source', 'property']}, {text: ' = ', scopes: ['source']}, @@ -40,7 +40,7 @@ describe('TreeSitterLanguageMode', () => { {text: '(d.', scopes: ['source']}, {text: 'eee', scopes: ['source', 'method']}, {text: '());', scopes: ['source']} - ]) + ]]) }) it('can start or end multiple scopes at the same position', () => { @@ -58,7 +58,7 @@ describe('TreeSitterLanguageMode', () => { buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar})) buffer.setText('a = bb.ccc();') - expectTokensToEqual(editor, [ + expectTokensToEqual(editor, [[ {text: 'a', scopes: ['source', 'variable']}, {text: ' = ', scopes: ['source']}, {text: 'bb', scopes: ['source', 'call', 'member', 'variable']}, @@ -66,6 +66,31 @@ describe('TreeSitterLanguageMode', () => { {text: '(', scopes: ['source', 'call', 'open-paren']}, {text: ')', scopes: ['source', 'call', 'close-paren']}, {text: ';', scopes: ['source']} + ]]) + }) + + it('can resume highlighting on a line that starts with whitespace', () => { + const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { + parser: 'tree-sitter-javascript', + scopes: { + 'call_expression > member_expression > property_identifier': 'function', + 'property_identifier': 'member', + 'identifier': 'variable' + } + }) + + buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar})) + buffer.setText('a\n .b();') + expectTokensToEqual(editor, [ + [ + {text: 'a', scopes: ['variable']}, + ], + [ + {text: ' ', scopes: ['whitespace']}, + {text: '.', scopes: []}, + {text: 'b', scopes: ['function']}, + {text: '();', scopes: []} + ] ]) }) }) @@ -432,22 +457,34 @@ function getDisplayText (editor) { return editor.displayLayer.getText() } -function expectTokensToEqual (editor, expectedTokens) { - const tokens = [] - for (let row = 0, lastRow = editor.getLastScreenRow(); row <= lastRow; row++) { - tokens.push( - ...editor.tokensForScreenRow(row).map(({text, scopes}) => ({ +function expectTokensToEqual (editor, expectedTokenLines) { + const lastRow = editor.getLastScreenRow() + + // Assert that the correct tokens are returned regardless of which row + // the highlighting iterator starts on. + for (let startRow = 0; startRow <= lastRow; startRow++) { + editor.displayLayer.clearSpatialIndex() + editor.displayLayer.getScreenLines(startRow, Infinity) + + const tokenLines = [] + for (let row = startRow; row <= lastRow; row++) { + tokenLines[row] = editor.tokensForScreenRow(row).map(({text, scopes}) => ({ text, scopes: scopes.map(scope => scope .split(' ') .map(className => className.slice('syntax--'.length)) .join(' ')) })) - ) - } + } - expect(tokens.length).toEqual(expectedTokens.length) - for (let i = 0; i < tokens.length; i++) { - expect(tokens[i]).toEqual(expectedTokens[i], `Token ${i}`) + for (let row = startRow; row <= lastRow; row++) { + const tokenLine = tokenLines[row] + const expectedTokenLine = expectedTokenLines[row] + + expect(tokenLine.length).toEqual(expectedTokenLine.length) + for (let i = 0; i < tokenLine.length; i++) { + expect(tokenLine[i]).toEqual(expectedTokenLine[i], `Token ${i}, startRow: ${startRow}`) + } + } } } diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 33656cf35..5cd725108 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -349,9 +349,9 @@ class TreeSitterHighlightIterator { do { this.currentNode = node this.currentChildIndex = childIndex + if (!nodeContainsTarget) break this.containingNodeTypes.push(node.type) this.containingNodeChildIndices.push(childIndex) - if (!nodeContainsTarget) break const scopeName = this.currentScopeName() if (scopeName) { From 8efccf822103f6f420d1325fae4fd3e8f24e968d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 15 Dec 2017 16:55:18 -0800 Subject: [PATCH 098/176] :arrow_up: language packages --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a041e606f..bec57b304 100644 --- a/package.json +++ b/package.json @@ -137,18 +137,18 @@ "welcome": "0.36.6", "whitespace": "0.37.5", "wrap-guide": "0.40.3", - "language-c": "0.59.0-1", + "language-c": "0.59.0-2", "language-clojure": "0.22.5", "language-coffee-script": "0.49.3", "language-csharp": "0.14.3", "language-css": "0.42.8", "language-gfm": "0.90.3", "language-git": "0.19.1", - "language-go": "0.45.0-2", + "language-go": "0.45.0-3", "language-html": "0.48.4", "language-hyperlink": "0.16.3", "language-java": "0.27.6", - "language-javascript": "0.128.0-2", + "language-javascript": "0.128.0-3", "language-json": "0.19.1", "language-less": "0.34.1", "language-make": "0.22.3", @@ -157,17 +157,17 @@ "language-perl": "0.38.1", "language-php": "0.43.0", "language-property-list": "0.9.1", - "language-python": "0.46.0-0", + "language-python": "0.46.0-1", "language-ruby": "0.71.4", "language-ruby-on-rails": "0.25.3", "language-sass": "0.61.3", - "language-shellscript": "0.26.0-0", + "language-shellscript": "0.26.0-1", "language-source": "0.9.0", "language-sql": "0.25.9", "language-text": "0.7.3", "language-todo": "0.29.3", "language-toml": "0.18.1", - "language-typescript": "0.3.0-1", + "language-typescript": "0.3.0-2", "language-xml": "0.35.2", "language-yaml": "0.31.1" }, From 4adfba47cca5c4aef147918f33a6b2ffb51e6a4a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 15 Dec 2017 16:57:36 -0800 Subject: [PATCH 099/176] Support legacyScopeName field on tree-sitter grammars * Use the field for mapping scope names in GrammarRegistry.grammarForId * Use the field for adapting legacy scoped settings to work with tree-sitter parsers Signed-off-by: Nathan Sobo --- spec/config-spec.coffee | 23 ++++++++++++++++++ spec/grammar-registry-spec.js | 6 +++++ src/config.coffee | 44 ++++++++++++++++++++++++++++++++--- src/grammar-registry.js | 38 ++++++++++++++++-------------- src/scope-descriptor.coffee | 16 +++++++++---- src/tree-sitter-grammar.js | 1 + 6 files changed, 103 insertions(+), 25 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index bcf50c268..090bc7a29 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -106,6 +106,15 @@ describe "Config", -> atom.config.set("foo.bar.baz", 1, scopeSelector: ".source.coffee", source: "some-package") expect(atom.config.get("foo.bar.baz", scope: [".source.coffee"])).toBe 100 + describe "when the first component of the scope descriptor matches a legacy scope alias", -> + it "falls back to properties defined for the legacy scope if no value is found for the original scope descriptor", -> + atom.config.addLegacyScopeAlias('javascript', '.source.js') + atom.config.set('foo', 100, scopeSelector: '.source.js') + atom.config.set('foo', 200, scopeSelector: 'javascript for_statement') + + expect(atom.config.get('foo', scope: ['javascript', 'for_statement', 'identifier'])).toBe(200) + expect(atom.config.get('foo', scope: ['javascript', 'function', 'identifier'])).toBe(100) + describe ".getAll(keyPath, {scope, sources, excludeSources})", -> it "reads all of the values for a given key-path", -> expect(atom.config.set("foo", 41)).toBe true @@ -130,6 +139,20 @@ describe "Config", -> {scopeSelector: '*', value: 40} ] + describe "when the first component of the scope descriptor matches a legacy scope alias", -> + it "includes the values defined for the legacy scope", -> + atom.config.addLegacyScopeAlias('javascript', '.source.js') + + expect(atom.config.set('foo', 41)).toBe true + expect(atom.config.set('foo', 42, scopeSelector: 'javascript')).toBe true + expect(atom.config.set('foo', 43, scopeSelector: '.source.js')).toBe true + + expect(atom.config.getAll('foo', scope: ['javascript'])).toEqual([ + {scopeSelector: 'javascript', value: 42}, + {scopeSelector: '.js.source', value: 43}, + {scopeSelector: '*', value: 41} + ]) + describe ".set(keyPath, value, {source, scopeSelector})", -> it "allows a key path's value to be written", -> expect(atom.config.set("foo.bar.baz", 42)).toBe true diff --git a/spec/grammar-registry-spec.js b/spec/grammar-registry-spec.js index 7b8f6f1b2..e6d815f8d 100644 --- a/spec/grammar-registry-spec.js +++ b/spec/grammar-registry-spec.js @@ -61,6 +61,9 @@ describe('GrammarRegistry', () => { const grammar = grammarRegistry.grammarForId('javascript') expect(grammar instanceof FirstMate.Grammar).toBe(true) expect(grammar.scopeName).toBe('source.js') + + grammarRegistry.removeGrammar(grammar) + expect(grammarRegistry.grammarForId('javascript')).toBe(undefined) }) it('converts the language id to a tree-sitter language id when `core.useTreeSitterParsers` is true', () => { @@ -72,6 +75,9 @@ describe('GrammarRegistry', () => { const grammar = grammarRegistry.grammarForId('source.js') expect(grammar instanceof TreeSitterGrammar).toBe(true) expect(grammar.id).toBe('javascript') + + grammarRegistry.removeGrammar(grammar) + expect(grammarRegistry.grammarForId('source.js') instanceof FirstMate.Grammar).toBe(true) }) }) diff --git a/src/config.coffee b/src/config.coffee index b8bf8a76f..84e726700 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -423,6 +423,7 @@ class Config @configFileHasErrors = false @transactDepth = 0 @pendingOperations = [] + @legacyScopeAliases = {} @requestLoad = _.debounce => @loadUserConfig() @@ -599,11 +600,22 @@ class Config # * `value` The value for the key-path getAll: (keyPath, options) -> {scope} = options if options? - result = [] if scope? scopeDescriptor = ScopeDescriptor.fromObject(scope) - result = result.concat @scopedSettingsStore.getAll(scopeDescriptor.getScopeChain(), keyPath, options) + result = @scopedSettingsStore.getAll( + scopeDescriptor.getScopeChain(), + keyPath, + options + ) + if legacyScopeDescriptor = @getLegacyScopeDescriptor(scopeDescriptor) + result.push(@scopedSettingsStore.getAll( + legacyScopeDescriptor.getScopeChain(), + keyPath, + options + )...) + else + result = [] if globalValue = @getRawValue(keyPath, options) result.push(scopeSelector: '*', value: globalValue) @@ -762,6 +774,12 @@ class Config finally @endTransaction() + addLegacyScopeAlias: (languageId, legacyScopeName) -> + @legacyScopeAliases[languageId] = legacyScopeName + + removeLegacyScopeAlias: (languageId) -> + delete @legacyScopeAliases[languageId] + ### Section: Internal methods used by core ### @@ -1145,7 +1163,20 @@ class Config getRawScopedValue: (scopeDescriptor, keyPath, options) -> scopeDescriptor = ScopeDescriptor.fromObject(scopeDescriptor) - @scopedSettingsStore.getPropertyValue(scopeDescriptor.getScopeChain(), keyPath, options) + result = @scopedSettingsStore.getPropertyValue( + scopeDescriptor.getScopeChain(), + keyPath, + options + ) + + if result? + result + else if legacyScopeDescriptor = @getLegacyScopeDescriptor(scopeDescriptor) + @scopedSettingsStore.getPropertyValue( + legacyScopeDescriptor.getScopeChain(), + keyPath, + options + ) observeScopedKeyPath: (scope, keyPath, callback) -> callback(@get(keyPath, {scope})) @@ -1160,6 +1191,13 @@ class Config oldValue = newValue callback(event) + getLegacyScopeDescriptor: (scopeDescriptor) -> + legacyAlias = @legacyScopeAliases[scopeDescriptor.scopes[0]] + if legacyAlias + scopes = scopeDescriptor.scopes.slice() + scopes[0] = legacyAlias + new ScopeDescriptor({scopes}) + # Base schema enforcers. These will coerce raw input into the specified type, # and will throw an error when the value cannot be coerced. Throwing the error # will indicate that the value should not be set. diff --git a/src/grammar-registry.js b/src/grammar-registry.js index dd11171ba..b2c4129f7 100644 --- a/src/grammar-registry.js +++ b/src/grammar-registry.js @@ -13,16 +13,6 @@ const {Point, Range} = require('text-buffer') const GRAMMAR_TYPE_BONUS = 1000 const PATH_SPLIT_REGEX = new RegExp('[/.]') -const LANGUAGE_ID_MAP = [ - ['source.js', 'javascript'], - ['source.ts', 'typescript'], - ['source.c', 'c'], - ['source.cpp', 'cpp'], - ['source.go', 'go'], - ['source.python', 'python'], - ['source.sh', 'bash'] -] - // Extended: This class holds the grammars used for tokenizing. // // An instance of this class is always available as the `atom.grammars` global. @@ -42,6 +32,8 @@ class GrammarRegistry { this.subscriptions = new CompositeDisposable() this.languageOverridesByBufferId = new Map() this.grammarScoresByBuffer = new Map() + this.textMateScopeNamesByTreeSitterLanguageId = new Map() + this.treeSitterLanguageIdsByTextMateScopeName = new Map() const grammarAddedOrUpdated = this.grammarAddedOrUpdated.bind(this) this.textmateRegistry.onDidAddGrammar(grammarAddedOrUpdated) @@ -116,7 +108,7 @@ class GrammarRegistry { // Extended: Force a {TextBuffer} to use a different grammar than the // one that would otherwise be selected for it. // - // * `buffer` The {TextBuffer} whose gramamr will be set. + // * `buffer` The {TextBuffer} whose grammar will be set. // * `languageId` The {String} id of the desired language. // // Returns a {Boolean} that indicates whether the language was successfully @@ -398,15 +390,29 @@ class GrammarRegistry { addGrammar (grammar) { if (grammar instanceof TreeSitterGrammar) { this.treeSitterGrammarsById[grammar.id] = grammar + if (grammar.legacyScopeName) { + this.config.addLegacyScopeAlias(grammar.id, grammar.legacyScopeName) + this.textMateScopeNamesByTreeSitterLanguageId.set(grammar.id, grammar.legacyScopeName) + this.treeSitterLanguageIdsByTextMateScopeName.set(grammar.legacyScopeName, grammar.id) + } this.grammarAddedOrUpdated(grammar) - return new Disposable(() => delete this.treeSitterGrammarsById[grammar.id]) + return new Disposable(() => this.removeGrammar(grammar)) } else { return this.textmateRegistry.addGrammar(grammar) } } removeGrammar (grammar) { - return this.textmateRegistry.removeGrammar(grammar) + if (grammar instanceof TreeSitterGrammar) { + delete this.treeSitterGrammarsById[grammar.id] + if (grammar.legacyScopeName) { + this.config.removeLegacyScopeAlias(grammar.id) + this.textMateScopeNamesByTreeSitterLanguageId.delete(grammar.id) + this.treeSitterLanguageIdsByTextMateScopeName.delete(grammar.legacyScopeName) + } + } else { + return this.textmateRegistry.removeGrammar(grammar) + } } removeGrammarForScopeName (scopeName) { @@ -497,11 +503,9 @@ class GrammarRegistry { normalizeLanguageId (languageId) { if (this.config.get('core.useTreeSitterParsers')) { - const row = LANGUAGE_ID_MAP.find(entry => entry[0] === languageId) - return row ? row[1] : languageId + return this.treeSitterLanguageIdsByTextMateScopeName.get(languageId) || languageId } else { - const row = LANGUAGE_ID_MAP.find(entry => entry[1] === languageId) - return row ? row[0] : languageId + return this.textMateScopeNamesByTreeSitterLanguageId.get(languageId) || languageId } } } diff --git a/src/scope-descriptor.coffee b/src/scope-descriptor.coffee index 95539cc69..2085bd6b2 100644 --- a/src/scope-descriptor.coffee +++ b/src/scope-descriptor.coffee @@ -39,11 +39,17 @@ class ScopeDescriptor getScopesArray: -> @scopes getScopeChain: -> - @scopes - .map (scope) -> - scope = ".#{scope}" unless scope[0] is '.' - scope - .join(' ') + # For backward compatibility, prefix TextMate-style scope names with + # leading dots (e.g. 'source.js' -> '.source.js'). + if @scopes[0].includes('.') + result = '' + for scope, i in @scopes + result += ' ' if i > 0 + result += '.' if scope[0] isnt '.' + result += scope + result + else + @scopes.join(' ') toString: -> @getScopeChain() diff --git a/src/tree-sitter-grammar.js b/src/tree-sitter-grammar.js index b36505a0b..d00344fb1 100644 --- a/src/tree-sitter-grammar.js +++ b/src/tree-sitter-grammar.js @@ -8,6 +8,7 @@ class TreeSitterGrammar { this.registry = registry this.id = params.id this.name = params.name + this.legacyScopeName = params.legacyScopeName if (params.contentRegExp) this.contentRegExp = new RegExp(params.contentRegExp) this.folds = params.folds || [] From c844a253e05cc51814f9a2f3359e40b76b1d44d5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 15 Dec 2017 17:10:20 -0800 Subject: [PATCH 100/176] Implement TreeSitterLanguageMode.scopeDescriptorForPosition --- spec/tree-sitter-language-mode-spec.js | 25 +++++++++++++++++++++++++ src/tree-sitter-language-mode.js | 9 ++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js index 91070710f..ceb0ec03b 100644 --- a/spec/tree-sitter-language-mode-spec.js +++ b/spec/tree-sitter-language-mode-spec.js @@ -410,6 +410,31 @@ describe('TreeSitterLanguageMode', () => { }) }) + describe('.scopeDescriptorForPosition', () => { + it('returns a scope descriptor representing the given position in the syntax tree', () => { + const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { + id: 'javascript', + parser: 'tree-sitter-javascript' + }) + + buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar})) + + buffer.setText('foo({bar: baz});') + + editor.screenLineForScreenRow(0) + expect(editor.scopeDescriptorForBufferPosition({row: 0, column: 6}).getScopesArray()).toEqual([ + 'javascript', + 'program', + 'expression_statement', + 'call_expression', + 'arguments', + 'object', + 'pair', + 'property_identifier' + ]) + }) + }) + describe('TextEditor.selectLargerSyntaxNode and .selectSmallerSyntaxNode', () => { it('expands and contract the selection based on the syntax tree', () => { const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 5cd725108..310af5fea 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -293,7 +293,14 @@ class TreeSitterLanguageMode { } scopeDescriptorForPosition (point) { - return this.rootScopeDescriptor + const result = [] + let node = this.document.rootNode.descendantForPosition(point) + while (node) { + result.push(node.type) + node = node.parent + } + result.push(this.grammar.id) + return new ScopeDescriptor({scopes: result.reverse()}) } hasTokenForSelector (scopeSelector) { From 5490a8b258a9990d843b568c5be6f630f624679b Mon Sep 17 00:00:00 2001 From: Segev Finer Date: Sat, 16 Dec 2017 23:34:37 +0200 Subject: [PATCH 101/176] Initialize ProtocolHandlerInstaller after initializing Config This allows it to correctly read `core.uriHandlerRegistration` and avoids popping the notification even if set to 'never'. Fixes #16201 --- src/atom-environment.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/atom-environment.js b/src/atom-environment.js index 1671ea7c7..50a41fb53 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -206,12 +206,13 @@ class AtomEnvironment { this.themes.initialize({configDirPath: this.configDirPath, resourcePath, safeMode, devMode}) this.commandInstaller.initialize(this.getVersion()) - this.protocolHandlerInstaller.initialize(this.config, this.notifications) this.uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(this)) this.autoUpdater.initialize() this.config.load() + this.protocolHandlerInstaller.initialize(this.config, this.notifications) + this.themes.loadBaseStylesheets() this.initialStyleElements = this.styles.getSnapshot() if (params.onlyLoadBaseStyleSheets) this.themes.initialLoadComplete = true From 7d28908627a8a6570e3b13ce0ce16125e2509fab Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 19 Dec 2017 13:20:25 -0800 Subject: [PATCH 102/176] :arrow_up: atom-package-manager --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index b15e4a30f..d8f4f906e 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.11" + "atom-package-manager": "1.18.12" } } From cd7491bc46640d7816757a288b83cf129fbecf4e Mon Sep 17 00:00:00 2001 From: simurai Date: Wed, 20 Dec 2017 19:59:35 +0900 Subject: [PATCH 103/176] Increase dock hover affordance --- static/docks.less | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/static/docks.less b/static/docks.less index ca40a2c45..301d7aee5 100644 --- a/static/docks.less +++ b/static/docks.less @@ -16,9 +16,10 @@ atom-dock { .atom-dock-inner { display: flex; - // Keep the area at least a pixel wide so that you have something to hover + // Keep the area at least 2 pixels wide so that you have something to hover // over to trigger the toggle button affordance even when fullscreen. - &.left, &.right { min-width: 1px; } + // Needs to be 2 pixels to work on Windows when scaled to 150%. See atom/atom #15728 + &.left, &.right { min-width: 2px; } &.bottom { min-height: 1px; } &.bottom { width: 100%; } From 9519d0ff37fef7a63e030244234a08c31fe728ed Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Wed, 20 Dec 2017 16:04:56 -0700 Subject: [PATCH 104/176] :arrow_up: language-sass@0.61.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a9374db3..6dd6c8874 100644 --- a/package.json +++ b/package.json @@ -159,7 +159,7 @@ "language-python": "0.45.6", "language-ruby": "0.71.4", "language-ruby-on-rails": "0.25.3", - "language-sass": "0.61.3", + "language-sass": "0.61.4", "language-shellscript": "0.25.4", "language-source": "0.9.0", "language-sql": "0.25.9", From 4bc9ab1c2b3cba59689f39fad5cac64190c61442 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 20 Dec 2017 18:16:06 -0500 Subject: [PATCH 105/176] :arrow_up: language-html@0.48.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6dd6c8874..c535dbd40 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "language-gfm": "0.90.3", "language-git": "0.19.1", "language-go": "0.44.4", - "language-html": "0.48.4", + "language-html": "0.48.5", "language-hyperlink": "0.16.3", "language-java": "0.27.6", "language-javascript": "0.127.7", From f1f2d2f60fe5d7d39054002bb14072dd2bb1cb13 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 20 Dec 2017 23:25:59 -0500 Subject: [PATCH 106/176] :arrow_up: autoflow@0.29.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c535dbd40..dc4df856d 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "autocomplete-html": "0.8.4", "autocomplete-plus": "2.39.1", "autocomplete-snippets": "1.11.2", - "autoflow": "0.29.1", + "autoflow": "0.29.2", "autosave": "0.24.6", "background-tips": "0.27.1", "bookmarks": "0.45.1", From 48dfffda6c8956eba7e329aa2de789ba5b4c1b83 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 20 Dec 2017 23:45:53 -0500 Subject: [PATCH 107/176] :arrow_up: autoflow@0.29.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc4df856d..b767e1a32 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "autocomplete-html": "0.8.4", "autocomplete-plus": "2.39.1", "autocomplete-snippets": "1.11.2", - "autoflow": "0.29.2", + "autoflow": "0.29.3", "autosave": "0.24.6", "background-tips": "0.27.1", "bookmarks": "0.45.1", From 58f125fe24ee50b376efd5863b2cf6c64e29767f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 21 Dec 2017 09:59:42 -0700 Subject: [PATCH 108/176] :arrow_up: electron --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7451e7e58..4e3a24691 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "url": "https://github.com/atom/atom/issues" }, "license": "MIT", - "electronVersion": "1.7.9", + "electronVersion": "1.7.10", "dependencies": { "@atom/nsfw": "^1.0.18", "@atom/source-map-support": "^0.3.4", From b11b0b9f9feea4354c018650611d397fa6b1556a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 21 Dec 2017 09:59:59 -0700 Subject: [PATCH 109/176] Disable inline caches during snapshot generation to work around crash --- script/lib/generate-startup-snapshot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index 333acdc0a..d0deea277 100644 --- a/script/lib/generate-startup-snapshot.js +++ b/script/lib/generate-startup-snapshot.js @@ -85,7 +85,7 @@ module.exports = function (packagedAppPath) { console.log(`Generating startup blob at "${generatedStartupBlobPath}"`) childProcess.execFileSync( path.join(CONFIG.repositoryRootPath, 'script', 'node_modules', 'electron-mksnapshot', 'bin', 'mksnapshot'), - [snapshotScriptPath, '--startup_blob', generatedStartupBlobPath] + ['--no-use_ic', snapshotScriptPath, '--startup_blob', generatedStartupBlobPath] ) let startupBlobDestinationPath From aeb8db2e14640b738a006f5f90a315cfc0923c14 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 21 Dec 2017 10:10:12 -0700 Subject: [PATCH 110/176] Don't snapshot lodash.isequal because it builds an ArrayBuffer on eval This won't work until v8 6.2. --- script/lib/generate-startup-snapshot.js | 1 + 1 file changed, 1 insertion(+) diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index d0deea277..85e147c20 100644 --- a/script/lib/generate-startup-snapshot.js +++ b/script/lib/generate-startup-snapshot.js @@ -46,6 +46,7 @@ module.exports = function (packagedAppPath) { relativePath === path.join('..', 'node_modules', 'less', 'index.js') || relativePath === path.join('..', 'node_modules', 'less', 'lib', 'less', 'fs.js') || relativePath === path.join('..', 'node_modules', 'less', 'lib', 'less-node', 'index.js') || + relativePath === path.join('..', 'node_modules', 'lodash.isequal', 'index.js') || relativePath === path.join('..', 'node_modules', 'node-fetch', 'lib', 'fetch-error.js') || relativePath === path.join('..', 'node_modules', 'superstring', 'index.js') || relativePath === path.join('..', 'node_modules', 'oniguruma', 'src', 'oniguruma.js') || From 1d4f516d62d0ea5fb53a9b2da3d852d96d1aa99f Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 21 Dec 2017 10:02:06 -0800 Subject: [PATCH 111/176] Update Linux installation instructions in README.md Resolves #2956. --- README.md | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index dc4062ea9..456a30d50 100644 --- a/README.md +++ b/README.md @@ -42,27 +42,11 @@ The `.zip` version will not automatically update. Using [Chocolatey](https://chocolatey.org)? Run `cinst Atom` to install the latest version of Atom. -### Debian based (Debian, Ubuntu, Linux Mint) +### Linux Atom is only available for 64-bit Linux systems. -1. Download `atom-amd64.deb` from the [Atom releases page](https://github.com/atom/atom/releases/latest). -2. Run `sudo dpkg --install atom-amd64.deb` on the downloaded package. -3. Launch Atom using the installed `atom` command. - -The Linux version does not currently automatically update so you will need to -repeat these steps to upgrade to future releases. - -### RPM based (Red Hat, openSUSE, Fedora, CentOS) - -Atom is only available for 64-bit Linux systems. - -1. Download `atom.x86_64.rpm` from the [Atom releases page](https://github.com/atom/atom/releases/latest). -2. Run `sudo rpm -i atom.x86_64.rpm` on the downloaded package. -3. Launch Atom using the installed `atom` command. - -The Linux version does not currently automatically update so you will need to -repeat these steps to upgrade to future releases. +Configure your distribution's package manager to install and update Atom by following the [Linux installation instructions](http://flight-manual.atom.io/getting-started/sections/installing-atom/#platform-linux) in the Flight Manual. You will also find instructions on how to install Atom's official Linux packages without using a package repository, though you will not get automatic updates after installing Atom this way. ### Archive extraction From 4ed59b3ee72e928129141e4f821c999f12dc2c49 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 21 Dec 2017 21:20:31 -0500 Subject: [PATCH 112/176] :memo: Update TextEditor::scopeDescriptorForBufferPosition docs [ci skip] --- src/text-editor.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/text-editor.js b/src/text-editor.js index 4daca5d49..c7ddcf031 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -3600,14 +3600,15 @@ class TextEditor { return this.buffer.getLanguageMode().rootScopeDescriptor } - // Essential: Get the syntactic scopeDescriptor for the given position in buffer + // Essential: Get the syntactic {ScopeDescriptor} for the given position in buffer // coordinates. Useful with {Config::get}. // // For example, if called with a position inside the parameter list of an - // anonymous CoffeeScript function, the method returns the following array: - // `["source.coffee", "meta.inline.function.coffee", "variable.parameter.function.coffee"]` + // anonymous CoffeeScript function, this method returns a {ScopeDescriptor} with + // the following scopes array: + // `["source.coffee", "meta.function.inline.coffee", "meta.parameters.coffee", "variable.parameter.function.coffee"]` // - // * `bufferPosition` A {Point} or {Array} of [row, column]. + // * `bufferPosition` A {Point} or {Array} of `[row, column]`. // // Returns a {ScopeDescriptor}. scopeDescriptorForBufferPosition (bufferPosition) { From 011766768a77e543bac0c23cde20ff211288c5da Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 22 Dec 2017 12:05:03 +0100 Subject: [PATCH 113/176] Fix AtomEnvironment tests --- spec/atom-environment-spec.js | 2 +- src/atom-environment.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/spec/atom-environment-spec.js b/spec/atom-environment-spec.js index e3b7b83e7..70ca9c309 100644 --- a/spec/atom-environment-spec.js +++ b/spec/atom-environment-spec.js @@ -592,7 +592,7 @@ describe('AtomEnvironment', () => { const promise = new Promise((r) => { resolve = r }) envLoaded = () => { resolve() - promise + return promise } atomEnvironment = new AtomEnvironment({ applicationDelegate: atom.applicationDelegate, diff --git a/src/atom-environment.js b/src/atom-environment.js index 50a41fb53..ae0ba8276 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -1013,8 +1013,10 @@ class AtomEnvironment { } addProjectFolder () { - this.pickFolder((selectedPaths = []) => { - this.addToProject(selectedPaths) + return new Promise((resolve) => { + this.pickFolder((selectedPaths) => { + this.addToProject(selectedPaths || []).then(resolve) + }) }) } From c05615b8f8a43c18a3aafaf9edca652cfdd511fc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 22 Dec 2017 12:13:39 +0100 Subject: [PATCH 114/176] Fix WorkspaceElement tests --- src/pane-resize-handle-element.coffee | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pane-resize-handle-element.coffee b/src/pane-resize-handle-element.coffee index 836dead52..69562c357 100644 --- a/src/pane-resize-handle-element.coffee +++ b/src/pane-resize-handle-element.coffee @@ -9,8 +9,12 @@ class PaneResizeHandleElement extends HTMLElement @addEventListener 'mousedown', @resizeStarted.bind(this) attachedCallback: -> - @isHorizontal = @parentElement.classList.contains("horizontal") - @classList.add if @isHorizontal then 'horizontal' else 'vertical' + # For some reason Chromium 58 is firing the attached callback after the + # element has been detached, so we ignore the callback when a parent element + # can't be found. + if @parentElement + @isHorizontal = @parentElement.classList.contains("horizontal") + @classList.add if @isHorizontal then 'horizontal' else 'vertical' detachedCallback: -> @resizeStopped() From 645252e0c2aabaefcd381f2ed50ddc40170c44ca Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 22 Dec 2017 14:13:51 +0100 Subject: [PATCH 115/176] :shirt: Fix linter error --- src/text-editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor.js b/src/text-editor.js index c7ddcf031..05e510b75 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -3604,7 +3604,7 @@ class TextEditor { // coordinates. Useful with {Config::get}. // // For example, if called with a position inside the parameter list of an - // anonymous CoffeeScript function, this method returns a {ScopeDescriptor} with + // anonymous CoffeeScript function, this method returns a {ScopeDescriptor} with // the following scopes array: // `["source.coffee", "meta.function.inline.coffee", "meta.parameters.coffee", "variable.parameter.function.coffee"]` // From 8b3c3bcfcf5b4645cd99752d6fb3cd0cc4b0c925 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 22 Dec 2017 14:29:22 +0100 Subject: [PATCH 116/176] Loosen containment rules on dummy scrollbar elements This commit uses `content` containment (i.e. `layout paint style`) as opposed to `strict` containment (i.e. `layout paint style size`) for dummy scrollbar elements. By removing `size` containment we are fixing a rendering bug that was preventing the scrollbar from being sized correctly. This problem was caught by a TextEditorComponent test (https://circleci.com/gh/atom/atom/6393). --- 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 8032ba939..257656552 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -3023,7 +3023,7 @@ class DummyScrollbarComponent { const outerStyle = { position: 'absolute', - contain: 'strict', + contain: 'content', zIndex: 1, willChange: 'transform' } From 69799d35b2c10832a9b8cf94c165700568a6b05c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 22 Dec 2017 14:39:47 +0100 Subject: [PATCH 117/176] Delete Chrome 56 workarounds --- spec/text-editor-component-spec.js | 593 +++++++++-------------------- src/text-editor-component.js | 15 +- 2 files changed, 186 insertions(+), 422 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 578f6ec62..deca42eea 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -3678,421 +3678,198 @@ describe('TextEditorComponent', () => { }) describe('keyboard input', () => { - 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]) + 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 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.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 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') - // 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 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 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 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') - }) + // 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') }) }) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 257656552..5f0a10664 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1716,10 +1716,6 @@ class TextEditorComponent { return } - if (this.getChromeVersion() === 56) { - this.getHiddenInput().value = '' - } - this.compositionCheckpoint = this.props.model.createCheckpoint() if (this.accentedCharacterMenuIsOpen) { this.props.model.selectLeft() @@ -1727,16 +1723,7 @@ class TextEditorComponent { } didCompositionUpdate (event) { - if (this.getChromeVersion() === 56) { - process.nextTick(() => { - if (this.compositionCheckpoint != null) { - const previewText = this.getHiddenInput().value - this.props.model.insertText(previewText, {select: true}) - } - }) - } else { - this.props.model.insertText(event.data, {select: true}) - } + this.props.model.insertText(event.data, {select: true}) } didCompositionEnd (event) { From 2c2d9597a7445a1505ffd0560179aadad4e9d390 Mon Sep 17 00:00:00 2001 From: Mark Lee Date: Sun, 24 Dec 2017 12:10:29 -0800 Subject: [PATCH 118/176] :green_heart: remove trailing whitespace from text editor docs --- src/text-editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor.js b/src/text-editor.js index c7ddcf031..05e510b75 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -3604,7 +3604,7 @@ class TextEditor { // coordinates. Useful with {Config::get}. // // For example, if called with a position inside the parameter list of an - // anonymous CoffeeScript function, this method returns a {ScopeDescriptor} with + // anonymous CoffeeScript function, this method returns a {ScopeDescriptor} with // the following scopes array: // `["source.coffee", "meta.function.inline.coffee", "meta.parameters.coffee", "variable.parameter.function.coffee"]` // From 9fdec777394df95dc4759f64db45fbcf30e89c45 Mon Sep 17 00:00:00 2001 From: Ronald Eddy Jr Date: Mon, 25 Dec 2017 22:55:44 -0800 Subject: [PATCH 119/176] Docs: Update HTTP -> HTTPS URLs updated to use HTTPS protocol where appropriate to improve security and privacy. --- CODE_OF_CONDUCT.md | 6 +++--- ISSUE_TEMPLATE.md | 4 ++-- README.md | 2 +- docs/apm-rest-api.md | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index c7d7eeb14..598b7e9b5 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -40,7 +40,7 @@ Project maintainers who do not follow or enforce the Code of Conduct in good fai ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/4/ diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index b60bb86c9..cf1773856 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -9,8 +9,8 @@ Do you want to ask a question? Are you looking for support? The Atom message boa ### Prerequisites * [ ] Put an X between the brackets on this line if you have done all of the following: - * Reproduced the problem in Safe Mode: http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode - * Followed all applicable steps in the debugging guide: http://flight-manual.atom.io/hacking-atom/sections/debugging/ + * Reproduced the problem in Safe Mode: https://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode + * Followed all applicable steps in the debugging guide: https://flight-manual.atom.io/hacking-atom/sections/debugging/ * Checked the FAQs on the message board for common solutions: https://discuss.atom.io/c/faq * Checked that your issue isn't already filed: https://github.com/issues?utf8=✓&q=is%3Aissue+user%3Aatom * Checked that there is not already an Atom package that provides the described functionality: https://atom.io/packages diff --git a/README.md b/README.md index 456a30d50..8078c179b 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Using [Chocolatey](https://chocolatey.org)? Run `cinst Atom` to install the late Atom is only available for 64-bit Linux systems. -Configure your distribution's package manager to install and update Atom by following the [Linux installation instructions](http://flight-manual.atom.io/getting-started/sections/installing-atom/#platform-linux) in the Flight Manual. You will also find instructions on how to install Atom's official Linux packages without using a package repository, though you will not get automatic updates after installing Atom this way. +Configure your distribution's package manager to install and update Atom by following the [Linux installation instructions](https://flight-manual.atom.io/getting-started/sections/installing-atom/#platform-linux) in the Flight Manual. You will also find instructions on how to install Atom's official Linux packages without using a package repository, though you will not get automatic updates after installing Atom this way. ### Archive extraction diff --git a/docs/apm-rest-api.md b/docs/apm-rest-api.md index a3c8e5c25..ab511a4a8 100644 --- a/docs/apm-rest-api.md +++ b/docs/apm-rest-api.md @@ -83,7 +83,7 @@ Returns package details and versions for a single package Parameters: - **engine** (optional) - Only show packages with versions compatible with this - Atom version. Must be valid [SemVer](http://semver.org). + Atom version. Must be valid [SemVer](https://semver.org). Returns: From 88a7eab33775101b8e4025752bbea98094445089 Mon Sep 17 00:00:00 2001 From: Lee Dohm <1038121+lee-dohm@users.noreply.github.com> Date: Tue, 26 Dec 2017 11:43:29 -0800 Subject: [PATCH 120/176] Point API docs to new Flight Manual section --- docs/apm-rest-api.md | 284 +------------------------------------------ 1 file changed, 1 insertion(+), 283 deletions(-) diff --git a/docs/apm-rest-api.md b/docs/apm-rest-api.md index a3c8e5c25..86c7942d1 100644 --- a/docs/apm-rest-api.md +++ b/docs/apm-rest-api.md @@ -1,285 +1,3 @@ # Atom.io package and update API -This guide describes the web API used by [apm](https://github.com/atom/apm) and -Atom. The vast majority of use cases are met by the `apm` command-line tool, -which does other useful things like incrementing your version in `package.json` -and making sure you have pushed your git tag. In fact, Atom itself shells out to -`apm` rather than hitting the API directly. If you're curious about how Atom -uses `apm`, see the [PackageManager class](https://github.com/atom/settings-view/blob/master/lib/package-manager.coffee) -in the `settings-view` package. - -*This API should be considered pre-release and is subject to change (though significant breaking changes are unlikely).* - -### Authorization - -For calls to the API that require authentication, provide a valid token from your -[Atom.io account page](https://atom.io/account) in the `Authorization` header. - -### Media type - -All requests that take parameters require `application/json`. - -# API Resources - -## Packages - -### Listing packages - -#### GET /api/packages - -Parameters: - -- **page** (optional) -- **sort** (optional) - One of `downloads`, `created_at`, `updated_at`, `stars`. Defaults to `downloads` -- **direction** (optional) - `asc` or `desc`. Defaults to `desc`. `stars` can only be ordered `desc` - -Returns a list of all packages in the following format: -```json - [ - { - "releases": { - "latest": "0.6.0" - }, - "name": "thedaniel-test-package", - "repository": { - "type": "git", - "url": "https://github.com/thedaniel/test-package" - } - }, - ... - ] -``` - -Results are paginated 30 at a time, and links to the next and last pages are -provided in the `Link` header: - -``` -Link: ; rel="self", - ; rel="last", - ; rel="next" -``` - -By default, results are sorted by download count, descending. - -### Searching packages - -#### GET /api/packages/search - -Parameters: - -- **q** (required) - Search query -- **page** (optional) -- **sort** (optional) - One of `downloads`, `created_at`, `updated_at`, `stars`. Defaults to the relevance of the search query. -- **direction** (optional) - `asc` or `desc`. Defaults to `desc`. - -Returns results in the same format as [listing packages](#listing-packages). - -### Showing package details - -#### GET /api/packages/:package_name - -Returns package details and versions for a single package - -Parameters: - -- **engine** (optional) - Only show packages with versions compatible with this - Atom version. Must be valid [SemVer](http://semver.org). - -Returns: - -```json - { - "releases": { - "latest": "0.6.0" - }, - "name": "thedaniel-test-package", - "repository": { - "type": "git", - "url": "https://github.com/thedaniel/test-package" - }, - "versions": [ - (see single version output below) - ..., - ] - } -``` - -### Creating a package - -#### POST /api/packages - -Create a new package; requires authentication. - -The name and version will be fetched from the `package.json` -file in the specified repository. The authenticating user *must* have access -to the indicated repository. - -Parameters: - -- **repository** - String. The repository containing the plugin, in the form "owner/repo" - -Returns: - -- **201** - Successfully created, returns created package. -- **400** - Repository is inaccessible, nonexistent, not an atom package. Possible - error messages include: - - That repo does not exist, isn't an atom package, or atombot does not have access - - The package.json at owner/repo isn't valid -- **409** - A package by that name already exists - -### Deleting a package - -#### DELETE /api/packages/:package_name - -Delete a package; requires authentication. - -Returns: - -- **204** - Success -- **400** - Repository is inaccessible -- **401** - Unauthorized - -### Renaming a package - -Packages are renamed by publishing a new version with the name changed in `package.json` -See [Creating a new package version](#creating-a-new-package-version) for details. - -Requests made to the previous name will forward to the new name. - -### Package Versions - -#### GET /api/packages/:package_name/versions/:version_name - -Returns `package.json` with `dist` key added for e.g. tarball download: - -```json - { - "bugs": { - "url": "https://github.com/thedaniel/test-package/issues" - }, - "dependencies": { - "async": "~0.2.6", - "pegjs": "~0.7.0", - "season": "~0.13.0" - }, - "description": "Expand snippets matching the current prefix with `tab`.", - "dist": { - "tarball": "https://codeload.github.com/..." - }, - "engines": { - "atom": "*" - }, - "main": "./lib/snippets", - "name": "thedaniel-test-package", - "publishConfig": { - "registry": "https://...", - }, - "repository": { - "type": "git", - "url": "https://github.com/thedaniel/test-package.git" - }, - "version": "0.6.0" - } -``` - - -### Creating a new package version - -#### POST /api/packages/:package_name/versions - -Creates a new package version from a git tag; requires authentication. If `rename` -is not `true`, the `name` field in `package.json` *must* match the current package -name. - -#### Parameters - -- **tag** - A git tag for the version you'd like to create. It's important to note - that the version name will not be taken from the tag, but from the `version` - key in the `package.json` file at that ref. The authenticating user *must* have - access to the package repository. -- **rename** - Boolean indicating whether this version contains a new name for the package. - -#### Returns - -- **201** - Successfully created. Returns created version. -- **400** - Git tag not found / Repository inaccessible / package.json invalid -- **409** - Version exists - -### Deleting a version - -#### DELETE /api/packages/:package_name/versions/:version_name - -Deletes a package version; requires authentication. - -Note that a version cannot be republished with a different tag if it is deleted. -If you need to delete the latest version of a package for e.g. security reasons, -you'll need to increment the version when republishing. - -Returns 204 No Content - - -## Stars - -### Listing user stars - -#### GET /api/users/:login/stars - -List a user's starred packages. - -Return value is similar to **GET /api/packages** - -#### GET /api/stars - -List the authenticated user's starred packages; requires authentication. - -Return value is similar to **GET /api/packages** - -### Starring a package - -#### POST /api/packages/:name/star - -Star a package; requires authentication. - -Returns a package. - -### Unstarring a package - -#### DELETE /api/packages/:name/star - -Unstar a package; requires authentication. - -Returns 204 No Content. - -### Listing a package's stargazers - -#### GET /api/packages/:name/stargazers - -List the users that have starred a package. - -Returns a list of user objects: - -```json -[ - {"login":"aperson"}, - {"login":"anotherperson"}, -] -``` - -## Atom updates - -### Listing Atom updates - -#### GET /api/updates - -Atom update feed, following the format expected by [Squirrel](https://github.com/Squirrel/). - -Returns: - -```json -{ - "name": "0.96.0", - "notes": "[HTML release notes]", - "pub_date": "2014-05-19T15:52:06.000Z", - "url": "https://www.atom.io/api/updates/download" -} -``` +The information that was here has been moved to [a permanent home inside Atom's Flight Manual.](https://flight-manual.atom.io/atom-server-side-apis/) From 37cae78bc15f2ab5cfe1c30fc3c4e55152f30443 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Dec 2017 13:47:34 -0800 Subject: [PATCH 121/176] :arrow_up: tree-sitter and language packages --- package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index bec57b304..7fb9f62fe 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "sinon": "1.17.4", "temp": "^0.8.3", "text-buffer": "13.9.2", - "tree-sitter": "0.7.5", + "tree-sitter": "^0.8.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", @@ -137,18 +137,18 @@ "welcome": "0.36.6", "whitespace": "0.37.5", "wrap-guide": "0.40.3", - "language-c": "0.59.0-2", + "language-c": "0.59.0-3", "language-clojure": "0.22.5", "language-coffee-script": "0.49.3", "language-csharp": "0.14.3", "language-css": "0.42.8", "language-gfm": "0.90.3", "language-git": "0.19.1", - "language-go": "0.45.0-3", + "language-go": "0.45.0-4", "language-html": "0.48.4", "language-hyperlink": "0.16.3", "language-java": "0.27.6", - "language-javascript": "0.128.0-3", + "language-javascript": "0.128.0-4", "language-json": "0.19.1", "language-less": "0.34.1", "language-make": "0.22.3", @@ -157,17 +157,17 @@ "language-perl": "0.38.1", "language-php": "0.43.0", "language-property-list": "0.9.1", - "language-python": "0.46.0-1", + "language-python": "0.46.0-2", "language-ruby": "0.71.4", "language-ruby-on-rails": "0.25.3", "language-sass": "0.61.3", - "language-shellscript": "0.26.0-1", + "language-shellscript": "0.26.0-2", "language-source": "0.9.0", "language-sql": "0.25.9", "language-text": "0.7.3", "language-todo": "0.29.3", "language-toml": "0.18.1", - "language-typescript": "0.3.0-2", + "language-typescript": "0.3.0-3", "language-xml": "0.35.2", "language-yaml": "0.31.1" }, From 662d38135beb8672412ecf24ebf0302e7304494d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Dec 2017 14:14:22 -0800 Subject: [PATCH 122/176] Use zero as the minimum value of getGrammarPathScore This way, we can determine if the grammar matches a buffer in any way by checking for a positive score. --- src/grammar-registry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/grammar-registry.js b/src/grammar-registry.js index b2c4129f7..b316bdbb0 100644 --- a/src/grammar-registry.js +++ b/src/grammar-registry.js @@ -213,7 +213,7 @@ class GrammarRegistry { if (process.platform === 'win32') { filePath = filePath.replace(/\\/g, '/') } const pathComponents = filePath.toLowerCase().split(PATH_SPLIT_REGEX) - let pathScore = -1 + let pathScore = 0 let customFileTypes if (this.config.get('core.customFileTypes')) { From 874e70a3d7a850a05fc5a8455dabe52e9443b9c6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Dec 2017 14:14:58 -0800 Subject: [PATCH 123/176] :arrow_up: language-shellscript --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7fb9f62fe..c3fadd743 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "language-ruby": "0.71.4", "language-ruby-on-rails": "0.25.3", "language-sass": "0.61.3", - "language-shellscript": "0.26.0-2", + "language-shellscript": "0.26.0-3", "language-source": "0.9.0", "language-sql": "0.25.9", "language-text": "0.7.3", From f96a0d922ed95949eb67b530d95ce808409086f5 Mon Sep 17 00:00:00 2001 From: Ford Hurley Date: Wed, 27 Dec 2017 13:39:50 -0500 Subject: [PATCH 124/176] Ensure that new editors get unique ids This restores the behavior from when TextEditor was written in coffeescript, and extended the Model class. --- src/text-editor.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/text-editor.js b/src/text-editor.js index 05e510b75..6f9993eed 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -119,6 +119,10 @@ class TextEditor { } this.id = params.id != null ? params.id : nextId++ + if (this.id >= nextId) { + // Ensure that new editors get unique ids: + nextId = this.id + 1; + } this.initialScrollTopRow = params.initialScrollTopRow this.initialScrollLeftColumn = params.initialScrollLeftColumn this.decorationManager = params.decorationManager From b5189e4e4ab90cc72ba1767d2462496a2b57fa21 Mon Sep 17 00:00:00 2001 From: Ford Hurley Date: Wed, 27 Dec 2017 15:16:13 -0500 Subject: [PATCH 125/176] Delint --- src/text-editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor.js b/src/text-editor.js index 6f9993eed..18c767f81 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -121,7 +121,7 @@ class TextEditor { this.id = params.id != null ? params.id : nextId++ if (this.id >= nextId) { // Ensure that new editors get unique ids: - nextId = this.id + 1; + nextId = this.id + 1 } this.initialScrollTopRow = params.initialScrollTopRow this.initialScrollLeftColumn = params.initialScrollLeftColumn From 3ad3852dd6a2fbdc5d64f21d4838fdd81ff1dbc4 Mon Sep 17 00:00:00 2001 From: Ford Hurley Date: Wed, 27 Dec 2017 15:16:22 -0500 Subject: [PATCH 126/176] Add a test for generated TextEditor ids --- spec/text-editor-spec.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index 89af72137..afd0d9068 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -20,6 +20,16 @@ describe('TextEditor', () => { await atom.packages.activatePackage('language-javascript') }) + it('generates unique ids for each editor', () => { + // Deserialized editors are initialized with an id: + new TextEditor({id: 0}) + new TextEditor({id: 1}) + new TextEditor({id: 2}) + // Initializing an editor without an id causes a new id to be generated: + const generatedId = new TextEditor().id + expect(generatedId).toBe(3) + }) + describe('when the editor is deserialized', () => { it('restores selections and folds based on markers in the buffer', async () => { editor.setSelectedBufferRange([[1, 2], [3, 4]]) From 2da2c1088f49cd2cbecbfc1e033408ea02823114 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Dec 2017 12:28:29 -0800 Subject: [PATCH 127/176] :arrow_up: tree-sitter --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c3fadd743..56ad73666 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "sinon": "1.17.4", "temp": "^0.8.3", "text-buffer": "13.9.2", - "tree-sitter": "^0.8.0", + "tree-sitter": "^0.8.2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From a8e457df61168d8e85d3829a33088e1df6f49036 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Dec 2017 12:37:50 -0800 Subject: [PATCH 128/176] Tweak syntax selection key bindings --- keymaps/darwin.cson | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson index d5cc7b7da..6d576f102 100644 --- a/keymaps/darwin.cson +++ b/keymaps/darwin.cson @@ -133,6 +133,8 @@ 'cmd-ctrl-left': 'editor:move-selection-left' 'cmd-ctrl-right': 'editor:move-selection-right' 'cmd-shift-V': 'editor:paste-without-reformatting' + 'alt-up': 'editor:select-larger-syntax-node' + 'alt-down': 'editor:select-smaller-syntax-node' # Emacs 'alt-f': 'editor:move-to-end-of-word' @@ -161,8 +163,6 @@ 'ctrl-alt-shift-right': 'editor:select-to-next-subword-boundary' 'ctrl-alt-backspace': 'editor:delete-to-beginning-of-subword' 'ctrl-alt-delete': 'editor:delete-to-end-of-subword' - 'ctrl-alt-up': 'editor:select-larger-syntax-node' - 'ctrl-alt-down': 'editor:select-smaller-syntax-node' 'atom-workspace atom-text-editor:not([mini])': # Atom specific From 798bbe3c32938aaf6e5677af05622df8459f5cec Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Dec 2017 13:35:01 -0800 Subject: [PATCH 129/176] Revert "Independent Atom instances (per $ATOM_HOME)" --- src/main-process/atom-application.coffee | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 8e889ecca..f6802705e 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -8,7 +8,6 @@ FileRecoveryService = require './file-recovery-service' ipcHelpers = require '../ipc-helpers' {BrowserWindow, Menu, app, dialog, ipcMain, shell, screen} = require 'electron' {CompositeDisposable, Disposable} = require 'event-kit' -crypto = require 'crypto' fs = require 'fs-plus' path = require 'path' os = require 'os' @@ -34,16 +33,11 @@ class AtomApplication # Public: The entry point into the Atom application. @open: (options) -> unless options.socketPath? - username = if process.platform is 'win32' then process.env.USERNAME else process.env.USER - # Lowercasing the ATOM_HOME to make sure that we don't get multiple sockets - # on case-insensitive filesystems due to arbitrary case differences in paths. - atomHomeUnique = path.resolve(process.env.ATOM_HOME).toLowerCase() - hash = crypto.createHash('sha1').update(username).update('|').update(atomHomeUnique) - atomInstanceDigest = hash.digest('hex').substring(0, 32) if process.platform is 'win32' - options.socketPath = "\\\\.\\pipe\\atom-#{options.version}-#{process.arch}-#{atomInstanceDigest}-sock" + userNameSafe = new Buffer(process.env.USERNAME).toString('base64') + options.socketPath = "\\\\.\\pipe\\atom-#{options.version}-#{userNameSafe}-#{process.arch}-sock" else - options.socketPath = path.join(os.tmpdir(), "atom-#{options.version}-#{process.arch}-#{atomInstanceDigest}.sock") + options.socketPath = path.join(os.tmpdir(), "atom-#{options.version}-#{process.env.USER}.sock") # FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely # take a few seconds to trigger 'error' event, it could be a bug of node From 065f4c48ec66654a604af5408fb5efb6ddb461fa Mon Sep 17 00:00:00 2001 From: Ford Hurley Date: Wed, 27 Dec 2017 16:37:12 -0500 Subject: [PATCH 130/176] Avoid dependency on shared state The test was passing only when run in isolation. --- spec/text-editor-spec.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index afd0d9068..ab84d88c8 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -20,14 +20,15 @@ describe('TextEditor', () => { await atom.packages.activatePackage('language-javascript') }) - it('generates unique ids for each editor', () => { - // Deserialized editors are initialized with an id: - new TextEditor({id: 0}) - new TextEditor({id: 1}) - new TextEditor({id: 2}) - // Initializing an editor without an id causes a new id to be generated: - const generatedId = new TextEditor().id - expect(generatedId).toBe(3) + it('generates unique ids for each editor', async () => { + // Deserialized editors are initialized with the serialized id. We can + // initialize an editor with what we expect to be the next id: + const deserialized = new TextEditor({id: editor.id+1}) + expect(deserialized.id).toEqual(editor.id+1) + + // The id generator should skip the id used up by the deserialized one: + const fresh = new TextEditor() + expect(fresh.id).toNotEqual(deserialized.id) }) describe('when the editor is deserialized', () => { From 2b3e22a39d689592d16cd0c466508281c51e28bf Mon Sep 17 00:00:00 2001 From: Morten Piibeleht Date: Tue, 18 Jul 2017 10:40:24 +1200 Subject: [PATCH 131/176] Allow independent Atom instances By having an $ATOM_HOME-dependent part in the socket name, Atom instances that have different homes will run in independent processes. Fixes the current behaviour where starting Atom with a new $ATOM_HOME "opens" an Atom window with settings and packages from the original $ATOM_HOME. Useful for IDEs. --- src/main-process/atom-application.coffee | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index f6802705e..8e889ecca 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -8,6 +8,7 @@ FileRecoveryService = require './file-recovery-service' ipcHelpers = require '../ipc-helpers' {BrowserWindow, Menu, app, dialog, ipcMain, shell, screen} = require 'electron' {CompositeDisposable, Disposable} = require 'event-kit' +crypto = require 'crypto' fs = require 'fs-plus' path = require 'path' os = require 'os' @@ -33,11 +34,16 @@ class AtomApplication # Public: The entry point into the Atom application. @open: (options) -> unless options.socketPath? + username = if process.platform is 'win32' then process.env.USERNAME else process.env.USER + # Lowercasing the ATOM_HOME to make sure that we don't get multiple sockets + # on case-insensitive filesystems due to arbitrary case differences in paths. + atomHomeUnique = path.resolve(process.env.ATOM_HOME).toLowerCase() + hash = crypto.createHash('sha1').update(username).update('|').update(atomHomeUnique) + atomInstanceDigest = hash.digest('hex').substring(0, 32) if process.platform is 'win32' - userNameSafe = new Buffer(process.env.USERNAME).toString('base64') - options.socketPath = "\\\\.\\pipe\\atom-#{options.version}-#{userNameSafe}-#{process.arch}-sock" + options.socketPath = "\\\\.\\pipe\\atom-#{options.version}-#{process.arch}-#{atomInstanceDigest}-sock" else - options.socketPath = path.join(os.tmpdir(), "atom-#{options.version}-#{process.env.USER}.sock") + options.socketPath = path.join(os.tmpdir(), "atom-#{options.version}-#{process.arch}-#{atomInstanceDigest}.sock") # FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely # take a few seconds to trigger 'error' event, it could be a bug of node From 1964b0094b611904ab177ef9963501bb94b08ce6 Mon Sep 17 00:00:00 2001 From: Morten Piibeleht Date: Thu, 28 Dec 2017 13:06:52 +1300 Subject: [PATCH 132/176] Make socketPath shorter To work around the limited socket file length on macOS/BSD. --- src/main-process/atom-application.coffee | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 8e889ecca..e0d2d691f 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -38,12 +38,15 @@ class AtomApplication # Lowercasing the ATOM_HOME to make sure that we don't get multiple sockets # on case-insensitive filesystems due to arbitrary case differences in paths. atomHomeUnique = path.resolve(process.env.ATOM_HOME).toLowerCase() - hash = crypto.createHash('sha1').update(username).update('|').update(atomHomeUnique) - atomInstanceDigest = hash.digest('hex').substring(0, 32) + hash = crypto.createHash('sha1').update(options.version).update('|').update(process.arch).update('|').update(username).update('|').update(atomHomeUnique) + # We only keep the first 12 characters of the hash as not to have excessively long + # socket file. Note that macOS/BSD limit the length of socket file paths (see #15081). + # The replace calls convert the digest into "URL and Filename Safe" encoding (see RFC 4648). + atomInstanceDigest = hash.digest('base64').substring(0, 12).replace(/\+/g, '-').replace(/\//g, '_') if process.platform is 'win32' - options.socketPath = "\\\\.\\pipe\\atom-#{options.version}-#{process.arch}-#{atomInstanceDigest}-sock" + options.socketPath = "\\\\.\\pipe\\atom-#{atomInstanceDigest}-sock" else - options.socketPath = path.join(os.tmpdir(), "atom-#{options.version}-#{process.arch}-#{atomInstanceDigest}.sock") + options.socketPath = path.join(os.tmpdir(), "atom-#{atomInstanceDigest}.sock") # FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely # take a few seconds to trigger 'error' event, it could be a bug of node From 7e0f4f377ef588ff7167bad5a83a0767bc48ce96 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sat, 30 Dec 2017 21:55:13 -0800 Subject: [PATCH 133/176] :arrow_up: tree-sitter --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e047c40c0..799fa0a1d 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "sinon": "1.17.4", "temp": "^0.8.3", "text-buffer": "13.9.2", - "tree-sitter": "^0.8.2", + "tree-sitter": "^0.8.4", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From e35a89e6886f41476a8680637c9c6eb21538d011 Mon Sep 17 00:00:00 2001 From: Miguel Piedrafita Date: Mon, 1 Jan 2018 00:52:24 +0100 Subject: [PATCH 134/176] Update license year --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index 5bdf03cde..58684e683 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2011-2017 GitHub Inc. +Copyright (c) 2011-2018 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the From d6a32c8eb6b80ca6a174964d43ad26b1a0d7606f Mon Sep 17 00:00:00 2001 From: Lee Dohm <1038121+lee-dohm@users.noreply.github.com> Date: Mon, 1 Jan 2018 10:13:18 -0800 Subject: [PATCH 135/176] Update /docs/README.md to direct people to the new locations of the documentation --- docs/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/README.md b/docs/README.md index c555306b5..c45e117e4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,20 +2,20 @@ ![Atom](https://cloud.githubusercontent.com/assets/72919/2874231/3af1db48-d3dd-11e3-98dc-6066f8bc766f.png) -Most of the Atom user and developer documentation is contained in the [Atom Flight Manual](https://github.com/atom/flight-manual.atom.io) repository. - -In this directory you can only find very specific build and API level documentation. Some of this may eventually move to the Flight Manual as well. +Most of the Atom user and developer documentation is contained in the [Atom Flight Manual](https://github.com/atom/flight-manual.atom.io). ## Build documentation Instructions for building Atom on various platforms from source. -* [macOS](./build-instructions/macOS.md) -* [Windows](./build-instructions/windows.md) -* [Linux](./build-instructions/linux.md) * [FreeBSD](./build-instructions/freebsd.md) +* Moved to [the Flight Manual](https://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/) + * Linux + * macOS + * Windows -## Other documentation here +## Other documentation -* [apm REST API](./apm-rest-api.md) -* [Tips for contributing to packages](./contributing-to-packages.md) +[Native Profiling on macOS](./native-profiling.md) + +The other documentation that was listed here previously has been moved to [the Flight Manual](https://flight-manual.atom.io). From c885a9c4f651a9e5c56f8a12fb4c916d36b8546b Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 2 Jan 2018 22:52:12 -0500 Subject: [PATCH 136/176] :arrow_up: settings-view@0.253.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b767e1a32..6fcdb57b8 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "notifications": "0.70.2", "open-on-github": "1.3.1", "package-generator": "1.3.0", - "settings-view": "0.253.2", + "settings-view": "0.253.3", "snippets": "1.1.11", "spell-check": "0.72.4", "status-bar": "1.8.15", From 27a19ee703930ec85a075b9b1ed5aa25892c6d8b Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Wed, 3 Jan 2018 10:45:50 -0500 Subject: [PATCH 137/176] Add "Verification Process" section to pull request template --- PULL_REQUEST_TEMPLATE.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index a578c38ce..a3356809d 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -27,6 +27,20 @@ We must be able to understand the design of your change from this description. I +### Verification Process + + + ### Applicable Issues From 7f39e96b9577f680776bc195f9568642d4ea3c56 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 3 Jan 2018 11:10:18 -0500 Subject: [PATCH 138/176] Revert ":arrow_up: settings-view@0.253.3" This reverts commit c885a9c4f651a9e5c56f8a12fb4c916d36b8546b. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6fcdb57b8..b767e1a32 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "notifications": "0.70.2", "open-on-github": "1.3.1", "package-generator": "1.3.0", - "settings-view": "0.253.3", + "settings-view": "0.253.2", "snippets": "1.1.11", "spell-check": "0.72.4", "status-bar": "1.8.15", From 629cb206ec77bed393b58e51d34605aece9bf6c5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 3 Jan 2018 09:34:12 -0800 Subject: [PATCH 139/176] Fix handling of empty tokens in TreeSitterHighlightIterator --- spec/tree-sitter-language-mode-spec.js | 45 ++++++++++++++++++++++++++ src/tree-sitter-language-mode.js | 6 +++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js index ceb0ec03b..ec38c1a06 100644 --- a/spec/tree-sitter-language-mode-spec.js +++ b/spec/tree-sitter-language-mode-spec.js @@ -2,6 +2,7 @@ const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-help const dedent = require('dedent') const TextBuffer = require('text-buffer') +const {Point} = TextBuffer const TextEditor = require('../src/text-editor') const TreeSitterGrammar = require('../src/tree-sitter-grammar') const TreeSitterLanguageMode = require('../src/tree-sitter-language-mode') @@ -93,6 +94,50 @@ describe('TreeSitterLanguageMode', () => { ] ]) }) + + it('correctly skips over tokens with zero size', () => { + const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { + parser: 'tree-sitter-c', + scopes: { + 'primitive_type': 'type', + 'identifier': 'variable', + } + }) + + const languageMode = new TreeSitterLanguageMode({buffer, grammar}) + buffer.setLanguageMode(languageMode) + buffer.setText('int main() {\n int a\n int b;\n}'); + + editor.screenLineForScreenRow(0) + expect( + languageMode.document.rootNode.descendantForPosition(Point(1, 2), Point(1, 6)).toString() + ).toBe('(declaration (primitive_type) (identifier) (MISSING))') + + expectTokensToEqual(editor, [ + [ + {text: 'int', scopes: ['type']}, + {text: ' ', scopes: []}, + {text: 'main', scopes: ['variable']}, + {text: '() {', scopes: []} + ], + [ + {text: ' ', scopes: ['whitespace']}, + {text: 'int', scopes: ['type']}, + {text: ' ', scopes: []}, + {text: 'a', scopes: ['variable']} + ], + [ + {text: ' ', scopes: ['whitespace']}, + {text: 'int', scopes: ['type']}, + {text: ' ', scopes: []}, + {text: 'b', scopes: ['variable']}, + {text: ';', scopes: []} + ], + [ + {text: '}', scopes: []} + ] + ]) + }) }) describe('folding', () => { diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 310af5fea..8cba4e25f 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -422,7 +422,7 @@ class TreeSitterHighlightIterator { if (!this.currentNode) break } } - } else { + } else if (this.currentNode.startIndex < this.currentNode.endIndex) { this.currentNode = this.currentNode.nextSibling if (this.currentNode) { this.currentChildIndex++ @@ -431,6 +431,10 @@ class TreeSitterHighlightIterator { this.pushOpenTag() this.descendLeft() } + } else { + this.pushCloseTag() + this.currentNode = this.currentNode.parent + this.currentChildIndex = last(this.containingNodeChildIndices) } } while (this.closeTags.length === 0 && this.openTags.length === 0 && this.currentNode) From fff5f39db6d28d7c155933a74b15bab0b81d49a4 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 3 Jan 2018 11:30:13 -0800 Subject: [PATCH 140/176] :arrow_up: apm@1.19.0 --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index d8f4f906e..90093b3d4 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.12" + "atom-package-manager": "1.19.0" } } From 3bdda7c5460a2e5f053c1aec426dc60d1b8c1a96 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 3 Jan 2018 13:05:27 -0700 Subject: [PATCH 141/176] :arrow_up: autocomplete-plus --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b767e1a32..c1808fdeb 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.6", "autocomplete-css": "0.17.5", "autocomplete-html": "0.8.4", - "autocomplete-plus": "2.39.1", + "autocomplete-plus": "2.40.0", "autocomplete-snippets": "1.11.2", "autoflow": "0.29.3", "autosave": "0.24.6", From 75f43b0b0e47641cb0b1b94d68083b2f213e1409 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 3 Jan 2018 12:20:46 -0800 Subject: [PATCH 142/176] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c1808fdeb..7002ea1c2 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.9.2", + "text-buffer": "13.10.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 733d6381cc20d9a2cd27ab6fe3a0a0075fca88d6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 3 Jan 2018 13:00:53 -0800 Subject: [PATCH 143/176] Fix handling of {undo: 'skip'} in TextEditor.insertText Signed-off-by: Nathan Sobo --- spec/text-editor-spec.js | 15 +++++++++------ src/text-editor.js | 11 ++++++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index ab84d88c8..ef2ced5e6 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -3507,13 +3507,16 @@ describe('TextEditor', () => { }) describe("when the undo option is set to 'skip'", () => { - beforeEach(() => editor.setSelectedBufferRange([[1, 2], [1, 2]])) - - it('does not undo the skipped operation', () => { - let range = editor.insertText('x') - range = editor.insertText('y', {undo: 'skip'}) + it('groups the change with the previous change for purposes of undo and redo', () => { + editor.setSelectedBufferRanges([ + [[0, 0], [0, 0]], + [[1, 0], [1, 0]] + ]) + editor.insertText('x') + editor.insertText('y', {undo: 'skip'}) editor.undo() - expect(buffer.lineForRow(1)).toBe(' yvar sort = function(items) {') + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {') }) }) }) diff --git a/src/text-editor.js b/src/text-editor.js index 18c767f81..3964323e1 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -1330,15 +1330,24 @@ class TextEditor { insertText (text, options = {}) { if (!this.emitWillInsertTextEvent(text)) return false + let groupLastChanges = false + if (options.undo === 'skip') { + options = Object.assign({}, options) + delete options.undo + groupLastChanges = true + } + const groupingInterval = options.groupUndo ? this.undoGroupingInterval : 0 if (options.autoIndentNewline == null) options.autoIndentNewline = this.shouldAutoIndent() if (options.autoDecreaseIndent == null) options.autoDecreaseIndent = this.shouldAutoIndent() - return this.mutateSelectedText(selection => { + const result = this.mutateSelectedText(selection => { const range = selection.insertText(text, options) const didInsertEvent = {text, range} this.emitter.emit('did-insert-text', didInsertEvent) return range }, groupingInterval) + if (groupLastChanges) this.buffer.groupLastChanges() + return result } // Essential: For each selection, replace the selected text with a newline. From 389b9b6bf1cd3ebda5e4ea8b3c634b0f81d7818b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 3 Jan 2018 16:39:53 -0700 Subject: [PATCH 144/176] :arrow_up: spell-check --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 55e733611..d21e74d3b 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "package-generator": "1.3.0", "settings-view": "0.253.2", "snippets": "1.1.11", - "spell-check": "0.72.4", + "spell-check": "0.72.5", "status-bar": "1.8.15", "styleguide": "0.49.10", "symbols-view": "0.118.2", From 33d2a7ddc4e7204351253eaa8539e507a9a37901 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 3 Jan 2018 17:27:20 -0800 Subject: [PATCH 145/176] :arrow_up: fuzzy-finder --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d21e74d3b..9c557c4fb 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "encoding-selector": "0.23.8", "exception-reporting": "0.42.0", "find-and-replace": "0.215.0", - "fuzzy-finder": "1.7.4", + "fuzzy-finder": "1.7.5", "github": "0.9.1", "git-diff": "1.3.7", "go-to-line": "0.32.1", From a79a605f9b674a55b9ff60d4984c2d1477eef4b8 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 3 Jan 2018 17:39:38 -0800 Subject: [PATCH 146/176] :arrow_up: language-csharp --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9c557c4fb..579afbc68 100644 --- a/package.json +++ b/package.json @@ -139,7 +139,7 @@ "language-c": "0.58.1", "language-clojure": "0.22.5", "language-coffee-script": "0.49.3", - "language-csharp": "0.14.3", + "language-csharp": "0.14.4", "language-css": "0.42.8", "language-gfm": "0.90.3", "language-git": "0.19.1", From d848c15d4230221dae8e4e003639a661203c5512 Mon Sep 17 00:00:00 2001 From: "Tobias V. Langhoff" Date: Thu, 4 Jan 2018 19:36:21 +0100 Subject: [PATCH 147/176] Fix typo in protocol handler installer popup Correct the typo "defaut" to the correct "default" in the atom:// URI protocol handler popup --- src/protocol-handler-installer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/protocol-handler-installer.js b/src/protocol-handler-installer.js index 0a55bff41..37df68389 100644 --- a/src/protocol-handler-installer.js +++ b/src/protocol-handler-installer.js @@ -63,7 +63,7 @@ class ProtocolHandlerInstaller { notification = notifications.addInfo('Register as default atom:// URI handler?', { dismissable: true, icon: 'link', - description: 'Atom is not currently set as the defaut handler for atom:// URIs. Would you like Atom to handle ' + + description: 'Atom is not currently set as the default handler for atom:// URIs. Would you like Atom to handle ' + 'atom:// URIs?', buttons: [ { From 9eac520e6a369882827ac637e31dbd24804cf44b Mon Sep 17 00:00:00 2001 From: itsmichaelwang Date: Wed, 8 Nov 2017 00:34:55 -0800 Subject: [PATCH 148/176] Allow you to tab through modal text box --- src/text-editor-component.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 5f0a10664..97982b362 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -170,6 +170,7 @@ class TextEditorComponent { this.textDecorationBoundaries = [] this.pendingScrollTopRow = this.props.initialScrollTopRow this.pendingScrollLeftColumn = this.props.initialScrollLeftColumn + this.tabIndex = this.props.element && this.props.element.tabIndex ? this.props.element.tabIndex : -1; this.measuredContent = false this.queryGuttersToRender() @@ -481,7 +482,7 @@ class TextEditorComponent { style, attributes, dataset, - tabIndex: -1, + tabIndex: this.tabIndex, on: {mousewheel: this.didMouseWheel} }, $.div( @@ -3574,7 +3575,7 @@ class CursorsAndInputComponent { compositionupdate: didCompositionUpdate, compositionend: didCompositionEnd }, - tabIndex: -1, + tabIndex: this.tabIndex, style: { position: 'absolute', width: '1px', From e462d0d29818db75ca5d6897d0e008937e4d57c5 Mon Sep 17 00:00:00 2001 From: itsmichaelwang Date: Fri, 17 Nov 2017 00:23:07 -0800 Subject: [PATCH 149/176] Fix lint issue --- 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 97982b362..da08a6c11 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -170,7 +170,7 @@ class TextEditorComponent { this.textDecorationBoundaries = [] this.pendingScrollTopRow = this.props.initialScrollTopRow this.pendingScrollLeftColumn = this.props.initialScrollLeftColumn - this.tabIndex = this.props.element && this.props.element.tabIndex ? this.props.element.tabIndex : -1; + this.tabIndex = this.props.element && this.props.element.tabIndex ? this.props.element.tabIndex : -1 this.measuredContent = false this.queryGuttersToRender() From 408070e9138a8ee4cc2b30b8a16583058072e4d0 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 4 Jan 2018 14:31:11 -0500 Subject: [PATCH 150/176] Set the tabIndex on the input element MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit reverts a portion of the changes introduced in 9eac520e6a369882827ac637e31dbd24804cf44b. Prior to that commit, we were setting the tabIndex to -1 on the atom-text-editor element. This commit restores that behavior. Instead of setting a custom tab index directly on the atom-text-editor element, we instead set the tabIndex on the input element *inside* the atom-text-editor element. With these changes in place, you can successfully use the tabIndex to define the tab order for atom-text-editor elements. 😅 --- src/text-editor-component.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index da08a6c11..867a536fc 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -482,7 +482,7 @@ class TextEditorComponent { style, attributes, dataset, - tabIndex: this.tabIndex, + tabIndex: -1, on: {mousewheel: this.didMouseWheel} }, $.div( @@ -682,7 +682,8 @@ class TextEditorComponent { scrollWidth: this.getScrollWidth(), decorationsToRender: this.decorationsToRender, cursorsBlinkedOff: this.cursorsBlinkedOff, - hiddenInputPosition: this.hiddenInputPosition + hiddenInputPosition: this.hiddenInputPosition, + tabIndex: this.tabIndex }) } @@ -3547,7 +3548,7 @@ class CursorsAndInputComponent { const { lineHeight, hiddenInputPosition, didBlurHiddenInput, didFocusHiddenInput, didPaste, didTextInput, didKeydown, didKeyup, didKeypress, - didCompositionStart, didCompositionUpdate, didCompositionEnd + didCompositionStart, didCompositionUpdate, didCompositionEnd, tabIndex } = this.props let top, left @@ -3575,7 +3576,7 @@ class CursorsAndInputComponent { compositionupdate: didCompositionUpdate, compositionend: didCompositionEnd }, - tabIndex: this.tabIndex, + tabIndex: tabIndex, style: { position: 'absolute', width: '1px', From 75b4a7a984dc7afe83da12cc884acd46216c4417 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 4 Jan 2018 12:12:21 -0800 Subject: [PATCH 151/176] :arrow_up: joanna, put back AtomEnvironment public property docs --- script/package.json | 2 +- src/atom-environment.js | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/script/package.json b/script/package.json index 3f335417a..78025d223 100644 --- a/script/package.json +++ b/script/package.json @@ -16,7 +16,7 @@ "fs-admin": "^0.1.5", "fs-extra": "0.30.0", "glob": "7.0.3", - "joanna": "0.0.9", + "joanna": "0.0.10", "klaw-sync": "^1.1.2", "legal-eagle": "0.14.0", "lodash.template": "4.4.0", diff --git a/src/atom-environment.js b/src/atom-environment.js index ae0ba8276..fc3201dfc 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -51,13 +51,15 @@ let nextId = 0 // // An instance of this class is always available as the `atom` global. class AtomEnvironment { + /* - Section: Construction and Destruction + Section: Properties */ - // Call .loadOrCreate instead constructor (params = {}) { this.id = (params.id != null) ? params.id : nextId++ + + // Public: A {Clipboard} instance this.clipboard = params.clipboard this.updateProcessEnv = params.updateProcessEnv || updateProcessEnv this.enablePersistence = params.enablePersistence @@ -68,25 +70,43 @@ class AtomEnvironment { this.loadTime = null this.emitter = new Emitter() this.disposables = new CompositeDisposable() + + // Public: A {DeserializerManager} instance this.deserializers = new DeserializerManager(this) this.deserializeTimings = {} + + // Public: A {ViewRegistry} instance this.views = new ViewRegistry(this) + + // Public: A {NotificationManager} instance this.notifications = new NotificationManager() this.stateStore = new StateStore('AtomEnvironments', 1) + // Public: A {Config} instance this.config = new Config({ notificationManager: this.notifications, enablePersistence: this.enablePersistence }) this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)}) + // Public: A {KeymapManager} instance this.keymaps = new KeymapManager({notificationManager: this.notifications}) + + // Public: A {TooltipManager} instance this.tooltips = new TooltipManager({keymapManager: this.keymaps, viewRegistry: this.views}) + + // Public: A {CommandRegistry} instance this.commands = new CommandRegistry() this.uriHandlerRegistry = new URIHandlerRegistry() + + // Public: A {GrammarRegistry} instance this.grammars = new GrammarRegistry({config: this.config}) + + // Public: A {StyleManager} instance this.styles = new StyleManager() + + // Public: A {PackageManager} instance this.packages = new PackageManager({ config: this.config, styleManager: this.styles, @@ -98,6 +118,8 @@ class AtomEnvironment { viewRegistry: this.views, uriHandlerRegistry: this.uriHandlerRegistry }) + + // Public: A {ThemeManager} instance this.themes = new ThemeManager({ packageManager: this.packages, config: this.config, @@ -105,12 +127,18 @@ class AtomEnvironment { notificationManager: this.notifications, viewRegistry: this.views }) + + // Public: A {MenuManager} instance this.menu = new MenuManager({keymapManager: this.keymaps, packageManager: this.packages}) + + // Public: A {ContextMenuManager} instance this.contextMenu = new ContextMenuManager({keymapManager: this.keymaps}) + this.packages.setMenuManager(this.menu) this.packages.setContextMenuManager(this.contextMenu) this.packages.setThemeManager(this.themes) + // Public: A {Project} instance this.project = new Project({ notificationManager: this.notifications, packageManager: this.packages, @@ -121,6 +149,7 @@ class AtomEnvironment { this.commandInstaller = new CommandInstaller(this.applicationDelegate) this.protocolHandlerInstaller = new ProtocolHandlerInstaller() + // Public: A {TextEditorRegistry} instance this.textEditors = new TextEditorRegistry({ config: this.config, grammarRegistry: this.grammars, @@ -128,6 +157,7 @@ class AtomEnvironment { packageManager: this.packages }) + // Public: A {Workspace} instance this.workspace = new Workspace({ config: this.config, project: this.project, @@ -157,7 +187,9 @@ class AtomEnvironment { this.windowEventHandler = new WindowEventHandler({atomEnvironment: this, applicationDelegate: this.applicationDelegate}) + // Public: A {HistoryManager} instance this.history = new HistoryManager({project: this.project, commands: this.commands, stateStore: this.stateStore}) + // Keep instances of HistoryManager in sync this.disposables.add(this.history.onDidChangeProjects(event => { if (!event.reloaded) this.applicationDelegate.didChangeHistoryManager() From 7f923fc05fdc1001007dbf92eee4de97bd0bef0f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 4 Jan 2018 12:13:23 -0800 Subject: [PATCH 152/176] Fix section comments --- src/tree-sitter-language-mode.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 8cba4e25f..313c3574d 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -41,8 +41,8 @@ class TreeSitterLanguageMode { } /* - * Section - Highlighting - */ + Section - Highlighting + */ buildHighlightIterator () { const invalidatedRanges = this.document.parse() @@ -67,8 +67,8 @@ class TreeSitterLanguageMode { } /* - * Section - Commenting - */ + Section - Commenting + */ commentStringsForPosition () { return this.grammar.commentStrings @@ -79,8 +79,8 @@ class TreeSitterLanguageMode { } /* - * Section - Indentation - */ + Section - Indentation + */ suggestedIndentForLineAtBufferRow (row, line, tabLength) { return this.suggestedIndentForBufferRow(row, tabLength) @@ -119,8 +119,8 @@ class TreeSitterLanguageMode { } /* - * Section - Folding - */ + Section - Folding + */ isFoldableAtRow (row) { if (this.isFoldableCache[row] != null) return this.isFoldableCache[row] @@ -263,8 +263,8 @@ class TreeSitterLanguageMode { } /* - * Syntax Tree APIs - */ + Syntax Tree APIs + */ getRangeForSyntaxNodeContainingRange (range) { const startIndex = this.buffer.characterIndexForPosition(range.start) @@ -277,8 +277,8 @@ class TreeSitterLanguageMode { } /* - * Section - Backward compatibility shims - */ + Section - Backward compatibility shims + */ tokenizedLineForRow (row) { return new TokenizedLine({ From d0ebc45d0b12ac60d8ea61213319726b0f1f2c89 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 4 Jan 2018 14:53:15 -0700 Subject: [PATCH 153/176] :arrow_up: snippets --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 579afbc68..f55db38f9 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "open-on-github": "1.3.1", "package-generator": "1.3.0", "settings-view": "0.253.2", - "snippets": "1.1.11", + "snippets": "1.2.0", "spell-check": "0.72.5", "status-bar": "1.8.15", "styleguide": "0.49.10", From 5cce2b55bc6562cbeebbd0dbb4da5918f33327bd Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 4 Jan 2018 17:55:05 -0500 Subject: [PATCH 154/176] =?UTF-8?q?=E2=9C=85=20Add=20test=20for=20setting?= =?UTF-8?q?=20tabIndex=20on=20atom-text-editor=20element?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/text-editor-element-spec.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/text-editor-element-spec.js b/spec/text-editor-element-spec.js index b7181fa91..7ffdf374d 100644 --- a/spec/text-editor-element-spec.js +++ b/spec/text-editor-element-spec.js @@ -89,6 +89,22 @@ describe('TextEditorElement', () => { expect(element.getModel().getText()).toBe('testing') }) + describe('tabIndex', () => { + it('uses a default value of -1', () => { + jasmineContent.innerHTML = '' + const element = jasmineContent.firstChild + expect(element.tabIndex).toBe(-1) + expect(element.querySelector('input').tabIndex).toBe(-1) + }) + + it('uses the custom value when given', () => { + jasmineContent.innerHTML = '' + const element = jasmineContent.firstChild + expect(element.tabIndex).toBe(-1) + expect(element.querySelector('input').tabIndex).toBe(42) + }) + }) + describe('when the model is assigned', () => it("adds the 'mini' attribute if .isMini() returns true on the model", async () => { const element = buildTextEditorElement() From 9d23d37965bb5aebd33b078f00859f0b572d943f Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 4 Jan 2018 17:55:53 -0500 Subject: [PATCH 155/176] If a TextEditorElement has a tabIndex, use it; otherwise, use -1 --- src/text-editor-element.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-element.js b/src/text-editor-element.js index 7218b7f05..926f7af44 100644 --- a/src/text-editor-element.js +++ b/src/text-editor-element.js @@ -32,7 +32,7 @@ class TextEditorElement extends HTMLElement { createdCallback () { this.emitter = new Emitter() this.initialText = this.textContent - this.tabIndex = -1 + if (this.tabIndex == null) this.tabIndex = -1 this.addEventListener('focus', (event) => this.getComponent().didFocus(event)) this.addEventListener('blur', (event) => this.getComponent().didBlur(event)) } From e276114db0fe55db976ea6805ff832b818bd0e66 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 4 Jan 2018 17:16:23 -0700 Subject: [PATCH 156/176] :arrow_up: bracket-matcher --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f55db38f9..7aeda6190 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "autosave": "0.24.6", "background-tips": "0.27.1", "bookmarks": "0.45.1", - "bracket-matcher": "0.88.2", + "bracket-matcher": "0.88.3", "command-palette": "0.43.0", "dalek": "0.2.1", "deprecation-cop": "0.56.9", From 2d086fe328a02ea44525f7010c6795abe08ef23b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 4 Jan 2018 16:33:03 -0800 Subject: [PATCH 157/176] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7aeda6190..3330769cb 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.10.0", + "text-buffer": "13.10.1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 043f183b1a3202e323d2c7f517cdcc4636be77ba Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2018 09:05:02 -0800 Subject: [PATCH 158/176] Decaffeinate AtomApplication --- src/main-process/atom-application.coffee | 926 --------------- src/main-process/atom-application.js | 1350 ++++++++++++++++++++++ 2 files changed, 1350 insertions(+), 926 deletions(-) delete mode 100644 src/main-process/atom-application.coffee create mode 100644 src/main-process/atom-application.js diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee deleted file mode 100644 index e0d2d691f..000000000 --- a/src/main-process/atom-application.coffee +++ /dev/null @@ -1,926 +0,0 @@ -AtomWindow = require './atom-window' -ApplicationMenu = require './application-menu' -AtomProtocolHandler = require './atom-protocol-handler' -AutoUpdateManager = require './auto-update-manager' -StorageFolder = require '../storage-folder' -Config = require '../config' -FileRecoveryService = require './file-recovery-service' -ipcHelpers = require '../ipc-helpers' -{BrowserWindow, Menu, app, dialog, ipcMain, shell, screen} = require 'electron' -{CompositeDisposable, Disposable} = require 'event-kit' -crypto = require 'crypto' -fs = require 'fs-plus' -path = require 'path' -os = require 'os' -net = require 'net' -url = require 'url' -{EventEmitter} = require 'events' -_ = require 'underscore-plus' -FindParentDir = null -Resolve = null -ConfigSchema = require '../config-schema' - -LocationSuffixRegExp = /(:\d+)(:\d+)?$/ - -# The application's singleton class. -# -# It's the entry point into the Atom application and maintains the global state -# of the application. -# -module.exports = -class AtomApplication - Object.assign @prototype, EventEmitter.prototype - - # Public: The entry point into the Atom application. - @open: (options) -> - unless options.socketPath? - username = if process.platform is 'win32' then process.env.USERNAME else process.env.USER - # Lowercasing the ATOM_HOME to make sure that we don't get multiple sockets - # on case-insensitive filesystems due to arbitrary case differences in paths. - atomHomeUnique = path.resolve(process.env.ATOM_HOME).toLowerCase() - hash = crypto.createHash('sha1').update(options.version).update('|').update(process.arch).update('|').update(username).update('|').update(atomHomeUnique) - # We only keep the first 12 characters of the hash as not to have excessively long - # socket file. Note that macOS/BSD limit the length of socket file paths (see #15081). - # The replace calls convert the digest into "URL and Filename Safe" encoding (see RFC 4648). - atomInstanceDigest = hash.digest('base64').substring(0, 12).replace(/\+/g, '-').replace(/\//g, '_') - if process.platform is 'win32' - options.socketPath = "\\\\.\\pipe\\atom-#{atomInstanceDigest}-sock" - else - options.socketPath = path.join(os.tmpdir(), "atom-#{atomInstanceDigest}.sock") - - # FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely - # take a few seconds to trigger 'error' event, it could be a bug of node - # or atom-shell, before it's fixed we check the existence of socketPath to - # speedup startup. - if (process.platform isnt 'win32' and not fs.existsSync options.socketPath) or options.test or options.benchmark or options.benchmarkTest - new AtomApplication(options).initialize(options) - return - - client = net.connect {path: options.socketPath}, -> - client.write JSON.stringify(options), -> - client.end() - app.quit() - - client.on 'error', -> new AtomApplication(options).initialize(options) - - windows: null - applicationMenu: null - atomProtocolHandler: null - resourcePath: null - version: null - quitting: false - - exit: (status) -> app.exit(status) - - constructor: (options) -> - {@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, @logFile, @userDataDir} = options - @socketPath = null if options.test or options.benchmark or options.benchmarkTest - @pidsToOpenWindows = {} - @windowStack = new WindowStack() - - @config = new Config({enablePersistence: true}) - @config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)} - ConfigSchema.projectHome = { - type: 'string', - default: path.join(fs.getHomeDirectory(), 'github'), - description: 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.' - } - @config.initialize({configDirPath: process.env.ATOM_HOME, @resourcePath, projectHomeSchema: ConfigSchema.projectHome}) - @config.load() - @fileRecoveryService = new FileRecoveryService(path.join(process.env.ATOM_HOME, "recovery")) - @storageFolder = new StorageFolder(process.env.ATOM_HOME) - @autoUpdateManager = new AutoUpdateManager( - @version, - options.test or options.benchmark or options.benchmarkTest, - @config - ) - - @disposable = new CompositeDisposable - @handleEvents() - - # This stuff was previously done in the constructor, but we want to be able to construct this object - # for testing purposes without booting up the world. As you add tests, feel free to move instantiation - # of these various sub-objects into the constructor, but you'll need to remove the side-effects they - # perform during their construction, adding an initialize method that you call here. - initialize: (options) -> - global.atomApplication = this - - # DEPRECATED: This can be removed at some point (added in 1.13) - # It converts `useCustomTitleBar: true` to `titleBar: "custom"` - if process.platform is 'darwin' and @config.get('core.useCustomTitleBar') - @config.unset('core.useCustomTitleBar') - @config.set('core.titleBar', 'custom') - - @config.onDidChange 'core.titleBar', @promptForRestart.bind(this) - - process.nextTick => @autoUpdateManager.initialize() - @applicationMenu = new ApplicationMenu(@version, @autoUpdateManager) - @atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode) - - @listenForArgumentsFromNewProcess() - @setupDockMenu() - - @launch(options) - - destroy: -> - windowsClosePromises = @getAllWindows().map (window) -> - window.close() - window.closedPromise - Promise.all(windowsClosePromises).then(=> @disposable.dispose()) - - launch: (options) -> - if options.test or options.benchmark or options.benchmarkTest - @openWithOptions(options) - else if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0 - if @config.get('core.restorePreviousWindowsOnStart') is 'always' - @loadState(_.deepClone(options)) - @openWithOptions(options) - else - @loadState(options) or @openPath(options) - - openWithOptions: (options) -> - { - initialPaths, pathsToOpen, executedFrom, urlsToOpen, benchmark, - benchmarkTest, test, pidToKillWhenClosed, devMode, safeMode, newWindow, - logFile, profileStartup, timeout, clearWindowState, addToLastWindow, env - } = options - - app.focus() - - if test - @runTests({ - headless: true, devMode, @resourcePath, executedFrom, pathsToOpen, - logFile, timeout, env - }) - else if benchmark or benchmarkTest - @runBenchmarks({headless: true, test: benchmarkTest, @resourcePath, executedFrom, pathsToOpen, timeout, env}) - else if pathsToOpen.length > 0 - @openPaths({ - initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, - devMode, safeMode, profileStartup, clearWindowState, addToLastWindow, env - }) - else if urlsToOpen.length > 0 - for urlToOpen in urlsToOpen - @openUrl({urlToOpen, devMode, safeMode, env}) - else - # Always open a editor window if this is the first instance of Atom. - @openPath({ - initialPaths, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, - clearWindowState, addToLastWindow, env - }) - - # Public: Removes the {AtomWindow} from the global window list. - removeWindow: (window) -> - @windowStack.removeWindow(window) - if @getAllWindows().length is 0 - @applicationMenu?.enableWindowSpecificItems(false) - if process.platform in ['win32', 'linux'] - app.quit() - return - @saveState(true) unless window.isSpec - - # Public: Adds the {AtomWindow} to the global window list. - addWindow: (window) -> - @windowStack.addWindow(window) - @applicationMenu?.addWindow(window.browserWindow) - window.once 'window:loaded', => - @autoUpdateManager?.emitUpdateAvailableEvent(window) - - unless window.isSpec - focusHandler = => @windowStack.touch(window) - blurHandler = => @saveState(false) - window.browserWindow.on 'focus', focusHandler - window.browserWindow.on 'blur', blurHandler - window.browserWindow.once 'closed', => - @windowStack.removeWindow(window) - window.browserWindow.removeListener 'focus', focusHandler - window.browserWindow.removeListener 'blur', blurHandler - window.browserWindow.webContents.once 'did-finish-load', => @saveState(false) - - getAllWindows: => - @windowStack.all().slice() - - getLastFocusedWindow: (predicate) => - @windowStack.getLastFocusedWindow(predicate) - - # Creates server to listen for additional atom application launches. - # - # You can run the atom command multiple times, but after the first launch - # the other launches will just pass their information to this server and then - # close immediately. - listenForArgumentsFromNewProcess: -> - return unless @socketPath? - @deleteSocketFile() - server = net.createServer (connection) => - data = '' - connection.on 'data', (chunk) -> - data = data + chunk - - connection.on 'end', => - options = JSON.parse(data) - @openWithOptions(options) - - server.listen @socketPath - server.on 'error', (error) -> console.error 'Application server failed', error - - deleteSocketFile: -> - return if process.platform is 'win32' or not @socketPath? - - if fs.existsSync(@socketPath) - try - fs.unlinkSync(@socketPath) - catch error - # Ignore ENOENT errors in case the file was deleted between the exists - # check and the call to unlink sync. This occurred occasionally on CI - # which is why this check is here. - throw error unless error.code is 'ENOENT' - - # Registers basic application commands, non-idempotent. - handleEvents: -> - getLoadSettings = => - devMode: @focusedWindow()?.devMode - safeMode: @focusedWindow()?.safeMode - - @on 'application:quit', -> app.quit() - @on 'application:new-window', -> @openPath(getLoadSettings()) - @on 'application:new-file', -> (@focusedWindow() ? this).openPath() - @on 'application:open-dev', -> @promptForPathToOpen('all', devMode: true) - @on 'application:open-safe', -> @promptForPathToOpen('all', safeMode: true) - @on 'application:inspect', ({x, y, atomWindow}) -> - atomWindow ?= @focusedWindow() - atomWindow?.browserWindow.inspectElement(x, y) - - @on 'application:open-documentation', -> shell.openExternal('http://flight-manual.atom.io/') - @on 'application:open-discussions', -> shell.openExternal('https://discuss.atom.io') - @on 'application:open-faq', -> shell.openExternal('https://atom.io/faq') - @on 'application:open-terms-of-use', -> shell.openExternal('https://atom.io/terms') - @on 'application:report-issue', -> shell.openExternal('https://github.com/atom/atom/blob/master/CONTRIBUTING.md#reporting-bugs') - @on 'application:search-issues', -> shell.openExternal('https://github.com/search?q=+is%3Aissue+user%3Aatom') - - @on 'application:install-update', => - @quitting = true - @autoUpdateManager.install() - - @on 'application:check-for-update', => @autoUpdateManager.check() - - if process.platform is 'darwin' - @on 'application:bring-all-windows-to-front', -> Menu.sendActionToFirstResponder('arrangeInFront:') - @on 'application:hide', -> Menu.sendActionToFirstResponder('hide:') - @on 'application:hide-other-applications', -> Menu.sendActionToFirstResponder('hideOtherApplications:') - @on 'application:minimize', -> Menu.sendActionToFirstResponder('performMiniaturize:') - @on 'application:unhide-all-applications', -> Menu.sendActionToFirstResponder('unhideAllApplications:') - @on 'application:zoom', -> Menu.sendActionToFirstResponder('zoom:') - else - @on 'application:minimize', -> @focusedWindow()?.minimize() - @on 'application:zoom', -> @focusedWindow()?.maximize() - - @openPathOnEvent('application:about', 'atom://about') - @openPathOnEvent('application:show-settings', 'atom://config') - @openPathOnEvent('application:open-your-config', 'atom://.atom/config') - @openPathOnEvent('application:open-your-init-script', 'atom://.atom/init-script') - @openPathOnEvent('application:open-your-keymap', 'atom://.atom/keymap') - @openPathOnEvent('application:open-your-snippets', 'atom://.atom/snippets') - @openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet') - @openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md')) - - @disposable.add ipcHelpers.on app, 'before-quit', (event) => - resolveBeforeQuitPromise = null - @lastBeforeQuitPromise = new Promise((resolve) -> resolveBeforeQuitPromise = resolve) - if @quitting - resolveBeforeQuitPromise() - else - event.preventDefault() - @quitting = true - windowUnloadPromises = @getAllWindows().map((window) -> window.prepareToUnload()) - Promise.all(windowUnloadPromises).then((windowUnloadedResults) -> - didUnloadAllWindows = windowUnloadedResults.every((didUnloadWindow) -> didUnloadWindow) - app.quit() if didUnloadAllWindows - resolveBeforeQuitPromise() - ) - - @disposable.add ipcHelpers.on app, 'will-quit', => - @killAllProcesses() - @deleteSocketFile() - - @disposable.add ipcHelpers.on app, 'open-file', (event, pathToOpen) => - event.preventDefault() - @openPath({pathToOpen}) - - @disposable.add ipcHelpers.on app, 'open-url', (event, urlToOpen) => - event.preventDefault() - @openUrl({urlToOpen, @devMode, @safeMode}) - - @disposable.add ipcHelpers.on app, 'activate', (event, hasVisibleWindows) => - unless hasVisibleWindows - event?.preventDefault() - @emit('application:new-window') - - @disposable.add ipcHelpers.on ipcMain, 'restart-application', => - @restart() - - @disposable.add ipcHelpers.on ipcMain, 'resolve-proxy', (event, requestId, url) -> - event.sender.session.resolveProxy url, (proxy) -> - unless event.sender.isDestroyed() - event.sender.send('did-resolve-proxy', requestId, proxy) - - @disposable.add ipcHelpers.on ipcMain, 'did-change-history-manager', (event) => - for atomWindow in @getAllWindows() - webContents = atomWindow.browserWindow.webContents - if webContents isnt event.sender - webContents.send('did-change-history-manager') - - # A request from the associated render process to open a new render process. - @disposable.add ipcHelpers.on ipcMain, 'open', (event, options) => - window = @atomWindowForEvent(event) - if options? - if typeof options.pathsToOpen is 'string' - options.pathsToOpen = [options.pathsToOpen] - if options.pathsToOpen?.length > 0 - options.window = window - @openPaths(options) - else - new AtomWindow(this, @fileRecoveryService, options) - else - @promptForPathToOpen('all', {window}) - - @disposable.add ipcHelpers.on ipcMain, 'update-application-menu', (event, template, keystrokesByCommand) => - win = BrowserWindow.fromWebContents(event.sender) - @applicationMenu?.update(win, template, keystrokesByCommand) - - @disposable.add ipcHelpers.on ipcMain, 'run-package-specs', (event, packageSpecPath) => - @runTests({resourcePath: @devResourcePath, pathsToOpen: [packageSpecPath], headless: false}) - - @disposable.add ipcHelpers.on ipcMain, 'run-benchmarks', (event, benchmarksPath) => - @runBenchmarks({resourcePath: @devResourcePath, pathsToOpen: [benchmarksPath], headless: false, test: false}) - - @disposable.add ipcHelpers.on ipcMain, 'command', (event, command) => - @emit(command) - - @disposable.add ipcHelpers.on ipcMain, 'open-command', (event, command, args...) => - defaultPath = args[0] if args.length > 0 - switch command - when 'application:open' then @promptForPathToOpen('all', getLoadSettings(), defaultPath) - when 'application:open-file' then @promptForPathToOpen('file', getLoadSettings(), defaultPath) - when 'application:open-folder' then @promptForPathToOpen('folder', getLoadSettings(), defaultPath) - else console.log "Invalid open-command received: " + command - - @disposable.add ipcHelpers.on ipcMain, 'window-command', (event, command, args...) -> - win = BrowserWindow.fromWebContents(event.sender) - win.emit(command, args...) - - @disposable.add ipcHelpers.respondTo 'window-method', (browserWindow, method, args...) => - @atomWindowForBrowserWindow(browserWindow)?[method](args...) - - @disposable.add ipcHelpers.on ipcMain, 'pick-folder', (event, responseChannel) => - @promptForPath "folder", (selectedPaths) -> - event.sender.send(responseChannel, selectedPaths) - - @disposable.add ipcHelpers.respondTo 'set-window-size', (win, width, height) -> - win.setSize(width, height) - - @disposable.add ipcHelpers.respondTo 'set-window-position', (win, x, y) -> - win.setPosition(x, y) - - @disposable.add ipcHelpers.respondTo 'center-window', (win) -> - win.center() - - @disposable.add ipcHelpers.respondTo 'focus-window', (win) -> - win.focus() - - @disposable.add ipcHelpers.respondTo 'show-window', (win) -> - win.show() - - @disposable.add ipcHelpers.respondTo 'hide-window', (win) -> - win.hide() - - @disposable.add ipcHelpers.respondTo 'get-temporary-window-state', (win) -> - win.temporaryState - - @disposable.add ipcHelpers.respondTo 'set-temporary-window-state', (win, state) -> - win.temporaryState = state - - clipboard = require '../safe-clipboard' - @disposable.add ipcHelpers.on ipcMain, 'write-text-to-selection-clipboard', (event, selectedText) -> - clipboard.writeText(selectedText, 'selection') - - @disposable.add ipcHelpers.on ipcMain, 'write-to-stdout', (event, output) -> - process.stdout.write(output) - - @disposable.add ipcHelpers.on ipcMain, 'write-to-stderr', (event, output) -> - process.stderr.write(output) - - @disposable.add ipcHelpers.on ipcMain, 'add-recent-document', (event, filename) -> - app.addRecentDocument(filename) - - @disposable.add ipcHelpers.on ipcMain, 'execute-javascript-in-dev-tools', (event, code) -> - event.sender.devToolsWebContents?.executeJavaScript(code) - - @disposable.add ipcHelpers.on ipcMain, 'get-auto-update-manager-state', (event) => - event.returnValue = @autoUpdateManager.getState() - - @disposable.add ipcHelpers.on ipcMain, 'get-auto-update-manager-error', (event) => - event.returnValue = @autoUpdateManager.getErrorMessage() - - @disposable.add ipcHelpers.on ipcMain, 'will-save-path', (event, path) => - @fileRecoveryService.willSavePath(@atomWindowForEvent(event), path) - event.returnValue = true - - @disposable.add ipcHelpers.on ipcMain, 'did-save-path', (event, path) => - @fileRecoveryService.didSavePath(@atomWindowForEvent(event), path) - event.returnValue = true - - @disposable.add ipcHelpers.on ipcMain, 'did-change-paths', => - @saveState(false) - - @disposable.add(@disableZoomOnDisplayChange()) - - setupDockMenu: -> - if process.platform is 'darwin' - dockMenu = Menu.buildFromTemplate [ - {label: 'New Window', click: => @emit('application:new-window')} - ] - app.dock.setMenu dockMenu - - # Public: Executes the given command. - # - # If it isn't handled globally, delegate to the currently focused window. - # - # command - The string representing the command. - # args - The optional arguments to pass along. - sendCommand: (command, args...) -> - unless @emit(command, args...) - focusedWindow = @focusedWindow() - if focusedWindow? - focusedWindow.sendCommand(command, args...) - else - @sendCommandToFirstResponder(command) - - # Public: Executes the given command on the given window. - # - # command - The string representing the command. - # atomWindow - The {AtomWindow} to send the command to. - # args - The optional arguments to pass along. - sendCommandToWindow: (command, atomWindow, args...) -> - unless @emit(command, args...) - if atomWindow? - atomWindow.sendCommand(command, args...) - else - @sendCommandToFirstResponder(command) - - # Translates the command into macOS action and sends it to application's first - # responder. - sendCommandToFirstResponder: (command) -> - return false unless process.platform is 'darwin' - - switch command - when 'core:undo' then Menu.sendActionToFirstResponder('undo:') - when 'core:redo' then Menu.sendActionToFirstResponder('redo:') - when 'core:copy' then Menu.sendActionToFirstResponder('copy:') - when 'core:cut' then Menu.sendActionToFirstResponder('cut:') - when 'core:paste' then Menu.sendActionToFirstResponder('paste:') - when 'core:select-all' then Menu.sendActionToFirstResponder('selectAll:') - else return false - true - - # Public: Open the given path in the focused window when the event is - # triggered. - # - # A new window will be created if there is no currently focused window. - # - # eventName - The event to listen for. - # pathToOpen - The path to open when the event is triggered. - openPathOnEvent: (eventName, pathToOpen) -> - @on eventName, -> - if window = @focusedWindow() - window.openPath(pathToOpen) - else - @openPath({pathToOpen}) - - # Returns the {AtomWindow} for the given paths. - windowForPaths: (pathsToOpen, devMode) -> - _.find @getAllWindows(), (atomWindow) -> - atomWindow.devMode is devMode and atomWindow.containsPaths(pathsToOpen) - - # Returns the {AtomWindow} for the given ipcMain event. - atomWindowForEvent: ({sender}) -> - @atomWindowForBrowserWindow(BrowserWindow.fromWebContents(sender)) - - atomWindowForBrowserWindow: (browserWindow) -> - @getAllWindows().find((atomWindow) -> atomWindow.browserWindow is browserWindow) - - # Public: Returns the currently focused {AtomWindow} or undefined if none. - focusedWindow: -> - _.find @getAllWindows(), (atomWindow) -> atomWindow.isFocused() - - # Get the platform-specific window offset for new windows. - getWindowOffsetForCurrentPlatform: -> - offsetByPlatform = - darwin: 22 - win32: 26 - offsetByPlatform[process.platform] ? 0 - - # Get the dimensions for opening a new window by cascading as appropriate to - # the platform. - getDimensionsForNewWindow: -> - return if (@focusedWindow() ? @getLastFocusedWindow())?.isMaximized() - dimensions = (@focusedWindow() ? @getLastFocusedWindow())?.getDimensions() - offset = @getWindowOffsetForCurrentPlatform() - if dimensions? and offset? - dimensions.x += offset - dimensions.y += offset - dimensions - - # Public: Opens a single path, in an existing window if possible. - # - # options - - # :pathToOpen - The file path to open - # :pidToKillWhenClosed - The integer of the pid to kill - # :newWindow - Boolean of whether this should be opened in a new window. - # :devMode - Boolean to control the opened window's dev mode. - # :safeMode - Boolean to control the opened window's safe mode. - # :profileStartup - Boolean to control creating a profile of the startup time. - # :window - {AtomWindow} to open file paths in. - # :addToLastWindow - Boolean of whether this should be opened in last focused window. - openPath: ({initialPaths, pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow, env} = {}) -> - @openPaths({initialPaths, pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow, env}) - - # Public: Opens multiple paths, in existing windows if possible. - # - # options - - # :pathsToOpen - The array of file paths to open - # :pidToKillWhenClosed - The integer of the pid to kill - # :newWindow - Boolean of whether this should be opened in a new window. - # :devMode - Boolean to control the opened window's dev mode. - # :safeMode - Boolean to control the opened window's safe mode. - # :windowDimensions - Object with height and width keys. - # :window - {AtomWindow} to open file paths in. - # :addToLastWindow - Boolean of whether this should be opened in last focused window. - openPaths: ({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window, clearWindowState, addToLastWindow, env}={}) -> - if not pathsToOpen? or pathsToOpen.length is 0 - return - env = process.env unless env? - devMode = Boolean(devMode) - safeMode = Boolean(safeMode) - clearWindowState = Boolean(clearWindowState) - locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom, addToLastWindow) for pathToOpen in pathsToOpen) - pathsToOpen = (locationToOpen.pathToOpen for locationToOpen in locationsToOpen) - - unless pidToKillWhenClosed or newWindow - existingWindow = @windowForPaths(pathsToOpen, devMode) - stats = (fs.statSyncNoException(pathToOpen) for pathToOpen in pathsToOpen) - unless existingWindow? - if currentWindow = window ? @getLastFocusedWindow() - existingWindow = currentWindow if ( - addToLastWindow or - currentWindow.devMode is devMode and - ( - stats.every((stat) -> stat.isFile?()) or - stats.some((stat) -> stat.isDirectory?() and not currentWindow.hasProjectPath()) - ) - ) - - if existingWindow? - openedWindow = existingWindow - openedWindow.openLocations(locationsToOpen) - if openedWindow.isMinimized() - openedWindow.restore() - else - openedWindow.focus() - openedWindow.replaceEnvironment(env) - else - if devMode - try - windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window')) - resourcePath = @devResourcePath - - windowInitializationScript ?= require.resolve('../initialize-application-window') - resourcePath ?= @resourcePath - windowDimensions ?= @getDimensionsForNewWindow() - openedWindow = new AtomWindow(this, @fileRecoveryService, {initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env}) - openedWindow.focus() - @windowStack.addWindow(openedWindow) - - if pidToKillWhenClosed? - @pidsToOpenWindows[pidToKillWhenClosed] = openedWindow - - openedWindow.browserWindow.once 'closed', => - @killProcessForWindow(openedWindow) - - openedWindow - - # Kill all processes associated with opened windows. - killAllProcesses: -> - @killProcess(pid) for pid of @pidsToOpenWindows - return - - # Kill process associated with the given opened window. - killProcessForWindow: (openedWindow) -> - for pid, trackedWindow of @pidsToOpenWindows - @killProcess(pid) if trackedWindow is openedWindow - return - - # Kill the process with the given pid. - killProcess: (pid) -> - try - parsedPid = parseInt(pid) - process.kill(parsedPid) if isFinite(parsedPid) - catch error - if error.code isnt 'ESRCH' - console.log("Killing process #{pid} failed: #{error.code ? error.message}") - delete @pidsToOpenWindows[pid] - - saveState: (allowEmpty=false) -> - return if @quitting - states = [] - for window in @getAllWindows() - unless window.isSpec - states.push({initialPaths: window.representedDirectoryPaths}) - states.reverse() - if states.length > 0 or allowEmpty - @storageFolder.storeSync('application.json', states) - @emit('application:did-save-state') - - loadState: (options) -> - if (@config.get('core.restorePreviousWindowsOnStart') in ['yes', 'always']) and (states = @storageFolder.load('application.json'))?.length > 0 - for state in states - @openWithOptions(Object.assign(options, { - initialPaths: state.initialPaths - pathsToOpen: state.initialPaths.filter (directoryPath) -> fs.isDirectorySync(directoryPath) - urlsToOpen: [] - devMode: @devMode - safeMode: @safeMode - })) - else - null - - # Open an atom:// url. - # - # The host of the URL being opened is assumed to be the package name - # responsible for opening the URL. A new window will be created with - # that package's `urlMain` as the bootstrap script. - # - # options - - # :urlToOpen - The atom:// url to open. - # :devMode - Boolean to control the opened window's dev mode. - # :safeMode - Boolean to control the opened window's safe mode. - openUrl: ({urlToOpen, devMode, safeMode, env}) -> - parsedUrl = url.parse(urlToOpen, true) - return unless parsedUrl.protocol is "atom:" - - pack = @findPackageWithName(parsedUrl.host, devMode) - if pack?.urlMain - @openPackageUrlMain(parsedUrl.host, pack.urlMain, urlToOpen, devMode, safeMode, env) - else - @openPackageUriHandler(urlToOpen, parsedUrl, devMode, safeMode, env) - - openPackageUriHandler: (url, parsedUrl, devMode, safeMode, env) -> - bestWindow = null - if parsedUrl.host is 'core' - predicate = require('../core-uri-handlers').windowPredicate(parsedUrl) - bestWindow = @getLastFocusedWindow (win) -> - not win.isSpecWindow() and predicate(win) - - bestWindow ?= @getLastFocusedWindow (win) -> not win.isSpecWindow() - if bestWindow? - bestWindow.sendURIMessage url - bestWindow.focus() - else - resourcePath = @resourcePath - if devMode - try - windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window')) - resourcePath = @devResourcePath - - windowInitializationScript ?= require.resolve('../initialize-application-window') - windowDimensions = @getDimensionsForNewWindow() - win = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env}) - @windowStack.addWindow(win) - win.on 'window:loaded', -> - win.sendURIMessage url - - findPackageWithName: (packageName, devMode) -> - _.find @getPackageManager(devMode).getAvailablePackageMetadata(), ({name}) -> name is packageName - - openPackageUrlMain: (packageName, packageUrlMain, urlToOpen, devMode, safeMode, env) -> - packagePath = @getPackageManager(devMode).resolvePackagePath(packageName) - windowInitializationScript = path.resolve(packagePath, packageUrlMain) - windowDimensions = @getDimensionsForNewWindow() - new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env}) - - getPackageManager: (devMode) -> - unless @packages? - PackageManager = require '../package-manager' - @packages = new PackageManager({}) - @packages.initialize - configDirPath: process.env.ATOM_HOME - devMode: devMode - resourcePath: @resourcePath - - @packages - - - # Opens up a new {AtomWindow} to run specs within. - # - # options - - # :headless - A Boolean that, if true, will close the window upon - # completion. - # :resourcePath - The path to include specs from. - # :specPath - The directory to load specs from. - # :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages - # and ~/.atom/dev/packages, defaults to false. - runTests: ({headless, resourcePath, executedFrom, pathsToOpen, logFile, safeMode, timeout, env}) -> - if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath) - resourcePath = @resourcePath - - timeoutInSeconds = Number.parseFloat(timeout) - unless Number.isNaN(timeoutInSeconds) - timeoutHandler = -> - console.log "The test suite has timed out because it has been running for more than #{timeoutInSeconds} seconds." - process.exit(124) # Use the same exit code as the UNIX timeout util. - setTimeout(timeoutHandler, timeoutInSeconds * 1000) - - try - windowInitializationScript = require.resolve(path.resolve(@devResourcePath, 'src', 'initialize-test-window')) - catch error - windowInitializationScript = require.resolve(path.resolve(__dirname, '..', '..', 'src', 'initialize-test-window')) - - testPaths = [] - if pathsToOpen? - for pathToOpen in pathsToOpen - testPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen))) - - if testPaths.length is 0 - process.stderr.write 'Error: Specify at least one test path\n\n' - process.exit(1) - - legacyTestRunnerPath = @resolveLegacyTestRunnerPath() - testRunnerPath = @resolveTestRunnerPath(testPaths[0]) - devMode = true - isSpec = true - safeMode ?= false - new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode, env}) - - runBenchmarks: ({headless, test, resourcePath, executedFrom, pathsToOpen, env}) -> - if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath) - resourcePath = @resourcePath - - try - windowInitializationScript = require.resolve(path.resolve(@devResourcePath, 'src', 'initialize-benchmark-window')) - catch error - windowInitializationScript = require.resolve(path.resolve(__dirname, '..', '..', 'src', 'initialize-benchmark-window')) - - benchmarkPaths = [] - if pathsToOpen? - for pathToOpen in pathsToOpen - benchmarkPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen))) - - if benchmarkPaths.length is 0 - process.stderr.write 'Error: Specify at least one benchmark path.\n\n' - process.exit(1) - - devMode = true - isSpec = true - safeMode = false - new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, resourcePath, headless, test, isSpec, devMode, benchmarkPaths, safeMode, env}) - - resolveTestRunnerPath: (testPath) -> - FindParentDir ?= require 'find-parent-dir' - - if packageRoot = FindParentDir.sync(testPath, 'package.json') - packageMetadata = require(path.join(packageRoot, 'package.json')) - if packageMetadata.atomTestRunner - Resolve ?= require('resolve') - if testRunnerPath = Resolve.sync(packageMetadata.atomTestRunner, basedir: packageRoot, extensions: Object.keys(require.extensions)) - return testRunnerPath - else - process.stderr.write "Error: Could not resolve test runner path '#{packageMetadata.atomTestRunner}'" - process.exit(1) - - @resolveLegacyTestRunnerPath() - - resolveLegacyTestRunnerPath: -> - try - require.resolve(path.resolve(@devResourcePath, 'spec', 'jasmine-test-runner')) - catch error - require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner')) - - locationForPathToOpen: (pathToOpen, executedFrom='', forceAddToWindow) -> - return {pathToOpen} unless pathToOpen - - pathToOpen = pathToOpen.replace(/[:\s]+$/, '') - match = pathToOpen.match(LocationSuffixRegExp) - - if match? - pathToOpen = pathToOpen.slice(0, -match[0].length) - initialLine = Math.max(0, parseInt(match[1].slice(1)) - 1) if match[1] - initialColumn = Math.max(0, parseInt(match[2].slice(1)) - 1) if match[2] - else - initialLine = initialColumn = null - - unless url.parse(pathToOpen).protocol? - pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen)) - - {pathToOpen, initialLine, initialColumn, forceAddToWindow} - - # Opens a native dialog to prompt the user for a path. - # - # Once paths are selected, they're opened in a new or existing {AtomWindow}s. - # - # options - - # :type - A String which specifies the type of the dialog, could be 'file', - # 'folder' or 'all'. The 'all' is only available on macOS. - # :devMode - A Boolean which controls whether any newly opened windows - # should be in dev mode or not. - # :safeMode - A Boolean which controls whether any newly opened windows - # should be in safe mode or not. - # :window - An {AtomWindow} to use for opening a selected file path. - # :path - An optional String which controls the default path to which the - # file dialog opens. - promptForPathToOpen: (type, {devMode, safeMode, window}, path=null) -> - @promptForPath type, ((pathsToOpen) => - @openPaths({pathsToOpen, devMode, safeMode, window})), path - - promptForPath: (type, callback, path) -> - properties = - switch type - when 'file' then ['openFile'] - when 'folder' then ['openDirectory'] - when 'all' then ['openFile', 'openDirectory'] - else throw new Error("#{type} is an invalid type for promptForPath") - - # Show the open dialog as child window on Windows and Linux, and as - # independent dialog on macOS. This matches most native apps. - parentWindow = - if process.platform is 'darwin' - null - else - BrowserWindow.getFocusedWindow() - - openOptions = - properties: properties.concat(['multiSelections', 'createDirectory']) - title: switch type - when 'file' then 'Open File' - when 'folder' then 'Open Folder' - else 'Open' - - # File dialog defaults to project directory of currently active editor - if path? - openOptions.defaultPath = path - - dialog.showOpenDialog(parentWindow, openOptions, callback) - - promptForRestart: -> - chosen = dialog.showMessageBox BrowserWindow.getFocusedWindow(), - type: 'warning' - title: 'Restart required' - message: "You will need to restart Atom for this change to take effect." - buttons: ['Restart Atom', 'Cancel'] - if chosen is 0 - @restart() - - restart: -> - args = [] - args.push("--safe") if @safeMode - args.push("--log-file=#{@logFile}") if @logFile? - args.push("--socket-path=#{@socketPath}") if @socketPath? - args.push("--user-data-dir=#{@userDataDir}") if @userDataDir? - if @devMode - args.push('--dev') - args.push("--resource-path=#{@resourcePath}") - app.relaunch({args}) - app.quit() - - disableZoomOnDisplayChange: -> - outerCallback = => - for window in @getAllWindows() - window.disableZoom() - - # Set the limits every time a display is added or removed, otherwise the - # configuration gets reset to the default, which allows zooming the - # webframe. - screen.on('display-added', outerCallback) - screen.on('display-removed', outerCallback) - new Disposable -> - screen.removeListener('display-added', outerCallback) - screen.removeListener('display-removed', outerCallback) - -class WindowStack - constructor: (@windows = []) -> - - addWindow: (window) => - @removeWindow(window) - @windows.unshift(window) - - touch: (window) => - @addWindow(window) - - removeWindow: (window) => - currentIndex = @windows.indexOf(window) - @windows.splice(currentIndex, 1) if currentIndex > -1 - - getLastFocusedWindow: (predicate) => - predicate ?= (win) -> true - @windows.find(predicate) - - all: => - @windows diff --git a/src/main-process/atom-application.js b/src/main-process/atom-application.js new file mode 100644 index 000000000..1797d5090 --- /dev/null +++ b/src/main-process/atom-application.js @@ -0,0 +1,1350 @@ +const AtomWindow = require('./atom-window') +const ApplicationMenu = require('./application-menu') +const AtomProtocolHandler = require('./atom-protocol-handler') +const AutoUpdateManager = require('./auto-update-manager') +const StorageFolder = require('../storage-folder') +const Config = require('../config') +const FileRecoveryService = require('./file-recovery-service') +const ipcHelpers = require('../ipc-helpers') +const {BrowserWindow, Menu, app, dialog, ipcMain, shell, screen} = require('electron') +const {CompositeDisposable, Disposable} = require('event-kit') +const crypto = require('crypto') +const fs = require('fs-plus') +const path = require('path') +const os = require('os') +const net = require('net') +const url = require('url') +const {EventEmitter} = require('events') +const _ = require('underscore-plus') +let FindParentDir = null +let Resolve = null +const ConfigSchema = require('../config-schema') + +const LocationSuffixRegExp = /(:\d+)(:\d+)?$/ + +// The application's singleton class. +// +// It's the entry point into the Atom application and maintains the global state +// of the application. +// +module.exports = +class AtomApplication extends EventEmitter { + // Public: The entry point into the Atom application. + static open (options) { + if (!options.socketPath) { + const username = process.platform === 'win32' ? process.env.USERNAME : process.env.USER + + // Lowercasing the ATOM_HOME to make sure that we don't get multiple sockets + // on case-insensitive filesystems due to arbitrary case differences in paths. + const atomHomeUnique = path.resolve(process.env.ATOM_HOME).toLowerCase() + const hash = crypto + .createHash('sha1') + .update(options.version) + .update('|') + .update(process.arch) + .update('|') + .update(username) + .update('|') + .update(atomHomeUnique) + + // We only keep the first 12 characters of the hash as not to have excessively long + // socket file. Note that macOS/BSD limit the length of socket file paths (see #15081). + // The replace calls convert the digest into "URL and Filename Safe" encoding (see RFC 4648). + const atomInstanceDigest = hash + .digest('base64') + .substring(0, 12) + .replace(/\+/g, '-') + .replace(/\//g, '_') + + if (process.platform === 'win32') { + options.socketPath = `\\\\.\\pipe\\atom-${atomInstanceDigest}-sock` + } else { + options.socketPath = path.join(os.tmpdir(), `atom-${atomInstanceDigest}.sock`) + } + } + + // FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely + // take a few seconds to trigger 'error' event, it could be a bug of node + // or electron, before it's fixed we check the existence of socketPath to + // speedup startup. + if ((process.platform !== 'win32' && !fs.existsSync(options.socketPath)) || + options.test || options.benchmark || options.benchmarkTest) { + new AtomApplication(options).initialize(options) + return + } + + const client = net.connect({path: options.socketPath}, () => { + client.write(JSON.stringify(options), () => { + client.end() + app.quit() + }) + }) + + client.on('error', () => new AtomApplication(options).initialize(options)) + } + + exit (status) { + app.exit(status) + } + + constructor (options) { + super() + this.quitting = false + this.getAllWindows = this.getAllWindows.bind(this) + this.getLastFocusedWindow = this.getLastFocusedWindow.bind(this) + + this.resourcePath = options.resourcePath + this.devResourcePath = options.devResourcePath + this.version = options.version + this.devMode = options.devMode + this.safeMode = options.safeMode + this.socketPath = options.socketPath + this.logFile = options.logFile + this.userDataDir = options.userDataDir + if (options.test || options.benchmark || options.benchmarkTest) this.socketPath = null + + this.pidsToOpenWindows = {} + this.windowStack = new WindowStack() + + this.config = new Config({enablePersistence: true}) + this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)}) + ConfigSchema.projectHome = { + type: 'string', + default: path.join(fs.getHomeDirectory(), 'github'), + description: + 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.' + } + this.config.initialize({ + configDirPath: process.env.ATOM_HOME, + resourcePath: this.resourcePath, + projectHomeSchema: ConfigSchema.projectHome + }) + this.config.load() + + this.fileRecoveryService = new FileRecoveryService(path.join(process.env.ATOM_HOME, 'recovery')) + this.storageFolder = new StorageFolder(process.env.ATOM_HOME) + this.autoUpdateManager = new AutoUpdateManager( + this.version, + options.test || options.benchmark || options.benchmarkTest, + this.config + ) + + this.disposable = new CompositeDisposable() + this.handleEvents() + } + + // This stuff was previously done in the constructor, but we want to be able to construct this object + // for testing purposes without booting up the world. As you add tests, feel free to move instantiation + // of these various sub-objects into the constructor, but you'll need to remove the side-effects they + // perform during their construction, adding an initialize method that you call here. + initialize (options) { + global.atomApplication = this + + // DEPRECATED: This can be removed at some point (added in 1.13) + // It converts `useCustomTitleBar: true` to `titleBar: "custom"` + if (process.platform === 'darwin' && this.config.get('core.useCustomTitleBar')) { + this.config.unset('core.useCustomTitleBar') + this.config.set('core.titleBar', 'custom') + } + + this.config.onDidChange('core.titleBar', this.promptForRestart.bind(this)) + + process.nextTick(() => this.autoUpdateManager.initialize()) + this.applicationMenu = new ApplicationMenu(this.version, this.autoUpdateManager) + this.atomProtocolHandler = new AtomProtocolHandler(this.resourcePath, this.safeMode) + + this.listenForArgumentsFromNewProcess() + this.setupDockMenu() + + return this.launch(options) + } + + async destroy () { + const windowsClosePromises = this.getAllWindows().map(window => { + window.close() + return window.closedPromise + }) + await Promise.all(windowsClosePromises) + this.disposable.dispose() + } + + launch (options) { + if (options.test || options.benchmark || options.benchmarkTest) { + return this.openWithOptions(options) + } else if ((options.pathsToOpen && options.pathsToOpen.length > 0) || + (options.urlsToOpen && options.urlsToOpen.length > 0)) { + if (this.config.get('core.restorePreviousWindowsOnStart') === 'always') { + this.loadState(_.deepClone(options)) + } + return this.openWithOptions(options) + } else { + return this.loadState(options) || this.openPath(options) + } + } + + openWithOptions (options) { + const { + initialPaths, + pathsToOpen, + executedFrom, + urlsToOpen, + benchmark, + benchmarkTest, + test, + pidToKillWhenClosed, + devMode, + safeMode, + newWindow, + logFile, + profileStartup, + timeout, + clearWindowState, + addToLastWindow, + env + } = options + + app.focus() + + if (test) { + return this.runTests({ + headless: true, + devMode, + resourcePath: this.resourcePath, + executedFrom, + pathsToOpen, + logFile, + timeout, + env + }) + } else if (benchmark || benchmarkTest) { + return this.runBenchmarks({ + headless: true, + test: benchmarkTest, + resourcePath: this.resourcePath, + executedFrom, + pathsToOpen, + timeout, + env + }) + } else if (pathsToOpen.length > 0) { + return this.openPaths({ + initialPaths, + pathsToOpen, + executedFrom, + pidToKillWhenClosed, + newWindow, + devMode, + safeMode, + profileStartup, + clearWindowState, + addToLastWindow, + env + }) + } else if (urlsToOpen.length > 0) { + return urlsToOpen.map(urlToOpen => this.openUrl({urlToOpen, devMode, safeMode, env})) + } else { + // Always open a editor window if this is the first instance of Atom. + return this.openPath({ + initialPaths, + pidToKillWhenClosed, + newWindow, + devMode, + safeMode, + profileStartup, + clearWindowState, + addToLastWindow, + env + }) + } + } + + // Public: Removes the {AtomWindow} from the global window list. + removeWindow (window) { + this.windowStack.removeWindow(window) + if (this.getAllWindows().length === 0) { + if (this.applicationMenu != null) { + this.applicationMenu.enableWindowSpecificItems(false) + } + if (['win32', 'linux'].includes(process.platform)) { + app.quit() + return + } + } + if (!window.isSpec) this.saveState(true) + } + + // Public: Adds the {AtomWindow} to the global window list. + addWindow (window) { + this.windowStack.addWindow(window) + if (this.applicationMenu) this.applicationMenu.addWindow(window.browserWindow) + + window.once('window:loaded', () => { + this.autoUpdateManager && this.autoUpdateManager.emitUpdateAvailableEvent(window) + }) + + if (!window.isSpec) { + const focusHandler = () => this.windowStack.touch(window) + const blurHandler = () => this.saveState(false) + window.browserWindow.on('focus', focusHandler) + window.browserWindow.on('blur', blurHandler) + window.browserWindow.once('closed', () => { + this.windowStack.removeWindow(window) + window.browserWindow.removeListener('focus', focusHandler) + window.browserWindow.removeListener('blur', blurHandler) + }) + window.browserWindow.webContents.once('did-finish-load', blurHandler) + } + } + + getAllWindows () { + return this.windowStack.all().slice() + } + + getLastFocusedWindow (predicate) { + return this.windowStack.getLastFocusedWindow(predicate) + } + + // Creates server to listen for additional atom application launches. + // + // You can run the atom command multiple times, but after the first launch + // the other launches will just pass their information to this server and then + // close immediately. + listenForArgumentsFromNewProcess () { + if (!this.socketPath) return + + this.deleteSocketFile() + const server = net.createServer(connection => { + let data = '' + connection.on('data', chunk => { data += chunk }) + connection.on('end', () => this.openWithOptions(JSON.parse(data))) + }) + + server.listen(this.socketPath) + server.on('error', error => console.error('Application server failed', error)) + } + + deleteSocketFile () { + if (process.platform === 'win32' || !this.socketPath) return + + if (fs.existsSync(this.socketPath)) { + try { + fs.unlinkSync(this.socketPath) + } catch (error) { + // Ignore ENOENT errors in case the file was deleted between the exists + // check and the call to unlink sync. This occurred occasionally on CI + // which is why this check is here. + if (error.code !== 'ENOENT') throw error + } + } + } + + // Registers basic application commands, non-idempotent. + handleEvents () { + const getLoadSettings = () => { + const window = this.focusedWindow() + return {devMode: window && window.devMode, safeMode: window && window.safeMode} + } + + this.on('application:quit', () => app.quit()) + this.on('application:new-window', () => this.openPath(getLoadSettings())) + this.on('application:new-file', () => (this.focusedWindow() || this).openPath()) + this.on('application:open-dev', () => this.promptForPathToOpen('all', {devMode: true})) + this.on('application:open-safe', () => this.promptForPathToOpen('all', {safeMode: true})) + this.on('application:inspect', ({x, y, atomWindow}) => { + if (!atomWindow) atomWindow = this.focusedWindow() + if (atomWindow) atomWindow.browserWindow.inspectElement(x, y) + }) + + this.on('application:open-documentation', () => shell.openExternal('http://flight-manual.atom.io')) + this.on('application:open-discussions', () => shell.openExternal('https://discuss.atom.io')) + this.on('application:open-faq', () => shell.openExternal('https://atom.io/faq')) + this.on('application:open-terms-of-use', () => shell.openExternal('https://atom.io/terms')) + this.on('application:report-issue', () => shell.openExternal('https://github.com/atom/atom/blob/master/CONTRIBUTING.md#reporting-bugs')) + this.on('application:search-issues', () => shell.openExternal('https://github.com/search?q=+is%3Aissue+user%3Aatom')) + + this.on('application:install-update', () => { + this.quitting = true + this.autoUpdateManager.install() + }) + + this.on('application:check-for-update', () => this.autoUpdateManager.check()) + + if (process.platform === 'darwin') { + this.on('application:bring-all-windows-to-front', () => Menu.sendActionToFirstResponder('arrangeInFront:')) + this.on('application:hide', () => Menu.sendActionToFirstResponder('hide:')) + this.on('application:hide-other-applications', () => Menu.sendActionToFirstResponder('hideOtherApplications:')) + this.on('application:minimize', () => Menu.sendActionToFirstResponder('performMiniaturize:')) + this.on('application:unhide-all-applications', () => Menu.sendActionToFirstResponder('unhideAllApplications:')) + this.on('application:zoom', () => Menu.sendActionToFirstResponder('zoom:')) + } else { + this.on('application:minimize', () => { + const window = this.focusedWindow() + if (window) window.minimize() + }) + this.on('application:zoom', function () { + const window = this.focusedWindow() + if (window) window.maximize() + }) + } + + this.openPathOnEvent('application:about', 'atom://about') + this.openPathOnEvent('application:show-settings', 'atom://config') + this.openPathOnEvent('application:open-your-config', 'atom://.atom/config') + this.openPathOnEvent('application:open-your-init-script', 'atom://.atom/init-script') + this.openPathOnEvent('application:open-your-keymap', 'atom://.atom/keymap') + this.openPathOnEvent('application:open-your-snippets', 'atom://.atom/snippets') + this.openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet') + this.openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md')) + + this.disposable.add(ipcHelpers.on(app, 'before-quit', event => { + let resolveBeforeQuitPromise + this.lastBeforeQuitPromise = new Promise(resolve => { + resolveBeforeQuitPromise = resolve + }) + + if (this.quitting) return resolveBeforeQuitPromise() + + this.quitting = true + event.preventDefault() + const windowUnloadPromises = this.getAllWindows().map(window => window.prepareToUnload()) + return Promise.all(windowUnloadPromises).then(windowUnloadedResults => { + const didUnloadAllWindows = windowUnloadedResults.every(Boolean) + if (didUnloadAllWindows) app.quit() + resolveBeforeQuitPromise() + }) + })) + + this.disposable.add(ipcHelpers.on(app, 'will-quit', () => { + this.killAllProcesses() + this.deleteSocketFile() + })) + + this.disposable.add(ipcHelpers.on(app, 'open-file', (event, pathToOpen) => { + event.preventDefault() + this.openPath({pathToOpen}) + })) + + this.disposable.add(ipcHelpers.on(app, 'open-url', (event, urlToOpen) => { + event.preventDefault() + this.openUrl({urlToOpen, devMode: this.devMode, safeMode: this.safeMode}) + })) + + this.disposable.add(ipcHelpers.on(app, 'activate', (event, hasVisibleWindows) => { + if (hasVisibleWindows) return + if (event) event.preventDefault() + this.emit('application:new-window') + })) + + this.disposable.add(ipcHelpers.on(ipcMain, 'restart-application', () => { + this.restart() + })) + + this.disposable.add(ipcHelpers.on(ipcMain, 'resolve-proxy', (event, requestId, url) => { + event.sender.session.resolveProxy(url, proxy => { + if (!event.sender.isDestroyed()) event.sender.send('did-resolve-proxy', requestId, proxy) + }) + })) + + this.disposable.add(ipcHelpers.on(ipcMain, 'did-change-history-manager', event => { + for (let atomWindow of this.getAllWindows()) { + const {webContents} = atomWindow.browserWindow + if (webContents !== event.sender) webContents.send('did-change-history-manager') + } + })) + + // A request from the associated render process to open a new render process. + this.disposable.add(ipcHelpers.on(ipcMain, 'open', (event, options) => { + const window = this.atomWindowForEvent(event) + if (options) { + if (typeof options.pathsToOpen === 'string') { + options.pathsToOpen = [options.pathsToOpen] + } + + if (options.pathsToOpen && options.pathsToOpen.length > 0) { + options.window = window + this.openPaths(options) + } else { + new AtomWindow(this, this.fileRecoveryService, options) + } + } else { + this.promptForPathToOpen('all', {window}) + } + })) + + this.disposable.add(ipcHelpers.on(ipcMain, 'update-application-menu', (event, template, menu) => { + const window = BrowserWindow.fromWebContents(event.sender) + if (this.applicationMenu) this.applicationMenu.update(window, template, menu) + })) + + this.disposable.add(ipcHelpers.on(ipcMain, 'run-package-specs', (event, packageSpecPath) => { + this.runTests({ + resourcePath: this.devResourcePath, + pathsToOpen: [packageSpecPath], + headless: false + }) + })) + + this.disposable.add(ipcHelpers.on(ipcMain, 'run-benchmarks', (event, benchmarksPath) => { + this.runBenchmarks({ + resourcePath: this.devResourcePath, + pathsToOpen: [benchmarksPath], + headless: false, + test: false + }) + })) + + this.disposable.add(ipcHelpers.on(ipcMain, 'command', (event, command) => { + this.emit(command) + })) + + this.disposable.add(ipcHelpers.on(ipcMain, 'open-command', (event, command, defaultPath) => { + switch (command) { + case 'application:open': + return this.promptForPathToOpen('all', getLoadSettings(), defaultPath) + case 'application:open-file': + return this.promptForPathToOpen('file', getLoadSettings(), defaultPath) + case 'application:open-folder': + return this.promptForPathToOpen('folder', getLoadSettings(), defaultPath) + default: + return console.log(`Invalid open-command received: ${command}`) + } + })) + + this.disposable.add(ipcHelpers.on(ipcMain, 'window-command', (event, command, ...args) => { + const window = BrowserWindow.fromWebContents(event.sender) + return window.emit(command, ...args) + })) + + this.disposable.add(ipcHelpers.respondTo('window-method', (browserWindow, method, ...args) => { + const window = this.atomWindowForBrowserWindow(browserWindow) + if (window) window[method](...args) + })) + + this.disposable.add(ipcHelpers.on(ipcMain, 'pick-folder', (event, responseChannel) => { + this.promptForPath('folder', paths => event.sender.send(responseChannel, paths)) + })) + + this.disposable.add(ipcHelpers.respondTo('set-window-size', (window, width, height) => { + window.setSize(width, height) + })) + + this.disposable.add(ipcHelpers.respondTo('set-window-position', (window, x, y) => { + window.setPosition(x, y) + })) + + this.disposable.add(ipcHelpers.respondTo('center-window', window => window.center())) + this.disposable.add(ipcHelpers.respondTo('focus-window', window => window.focus())) + this.disposable.add(ipcHelpers.respondTo('show-window', window => window.show())) + this.disposable.add(ipcHelpers.respondTo('hide-window', window => window.hide())) + this.disposable.add(ipcHelpers.respondTo('get-temporary-window-state', window => window.temporaryState)) + + this.disposable.add(ipcHelpers.respondTo('set-temporary-window-state', (win, state) => { + win.temporaryState = state + })) + + const clipboard = require('../safe-clipboard') + this.disposable.add(ipcHelpers.on(ipcMain, 'write-text-to-selection-clipboard', (event, text) => + clipboard.writeText(text, 'selection') + )) + + this.disposable.add(ipcHelpers.on(ipcMain, 'write-to-stdout', (event, output) => + process.stdout.write(output) + )) + + this.disposable.add(ipcHelpers.on(ipcMain, 'write-to-stderr', (event, output) => + process.stderr.write(output) + )) + + this.disposable.add(ipcHelpers.on(ipcMain, 'add-recent-document', (event, filename) => + app.addRecentDocument(filename) + )) + + this.disposable.add(ipcHelpers.on(ipcMain, 'execute-javascript-in-dev-tools', (event, code) => + event.sender.devToolsWebContents && event.sender.devToolsWebContents.executeJavaScript(code) + )) + + this.disposable.add(ipcHelpers.on(ipcMain, 'get-auto-update-manager-state', event => { + event.returnValue = this.autoUpdateManager.getState() + })) + + this.disposable.add(ipcHelpers.on(ipcMain, 'get-auto-update-manager-error', event => { + event.returnValue = this.autoUpdateManager.getErrorMessage() + })) + + this.disposable.add(ipcHelpers.on(ipcMain, 'will-save-path', (event, path) => { + this.fileRecoveryService.willSavePath(this.atomWindowForEvent(event), path) + event.returnValue = true + })) + + this.disposable.add(ipcHelpers.on(ipcMain, 'did-save-path', (event, path) => { + this.fileRecoveryService.didSavePath(this.atomWindowForEvent(event), path) + event.returnValue = true + })) + + this.disposable.add(ipcHelpers.on(ipcMain, 'did-change-paths', () => + this.saveState(false) + )) + + this.disposable.add(this.disableZoomOnDisplayChange()) + } + + setupDockMenu () { + if (process.platform === 'darwin') { + return app.dock.setMenu(Menu.buildFromTemplate([ + {label: 'New Window', click: () => this.emit('application:new-window')} + ])) + } + } + + // Public: Executes the given command. + // + // If it isn't handled globally, delegate to the currently focused window. + // + // command - The string representing the command. + // args - The optional arguments to pass along. + sendCommand (command, ...args) { + if (!this.emit(command, ...args)) { + const focusedWindow = this.focusedWindow() + if (focusedWindow) { + return focusedWindow.sendCommand(command, ...args) + } else { + return this.sendCommandToFirstResponder(command) + } + } + } + + // Public: Executes the given command on the given window. + // + // command - The string representing the command. + // atomWindow - The {AtomWindow} to send the command to. + // args - The optional arguments to pass along. + sendCommandToWindow (command, atomWindow, ...args) { + if (!this.emit(command, ...args)) { + if (atomWindow) { + return atomWindow.sendCommand(command, ...args) + } else { + return this.sendCommandToFirstResponder(command) + } + } + } + + // Translates the command into macOS action and sends it to application's first + // responder. + sendCommandToFirstResponder (command) { + if (process.platform !== 'darwin') return false + + switch (command) { + case 'core:undo': + Menu.sendActionToFirstResponder('undo:') + break + case 'core:redo': + Menu.sendActionToFirstResponder('redo:') + break + case 'core:copy': + Menu.sendActionToFirstResponder('copy:') + break + case 'core:cut': + Menu.sendActionToFirstResponder('cut:') + break + case 'core:paste': + Menu.sendActionToFirstResponder('paste:') + break + case 'core:select-all': + Menu.sendActionToFirstResponder('selectAll:') + break + default: + return false + } + return true + } + + // Public: Open the given path in the focused window when the event is + // triggered. + // + // A new window will be created if there is no currently focused window. + // + // eventName - The event to listen for. + // pathToOpen - The path to open when the event is triggered. + openPathOnEvent (eventName, pathToOpen) { + this.on(eventName, () => { + const window = this.focusedWindow() + if (window) { + return window.openPath(pathToOpen) + } else { + return this.openPath({pathToOpen}) + } + }) + } + + // Returns the {AtomWindow} for the given paths. + windowForPaths (pathsToOpen, devMode) { + return this.getAllWindows().find(window => + window.devMode === devMode && window.containsPaths(pathsToOpen) + ) + } + + // Returns the {AtomWindow} for the given ipcMain event. + atomWindowForEvent ({sender}) { + return this.atomWindowForBrowserWindow(BrowserWindow.fromWebContents(sender)) + } + + atomWindowForBrowserWindow (browserWindow) { + return this.getAllWindows().find(atomWindow => atomWindow.browserWindow === browserWindow) + } + + // Public: Returns the currently focused {AtomWindow} or undefined if none. + focusedWindow () { + return this.getAllWindows().find(window => window.isFocused()) + } + + // Get the platform-specific window offset for new windows. + getWindowOffsetForCurrentPlatform () { + const offsetByPlatform = { + darwin: 22, + win32: 26 + } + return offsetByPlatform[process.platform] || 0 + } + + // Get the dimensions for opening a new window by cascading as appropriate to + // the platform. + getDimensionsForNewWindow () { + const window = this.focusedWindow() || this.getLastFocusedWindow() + if (!window || window.isMaximized()) return + const dimensions = window.getDimensions() + if (dimensions) { + const offset = this.getWindowOffsetForCurrentPlatform() + dimensions.x += offset + dimensions.y += offset + return dimensions + } + } + + // Public: Opens a single path, in an existing window if possible. + // + // options - + // :pathToOpen - The file path to open + // :pidToKillWhenClosed - The integer of the pid to kill + // :newWindow - Boolean of whether this should be opened in a new window. + // :devMode - Boolean to control the opened window's dev mode. + // :safeMode - Boolean to control the opened window's safe mode. + // :profileStartup - Boolean to control creating a profile of the startup time. + // :window - {AtomWindow} to open file paths in. + // :addToLastWindow - Boolean of whether this should be opened in last focused window. + openPath ({ + initialPaths, + pathToOpen, + pidToKillWhenClosed, + newWindow, + devMode, + safeMode, + profileStartup, + window, + clearWindowState, + addToLastWindow, + env + } = {}) { + return this.openPaths({ + initialPaths, + pathsToOpen: [pathToOpen], + pidToKillWhenClosed, + newWindow, + devMode, + safeMode, + profileStartup, + window, + clearWindowState, + addToLastWindow, + env + }) + } + + // Public: Opens multiple paths, in existing windows if possible. + // + // options - + // :pathsToOpen - The array of file paths to open + // :pidToKillWhenClosed - The integer of the pid to kill + // :newWindow - Boolean of whether this should be opened in a new window. + // :devMode - Boolean to control the opened window's dev mode. + // :safeMode - Boolean to control the opened window's safe mode. + // :windowDimensions - Object with height and width keys. + // :window - {AtomWindow} to open file paths in. + // :addToLastWindow - Boolean of whether this should be opened in last focused window. + openPaths ({ + initialPaths, + pathsToOpen, + executedFrom, + pidToKillWhenClosed, + newWindow, + devMode, + safeMode, + windowDimensions, + profileStartup, + window, + clearWindowState, + addToLastWindow, + env + } = {}) { + if (!pathsToOpen || pathsToOpen.length === 0) return + if (!env) env = process.env + devMode = Boolean(devMode) + safeMode = Boolean(safeMode) + clearWindowState = Boolean(clearWindowState) + + const locationsToOpen = pathsToOpen.map(pathToOpen => + this.locationForPathToOpen(pathToOpen, executedFrom, addToLastWindow) + ) + pathsToOpen = locationsToOpen.map(locationToOpen => locationToOpen.pathToOpen) + + let existingWindow + if (!pidToKillWhenClosed && !newWindow) { + existingWindow = this.windowForPaths(pathsToOpen, devMode) + const stats = pathsToOpen.map(pathToOpen => fs.statSyncNoException(pathToOpen)) + if (!existingWindow) { + let lastWindow = window || this.getLastFocusedWindow() + if (lastWindow && lastWindow.devMode === devMode) { + if (addToLastWindow || ( + stats.every(s => s.isFile && s.isFile()) || + (stats.some(s => s.isDirectory && s.isDirectory()) && !lastWindow.hasProjectPath()))) { + existingWindow = lastWindow + } + } + } + } + + let openedWindow + if (existingWindow) { + openedWindow = existingWindow + openedWindow.openLocations(locationsToOpen) + if (openedWindow.isMinimized()) { + openedWindow.restore() + } else { + openedWindow.focus() + } + openedWindow.replaceEnvironment(env) + } else { + let resourcePath, windowInitializationScript + if (devMode) { + try { + windowInitializationScript = require.resolve( + path.join(this.devResourcePath, 'src', 'initialize-application-window') + ) + resourcePath = this.devResourcePath + } catch (error) {} + } + + if (!windowInitializationScript) { + windowInitializationScript = require.resolve('../initialize-application-window') + } + if (!resourcePath) resourcePath = this.resourcePath + if (!windowDimensions) windowDimensions = this.getDimensionsForNewWindow() + openedWindow = new AtomWindow(this, this.fileRecoveryService, { + initialPaths, + locationsToOpen, + windowInitializationScript, + resourcePath, + devMode, + safeMode, + windowDimensions, + profileStartup, + clearWindowState, + env + }) + openedWindow.focus() + this.windowStack.addWindow(openedWindow) + } + + if (pidToKillWhenClosed != null) { + this.pidsToOpenWindows[pidToKillWhenClosed] = openedWindow + } + + openedWindow.browserWindow.once('closed', () => this.killProcessForWindow(openedWindow)) + return openedWindow + } + + // Kill all processes associated with opened windows. + killAllProcesses () { + for (let pid in this.pidsToOpenWindows) { + this.killProcess(pid) + } + } + + // Kill process associated with the given opened window. + killProcessForWindow (openedWindow) { + for (let pid in this.pidsToOpenWindows) { + const trackedWindow = this.pidsToOpenWindows[pid] + if (trackedWindow === openedWindow) { + this.killProcess(pid) + } + } + } + + // Kill the process with the given pid. + killProcess (pid) { + try { + const parsedPid = parseInt(pid) + if (isFinite(parsedPid)) process.kill(parsedPid) + } catch (error) { + if (error.code !== 'ESRCH') { + console.log(`Killing process ${pid} failed: ${error.code != null ? error.code : error.message}`) + } + } + delete this.pidsToOpenWindows[pid] + } + + saveState (allowEmpty = false) { + if (this.quitting) return + + const states = [] + for (let window of this.getAllWindows()) { + if (!window.isSpec) states.push({initialPaths: window.representedDirectoryPaths}) + } + states.reverse() + + if (states.length > 0 || allowEmpty) { + this.storageFolder.storeSync('application.json', states) + this.emit('application:did-save-state') + } + } + + loadState (options) { + const states = this.storageFolder.load('application.json') + if ( + ['yes', 'always'].includes(this.config.get('core.restorePreviousWindowsOnStart')) && + states && states.length > 0 + ) { + return states.map(state => + this.openWithOptions(Object.assign(options, { + initialPaths: state.initialPaths, + pathsToOpen: state.initialPaths.filter(p => fs.isDirectorySync(p)), + urlsToOpen: [], + devMode: this.devMode, + safeMode: this.safeMode + })) + ) + } else { + return null + } + } + + // Open an atom:// url. + // + // The host of the URL being opened is assumed to be the package name + // responsible for opening the URL. A new window will be created with + // that package's `urlMain` as the bootstrap script. + // + // options - + // :urlToOpen - The atom:// url to open. + // :devMode - Boolean to control the opened window's dev mode. + // :safeMode - Boolean to control the opened window's safe mode. + openUrl ({urlToOpen, devMode, safeMode, env}) { + const parsedUrl = url.parse(urlToOpen, true) + if (parsedUrl.protocol !== 'atom:') return + + const pack = this.findPackageWithName(parsedUrl.host, devMode) + if (pack && pack.urlMain) { + return this.openPackageUrlMain( + parsedUrl.host, + pack.urlMain, + urlToOpen, + devMode, + safeMode, + env + ) + } else { + return this.openPackageUriHandler(urlToOpen, parsedUrl, devMode, safeMode, env) + } + } + + openPackageUriHandler (url, parsedUrl, devMode, safeMode, env) { + let bestWindow + + if (parsedUrl.host === 'core') { + const predicate = require('../core-uri-handlers').windowPredicate(parsedUrl) + bestWindow = this.getLastFocusedWindow(win => !win.isSpecWindow() && predicate(win)) + } + + if (!bestWindow) bestWindow = this.getLastFocusedWindow(win => !win.isSpecWindow()) + + if (bestWindow) { + bestWindow.sendURIMessage(url) + bestWindow.focus() + } else { + let windowInitializationScript + let {resourcePath} = this + if (devMode) { + try { + windowInitializationScript = require.resolve( + path.join(this.devResourcePath, 'src', 'initialize-application-window') + ) + resourcePath = this.devResourcePath + } catch (error) {} + } + + if (!windowInitializationScript) { + windowInitializationScript = require.resolve('../initialize-application-window') + } + + const windowDimensions = this.getDimensionsForNewWindow() + const window = new AtomWindow(this, this.fileRecoveryService, { + resourcePath, + windowInitializationScript, + devMode, + safeMode, + windowDimensions, + env + }) + this.windowStack.addWindow(window) + window.on('window:loaded', () => window.sendURIMessage(url)) + } + } + + findPackageWithName (packageName, devMode) { + return this.getPackageManager(devMode).getAvailablePackageMetadata().find(({name}) => + name === packageName + ) + } + + openPackageUrlMain (packageName, packageUrlMain, urlToOpen, devMode, safeMode, env) { + const packagePath = this.getPackageManager(devMode).resolvePackagePath(packageName) + const windowInitializationScript = path.resolve(packagePath, packageUrlMain) + const windowDimensions = this.getDimensionsForNewWindow() + return new AtomWindow(this, this.fileRecoveryService, { + windowInitializationScript, + resourcePath: this.resourcePath, + devMode, + safeMode, + urlToOpen, + windowDimensions, + env + }) + } + + getPackageManager (devMode) { + if (this.packages == null) { + const PackageManager = require('../package-manager') + this.packages = new PackageManager({}) + this.packages.initialize({ + configDirPath: process.env.ATOM_HOME, + devMode, + resourcePath: this.resourcePath + }) + } + + return this.packages + } + + // Opens up a new {AtomWindow} to run specs within. + // + // options - + // :headless - A Boolean that, if true, will close the window upon + // completion. + // :resourcePath - The path to include specs from. + // :specPath - The directory to load specs from. + // :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages + // and ~/.atom/dev/packages, defaults to false. + runTests ({headless, resourcePath, executedFrom, pathsToOpen, logFile, safeMode, timeout, env}) { + let windowInitializationScript + if (resourcePath !== this.resourcePath && !fs.existsSync(resourcePath)) { + ;({resourcePath} = this) + } + + const timeoutInSeconds = Number.parseFloat(timeout) + if (!Number.isNaN(timeoutInSeconds)) { + const timeoutHandler = function () { + console.log( + `The test suite has timed out because it has been running for more than ${timeoutInSeconds} seconds.` + ) + return process.exit(124) // Use the same exit code as the UNIX timeout util. + } + setTimeout(timeoutHandler, timeoutInSeconds * 1000) + } + + try { + windowInitializationScript = require.resolve( + path.resolve(this.devResourcePath, 'src', 'initialize-test-window') + ) + } catch (error) { + windowInitializationScript = require.resolve( + path.resolve(__dirname, '..', '..', 'src', 'initialize-test-window') + ) + } + + const testPaths = [] + if (pathsToOpen != null) { + for (let pathToOpen of pathsToOpen) { + testPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen))) + } + } + + if (testPaths.length === 0) { + process.stderr.write('Error: Specify at least one test path\n\n') + process.exit(1) + } + + const legacyTestRunnerPath = this.resolveLegacyTestRunnerPath() + const testRunnerPath = this.resolveTestRunnerPath(testPaths[0]) + const devMode = true + const isSpec = true + if (safeMode == null) { + safeMode = false + } + return new AtomWindow(this, this.fileRecoveryService, { + windowInitializationScript, + resourcePath, + headless, + isSpec, + devMode, + testRunnerPath, + legacyTestRunnerPath, + testPaths, + logFile, + safeMode, + env + }) + } + + runBenchmarks ({headless, test, resourcePath, executedFrom, pathsToOpen, env}) { + let windowInitializationScript + if (resourcePath !== this.resourcePath && !fs.existsSync(resourcePath)) { + ;({resourcePath} = this) + } + + try { + windowInitializationScript = require.resolve( + path.resolve(this.devResourcePath, 'src', 'initialize-benchmark-window') + ) + } catch (error) { + windowInitializationScript = require.resolve( + path.resolve(__dirname, '..', '..', 'src', 'initialize-benchmark-window') + ) + } + + const benchmarkPaths = [] + if (pathsToOpen != null) { + for (let pathToOpen of pathsToOpen) { + benchmarkPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen))) + } + } + + if (benchmarkPaths.length === 0) { + process.stderr.write('Error: Specify at least one benchmark path.\n\n') + process.exit(1) + } + + const devMode = true + const isSpec = true + const safeMode = false + return new AtomWindow(this, this.fileRecoveryService, { + windowInitializationScript, + resourcePath, + headless, + test, + isSpec, + devMode, + benchmarkPaths, + safeMode, + env + }) + } + + resolveTestRunnerPath (testPath) { + let packageRoot + if (FindParentDir == null) { + FindParentDir = require('find-parent-dir') + } + + if ((packageRoot = FindParentDir.sync(testPath, 'package.json'))) { + const packageMetadata = require(path.join(packageRoot, 'package.json')) + if (packageMetadata.atomTestRunner) { + let testRunnerPath + if (Resolve == null) { + Resolve = require('resolve') + } + if ( + (testRunnerPath = Resolve.sync(packageMetadata.atomTestRunner, { + basedir: packageRoot, + extensions: Object.keys(require.extensions) + })) + ) { + return testRunnerPath + } else { + process.stderr.write( + `Error: Could not resolve test runner path '${packageMetadata.atomTestRunner}'` + ) + process.exit(1) + } + } + } + + return this.resolveLegacyTestRunnerPath() + } + + resolveLegacyTestRunnerPath () { + try { + return require.resolve(path.resolve(this.devResourcePath, 'spec', 'jasmine-test-runner')) + } catch (error) { + return require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner')) + } + } + + locationForPathToOpen (pathToOpen, executedFrom = '', forceAddToWindow) { + let initialColumn, initialLine + if (!pathToOpen) { + return {pathToOpen} + } + + pathToOpen = pathToOpen.replace(/[:\s]+$/, '') + const match = pathToOpen.match(LocationSuffixRegExp) + + if (match != null) { + pathToOpen = pathToOpen.slice(0, -match[0].length) + if (match[1]) { + initialLine = Math.max(0, parseInt(match[1].slice(1)) - 1) + } + if (match[2]) { + initialColumn = Math.max(0, parseInt(match[2].slice(1)) - 1) + } + } else { + initialLine = initialColumn = null + } + + if (url.parse(pathToOpen).protocol == null) { + pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen)) + } + + return {pathToOpen, initialLine, initialColumn, forceAddToWindow} + } + + // Opens a native dialog to prompt the user for a path. + // + // Once paths are selected, they're opened in a new or existing {AtomWindow}s. + // + // options - + // :type - A String which specifies the type of the dialog, could be 'file', + // 'folder' or 'all'. The 'all' is only available on macOS. + // :devMode - A Boolean which controls whether any newly opened windows + // should be in dev mode or not. + // :safeMode - A Boolean which controls whether any newly opened windows + // should be in safe mode or not. + // :window - An {AtomWindow} to use for opening a selected file path. + // :path - An optional String which controls the default path to which the + // file dialog opens. + promptForPathToOpen (type, {devMode, safeMode, window}, path = null) { + return this.promptForPath( + type, + pathsToOpen => { + return this.openPaths({pathsToOpen, devMode, safeMode, window}) + }, + path + ) + } + + promptForPath (type, callback, path) { + const properties = (() => { + switch (type) { + case 'file': return ['openFile'] + case 'folder': return ['openDirectory'] + case 'all': return ['openFile', 'openDirectory'] + default: throw new Error(`${type} is an invalid type for promptForPath`) + } + })() + + // Show the open dialog as child window on Windows and Linux, and as + // independent dialog on macOS. This matches most native apps. + const parentWindow = process.platform === 'darwin' ? null : BrowserWindow.getFocusedWindow() + + const openOptions = { + properties: properties.concat(['multiSelections', 'createDirectory']), + title: (() => { + switch (type) { + case 'file': return 'Open File' + case 'folder': return 'Open Folder' + default: return 'Open' + } + })() + } + + // File dialog defaults to project directory of currently active editor + if (path) openOptions.defaultPath = path + return dialog.showOpenDialog(parentWindow, openOptions, callback) + } + + promptForRestart () { + const chosen = dialog.showMessageBox(BrowserWindow.getFocusedWindow(), { + type: 'warning', + title: 'Restart required', + message: 'You will need to restart Atom for this change to take effect.', + buttons: ['Restart Atom', 'Cancel'] + }) + if (chosen === 0) return this.restart() + } + + restart () { + const args = [] + if (this.safeMode) args.push('--safe') + if (this.logFile != null) args.push(`--log-file=${this.logFile}`) + if (this.socketPath != null) args.push(`--socket-path=${this.socketPath}`) + if (this.userDataDir != null) args.push(`--user-data-dir=${this.userDataDir}`) + if (this.devMode) { + args.push('--dev') + args.push(`--resource-path=${this.resourcePath}`) + } + app.relaunch({args}) + app.quit() + } + + disableZoomOnDisplayChange () { + const callback = () => { + this.getAllWindows().map(window => window.disableZoom()) + } + + // Set the limits every time a display is added or removed, otherwise the + // configuration gets reset to the default, which allows zooming the + // webframe. + screen.on('display-added', callback) + screen.on('display-removed', callback) + return new Disposable(() => { + screen.removeListener('display-added', callback) + screen.removeListener('display-removed', callback) + }) + } +} + +class WindowStack { + constructor (windows = []) { + this.addWindow = this.addWindow.bind(this) + this.touch = this.touch.bind(this) + this.removeWindow = this.removeWindow.bind(this) + this.getLastFocusedWindow = this.getLastFocusedWindow.bind(this) + this.all = this.all.bind(this) + this.windows = windows + } + + addWindow (window) { + this.removeWindow(window) + return this.windows.unshift(window) + } + + touch (window) { + return this.addWindow(window) + } + + removeWindow (window) { + const currentIndex = this.windows.indexOf(window) + if (currentIndex > -1) { + return this.windows.splice(currentIndex, 1) + } + } + + getLastFocusedWindow (predicate) { + if (predicate == null) { + predicate = win => true + } + return this.windows.find(predicate) + } + + all () { + return this.windows + } +} From 822900f40ebbd18b2c4f0fc9f6ddc7440caa4f78 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2018 10:01:44 -0800 Subject: [PATCH 159/176] Decaffeinate AtomWindow --- src/main-process/atom-window.coffee | 323 --------------------- src/main-process/atom-window.js | 422 ++++++++++++++++++++++++++++ 2 files changed, 422 insertions(+), 323 deletions(-) delete mode 100644 src/main-process/atom-window.coffee create mode 100644 src/main-process/atom-window.js diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee deleted file mode 100644 index ca3995c05..000000000 --- a/src/main-process/atom-window.coffee +++ /dev/null @@ -1,323 +0,0 @@ -{BrowserWindow, app, dialog, ipcMain} = require 'electron' -path = require 'path' -fs = require 'fs' -url = require 'url' -{EventEmitter} = require 'events' - -module.exports = -class AtomWindow - Object.assign @prototype, EventEmitter.prototype - - @iconPath: path.resolve(__dirname, '..', '..', 'resources', 'atom.png') - @includeShellLoadTime: true - - browserWindow: null - loaded: null - isSpec: null - - constructor: (@atomApplication, @fileRecoveryService, settings={}) -> - {@resourcePath, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings - locationsToOpen ?= [{pathToOpen}] if pathToOpen - locationsToOpen ?= [] - - @loadedPromise = new Promise((@resolveLoadedPromise) =>) - @closedPromise = new Promise((@resolveClosedPromise) =>) - - options = - show: false - title: 'Atom' - tabbingIdentifier: 'atom' - webPreferences: - # Prevent specs from throttling when the window is in the background: - # this should result in faster CI builds, and an improvement in the - # local development experience when running specs through the UI (which - # now won't pause when e.g. minimizing the window). - backgroundThrottling: not @isSpec - # Disable the `auxclick` feature so that `click` events are triggered in - # response to a middle-click. - # (Ref: https://github.com/atom/atom/pull/12696#issuecomment-290496960) - disableBlinkFeatures: 'Auxclick' - - # Don't set icon on Windows so the exe's ico will be used as window and - # taskbar's icon. See https://github.com/atom/atom/issues/4811 for more. - if process.platform is 'linux' - options.icon = @constructor.iconPath - - if @shouldAddCustomTitleBar() - options.titleBarStyle = 'hidden' - - if @shouldAddCustomInsetTitleBar() - options.titleBarStyle = 'hidden-inset' - - if @shouldHideTitleBar() - options.frame = false - - @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 ?= - for {pathToOpen} in locationsToOpen when pathToOpen - stat = fs.statSyncNoException(pathToOpen) or null - if stat?.isDirectory() - pathToOpen - else - parentDirectory = path.dirname(pathToOpen) - if stat?.isFile() or fs.existsSync(parentDirectory) - parentDirectory - else - pathToOpen - @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 - - @representedDirectoryPaths = @loadSettings.initialPaths - @env = @loadSettings.env if @loadSettings.env? - - @browserWindow.loadSettingsJSON = JSON.stringify(@loadSettings) - - @browserWindow.on 'window:loaded', => - @disableZoom() - @emit 'window:loaded' - @resolveLoadedPromise() - - @browserWindow.on 'window:locations-opened', => - @emit 'window:locations-opened' - - @browserWindow.on 'enter-full-screen', => - @browserWindow.webContents.send('did-enter-full-screen') - - @browserWindow.on 'leave-full-screen', => - @browserWindow.webContents.send('did-leave-full-screen') - - @browserWindow.loadURL url.format - protocol: 'file' - pathname: "#{@resourcePath}/static/index.html" - slashes: true - - @browserWindow.showSaveDialog = @showSaveDialog.bind(this) - - @browserWindow.focusOnWebView() if @isSpec - @browserWindow.temporaryState = {windowDimensions} if windowDimensions? - - hasPathToOpen = not (locationsToOpen.length is 1 and not locationsToOpen[0].pathToOpen?) - @openLocations(locationsToOpen) if hasPathToOpen and not @isSpecWindow() - - @atomApplication.addWindow(this) - - hasProjectPath: -> @representedDirectoryPaths.length > 0 - - setupContextMenu: -> - ContextMenu = require './context-menu' - - @browserWindow.on 'context-menu', (menuTemplate) => - new ContextMenu(menuTemplate, this) - - containsPaths: (paths) -> - for pathToCheck in paths - return false unless @containsPath(pathToCheck) - true - - containsPath: (pathToCheck) -> - @representedDirectoryPaths.some (projectPath) -> - if not projectPath - false - else if not pathToCheck - false - else if pathToCheck is projectPath - true - else if fs.statSyncNoException(pathToCheck).isDirectory?() - false - else if pathToCheck.indexOf(path.join(projectPath, path.sep)) is 0 - true - else - false - - handleEvents: -> - @browserWindow.on 'close', (event) => - unless @atomApplication.quitting or @unloading - event.preventDefault() - @unloading = true - @atomApplication.saveState(false) - @prepareToUnload().then (result) => - @close() if result - - @browserWindow.on 'closed', => - @fileRecoveryService.didCloseWindow(this) - @atomApplication.removeWindow(this) - @resolveClosedPromise() - - @browserWindow.on 'unresponsive', => - return if @isSpec - - chosen = dialog.showMessageBox @browserWindow, - type: 'warning' - buttons: ['Force Close', 'Keep Waiting'] - message: 'Editor is not responding' - detail: 'The editor is not responding. Would you like to force close it or just keep waiting?' - @browserWindow.destroy() if chosen is 0 - - @browserWindow.webContents.on 'crashed', => - if @headless - console.log "Renderer process crashed, exiting" - @atomApplication.exit(100) - return - - @fileRecoveryService.didCrashWindow(this) - chosen = dialog.showMessageBox @browserWindow, - type: 'warning' - buttons: ['Close Window', 'Reload', 'Keep It Open'] - message: 'The editor has crashed' - detail: 'Please report this issue to https://github.com/atom/atom' - switch chosen - when 0 then @browserWindow.destroy() - when 1 then @browserWindow.reload() - - @browserWindow.webContents.on 'will-navigate', (event, url) => - unless url is @browserWindow.webContents.getURL() - event.preventDefault() - - @setupContextMenu() - - if @isSpec - # Spec window's web view should always have focus - @browserWindow.on 'blur', => - @browserWindow.focusOnWebView() - - prepareToUnload: -> - if @isSpecWindow() - return Promise.resolve(true) - @lastPrepareToUnloadPromise = new Promise (resolve) => - callback = (event, result) => - if BrowserWindow.fromWebContents(event.sender) is @browserWindow - ipcMain.removeListener('did-prepare-to-unload', callback) - unless result - @unloading = false - @atomApplication.quitting = false - resolve(result) - ipcMain.on('did-prepare-to-unload', callback) - @browserWindow.webContents.send('prepare-to-unload') - - openPath: (pathToOpen, initialLine, initialColumn) -> - @openLocations([{pathToOpen, initialLine, initialColumn}]) - - openLocations: (locationsToOpen) -> - @loadedPromise.then => @sendMessage 'open-locations', locationsToOpen - - replaceEnvironment: (env) -> - @browserWindow.webContents.send 'environment', env - - sendMessage: (message, detail) -> - @browserWindow.webContents.send 'message', message, detail - - sendCommand: (command, args...) -> - if @isSpecWindow() - unless @atomApplication.sendCommandToFirstResponder(command) - switch command - when 'window:reload' then @reload() - when 'window:toggle-dev-tools' then @toggleDevTools() - when 'window:close' then @close() - else if @isWebViewFocused() - @sendCommandToBrowserWindow(command, args...) - else - unless @atomApplication.sendCommandToFirstResponder(command) - @sendCommandToBrowserWindow(command, args...) - - sendURIMessage: (uri) -> - @browserWindow.webContents.send 'uri-message', uri - - sendCommandToBrowserWindow: (command, args...) -> - action = if args[0]?.contextCommand then 'context-command' else 'command' - @browserWindow.webContents.send action, command, args... - - getDimensions: -> - [x, y] = @browserWindow.getPosition() - [width, height] = @browserWindow.getSize() - {x, y, width, height} - - shouldAddCustomTitleBar: -> - not @isSpec and - process.platform is 'darwin' and - @atomApplication.config.get('core.titleBar') is 'custom' - - shouldAddCustomInsetTitleBar: -> - not @isSpec and - process.platform is 'darwin' and - @atomApplication.config.get('core.titleBar') is 'custom-inset' - - shouldHideTitleBar: -> - not @isSpec and - process.platform is 'darwin' and - @atomApplication.config.get('core.titleBar') is 'hidden' - - close: -> @browserWindow.close() - - focus: -> @browserWindow.focus() - - minimize: -> @browserWindow.minimize() - - maximize: -> @browserWindow.maximize() - - unmaximize: -> @browserWindow.unmaximize() - - restore: -> @browserWindow.restore() - - setFullScreen: (fullScreen) -> @browserWindow.setFullScreen(fullScreen) - - setAutoHideMenuBar: (autoHideMenuBar) -> @browserWindow.setAutoHideMenuBar(autoHideMenuBar) - - handlesAtomCommands: -> - not @isSpecWindow() and @isWebViewFocused() - - isFocused: -> @browserWindow.isFocused() - - isMaximized: -> @browserWindow.isMaximized() - - isMinimized: -> @browserWindow.isMinimized() - - isWebViewFocused: -> @browserWindow.isWebViewFocused() - - isSpecWindow: -> @isSpec - - reload: -> - @loadedPromise = new Promise((@resolveLoadedPromise) =>) - @prepareToUnload().then (result) => - @browserWindow.reload() if result - @loadedPromise - - showSaveDialog: (params) -> - params = Object.assign({ - title: 'Save File', - defaultPath: @representedDirectoryPaths[0] - }, params) - dialog.showSaveDialog(@browserWindow, params) - - toggleDevTools: -> @browserWindow.toggleDevTools() - - openDevTools: -> @browserWindow.openDevTools() - - closeDevTools: -> @browserWindow.closeDevTools() - - setDocumentEdited: (documentEdited) -> @browserWindow.setDocumentEdited(documentEdited) - - setRepresentedFilename: (representedFilename) -> @browserWindow.setRepresentedFilename(representedFilename) - - setRepresentedDirectoryPaths: (@representedDirectoryPaths) -> - @representedDirectoryPaths.sort() - @loadSettings.initialPaths = @representedDirectoryPaths - @browserWindow.loadSettingsJSON = JSON.stringify(@loadSettings) - @atomApplication.saveState() - - copy: -> @browserWindow.copy() - - disableZoom: -> - @browserWindow.webContents.setVisualZoomLevelLimits(1, 1) diff --git a/src/main-process/atom-window.js b/src/main-process/atom-window.js new file mode 100644 index 000000000..0ed4085fb --- /dev/null +++ b/src/main-process/atom-window.js @@ -0,0 +1,422 @@ +const {BrowserWindow, app, dialog, ipcMain} = require('electron') +const path = require('path') +const fs = require('fs') +const url = require('url') +const {EventEmitter} = require('events') + +const ICON_PATH = path.resolve(__dirname, '..', '..', 'resources', 'atom.png') + +let includeShellLoadTime = true +let nextId = 0 + +module.exports = +class AtomWindow extends EventEmitter { + constructor (atomApplication, fileRecoveryService, settings = {}) { + super() + + this.id = nextId++ + this.atomApplication = atomApplication + this.fileRecoveryService = fileRecoveryService + this.isSpec = settings.isSpec + this.headless = settings.headless + this.safeMode = settings.safeMode + this.devMode = settings.devMode + this.resourcePath = settings.resourcePath + + let {pathToOpen, locationsToOpen} = settings + if (!locationsToOpen && pathToOpen) locationsToOpen = [{pathToOpen}] + if (!locationsToOpen) locationsToOpen = [] + + this.loadedPromise = new Promise(resolve => { this.resolveLoadedPromise = resolve }) + this.closedPromise = new Promise(resolve => { this.resolveClosedPromise = resolve }) + + const options = { + show: false, + title: 'Atom', + tabbingIdentifier: 'atom', + webPreferences: { + // Prevent specs from throttling when the window is in the background: + // this should result in faster CI builds, and an improvement in the + // local development experience when running specs through the UI (which + // now won't pause when e.g. minimizing the window). + backgroundThrottling: !this.isSpec, + // Disable the `auxclick` feature so that `click` events are triggered in + // response to a middle-click. + // (Ref: https://github.com/atom/atom/pull/12696#issuecomment-290496960) + disableBlinkFeatures: 'Auxclick' + } + } + + // Don't set icon on Windows so the exe's ico will be used as window and + // taskbar's icon. See https://github.com/atom/atom/issues/4811 for more. + if (process.platform === 'linux') options.icon = ICON_PATH + if (this.shouldAddCustomTitleBar()) options.titleBarStyle = 'hidden' + if (this.shouldAddCustomInsetTitleBar()) options.titleBarStyle = 'hidden-inset' + if (this.shouldHideTitleBar()) options.frame = false + this.browserWindow = new BrowserWindow(options) + + this.handleEvents() + + this.loadSettings = Object.assign({}, settings) + this.loadSettings.appVersion = app.getVersion() + this.loadSettings.resourcePath = this.resourcePath + this.loadSettings.atomHome = process.env.ATOM_HOME + if (this.loadSettings.devMode == null) this.loadSettings.devMode = false + if (this.loadSettings.safeMode == null) this.loadSettings.safeMode = false + if (this.loadSettings.clearWindowState == null) this.loadSettings.clearWindowState = false + + if (!this.loadSettings.initialPaths) { + this.loadSettings.initialPaths = [] + for (const {pathToOpen} of locationsToOpen) { + if (!pathToOpen) continue + const stat = fs.statSyncNoException(pathToOpen) || null + if (stat && stat.isDirectory()) { + this.loadSettings.initialPaths.push(pathToOpen) + } else { + const parentDirectory = path.dirname(pathToOpen) + if ((stat && stat.isFile()) || fs.existsSync(parentDirectory)) { + this.loadSettings.initialPaths.push(parentDirectory) + } else { + this.loadSettings.initialPaths.push(pathToOpen) + } + } + } + } + + this.loadSettings.initialPaths.sort() + + // Only send to the first non-spec window created + if (includeShellLoadTime && !this.isSpec) { + includeShellLoadTime = false + if (!this.loadSettings.shellLoadTime) { + this.loadSettings.shellLoadTime = Date.now() - global.shellStartTime + } + } + + this.representedDirectoryPaths = this.loadSettings.initialPaths + if (!this.loadSettings.env) this.env = this.loadSettings.env + + this.browserWindow.loadSettingsJSON = JSON.stringify(this.loadSettings) + + this.browserWindow.on('window:loaded', () => { + this.disableZoom() + this.emit('window:loaded') + this.resolveLoadedPromise() + }) + + this.browserWindow.on('window:locations-opened', () => { + this.emit('window:locations-opened') + }) + + this.browserWindow.on('enter-full-screen', () => { + this.browserWindow.webContents.send('did-enter-full-screen') + }) + + this.browserWindow.on('leave-full-screen', () => { + this.browserWindow.webContents.send('did-leave-full-screen') + }) + + this.browserWindow.loadURL( + url.format({ + protocol: 'file', + pathname: `${this.resourcePath}/static/index.html`, + slashes: true + }) + ) + + this.browserWindow.showSaveDialog = this.showSaveDialog.bind(this) + + if (this.isSpec) this.browserWindow.focusOnWebView() + + const hasPathToOpen = !(locationsToOpen.length === 1 && locationsToOpen[0].pathToOpen == null) + if (hasPathToOpen && !this.isSpecWindow()) this.openLocations(locationsToOpen) + this.atomApplication.addWindow(this) + } + + hasProjectPath () { + return this.representedDirectoryPaths.length > 0 + } + + setupContextMenu () { + const ContextMenu = require('./context-menu') + + this.browserWindow.on('context-menu', menuTemplate => { + return new ContextMenu(menuTemplate, this) + }) + } + + containsPaths (paths) { + return paths.every(p => this.containsPath(p)) + } + + containsPath (pathToCheck) { + if (!pathToCheck) return false + const stat = fs.statSyncNoException(pathToCheck) + if (stat && stat.isDirectory()) return false + + return this.representedDirectoryPaths.some(projectPath => + pathToCheck === projectPath || pathToCheck.startsWith(path.join(projectPath, path.sep)) + ) + } + + handleEvents () { + this.browserWindow.on('close', async event => { + if (!this.atomApplication.quitting && !this.unloading) { + event.preventDefault() + this.unloading = true + this.atomApplication.saveState(false) + if (await this.prepareToUnload()) this.close() + } + }) + + this.browserWindow.on('closed', () => { + this.fileRecoveryService.didCloseWindow(this) + this.atomApplication.removeWindow(this) + this.resolveClosedPromise() + }) + + this.browserWindow.on('unresponsive', () => { + if (this.isSpec) return + const chosen = dialog.showMessageBox(this.browserWindow, { + type: 'warning', + buttons: ['Force Close', 'Keep Waiting'], + message: 'Editor is not responding', + detail: + 'The editor is not responding. Would you like to force close it or just keep waiting?' + }) + if (chosen === 0) this.browserWindow.destroy() + }) + + this.browserWindow.webContents.on('crashed', () => { + if (this.headless) { + console.log('Renderer process crashed, exiting') + this.atomApplication.exit(100) + return + } + + this.fileRecoveryService.didCrashWindow(this) + const chosen = dialog.showMessageBox(this.browserWindow, { + type: 'warning', + buttons: ['Close Window', 'Reload', 'Keep It Open'], + message: 'The editor has crashed', + detail: 'Please report this issue to https://github.com/atom/atom' + }) + switch (chosen) { + case 0: return this.browserWindow.destroy() + case 1: return this.browserWindow.reload() + } + }) + + this.browserWindow.webContents.on('will-navigate', (event, url) => { + if (url !== this.browserWindow.webContents.getURL()) event.preventDefault() + }) + + this.setupContextMenu() + + // Spec window's web view should always have focus + if (this.isSpec) this.browserWindow.on('blur', () => this.browserWindow.focusOnWebView()) + } + + async prepareToUnload () { + if (this.isSpecWindow()) return true + + this.lastPrepareToUnloadPromise = new Promise(resolve => { + const callback = (event, result) => { + if (BrowserWindow.fromWebContents(event.sender) === this.browserWindow) { + ipcMain.removeListener('did-prepare-to-unload', callback) + if (!result) { + this.unloading = false + this.atomApplication.quitting = false + } + resolve(result) + } + } + ipcMain.on('did-prepare-to-unload', callback) + this.browserWindow.webContents.send('prepare-to-unload') + }) + + return this.lastPrepareToUnloadPromise + } + + openPath (pathToOpen, initialLine, initialColumn) { + return this.openLocations([{pathToOpen, initialLine, initialColumn}]) + } + + async openLocations (locationsToOpen) { + await this.loadedPromise + this.sendMessage('open-locations', locationsToOpen) + } + + replaceEnvironment (env) { + this.browserWindow.webContents.send('environment', env) + } + + sendMessage (message, detail) { + this.browserWindow.webContents.send('message', message, detail) + } + + sendCommand (command, ...args) { + if (this.isSpecWindow()) { + if (!this.atomApplication.sendCommandToFirstResponder(command)) { + switch (command) { + case 'window:reload': return this.reload() + case 'window:toggle-dev-tools': return this.toggleDevTools() + case 'window:close': return this.close() + } + } + } else if (this.isWebViewFocused()) { + this.sendCommandToBrowserWindow(command, ...args) + } else if (!this.atomApplication.sendCommandToFirstResponder(command)) { + this.sendCommandToBrowserWindow(command, ...args) + } + } + + sendURIMessage (uri) { + this.browserWindow.webContents.send('uri-message', uri) + } + + sendCommandToBrowserWindow (command, ...args) { + const action = args[0] && args[0].contextCommand + ? 'context-command' + : 'command' + this.browserWindow.webContents.send(action, command, ...args) + } + + getDimensions () { + const [x, y] = Array.from(this.browserWindow.getPosition()) + const [width, height] = Array.from(this.browserWindow.getSize()) + return {x, y, width, height} + } + + shouldAddCustomTitleBar () { + return ( + !this.isSpec && + process.platform === 'darwin' && + this.atomApplication.config.get('core.titleBar') === 'custom' + ) + } + + shouldAddCustomInsetTitleBar () { + return ( + !this.isSpec && + process.platform === 'darwin' && + this.atomApplication.config.get('core.titleBar') === 'custom-inset' + ) + } + + shouldHideTitleBar () { + return ( + !this.isSpec && + process.platform === 'darwin' && + this.atomApplication.config.get('core.titleBar') === 'hidden' + ) + } + + close () { + return this.browserWindow.close() + } + + focus () { + return this.browserWindow.focus() + } + + minimize () { + return this.browserWindow.minimize() + } + + maximize () { + return this.browserWindow.maximize() + } + + unmaximize () { + return this.browserWindow.unmaximize() + } + + restore () { + return this.browserWindow.restore() + } + + setFullScreen (fullScreen) { + return this.browserWindow.setFullScreen(fullScreen) + } + + setAutoHideMenuBar (autoHideMenuBar) { + return this.browserWindow.setAutoHideMenuBar(autoHideMenuBar) + } + + handlesAtomCommands () { + return !this.isSpecWindow() && this.isWebViewFocused() + } + + isFocused () { + return this.browserWindow.isFocused() + } + + isMaximized () { + return this.browserWindow.isMaximized() + } + + isMinimized () { + return this.browserWindow.isMinimized() + } + + isWebViewFocused () { + return this.browserWindow.isWebViewFocused() + } + + isSpecWindow () { + return this.isSpec + } + + reload () { + this.loadedPromise = new Promise(resolve => { this.resolveLoadedPromise = resolve }) + this.prepareToUnload().then(canUnload => { + if (canUnload) this.browserWindow.reload() + }) + return this.loadedPromise + } + + showSaveDialog (params) { + params = Object.assign({ + title: 'Save File', + defaultPath: this.representedDirectoryPaths[0] + }, params) + return dialog.showSaveDialog(this.browserWindow, params) + } + + toggleDevTools () { + return this.browserWindow.toggleDevTools() + } + + openDevTools () { + return this.browserWindow.openDevTools() + } + + closeDevTools () { + return this.browserWindow.closeDevTools() + } + + setDocumentEdited (documentEdited) { + return this.browserWindow.setDocumentEdited(documentEdited) + } + + setRepresentedFilename (representedFilename) { + return this.browserWindow.setRepresentedFilename(representedFilename) + } + + setRepresentedDirectoryPaths (representedDirectoryPaths) { + this.representedDirectoryPaths = representedDirectoryPaths + this.representedDirectoryPaths.sort() + this.loadSettings.initialPaths = this.representedDirectoryPaths + this.browserWindow.loadSettingsJSON = JSON.stringify(this.loadSettings) + return this.atomApplication.saveState() + } + + copy () { + return this.browserWindow.copy() + } + + disableZoom () { + return this.browserWindow.webContents.setVisualZoomLevelLimits(1, 1) + } +} From cf3d272e47b7e9cd218a0425646497c30b9a43ed Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2018 10:16:01 -0800 Subject: [PATCH 160/176] Remove side effect from AtomWindow constructor Standard was complaining about using 'new' for its side effect --- src/main-process/atom-application.js | 19 +++++++++++++------ src/main-process/atom-window.js | 1 - 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main-process/atom-application.js b/src/main-process/atom-application.js index 1797d5090..02f063130 100644 --- a/src/main-process/atom-application.js +++ b/src/main-process/atom-application.js @@ -464,7 +464,7 @@ class AtomApplication extends EventEmitter { options.window = window this.openPaths(options) } else { - new AtomWindow(this, this.fileRecoveryService, options) + this.addWindow(new AtomWindow(this, this.fileRecoveryService, options)) } } else { this.promptForPathToOpen('all', {window}) @@ -850,8 +850,8 @@ class AtomApplication extends EventEmitter { clearWindowState, env }) + this.addWindow(openedWindow) openedWindow.focus() - this.windowStack.addWindow(openedWindow) } if (pidToKillWhenClosed != null) { @@ -994,8 +994,9 @@ class AtomApplication extends EventEmitter { windowDimensions, env }) - this.windowStack.addWindow(window) + this.addWindow(window) window.on('window:loaded', () => window.sendURIMessage(url)) + return window } } @@ -1009,7 +1010,7 @@ class AtomApplication extends EventEmitter { const packagePath = this.getPackageManager(devMode).resolvePackagePath(packageName) const windowInitializationScript = path.resolve(packagePath, packageUrlMain) const windowDimensions = this.getDimensionsForNewWindow() - return new AtomWindow(this, this.fileRecoveryService, { + const window = new AtomWindow(this, this.fileRecoveryService, { windowInitializationScript, resourcePath: this.resourcePath, devMode, @@ -1018,6 +1019,8 @@ class AtomApplication extends EventEmitter { windowDimensions, env }) + this.addWindow(window) + return window } getPackageManager (devMode) { @@ -1089,7 +1092,7 @@ class AtomApplication extends EventEmitter { if (safeMode == null) { safeMode = false } - return new AtomWindow(this, this.fileRecoveryService, { + const window = new AtomWindow(this, this.fileRecoveryService, { windowInitializationScript, resourcePath, headless, @@ -1102,6 +1105,8 @@ class AtomApplication extends EventEmitter { safeMode, env }) + this.addWindow(window) + return window } runBenchmarks ({headless, test, resourcePath, executedFrom, pathsToOpen, env}) { @@ -1135,7 +1140,7 @@ class AtomApplication extends EventEmitter { const devMode = true const isSpec = true const safeMode = false - return new AtomWindow(this, this.fileRecoveryService, { + const window = new AtomWindow(this, this.fileRecoveryService, { windowInitializationScript, resourcePath, headless, @@ -1146,6 +1151,8 @@ class AtomApplication extends EventEmitter { safeMode, env }) + this.addWindow(window) + return window } resolveTestRunnerPath (testPath) { diff --git a/src/main-process/atom-window.js b/src/main-process/atom-window.js index 0ed4085fb..582852ad4 100644 --- a/src/main-process/atom-window.js +++ b/src/main-process/atom-window.js @@ -130,7 +130,6 @@ class AtomWindow extends EventEmitter { const hasPathToOpen = !(locationsToOpen.length === 1 && locationsToOpen[0].pathToOpen == null) if (hasPathToOpen && !this.isSpecWindow()) this.openLocations(locationsToOpen) - this.atomApplication.addWindow(this) } hasProjectPath () { From 9b917dd8c8252187abacf97866068d22d8f4625e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2018 10:42:27 -0800 Subject: [PATCH 161/176] Don't use babel for atom-application test --- spec/main-process/atom-application.test.js | 138 ++++++++++----------- 1 file changed, 68 insertions(+), 70 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 7c19efb9c..1d965d522 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -1,14 +1,12 @@ -/** @babel */ - -import season from 'season' -import dedent from 'dedent' -import electron from 'electron' -import fs from 'fs-plus' -import path from 'path' -import sinon from 'sinon' -import AtomApplication from '../../src/main-process/atom-application' -import parseCommandLine from '../../src/main-process/parse-command-line' -import {timeoutPromise, conditionPromise, emitterEventPromise} from '../async-spec-helpers' +const season = require('season') +const dedent = require('dedent') +const electron = require('electron') +const fs = require('fs-plus') +const path = require('path') +const sinon = require('sinon') +const AtomApplication = require('../../src/main-process/atom-application') +const parseCommandLine = require('../../src/main-process/parse-command-line') +const {timeoutPromise, conditionPromise, emitterEventPromise} = require('../async-spec-helpers') const ATOM_RESOURCE_PATH = path.resolve(__dirname, '..', '..') @@ -17,7 +15,7 @@ describe('AtomApplication', function () { let originalAppQuit, originalShowMessageBox, originalAtomHome, atomApplicationsToDestroy - beforeEach(function () { + beforeEach(() => { originalAppQuit = electron.app.quit originalShowMessageBox = electron.dialog.showMessageBox mockElectronAppQuit() @@ -34,7 +32,7 @@ describe('AtomApplication', function () { atomApplicationsToDestroy = [] }) - afterEach(async function () { + afterEach(async () => { process.env.ATOM_HOME = originalAtomHome for (let atomApplication of atomApplicationsToDestroy) { await atomApplication.destroy() @@ -44,8 +42,8 @@ describe('AtomApplication', function () { electron.dialog.showMessageBox = originalShowMessageBox }) - describe('launch', function () { - it('can open to a specific line number of a file', async function () { + describe('launch', () => { + it('can open to a specific line number of a file', async () => { const filePath = path.join(makeTempDir(), 'new-file') fs.writeFileSync(filePath, '1\n2\n3\n4\n') const atomApplication = buildAtomApplication() @@ -53,8 +51,8 @@ describe('AtomApplication', function () { const window = atomApplication.launch(parseCommandLine([filePath + ':3'])) await focusWindow(window) - const cursorRow = await evalInWebContents(window.browserWindow.webContents, function (sendBackToMainProcess) { - atom.workspace.observeTextEditors(function (textEditor) { + const cursorRow = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => { + atom.workspace.observeTextEditors(textEditor => { sendBackToMainProcess(textEditor.getCursorBufferPosition().row) }) }) @@ -62,7 +60,7 @@ describe('AtomApplication', function () { assert.equal(cursorRow, 2) }) - it('can open to a specific line and column of a file', async function () { + it('can open to a specific line and column of a file', async () => { const filePath = path.join(makeTempDir(), 'new-file') fs.writeFileSync(filePath, '1\n2\n3\n4\n') const atomApplication = buildAtomApplication() @@ -70,8 +68,8 @@ describe('AtomApplication', function () { const window = atomApplication.launch(parseCommandLine([filePath + ':2:2'])) await focusWindow(window) - const cursorPosition = await evalInWebContents(window.browserWindow.webContents, function (sendBackToMainProcess) { - atom.workspace.observeTextEditors(function (textEditor) { + const cursorPosition = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => { + atom.workspace.observeTextEditors(textEditor => { sendBackToMainProcess(textEditor.getCursorBufferPosition()) }) }) @@ -79,7 +77,7 @@ describe('AtomApplication', function () { assert.deepEqual(cursorPosition, {row: 1, column: 1}) }) - it('removes all trailing whitespace and colons from the specified path', async function () { + it('removes all trailing whitespace and colons from the specified path', async () => { let filePath = path.join(makeTempDir(), 'new-file') fs.writeFileSync(filePath, '1\n2\n3\n4\n') const atomApplication = buildAtomApplication() @@ -87,8 +85,8 @@ describe('AtomApplication', function () { const window = atomApplication.launch(parseCommandLine([filePath + ':: '])) await focusWindow(window) - const openedPath = await evalInWebContents(window.browserWindow.webContents, function (sendBackToMainProcess) { - atom.workspace.observeTextEditors(function (textEditor) { + const openedPath = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => { + atom.workspace.observeTextEditors(textEditor => { sendBackToMainProcess(textEditor.getPath()) }) }) @@ -97,7 +95,7 @@ describe('AtomApplication', function () { }) if (process.platform === 'darwin' || process.platform === 'win32') { - it('positions new windows at an offset distance from the previous window', async function () { + it('positions new windows at an offset distance from the previous window', async () => { const atomApplication = buildAtomApplication() const window1 = atomApplication.launch(parseCommandLine([makeTempDir()])) @@ -115,7 +113,7 @@ describe('AtomApplication', function () { }) } - it('reuses existing windows when opening paths, but not directories', async function () { + it('reuses existing windows when opening paths, but not directories', async () => { const dirAPath = makeTempDir("a") const dirBPath = makeTempDir("b") const dirCPath = makeTempDir("c") @@ -127,8 +125,8 @@ describe('AtomApplication', function () { await emitterEventPromise(window1, 'window:locations-opened') await focusWindow(window1) - let activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { - atom.workspace.observeTextEditors(function (textEditor) { + let activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { + atom.workspace.observeTextEditors(textEditor => { sendBackToMainProcess(textEditor.getPath()) }) }) @@ -139,8 +137,8 @@ describe('AtomApplication', function () { const reusedWindow = atomApplication.launch(parseCommandLine([existingDirCFilePath])) assert.equal(reusedWindow, window1) assert.deepEqual(atomApplication.getAllWindows(), [window1]) - activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { - const subscription = atom.workspace.onDidChangeActivePaneItem(function (textEditor) { + activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { + const subscription = atom.workspace.onDidChangeActivePaneItem(textEditor => { sendBackToMainProcess(textEditor.getPath()) subscription.dispose() }) @@ -156,7 +154,7 @@ describe('AtomApplication', function () { assert.deepEqual(await getTreeViewRootDirectories(window2), [dirCPath]) }) - it('adds folders to existing windows when the --add option is used', async function () { + it('adds folders to existing windows when the --add option is used', async () => { const dirAPath = makeTempDir("a") const dirBPath = makeTempDir("b") const dirCPath = makeTempDir("c") @@ -167,8 +165,8 @@ describe('AtomApplication', function () { const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath, 'new-file')])) await focusWindow(window1) - let activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { - atom.workspace.observeTextEditors(function (textEditor) { + let activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { + atom.workspace.observeTextEditors(textEditor => { sendBackToMainProcess(textEditor.getPath()) }) }) @@ -179,8 +177,8 @@ describe('AtomApplication', function () { let reusedWindow = atomApplication.launch(parseCommandLine([existingDirCFilePath, '--add'])) assert.equal(reusedWindow, window1) assert.deepEqual(atomApplication.getAllWindows(), [window1]) - activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { - const subscription = atom.workspace.onDidChangeActivePaneItem(function (textEditor) { + activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { + const subscription = atom.workspace.onDidChangeActivePaneItem(textEditor => { sendBackToMainProcess(textEditor.getPath()) subscription.dispose() }) @@ -198,14 +196,14 @@ describe('AtomApplication', function () { assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath, dirCPath, dirBPath]) }) - it('persists window state based on the project directories', async function () { + it('persists window state based on the project directories', async () => { const tempDirPath = makeTempDir() const atomApplication = buildAtomApplication() const nonExistentFilePath = path.join(tempDirPath, 'new-file') const window1 = atomApplication.launch(parseCommandLine([nonExistentFilePath])) - await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { - atom.workspace.observeTextEditors(function (textEditor) { + await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { + atom.workspace.observeTextEditors(textEditor => { textEditor.insertText('Hello World!') sendBackToMainProcess(null) }) @@ -217,7 +215,7 @@ describe('AtomApplication', function () { // Restore unsaved state when opening the directory itself const window2 = atomApplication.launch(parseCommandLine([tempDirPath])) await window2.loadedPromise - const window2Text = await evalInWebContents(window2.browserWindow.webContents, function (sendBackToMainProcess) { + const window2Text = await evalInWebContents(window2.browserWindow.webContents, sendBackToMainProcess => { const textEditor = atom.workspace.getActiveTextEditor() textEditor.moveToBottom() textEditor.insertText(' How are you?') @@ -231,13 +229,13 @@ describe('AtomApplication', function () { // Restore unsaved state when opening a path to a non-existent file in the directory const window3 = atomApplication.launch(parseCommandLine([path.join(tempDirPath, 'another-non-existent-file')])) await window3.loadedPromise - const window3Texts = await evalInWebContents(window3.browserWindow.webContents, function (sendBackToMainProcess, nonExistentFilePath) { + const window3Texts = await evalInWebContents(window3.browserWindow.webContents, (sendBackToMainProcess, nonExistentFilePath) => { sendBackToMainProcess(atom.workspace.getTextEditors().map(editor => editor.getText())) }) assert.include(window3Texts, 'Hello World! How are you?') }) - it('shows all directories in the tree view when multiple directory paths are passed to Atom', async function () { + it('shows all directories in the tree view when multiple directory paths are passed to Atom', async () => { const dirAPath = makeTempDir("a") const dirBPath = makeTempDir("b") const dirBSubdirPath = path.join(dirBPath, 'c') @@ -250,7 +248,7 @@ describe('AtomApplication', function () { assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath, dirBPath]) }) - it('reuses windows with no project paths to open directories', async function () { + it('reuses windows with no project paths to open directories', async () => { const tempDirPath = makeTempDir() const atomApplication = buildAtomApplication() const window1 = atomApplication.launch(parseCommandLine([])) @@ -261,18 +259,18 @@ describe('AtomApplication', function () { await conditionPromise(async () => (await getTreeViewRootDirectories(reusedWindow)).length > 0) }) - it('opens a new window with a single untitled buffer when launched with no path, even if windows already exist', async function () { + it('opens a new window with a single untitled buffer when launched with no path, even if windows already exist', async () => { const atomApplication = buildAtomApplication() const window1 = atomApplication.launch(parseCommandLine([])) await focusWindow(window1) - const window1EditorTitle = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { + const window1EditorTitle = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { sendBackToMainProcess(atom.workspace.getActiveTextEditor().getTitle()) }) assert.equal(window1EditorTitle, 'untitled') const window2 = atomApplication.openWithOptions(parseCommandLine([])) await focusWindow(window2) - const window2EditorTitle = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { + const window2EditorTitle = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { sendBackToMainProcess(atom.workspace.getActiveTextEditor().getTitle()) }) assert.equal(window2EditorTitle, 'untitled') @@ -280,7 +278,7 @@ describe('AtomApplication', function () { assert.deepEqual(atomApplication.getAllWindows(), [window2, window1]) }) - it('does not open an empty editor when opened with no path if the core.openEmptyEditorOnStart config setting is false', async function () { + it('does not open an empty editor when opened with no path if the core.openEmptyEditorOnStart config setting is false', async () => { const configPath = path.join(process.env.ATOM_HOME, 'config.cson') const config = season.readFileSync(configPath) if (!config['*'].core) config['*'].core = {} @@ -294,19 +292,19 @@ describe('AtomApplication', function () { // wait a bit just to make sure we don't pass due to querying the render process before it loads await timeoutPromise(1000) - const itemCount = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { + const itemCount = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { sendBackToMainProcess(atom.workspace.getActivePane().getItems().length) }) assert.equal(itemCount, 0) }) - it('opens an empty text editor and loads its parent directory in the tree-view when launched with a new file path', async function () { + it('opens an empty text editor and loads its parent directory in the tree-view when launched with a new file path', async () => { const atomApplication = buildAtomApplication() const newFilePath = path.join(makeTempDir(), 'new-file') const window = atomApplication.launch(parseCommandLine([newFilePath])) await focusWindow(window) - const {editorTitle, editorText} = await evalInWebContents(window.browserWindow.webContents, function (sendBackToMainProcess) { - atom.workspace.observeTextEditors(function (editor) { + const {editorTitle, editorText} = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => { + atom.workspace.observeTextEditors(editor => { sendBackToMainProcess({editorTitle: editor.getTitle(), editorText: editor.getText()}) }) }) @@ -315,7 +313,7 @@ describe('AtomApplication', function () { assert.deepEqual(await getTreeViewRootDirectories(window), [path.dirname(newFilePath)]) }) - it('adds a remote directory to the project when launched with a remote directory', async function () { + it('adds a remote directory to the project when launched with a remote directory', async () => { const packagePath = path.join(__dirname, '..', 'fixtures', 'packages', 'package-with-directory-provider') const packagesDirPath = path.join(process.env.ATOM_HOME, 'packages') fs.mkdirSync(packagesDirPath) @@ -338,13 +336,13 @@ describe('AtomApplication', function () { assert.deepEqual(directories, [{type: 'FakeRemoteDirectory', path: remotePath}]) function getProjectDirectories () { - return evalInWebContents(window.browserWindow.webContents, function (sendBackToMainProcess) { + return evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => { sendBackToMainProcess(atom.project.getDirectories().map(d => ({ type: d.constructor.name, path: d.getPath() }))) }) } }) - it('reopens any previously opened windows when launched with no path', async function () { + it('reopens any previously opened windows when launched with no path', async () => { if (process.platform === 'win32') return; // Test is too flakey on Windows const tempDirPath1 = makeTempDir() @@ -372,7 +370,7 @@ describe('AtomApplication', function () { assert.deepEqual(await getTreeViewRootDirectories(app2Window2), [tempDirPath2]) }) - it('does not reopen any previously opened windows when launched with no path and `core.restorePreviousWindowsOnStart` is no', async function () { + it('does not reopen any previously opened windows when launched with no path and `core.restorePreviousWindowsOnStart` is no', async () => { const atomApplication1 = buildAtomApplication() const app1Window1 = atomApplication1.launch(parseCommandLine([makeTempDir()])) await focusWindow(app1Window1) @@ -391,9 +389,9 @@ describe('AtomApplication', function () { assert.deepEqual(app2Window.representedDirectoryPaths, []) }) - describe('when closing the last window', function () { + describe('when closing the last window', () => { if (process.platform === 'linux' || process.platform === 'win32') { - it('quits the application', async function () { + it('quits the application', async () => { const atomApplication = buildAtomApplication() const window = atomApplication.launch(parseCommandLine([path.join(makeTempDir("a"), 'file-a')])) await focusWindow(window) @@ -402,7 +400,7 @@ describe('AtomApplication', function () { assert(electron.app.hasQuitted()) }) } else if (process.platform === 'darwin') { - it('leaves the application open', async function () { + it('leaves the application open', async () => { const atomApplication = buildAtomApplication() const window = atomApplication.launch(parseCommandLine([path.join(makeTempDir("a"), 'file-a')])) await focusWindow(window) @@ -413,8 +411,8 @@ describe('AtomApplication', function () { } }) - describe('when adding or removing project folders', function () { - it('stores the window state immediately', async function () { + describe('when adding or removing project folders', () => { + it('stores the window state immediately', async () => { const dirA = makeTempDir() const dirB = makeTempDir() @@ -441,8 +439,8 @@ describe('AtomApplication', function () { }) }) - describe('when opening atom:// URLs', function () { - it('loads the urlMain file in a new window', async function () { + describe('when opening atom:// URLs', () => { + it('loads the urlMain file in a new window', async () => { const packagePath = path.join(__dirname, '..', 'fixtures', 'packages', 'package-with-url-main') const packagesDirPath = path.join(process.env.ATOM_HOME, 'packages') fs.mkdirSync(packagesDirPath) @@ -454,7 +452,7 @@ describe('AtomApplication', function () { let windows = atomApplication.launch(launchOptions) await windows[0].loadedPromise - let reached = await evalInWebContents(windows[0].browserWindow.webContents, function (sendBackToMainProcess) { + let reached = await evalInWebContents(windows[0].browserWindow.webContents, sendBackToMainProcess => { sendBackToMainProcess(global.reachedUrlMain) }) assert.equal(reached, true); @@ -488,7 +486,7 @@ describe('AtomApplication', function () { }) }) - it('waits until all the windows have saved their state before quitting', async function () { + it('waits until all the windows have saved their state before quitting', async () => { const dirAPath = makeTempDir("a") const dirBPath = makeTempDir("b") const atomApplication = buildAtomApplication() @@ -507,7 +505,7 @@ describe('AtomApplication', function () { const window1 = atomApplication.launch(parseCommandLine([])) const window2 = atomApplication.launch(parseCommandLine([])) await Promise.all([window1.loadedPromise, window2.loadedPromise]) - await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { + await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { atom.workspace.getActiveTextEditor().insertText('unsaved text') sendBackToMainProcess() }) @@ -543,7 +541,7 @@ describe('AtomApplication', function () { function mockElectronAppQuit () { let quitted = false - electron.app.quit = function () { + electron.app.quit = () => { if (electron.app.quit.callCount) { electron.app.quit.callCount++ } else { @@ -556,13 +554,13 @@ describe('AtomApplication', function () { quitted = true } } - electron.app.hasQuitted = function () { + electron.app.hasQuitted = () => { return quitted } } function mockElectronShowMessageBox ({choice}) { - electron.dialog.showMessageBox = function () { + electron.dialog.showMessageBox = () => { return choice } } @@ -575,7 +573,7 @@ describe('AtomApplication', function () { let channelIdCounter = 0 function evalInWebContents (webContents, source, ...args) { const channelId = 'eval-result-' + channelIdCounter++ - return new Promise(function (resolve) { + return new Promise(resolve => { electron.ipcMain.on(channelId, receiveResult) function receiveResult (event, result) { @@ -593,7 +591,7 @@ describe('AtomApplication', function () { } function getTreeViewRootDirectories (atomWindow) { - return evalInWebContents(atomWindow.browserWindow.webContents, function (sendBackToMainProcess) { + return evalInWebContents(atomWindow.browserWindow.webContents, sendBackToMainProcess => { atom.workspace.getLeftDock().observeActivePaneItem((treeView) => { if (treeView) { sendBackToMainProcess( @@ -607,8 +605,8 @@ describe('AtomApplication', function () { } function clearElectronSession () { - return new Promise(function (resolve) { - electron.session.defaultSession.clearStorageData(function () { + return new Promise(resolve => { + electron.session.defaultSession.clearStorageData(() => { // Resolve promise on next tick, otherwise the process stalls. This // might be a bug in Electron, but it's probably fixed on the newer // versions. From 1de37810f09c282878de0fb329591f28e193d9df Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2018 11:48:19 -0800 Subject: [PATCH 162/176] Rename hasQuitted -> didQuit --- spec/main-process/atom-application.test.js | 38 ++++++++++------------ 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 1d965d522..c68dc6fbe 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -397,7 +397,7 @@ describe('AtomApplication', function () { await focusWindow(window) window.close() await window.closedPromise - assert(electron.app.hasQuitted()) + assert(electron.app.didQuit()) }) } else if (process.platform === 'darwin') { it('leaves the application open', async () => { @@ -406,7 +406,7 @@ describe('AtomApplication', function () { await focusWindow(window) window.close() await window.closedPromise - assert(!electron.app.hasQuitted()) + assert(!electron.app.didQuit()) }) } }) @@ -495,9 +495,10 @@ describe('AtomApplication', function () { const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath, 'file-b')])) await focusWindow(window2) electron.app.quit() - assert(!electron.app.hasQuitted()) + assert(!electron.app.didQuit()) await Promise.all([window1.lastPrepareToUnloadPromise, window2.lastPrepareToUnloadPromise]) - assert(electron.app.hasQuitted()) + await new Promise(resolve => resolve()) + assert(electron.app.didQuit()) }) it('prevents quitting if user cancels when prompted to save an item', async () => { @@ -514,14 +515,14 @@ describe('AtomApplication', function () { mockElectronShowMessageBox({choice: 1}) electron.app.quit() await atomApplication.lastBeforeQuitPromise - assert(!electron.app.hasQuitted()) + assert(!electron.app.didQuit()) 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()) + assert(electron.app.didQuit()) }) function buildAtomApplication () { @@ -540,23 +541,18 @@ describe('AtomApplication', function () { } function mockElectronAppQuit () { - let quitted = false - electron.app.quit = () => { - if (electron.app.quit.callCount) { - electron.app.quit.callCount++ - } else { - electron.app.quit.callCount = 1 - } + let didQuit = false - let shouldQuit = true - electron.app.emit('before-quit', {preventDefault: () => { shouldQuit = false }}) - if (shouldQuit) { - quitted = true - } - } - electron.app.hasQuitted = () => { - return quitted + electron.app.quit = function () { + this.quit.callCount++ + let defaultPrevented = false + this.emit('before-quit', {preventDefault() { defaultPrevented = true }}) + if (!defaultPrevented) didQuit = true } + + electron.app.quit.callCount = 0 + + electron.app.didQuit = () => didQuit } function mockElectronShowMessageBox ({choice}) { From 9d30003e58ca837a1593b4cac03da0df706e6ea4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2018 12:10:39 -0800 Subject: [PATCH 163/176] Use async/await in before-quit handler --- src/main-process/atom-application.js | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/main-process/atom-application.js b/src/main-process/atom-application.js index 02f063130..459520722 100644 --- a/src/main-process/atom-application.js +++ b/src/main-process/atom-application.js @@ -396,22 +396,19 @@ class AtomApplication extends EventEmitter { this.openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet') this.openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md')) - this.disposable.add(ipcHelpers.on(app, 'before-quit', event => { + this.disposable.add(ipcHelpers.on(app, 'before-quit', async event => { let resolveBeforeQuitPromise - this.lastBeforeQuitPromise = new Promise(resolve => { - resolveBeforeQuitPromise = resolve - }) + this.lastBeforeQuitPromise = new Promise(resolve => { resolveBeforeQuitPromise = resolve }) - if (this.quitting) return resolveBeforeQuitPromise() + if (!this.quitting) { + this.quitting = true + event.preventDefault() + const windowUnloadPromises = this.getAllWindows().map(window => window.prepareToUnload()) + const windowUnloadedResults = await Promise.all(windowUnloadPromises) + if (windowUnloadedResults.every(Boolean)) app.quit() + } - this.quitting = true - event.preventDefault() - const windowUnloadPromises = this.getAllWindows().map(window => window.prepareToUnload()) - return Promise.all(windowUnloadPromises).then(windowUnloadedResults => { - const didUnloadAllWindows = windowUnloadedResults.every(Boolean) - if (didUnloadAllWindows) app.quit() - resolveBeforeQuitPromise() - }) + resolveBeforeQuitPromise() })) this.disposable.add(ipcHelpers.on(app, 'will-quit', () => { From b645852142e47515f621828635503ed1be7f3594 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2018 13:01:33 -0800 Subject: [PATCH 164/176] Don't rely on promise resolution timing in main process spec --- spec/main-process/atom-application.test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index c68dc6fbe..b73a57fab 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -397,6 +397,7 @@ describe('AtomApplication', function () { await focusWindow(window) window.close() await window.closedPromise + await atomApplication.lastBeforeQuitPromise assert(electron.app.didQuit()) }) } else if (process.platform === 'darwin') { @@ -406,6 +407,7 @@ describe('AtomApplication', function () { await focusWindow(window) window.close() await window.closedPromise + await timeoutPromise(1000) assert(!electron.app.didQuit()) }) } @@ -495,9 +497,11 @@ describe('AtomApplication', function () { const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath, 'file-b')])) await focusWindow(window2) electron.app.quit() + await new Promise(process.nextTick) assert(!electron.app.didQuit()) + await Promise.all([window1.lastPrepareToUnloadPromise, window2.lastPrepareToUnloadPromise]) - await new Promise(resolve => resolve()) + await new Promise(process.nextTick) assert(electron.app.didQuit()) }) From 2793498e0b3c1478ff9e64f2128758d48eb8ef10 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2018 13:13:30 -0800 Subject: [PATCH 165/176] Decaffeinate ApplicationMenu --- src/main-process/application-menu.coffee | 161 ---------------- src/main-process/application-menu.js | 225 +++++++++++++++++++++++ 2 files changed, 225 insertions(+), 161 deletions(-) delete mode 100644 src/main-process/application-menu.coffee create mode 100644 src/main-process/application-menu.js diff --git a/src/main-process/application-menu.coffee b/src/main-process/application-menu.coffee deleted file mode 100644 index 35bc7d66c..000000000 --- a/src/main-process/application-menu.coffee +++ /dev/null @@ -1,161 +0,0 @@ -{app, Menu} = require 'electron' -_ = require 'underscore-plus' -MenuHelpers = require '../menu-helpers' - -# Used to manage the global application menu. -# -# It's created by {AtomApplication} upon instantiation and used to add, remove -# and maintain the state of all menu items. -module.exports = -class ApplicationMenu - constructor: (@version, @autoUpdateManager) -> - @windowTemplates = new WeakMap() - @setActiveTemplate(@getDefaultTemplate()) - @autoUpdateManager.on 'state-changed', (state) => @showUpdateMenuItem(state) - - # Public: Updates the entire menu with the given keybindings. - # - # window - The BrowserWindow this menu template is associated with. - # template - The Object which describes the menu to display. - # keystrokesByCommand - An Object where the keys are commands and the values - # are Arrays containing the keystroke. - update: (window, template, keystrokesByCommand) -> - @translateTemplate(template, keystrokesByCommand) - @substituteVersion(template) - @windowTemplates.set(window, template) - @setActiveTemplate(template) if window is @lastFocusedWindow - - setActiveTemplate: (template) -> - unless _.isEqual(template, @activeTemplate) - @activeTemplate = template - @menu = Menu.buildFromTemplate(_.deepClone(template)) - Menu.setApplicationMenu(@menu) - - @showUpdateMenuItem(@autoUpdateManager.getState()) - - # Register a BrowserWindow with this application menu. - addWindow: (window) -> - @lastFocusedWindow ?= window - - focusHandler = => - @lastFocusedWindow = window - if template = @windowTemplates.get(window) - @setActiveTemplate(template) - - window.on 'focus', focusHandler - window.once 'closed', => - @lastFocusedWindow = null if window is @lastFocusedWindow - @windowTemplates.delete(window) - window.removeListener 'focus', focusHandler - - @enableWindowSpecificItems(true) - - # Flattens the given menu and submenu items into an single Array. - # - # menu - A complete menu configuration object for atom-shell's menu API. - # - # Returns an Array of native menu items. - flattenMenuItems: (menu) -> - items = [] - for index, item of menu.items or {} - items.push(item) - items = items.concat(@flattenMenuItems(item.submenu)) if item.submenu - items - - # Flattens the given menu template into an single Array. - # - # template - An object describing the menu item. - # - # Returns an Array of native menu items. - flattenMenuTemplate: (template) -> - items = [] - for item in template - items.push(item) - items = items.concat(@flattenMenuTemplate(item.submenu)) if item.submenu - items - - # Public: Used to make all window related menu items are active. - # - # enable - If true enables all window specific items, if false disables all - # window specific items. - enableWindowSpecificItems: (enable) -> - for item in @flattenMenuItems(@menu) - item.enabled = enable if item.metadata?.windowSpecific - return - - # Replaces VERSION with the current version. - substituteVersion: (template) -> - if (item = _.find(@flattenMenuTemplate(template), ({label}) -> label is 'VERSION')) - item.label = "Version #{@version}" - - # Sets the proper visible state the update menu items - showUpdateMenuItem: (state) -> - checkForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Check for Update') - checkingForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Checking for Update') - downloadingUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Downloading Update') - installUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Restart and Install Update') - - return unless checkForUpdateItem? and checkingForUpdateItem? and downloadingUpdateItem? and installUpdateItem? - - checkForUpdateItem.visible = false - checkingForUpdateItem.visible = false - downloadingUpdateItem.visible = false - installUpdateItem.visible = false - - switch state - when 'idle', 'error', 'no-update-available' - checkForUpdateItem.visible = true - when 'checking' - checkingForUpdateItem.visible = true - when 'downloading' - downloadingUpdateItem.visible = true - when 'update-available' - installUpdateItem.visible = true - - # Default list of menu items. - # - # Returns an Array of menu item Objects. - getDefaultTemplate: -> - [ - label: "Atom" - submenu: [ - {label: "Check for Update", metadata: {autoUpdate: true}} - {label: 'Reload', accelerator: 'Command+R', click: => @focusedWindow()?.reload()} - {label: 'Close Window', accelerator: 'Command+Shift+W', click: => @focusedWindow()?.close()} - {label: 'Toggle Dev Tools', accelerator: 'Command+Alt+I', click: => @focusedWindow()?.toggleDevTools()} - {label: 'Quit', accelerator: 'Command+Q', click: -> app.quit()} - ] - ] - - focusedWindow: -> - _.find global.atomApplication.getAllWindows(), (atomWindow) -> atomWindow.isFocused() - - # Combines a menu template with the appropriate keystroke. - # - # template - An Object conforming to atom-shell's menu api but lacking - # accelerator and click properties. - # keystrokesByCommand - An Object where the keys are commands and the values - # are Arrays containing the keystroke. - # - # Returns a complete menu configuration object for atom-shell's menu API. - translateTemplate: (template, keystrokesByCommand) -> - template.forEach (item) => - item.metadata ?= {} - if item.command - item.accelerator = @acceleratorForCommand(item.command, keystrokesByCommand) - item.click = -> global.atomApplication.sendCommand(item.command, item.commandDetail) - item.metadata.windowSpecific = true unless /^application:/.test(item.command, item.commandDetail) - @translateTemplate(item.submenu, keystrokesByCommand) if item.submenu - template - - # Determine the accelerator for a given command. - # - # command - The name of the command. - # keystrokesByCommand - An Object where the keys are commands and the values - # are Arrays containing the keystroke. - # - # Returns a String containing the keystroke in a format that can be interpreted - # by Electron to provide nice icons where available. - acceleratorForCommand: (command, keystrokesByCommand) -> - firstKeystroke = keystrokesByCommand[command]?[0] - MenuHelpers.acceleratorForKeystroke(firstKeystroke) diff --git a/src/main-process/application-menu.js b/src/main-process/application-menu.js new file mode 100644 index 000000000..26dcd1941 --- /dev/null +++ b/src/main-process/application-menu.js @@ -0,0 +1,225 @@ +const {app, Menu} = require('electron') +const _ = require('underscore-plus') +const MenuHelpers = require('../menu-helpers') + +// Used to manage the global application menu. +// +// It's created by {AtomApplication} upon instantiation and used to add, remove +// and maintain the state of all menu items. +module.exports = +class ApplicationMenu { + constructor (version, autoUpdateManager) { + this.version = version + this.autoUpdateManager = autoUpdateManager + this.windowTemplates = new WeakMap() + this.setActiveTemplate(this.getDefaultTemplate()) + this.autoUpdateManager.on('state-changed', state => this.showUpdateMenuItem(state)) + } + + // Public: Updates the entire menu with the given keybindings. + // + // window - The BrowserWindow this menu template is associated with. + // template - The Object which describes the menu to display. + // keystrokesByCommand - An Object where the keys are commands and the values + // are Arrays containing the keystroke. + update (window, template, keystrokesByCommand) { + this.translateTemplate(template, keystrokesByCommand) + this.substituteVersion(template) + this.windowTemplates.set(window, template) + if (window === this.lastFocusedWindow) return this.setActiveTemplate(template) + } + + setActiveTemplate (template) { + if (!_.isEqual(template, this.activeTemplate)) { + this.activeTemplate = template + this.menu = Menu.buildFromTemplate(_.deepClone(template)) + Menu.setApplicationMenu(this.menu) + } + + return this.showUpdateMenuItem(this.autoUpdateManager.getState()) + } + + // Register a BrowserWindow with this application menu. + addWindow (window) { + if (this.lastFocusedWindow == null) this.lastFocusedWindow = window + + const focusHandler = () => { + this.lastFocusedWindow = window + const template = this.windowTemplates.get(window) + if (template) this.setActiveTemplate(template) + } + + window.on('focus', focusHandler) + window.once('closed', () => { + if (window === this.lastFocusedWindow) this.lastFocusedWindow = null + this.windowTemplates.delete(window) + window.removeListener('focus', focusHandler) + }) + + this.enableWindowSpecificItems(true) + } + + // Flattens the given menu and submenu items into an single Array. + // + // menu - A complete menu configuration object for atom-shell's menu API. + // + // Returns an Array of native menu items. + flattenMenuItems (menu) { + const object = menu.items || {} + let items = [] + for (let index in object) { + const item = object[index] + items.push(item) + if (item.submenu) items = items.concat(this.flattenMenuItems(item.submenu)) + } + return items + } + + // Flattens the given menu template into an single Array. + // + // template - An object describing the menu item. + // + // Returns an Array of native menu items. + flattenMenuTemplate (template) { + let items = [] + for (let item of template) { + items.push(item) + if (item.submenu) items = items.concat(this.flattenMenuTemplate(item.submenu)) + } + return items + } + + // Public: Used to make all window related menu items are active. + // + // enable - If true enables all window specific items, if false disables all + // window specific items. + enableWindowSpecificItems (enable) { + for (let item of this.flattenMenuItems(this.menu)) { + if (item.metadata && item.metadata.windowSpecific) item.enabled = enable + } + } + + // Replaces VERSION with the current version. + substituteVersion (template) { + let item = this.flattenMenuTemplate(template).find(({label}) => label === 'VERSION') + if (item) item.label = `Version ${this.version}` + } + + // Sets the proper visible state the update menu items + showUpdateMenuItem (state) { + const items = this.flattenMenuItems(this.menu) + const checkForUpdateItem = items.find(({label}) => label === 'Check for Update') + const checkingForUpdateItem = items.find(({label}) => label === 'Checking for Update') + const downloadingUpdateItem = items.find(({label}) => label === 'Downloading Update') + const installUpdateItem = items.find(({label}) => label === 'Restart and Install Update') + + if (!checkForUpdateItem || !checkingForUpdateItem || + !downloadingUpdateItem || !installUpdateItem) return + + checkForUpdateItem.visible = false + checkingForUpdateItem.visible = false + downloadingUpdateItem.visible = false + installUpdateItem.visible = false + + switch (state) { + case 'idle': + case 'error': + case 'no-update-available': + checkForUpdateItem.visible = true + break + case 'checking': + checkingForUpdateItem.visible = true + break + case 'downloading': + downloadingUpdateItem.visible = true + break + case 'update-available': + installUpdateItem.visible = true + break + } + } + + // Default list of menu items. + // + // Returns an Array of menu item Objects. + getDefaultTemplate () { + return [{ + label: 'Atom', + submenu: [ + { + label: 'Check for Update', + metadata: {autoUpdate: true} + }, + { + label: 'Reload', + accelerator: 'Command+R', + click: () => { + const window = this.focusedWindow() + if (window) window.reload() + } + }, + { + label: 'Close Window', + accelerator: 'Command+Shift+W', + click: () => { + const window = this.focusedWindow() + if (window) window.close() + } + }, + { + label: 'Toggle Dev Tools', + accelerator: 'Command+Alt+I', + click: () => { + const window = this.focusedWindow() + if (window) window.toggleDevTools() + } + }, + { + label: 'Quit', + accelerator: 'Command+Q', + click: () => app.quit() + } + ] + }] + } + + focusedWindow () { + return global.atomApplication.getAllWindows().find(window => window.isFocused()) + } + + // Combines a menu template with the appropriate keystroke. + // + // template - An Object conforming to atom-shell's menu api but lacking + // accelerator and click properties. + // keystrokesByCommand - An Object where the keys are commands and the values + // are Arrays containing the keystroke. + // + // Returns a complete menu configuration object for atom-shell's menu API. + translateTemplate (template, keystrokesByCommand) { + template.forEach(item => { + if (item.metadata == null) item.metadata = {} + if (item.command) { + item.accelerator = this.acceleratorForCommand(item.command, keystrokesByCommand) + item.click = () => global.atomApplication.sendCommand(item.command, item.commandDetail) + if (!/^application:/.test(item.command, item.commandDetail)) { + item.metadata.windowSpecific = true + } + } + if (item.submenu) this.translateTemplate(item.submenu, keystrokesByCommand) + }) + return template + } + + // Determine the accelerator for a given command. + // + // command - The name of the command. + // keystrokesByCommand - An Object where the keys are commands and the values + // are Arrays containing the keystroke. + // + // Returns a String containing the keystroke in a format that can be interpreted + // by Electron to provide nice icons where available. + acceleratorForCommand (command, keystrokesByCommand) { + const firstKeystroke = keystrokesByCommand[command] && keystrokesByCommand[command][0] + return MenuHelpers.acceleratorForKeystroke(firstKeystroke) + } +} From 0085bc83e40319072aeba31bd7cead3893a728b9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2018 13:20:26 -0800 Subject: [PATCH 166/176] Decaffeinate AtomProtocolHandler --- src/main-process/atom-protocol-handler.coffee | 43 --------------- src/main-process/atom-protocol-handler.js | 54 +++++++++++++++++++ 2 files changed, 54 insertions(+), 43 deletions(-) delete mode 100644 src/main-process/atom-protocol-handler.coffee create mode 100644 src/main-process/atom-protocol-handler.js diff --git a/src/main-process/atom-protocol-handler.coffee b/src/main-process/atom-protocol-handler.coffee deleted file mode 100644 index db385b4b7..000000000 --- a/src/main-process/atom-protocol-handler.coffee +++ /dev/null @@ -1,43 +0,0 @@ -{protocol} = require 'electron' -fs = require 'fs' -path = require 'path' - -# Handles requests with 'atom' protocol. -# -# It's created by {AtomApplication} upon instantiation and is used to create a -# custom resource loader for 'atom://' URLs. -# -# The following directories are searched in order: -# * ~/.atom/assets -# * ~/.atom/dev/packages (unless in safe mode) -# * ~/.atom/packages -# * RESOURCE_PATH/node_modules -# -module.exports = -class AtomProtocolHandler - constructor: (resourcePath, safeMode) -> - @loadPaths = [] - - unless safeMode - @loadPaths.push(path.join(process.env.ATOM_HOME, 'dev', 'packages')) - - @loadPaths.push(path.join(process.env.ATOM_HOME, 'packages')) - @loadPaths.push(path.join(resourcePath, 'node_modules')) - - @registerAtomProtocol() - - # Creates the 'atom' custom protocol handler. - registerAtomProtocol: -> - protocol.registerFileProtocol 'atom', (request, callback) => - relativePath = path.normalize(request.url.substr(7)) - - if relativePath.indexOf('assets/') is 0 - assetsPath = path.join(process.env.ATOM_HOME, relativePath) - filePath = assetsPath if fs.statSyncNoException(assetsPath).isFile?() - - unless filePath - for loadPath in @loadPaths - filePath = path.join(loadPath, relativePath) - break if fs.statSyncNoException(filePath).isFile?() - - callback(filePath) diff --git a/src/main-process/atom-protocol-handler.js b/src/main-process/atom-protocol-handler.js new file mode 100644 index 000000000..1affba02a --- /dev/null +++ b/src/main-process/atom-protocol-handler.js @@ -0,0 +1,54 @@ +const {protocol} = require('electron') +const fs = require('fs') +const path = require('path') + +// Handles requests with 'atom' protocol. +// +// It's created by {AtomApplication} upon instantiation and is used to create a +// custom resource loader for 'atom://' URLs. +// +// The following directories are searched in order: +// * ~/.atom/assets +// * ~/.atom/dev/packages (unless in safe mode) +// * ~/.atom/packages +// * RESOURCE_PATH/node_modules +// +module.exports = +class AtomProtocolHandler { + constructor (resourcePath, safeMode) { + this.loadPaths = [] + + if (!safeMode) { + this.loadPaths.push(path.join(process.env.ATOM_HOME, 'dev', 'packages')) + } + + this.loadPaths.push(path.join(process.env.ATOM_HOME, 'packages')) + this.loadPaths.push(path.join(resourcePath, 'node_modules')) + + this.registerAtomProtocol() + } + + // Creates the 'atom' custom protocol handler. + registerAtomProtocol () { + protocol.registerFileProtocol('atom', (request, callback) => { + const relativePath = path.normalize(request.url.substr(7)) + + let filePath + if (relativePath.indexOf('assets/') === 0) { + const assetsPath = path.join(process.env.ATOM_HOME, relativePath) + const stat = fs.statSyncNoException(assetsPath) + if (stat && stat.isFile()) filePath = assetsPath + } + + if (!filePath) { + for (let loadPath of this.loadPaths) { + filePath = path.join(loadPath, relativePath) + const stat = fs.statSyncNoException(filePath) + if (stat && stat.isFile()) break + } + } + + callback(filePath) + }) + } +} From bbc8b54f91e3d8398b9846c42f0eab5672a6ac60 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2018 14:59:57 -0800 Subject: [PATCH 167/176] Decaffeinate ApplicationDelegate --- src/application-delegate.coffee | 293 -------------------------- src/application-delegate.js | 354 ++++++++++++++++++++++++++++++++ 2 files changed, 354 insertions(+), 293 deletions(-) delete mode 100644 src/application-delegate.coffee create mode 100644 src/application-delegate.js diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee deleted file mode 100644 index 70b0f91bc..000000000 --- a/src/application-delegate.coffee +++ /dev/null @@ -1,293 +0,0 @@ -{ipcRenderer, remote, shell} = require 'electron' -ipcHelpers = require './ipc-helpers' -{Disposable} = require 'event-kit' -getWindowLoadSettings = require './get-window-load-settings' - -module.exports = -class ApplicationDelegate - getWindowLoadSettings: -> getWindowLoadSettings() - - open: (params) -> - ipcRenderer.send('open', params) - - pickFolder: (callback) -> - responseChannel = "atom-pick-folder-response" - ipcRenderer.on responseChannel, (event, path) -> - ipcRenderer.removeAllListeners(responseChannel) - callback(path) - ipcRenderer.send("pick-folder", responseChannel) - - getCurrentWindow: -> - remote.getCurrentWindow() - - closeWindow: -> - ipcHelpers.call('window-method', 'close') - - getTemporaryWindowState: -> - ipcHelpers.call('get-temporary-window-state').then (stateJSON) -> JSON.parse(stateJSON) - - setTemporaryWindowState: (state) -> - ipcHelpers.call('set-temporary-window-state', JSON.stringify(state)) - - getWindowSize: -> - [width, height] = remote.getCurrentWindow().getSize() - {width, height} - - setWindowSize: (width, height) -> - ipcHelpers.call('set-window-size', width, height) - - getWindowPosition: -> - [x, y] = remote.getCurrentWindow().getPosition() - {x, y} - - setWindowPosition: (x, y) -> - ipcHelpers.call('set-window-position', x, y) - - centerWindow: -> - ipcHelpers.call('center-window') - - focusWindow: -> - ipcHelpers.call('focus-window') - - showWindow: -> - ipcHelpers.call('show-window') - - hideWindow: -> - ipcHelpers.call('hide-window') - - reloadWindow: -> - ipcHelpers.call('window-method', 'reload') - - restartApplication: -> - ipcRenderer.send("restart-application") - - minimizeWindow: -> - ipcHelpers.call('window-method', 'minimize') - - isWindowMaximized: -> - remote.getCurrentWindow().isMaximized() - - maximizeWindow: -> - ipcHelpers.call('window-method', 'maximize') - - unmaximizeWindow: -> - ipcHelpers.call('window-method', 'unmaximize') - - isWindowFullScreen: -> - remote.getCurrentWindow().isFullScreen() - - setWindowFullScreen: (fullScreen=false) -> - ipcHelpers.call('window-method', 'setFullScreen', fullScreen) - - onDidEnterFullScreen: (callback) -> - ipcHelpers.on(ipcRenderer, 'did-enter-full-screen', callback) - - onDidLeaveFullScreen: (callback) -> - ipcHelpers.on(ipcRenderer, 'did-leave-full-screen', callback) - - openWindowDevTools: -> - # Defer DevTools interaction to the next tick, because using them during - # event handling causes some wrong input events to be triggered on - # `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697). - new Promise(process.nextTick).then(-> ipcHelpers.call('window-method', 'openDevTools')) - - closeWindowDevTools: -> - # Defer DevTools interaction to the next tick, because using them during - # event handling causes some wrong input events to be triggered on - # `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697). - new Promise(process.nextTick).then(-> ipcHelpers.call('window-method', 'closeDevTools')) - - toggleWindowDevTools: -> - # Defer DevTools interaction to the next tick, because using them during - # event handling causes some wrong input events to be triggered on - # `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697). - new Promise(process.nextTick).then(-> ipcHelpers.call('window-method', 'toggleDevTools')) - - executeJavaScriptInWindowDevTools: (code) -> - ipcRenderer.send("execute-javascript-in-dev-tools", code) - - setWindowDocumentEdited: (edited) -> - ipcHelpers.call('window-method', 'setDocumentEdited', edited) - - setRepresentedFilename: (filename) -> - ipcHelpers.call('window-method', 'setRepresentedFilename', filename) - - addRecentDocument: (filename) -> - ipcRenderer.send("add-recent-document", filename) - - setRepresentedDirectoryPaths: (paths) -> - ipcHelpers.call('window-method', 'setRepresentedDirectoryPaths', paths) - - setAutoHideWindowMenuBar: (autoHide) -> - ipcHelpers.call('window-method', 'setAutoHideMenuBar', autoHide) - - setWindowMenuBarVisibility: (visible) -> - remote.getCurrentWindow().setMenuBarVisibility(visible) - - getPrimaryDisplayWorkAreaSize: -> - remote.screen.getPrimaryDisplay().workAreaSize - - getUserDefault: (key, type) -> - remote.systemPreferences.getUserDefault(key, type) - - confirm: ({message, detailedMessage, buttons}) -> - buttons ?= {} - if Array.isArray(buttons) - buttonLabels = buttons - else - buttonLabels = Object.keys(buttons) - - chosen = remote.dialog.showMessageBox(remote.getCurrentWindow(), { - type: 'info' - message: message - detail: detailedMessage - buttons: buttonLabels - normalizeAccessKeys: true - }) - - if Array.isArray(buttons) - chosen - else - callback = buttons[buttonLabels[chosen]] - callback?() - - showMessageDialog: (params) -> - - showSaveDialog: (params) -> - if typeof params is 'string' - params = {defaultPath: params} - @getCurrentWindow().showSaveDialog(params) - - playBeepSound: -> - shell.beep() - - onDidOpenLocations: (callback) -> - outerCallback = (event, message, detail) -> - callback(detail) if message is 'open-locations' - - ipcRenderer.on('message', outerCallback) - new Disposable -> - ipcRenderer.removeListener('message', outerCallback) - - onUpdateAvailable: (callback) -> - outerCallback = (event, message, detail) -> - # TODO: Yes, this is strange that `onUpdateAvailable` is listening for - # `did-begin-downloading-update`. We currently have no mechanism to know - # if there is an update, so begin of downloading is a good proxy. - callback(detail) if message is 'did-begin-downloading-update' - - ipcRenderer.on('message', outerCallback) - new Disposable -> - ipcRenderer.removeListener('message', outerCallback) - - onDidBeginDownloadingUpdate: (callback) -> - @onUpdateAvailable(callback) - - onDidBeginCheckingForUpdate: (callback) -> - outerCallback = (event, message, detail) -> - callback(detail) if message is 'checking-for-update' - - ipcRenderer.on('message', outerCallback) - new Disposable -> - ipcRenderer.removeListener('message', outerCallback) - - onDidCompleteDownloadingUpdate: (callback) -> - outerCallback = (event, message, detail) -> - # TODO: We could rename this event to `did-complete-downloading-update` - callback(detail) if message is 'update-available' - - ipcRenderer.on('message', outerCallback) - new Disposable -> - ipcRenderer.removeListener('message', outerCallback) - - onUpdateNotAvailable: (callback) -> - outerCallback = (event, message, detail) -> - callback(detail) if message is 'update-not-available' - - ipcRenderer.on('message', outerCallback) - new Disposable -> - ipcRenderer.removeListener('message', outerCallback) - - onUpdateError: (callback) -> - outerCallback = (event, message, detail) -> - callback(detail) if message is 'update-error' - - ipcRenderer.on('message', outerCallback) - new Disposable -> - ipcRenderer.removeListener('message', outerCallback) - - onApplicationMenuCommand: (callback) -> - outerCallback = (event, args...) -> - callback(args...) - - ipcRenderer.on('command', outerCallback) - new Disposable -> - ipcRenderer.removeListener('command', outerCallback) - - onContextMenuCommand: (callback) -> - outerCallback = (event, args...) -> - callback(args...) - - ipcRenderer.on('context-command', outerCallback) - new Disposable -> - ipcRenderer.removeListener('context-command', outerCallback) - - onURIMessage: (callback) -> - outerCallback = (event, args...) -> - callback(args...) - - ipcRenderer.on('uri-message', outerCallback) - new Disposable -> - ipcRenderer.removeListener('uri-message', outerCallback) - - onDidRequestUnload: (callback) -> - outerCallback = (event, message) -> - callback(event).then (shouldUnload) -> - ipcRenderer.send('did-prepare-to-unload', shouldUnload) - - ipcRenderer.on('prepare-to-unload', outerCallback) - new Disposable -> - ipcRenderer.removeListener('prepare-to-unload', outerCallback) - - onDidChangeHistoryManager: (callback) -> - outerCallback = (event, message) -> - callback(event) - - ipcRenderer.on('did-change-history-manager', outerCallback) - new Disposable -> - ipcRenderer.removeListener('did-change-history-manager', outerCallback) - - didChangeHistoryManager: -> - ipcRenderer.send('did-change-history-manager') - - openExternal: (url) -> - shell.openExternal(url) - - checkForUpdate: -> - ipcRenderer.send('command', 'application:check-for-update') - - restartAndInstallUpdate: -> - ipcRenderer.send('command', 'application:install-update') - - getAutoUpdateManagerState: -> - ipcRenderer.sendSync('get-auto-update-manager-state') - - getAutoUpdateManagerErrorMessage: -> - ipcRenderer.sendSync('get-auto-update-manager-error') - - emitWillSavePath: (path) -> - ipcRenderer.sendSync('will-save-path', path) - - emitDidSavePath: (path) -> - ipcRenderer.sendSync('did-save-path', path) - - resolveProxy: (requestId, url) -> - ipcRenderer.send('resolve-proxy', requestId, url) - - onDidResolveProxy: (callback) -> - outerCallback = (event, requestId, proxy) -> - callback(requestId, proxy) - - ipcRenderer.on('did-resolve-proxy', outerCallback) - new Disposable -> - ipcRenderer.removeListener('did-resolve-proxy', outerCallback) diff --git a/src/application-delegate.js b/src/application-delegate.js new file mode 100644 index 000000000..87e531b85 --- /dev/null +++ b/src/application-delegate.js @@ -0,0 +1,354 @@ +const {ipcRenderer, remote, shell} = require('electron') +const ipcHelpers = require('./ipc-helpers') +const {Disposable} = require('event-kit') +const getWindowLoadSettings = require('./get-window-load-settings') + +module.exports = +class ApplicationDelegate { + getWindowLoadSettings () { return getWindowLoadSettings() } + + open (params) { + return ipcRenderer.send('open', params) + } + + pickFolder (callback) { + const responseChannel = 'atom-pick-folder-response' + ipcRenderer.on(responseChannel, function (event, path) { + ipcRenderer.removeAllListeners(responseChannel) + return callback(path) + }) + return ipcRenderer.send('pick-folder', responseChannel) + } + + getCurrentWindow () { + return remote.getCurrentWindow() + } + + closeWindow () { + return ipcHelpers.call('window-method', 'close') + } + + async getTemporaryWindowState () { + const stateJSON = await ipcHelpers.call('get-temporary-window-state') + return JSON.parse(stateJSON) + } + + setTemporaryWindowState (state) { + return ipcHelpers.call('set-temporary-window-state', JSON.stringify(state)) + } + + getWindowSize () { + const [width, height] = Array.from(remote.getCurrentWindow().getSize()) + return {width, height} + } + + setWindowSize (width, height) { + return ipcHelpers.call('set-window-size', width, height) + } + + getWindowPosition () { + const [x, y] = Array.from(remote.getCurrentWindow().getPosition()) + return {x, y} + } + + setWindowPosition (x, y) { + return ipcHelpers.call('set-window-position', x, y) + } + + centerWindow () { + return ipcHelpers.call('center-window') + } + + focusWindow () { + return ipcHelpers.call('focus-window') + } + + showWindow () { + return ipcHelpers.call('show-window') + } + + hideWindow () { + return ipcHelpers.call('hide-window') + } + + reloadWindow () { + return ipcHelpers.call('window-method', 'reload') + } + + restartApplication () { + return ipcRenderer.send('restart-application') + } + + minimizeWindow () { + return ipcHelpers.call('window-method', 'minimize') + } + + isWindowMaximized () { + return remote.getCurrentWindow().isMaximized() + } + + maximizeWindow () { + return ipcHelpers.call('window-method', 'maximize') + } + + unmaximizeWindow () { + return ipcHelpers.call('window-method', 'unmaximize') + } + + isWindowFullScreen () { + return remote.getCurrentWindow().isFullScreen() + } + + setWindowFullScreen (fullScreen = false) { + return ipcHelpers.call('window-method', 'setFullScreen', fullScreen) + } + + onDidEnterFullScreen (callback) { + return ipcHelpers.on(ipcRenderer, 'did-enter-full-screen', callback) + } + + onDidLeaveFullScreen (callback) { + return ipcHelpers.on(ipcRenderer, 'did-leave-full-screen', callback) + } + + async openWindowDevTools () { + // Defer DevTools interaction to the next tick, because using them during + // event handling causes some wrong input events to be triggered on + // `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697). + await new Promise(process.nextTick) + return ipcHelpers.call('window-method', 'openDevTools') + } + + async closeWindowDevTools () { + // Defer DevTools interaction to the next tick, because using them during + // event handling causes some wrong input events to be triggered on + // `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697). + await new Promise(process.nextTick) + return ipcHelpers.call('window-method', 'closeDevTools') + } + + async toggleWindowDevTools () { + // Defer DevTools interaction to the next tick, because using them during + // event handling causes some wrong input events to be triggered on + // `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697). + await new Promise(process.nextTick) + return ipcHelpers.call('window-method', 'toggleDevTools') + } + + executeJavaScriptInWindowDevTools (code) { + return ipcRenderer.send('execute-javascript-in-dev-tools', code) + } + + setWindowDocumentEdited (edited) { + return ipcHelpers.call('window-method', 'setDocumentEdited', edited) + } + + setRepresentedFilename (filename) { + return ipcHelpers.call('window-method', 'setRepresentedFilename', filename) + } + + addRecentDocument (filename) { + return ipcRenderer.send('add-recent-document', filename) + } + + setRepresentedDirectoryPaths (paths) { + return ipcHelpers.call('window-method', 'setRepresentedDirectoryPaths', paths) + } + + setAutoHideWindowMenuBar (autoHide) { + return ipcHelpers.call('window-method', 'setAutoHideMenuBar', autoHide) + } + + setWindowMenuBarVisibility (visible) { + return remote.getCurrentWindow().setMenuBarVisibility(visible) + } + + getPrimaryDisplayWorkAreaSize () { + return remote.screen.getPrimaryDisplay().workAreaSize + } + + getUserDefault (key, type) { + return remote.systemPreferences.getUserDefault(key, type) + } + + confirm ({message, detailedMessage, buttons}) { + let buttonLabels + if (!buttons) buttons = {} + if (Array.isArray(buttons)) { + buttonLabels = buttons + } else { + buttonLabels = Object.keys(buttons) + } + + const chosen = remote.dialog.showMessageBox(remote.getCurrentWindow(), { + type: 'info', + message, + detail: detailedMessage, + buttons: buttonLabels, + normalizeAccessKeys: true + }) + + if (Array.isArray(buttons)) { + return chosen + } else { + const callback = buttons[buttonLabels[chosen]] + return (typeof callback === 'function' ? callback() : undefined) + } + } + + showMessageDialog (params) {} + + showSaveDialog (params) { + if (typeof params === 'string') { + params = {defaultPath: params} + } + return this.getCurrentWindow().showSaveDialog(params) + } + + playBeepSound () { + return shell.beep() + } + + onDidOpenLocations (callback) { + const outerCallback = (event, message, detail) => { + if (message === 'open-locations') callback(detail) + } + + ipcRenderer.on('message', outerCallback) + return new Disposable(() => ipcRenderer.removeListener('message', outerCallback)) + } + + onUpdateAvailable (callback) { + const outerCallback = (event, message, detail) => { + // TODO: Yes, this is strange that `onUpdateAvailable` is listening for + // `did-begin-downloading-update`. We currently have no mechanism to know + // if there is an update, so begin of downloading is a good proxy. + if (message === 'did-begin-downloading-update') callback(detail) + } + + ipcRenderer.on('message', outerCallback) + return new Disposable(() => ipcRenderer.removeListener('message', outerCallback)) + } + + onDidBeginDownloadingUpdate (callback) { + return this.onUpdateAvailable(callback) + } + + onDidBeginCheckingForUpdate (callback) { + const outerCallback = (event, message, detail) => { + if (message === 'checking-for-update') callback(detail) + } + + ipcRenderer.on('message', outerCallback) + return new Disposable(() => ipcRenderer.removeListener('message', outerCallback)) + } + + onDidCompleteDownloadingUpdate (callback) { + const outerCallback = (event, message, detail) => { + // TODO: We could rename this event to `did-complete-downloading-update` + if (message === 'update-available') callback(detail) + } + + ipcRenderer.on('message', outerCallback) + return new Disposable(() => ipcRenderer.removeListener('message', outerCallback)) + } + + onUpdateNotAvailable (callback) { + const outerCallback = (event, message, detail) => { + if (message === 'update-not-available') callback(detail) + } + + ipcRenderer.on('message', outerCallback) + return new Disposable(() => ipcRenderer.removeListener('message', outerCallback)) + } + + onUpdateError (callback) { + const outerCallback = (event, message, detail) => { + if (message === 'update-error') callback(detail) + } + + ipcRenderer.on('message', outerCallback) + return new Disposable(() => ipcRenderer.removeListener('message', outerCallback)) + } + + onApplicationMenuCommand (handler) { + const outerCallback = (event, ...args) => handler(...args) + + ipcRenderer.on('command', outerCallback) + return new Disposable(() => ipcRenderer.removeListener('command', outerCallback)) + } + + onContextMenuCommand (handler) { + const outerCallback = (event, ...args) => handler(...args) + + ipcRenderer.on('context-command', outerCallback) + return new Disposable(() => ipcRenderer.removeListener('context-command', outerCallback)) + } + + onURIMessage (handler) { + const outerCallback = (event, ...args) => handler(...args) + + ipcRenderer.on('uri-message', outerCallback) + return new Disposable(() => ipcRenderer.removeListener('uri-message', outerCallback)) + } + + onDidRequestUnload (callback) { + const outerCallback = async (event, message) => { + const shouldUnload = await callback(event) + ipcRenderer.send('did-prepare-to-unload', shouldUnload) + } + + ipcRenderer.on('prepare-to-unload', outerCallback) + return new Disposable(() => ipcRenderer.removeListener('prepare-to-unload', outerCallback)) + } + + onDidChangeHistoryManager (callback) { + const outerCallback = (event, message) => callback(event) + + ipcRenderer.on('did-change-history-manager', outerCallback) + return new Disposable(() => ipcRenderer.removeListener('did-change-history-manager', outerCallback)) + } + + didChangeHistoryManager () { + return ipcRenderer.send('did-change-history-manager') + } + + openExternal (url) { + return shell.openExternal(url) + } + + checkForUpdate () { + return ipcRenderer.send('command', 'application:check-for-update') + } + + restartAndInstallUpdate () { + return ipcRenderer.send('command', 'application:install-update') + } + + getAutoUpdateManagerState () { + return ipcRenderer.sendSync('get-auto-update-manager-state') + } + + getAutoUpdateManagerErrorMessage () { + return ipcRenderer.sendSync('get-auto-update-manager-error') + } + + emitWillSavePath (path) { + return ipcRenderer.sendSync('will-save-path', path) + } + + emitDidSavePath (path) { + return ipcRenderer.sendSync('did-save-path', path) + } + + resolveProxy (requestId, url) { + return ipcRenderer.send('resolve-proxy', requestId, url) + } + + onDidResolveProxy (callback) { + const outerCallback = (event, requestId, proxy) => callback(requestId, proxy) + + ipcRenderer.on('did-resolve-proxy', outerCallback) + return new Disposable(() => ipcRenderer.removeListener('did-resolve-proxy', outerCallback)) + } +} From 8ec54a04e2ac5cbcf3dfad7cf40ef41bca329eeb Mon Sep 17 00:00:00 2001 From: Bryant Ung Date: Fri, 5 Jan 2018 15:49:37 -0800 Subject: [PATCH 168/176] Add resize event spec --- spec/window-event-handler-spec.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/spec/window-event-handler-spec.js b/spec/window-event-handler-spec.js index 71c50d2c7..693387586 100644 --- a/spec/window-event-handler-spec.js +++ b/spec/window-event-handler-spec.js @@ -44,14 +44,21 @@ describe('WindowEventHandler', () => { }) ) }) - + + describe('resize event', () => + it('calls storeWindowDimensions', () => { + spyOn(atom, 'storeWindowDimensions') + window.dispatchEvent(new CustomEvent('resize')) + expect(atom.storeWindowDimensions).toHaveBeenCalled() + }) + ) + describe('window:close event', () => it('closes the window', () => { spyOn(atom, 'close') window.dispatchEvent(new CustomEvent('window:close')) expect(atom.close).toHaveBeenCalled() - }) - + }) ) describe('when a link is clicked', () => From 88e330d5774fd4c161deb9e689306403472d82c4 Mon Sep 17 00:00:00 2001 From: Bryant Ung Date: Fri, 5 Jan 2018 15:50:48 -0800 Subject: [PATCH 169/176] Remove trailing whitespace --- spec/window-event-handler-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/window-event-handler-spec.js b/spec/window-event-handler-spec.js index 693387586..2891aa2db 100644 --- a/spec/window-event-handler-spec.js +++ b/spec/window-event-handler-spec.js @@ -58,7 +58,7 @@ describe('WindowEventHandler', () => { spyOn(atom, 'close') window.dispatchEvent(new CustomEvent('window:close')) expect(atom.close).toHaveBeenCalled() - }) + }) ) describe('when a link is clicked', () => From 7f76320387e0e4e39a6b856237bc8bf572fa0831 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2018 14:28:27 -0800 Subject: [PATCH 170/176] Backfill a test for existing --wait functionality --- spec/main-process/atom-application.test.js | 36 +++++++++++++++++++--- src/main-process/atom-application.js | 3 +- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index b73a57fab..8d991f52c 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -389,6 +389,34 @@ describe('AtomApplication', function () { assert.deepEqual(app2Window.representedDirectoryPaths, []) }) + describe('when the `pidToKillWhenClosed` flag is passed', () => { + let killedPids, atomApplication + + beforeEach(() => { + killedPids = [] + atomApplication = buildAtomApplication({ + killProcess (pid) { killedPids.push(pid) } + }) + }) + + it('kills the specified pid after a newly-opened window is closed', async () => { + const window1 = atomApplication.launch(parseCommandLine([makeTempDir(), '--wait', '--pid', '101'])) + await focusWindow(window1) + + const [window2] = atomApplication.launch(parseCommandLine(['--wait', '--pid', '102'])) + await focusWindow(window2) + assert.deepEqual(killedPids, []) + + window1.close() + await window1.closedPromise + assert.deepEqual(killedPids, [101]) + + window2.close() + await window2.closedPromise + assert.deepEqual(killedPids, [101, 102]) + }) + }) + describe('when closing the last window', () => { if (process.platform === 'linux' || process.platform === 'win32') { it('quits the application', async () => { @@ -529,11 +557,11 @@ describe('AtomApplication', function () { assert(electron.app.didQuit()) }) - function buildAtomApplication () { - const atomApplication = new AtomApplication({ + function buildAtomApplication (params = {}) { + const atomApplication = new AtomApplication(Object.assign({ resourcePath: ATOM_RESOURCE_PATH, - atomHomeDirPath: process.env.ATOM_HOME - }) + atomHomeDirPath: process.env.ATOM_HOME, + }, params)) atomApplicationsToDestroy.push(atomApplication) return atomApplication } diff --git a/src/main-process/atom-application.js b/src/main-process/atom-application.js index 459520722..df5c5e202 100644 --- a/src/main-process/atom-application.js +++ b/src/main-process/atom-application.js @@ -101,6 +101,7 @@ class AtomApplication extends EventEmitter { this.socketPath = options.socketPath this.logFile = options.logFile this.userDataDir = options.userDataDir + this._killProcess = options.killProcess || process.kill.bind(process) if (options.test || options.benchmark || options.benchmarkTest) this.socketPath = null this.pidsToOpenWindows = {} @@ -880,7 +881,7 @@ class AtomApplication extends EventEmitter { killProcess (pid) { try { const parsedPid = parseInt(pid) - if (isFinite(parsedPid)) process.kill(parsedPid) + if (isFinite(parsedPid)) this._killProcess(parsedPid) } catch (error) { if (error.code !== 'ESRCH') { console.log(`Killing process ${pid} failed: ${error.code != null ? error.code : error.message}`) From 1f4ccf302426c7699122d5e817108456bef3efc4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2018 16:42:02 -0800 Subject: [PATCH 171/176] Allow existing windows to be reused when running --wait --- spec/main-process/atom-application.test.js | 65 +++++++++++++++++++--- src/application-delegate.js | 4 ++ src/atom-environment.js | 30 ++++++---- src/main-process/atom-application.js | 58 ++++++++++++------- src/main-process/atom-window.js | 4 ++ 5 files changed, 122 insertions(+), 39 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 8d991f52c..e9775d225 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -1,3 +1,4 @@ +const temp = require('temp').track() const season = require('season') const dedent = require('dedent') const electron = require('electron') @@ -389,32 +390,81 @@ describe('AtomApplication', function () { assert.deepEqual(app2Window.representedDirectoryPaths, []) }) - describe('when the `pidToKillWhenClosed` flag is passed', () => { - let killedPids, atomApplication + describe('when the `--wait` flag is passed', () => { + let killedPids, atomApplication, onDidKillProcess beforeEach(() => { killedPids = [] + onDidKillProcess = null atomApplication = buildAtomApplication({ - killProcess (pid) { killedPids.push(pid) } + killProcess (pid) { + killedPids.push(pid) + if (onDidKillProcess) onDidKillProcess() + } }) }) it('kills the specified pid after a newly-opened window is closed', async () => { - const window1 = atomApplication.launch(parseCommandLine([makeTempDir(), '--wait', '--pid', '101'])) + const window1 = atomApplication.launch(parseCommandLine(['--wait', '--pid', '101'])) await focusWindow(window1) - const [window2] = atomApplication.launch(parseCommandLine(['--wait', '--pid', '102'])) + const [window2] = atomApplication.launch(parseCommandLine(['--new-window', '--wait', '--pid', '102'])) await focusWindow(window2) assert.deepEqual(killedPids, []) + let processKillPromise = new Promise(resolve => { onDidKillProcess = resolve }) window1.close() - await window1.closedPromise + await processKillPromise assert.deepEqual(killedPids, [101]) + processKillPromise = new Promise(resolve => { onDidKillProcess = resolve }) window2.close() - await window2.closedPromise + await processKillPromise assert.deepEqual(killedPids, [101, 102]) }) + + it('kills the specified pid after a newly-opened file in an existing window is closed', async () => { + const window = atomApplication.launch(parseCommandLine(['--wait', '--pid', '101'])) + await focusWindow(window) + + const filePath1 = temp.openSync('test').path + const filePath2 = temp.openSync('test').path + fs.writeFileSync(filePath1, 'File 1') + fs.writeFileSync(filePath2, 'File 2') + + const reusedWindow = atomApplication.launch(parseCommandLine(['--wait', '--pid', '102', filePath1, filePath2])) + assert.equal(reusedWindow, window) + + const activeEditorPath = await evalInWebContents(window.browserWindow.webContents, send => { + const subscription = atom.workspace.onDidChangeActivePaneItem(editor => { + send(editor.getPath()) + subscription.dispose() + }) + }) + + assert([filePath1, filePath2].includes(activeEditorPath)) + assert.deepEqual(killedPids, []) + + await evalInWebContents(window.browserWindow.webContents, send => { + atom.workspace.getActivePaneItem().destroy() + send() + }) + await timeoutPromise(100) + assert.deepEqual(killedPids, []) + + let processKillPromise = new Promise(resolve => { onDidKillProcess = resolve }) + await evalInWebContents(window.browserWindow.webContents, send => { + atom.workspace.getActivePaneItem().destroy() + send() + }) + await processKillPromise + assert.deepEqual(killedPids, [102]) + + processKillPromise = new Promise(resolve => { onDidKillProcess = resolve }) + window.close() + await processKillPromise + assert.deepEqual(killedPids, [102, 101]) + }) }) describe('when closing the last window', () => { @@ -594,7 +644,6 @@ describe('AtomApplication', function () { } function makeTempDir (name) { - const temp = require('temp').track() return fs.realpathSync(temp.mkdirSync(name)) } diff --git a/src/application-delegate.js b/src/application-delegate.js index 87e531b85..6d6d892ca 100644 --- a/src/application-delegate.js +++ b/src/application-delegate.js @@ -139,6 +139,10 @@ class ApplicationDelegate { return ipcRenderer.send('execute-javascript-in-dev-tools', code) } + didCloseInitialPath (path) { + return ipcHelpers.call('window-method', 'didCloseInitialPath', path) + } + setWindowDocumentEdited (edited) { return ipcHelpers.call('window-method', 'setDocumentEdited', edited) } diff --git a/src/atom-environment.js b/src/atom-environment.js index fc3201dfc..b629e96d2 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -70,6 +70,7 @@ class AtomEnvironment { this.loadTime = null this.emitter = new Emitter() this.disposables = new CompositeDisposable() + this.pathsToNotifyWhenClosed = new Set() // Public: A {DeserializerManager} instance this.deserializers = new DeserializerManager(this) @@ -359,6 +360,7 @@ class AtomEnvironment { this.grammars.clear() this.textEditors.clear() this.views.clear() + this.pathsToNotifyWhenClosed.clear() } destroy () { @@ -822,7 +824,15 @@ class AtomEnvironment { this.document.body.appendChild(this.workspace.getElement()) if (this.backgroundStylesheet) this.backgroundStylesheet.remove() - this.watchProjectPaths() + this.disposables.add(this.project.onDidChangePaths(() => { + this.applicationDelegate.setRepresentedDirectoryPaths(this.project.getPaths()) + })) + this.disposables.add(this.workspace.onDidDestroyPaneItem(({item}) => { + const path = item.getPath && item.getPath() + if (this.pathsToNotifyWhenClosed.has(path)) { + this.applicationDelegate.didCloseInitialPath(path) + } + })) this.packages.activate() this.keymaps.loadUserKeymap() @@ -1025,13 +1035,6 @@ class AtomEnvironment { return this.themes.load() } - // Notify the browser project of the window's current project path - watchProjectPaths () { - this.disposables.add(this.project.onDidChangePaths(() => { - this.applicationDelegate.setRepresentedDirectoryPaths(this.project.getPaths()) - })) - } - setDocumentEdited (edited) { if (typeof this.applicationDelegate.setWindowDocumentEdited === 'function') { this.applicationDelegate.setWindowDocumentEdited(edited) @@ -1300,8 +1303,9 @@ class AtomEnvironment { } } - for (var {pathToOpen, initialLine, initialColumn, forceAddToWindow} of locations) { - if (pathToOpen && (needsProjectPaths || forceAddToWindow)) { + for (const location of locations) { + const {pathToOpen} = location + if (pathToOpen && (needsProjectPaths || location.forceAddToWindow)) { if (fs.existsSync(pathToOpen)) { pushFolderToOpen(this.project.getDirectoryForProjectPath(pathToOpen).getPath()) } else if (fs.existsSync(path.dirname(pathToOpen))) { @@ -1312,8 +1316,10 @@ class AtomEnvironment { } if (!fs.isDirectorySync(pathToOpen)) { - fileLocationsToOpen.push({pathToOpen, initialLine, initialColumn}) + fileLocationsToOpen.push(location) } + + if (location.notifyWhenClosed) this.pathsToNotifyWhenClosed.add(pathToOpen) } let restoredState = false @@ -1334,7 +1340,7 @@ class AtomEnvironment { if (!restoredState) { const fileOpenPromises = [] - for ({pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) { + for (const {pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) { fileOpenPromises.push(this.workspace && this.workspace.open(pathToOpen, {initialLine, initialColumn})) } await Promise.all(fileOpenPromises) diff --git a/src/main-process/atom-application.js b/src/main-process/atom-application.js index df5c5e202..46a5f8afa 100644 --- a/src/main-process/atom-application.js +++ b/src/main-process/atom-application.js @@ -104,7 +104,7 @@ class AtomApplication extends EventEmitter { this._killProcess = options.killProcess || process.kill.bind(process) if (options.test || options.benchmark || options.benchmarkTest) this.socketPath = null - this.pidsToOpenWindows = {} + this.waitSessionsByWindow = new Map() this.windowStack = new WindowStack() this.config = new Config({enablePersistence: true}) @@ -789,13 +789,17 @@ class AtomApplication extends EventEmitter { safeMode = Boolean(safeMode) clearWindowState = Boolean(clearWindowState) - const locationsToOpen = pathsToOpen.map(pathToOpen => - this.locationForPathToOpen(pathToOpen, executedFrom, addToLastWindow) - ) - pathsToOpen = locationsToOpen.map(locationToOpen => locationToOpen.pathToOpen) + const locationsToOpen = [] + for (let i = 0; i < pathsToOpen.length; i++) { + const location = this.parsePathToOpen(pathsToOpen[i], executedFrom, addToLastWindow) + location.forceAddToWindow = addToLastWindow + location.notifyWhenClosed = pidToKillWhenClosed != null + locationsToOpen.push(location) + pathsToOpen[i] = location.pathToOpen + } let existingWindow - if (!pidToKillWhenClosed && !newWindow) { + if (!newWindow) { existingWindow = this.windowForPaths(pathsToOpen, devMode) const stats = pathsToOpen.map(pathToOpen => fs.statSyncNoException(pathToOpen)) if (!existingWindow) { @@ -853,26 +857,43 @@ class AtomApplication extends EventEmitter { } if (pidToKillWhenClosed != null) { - this.pidsToOpenWindows[pidToKillWhenClosed] = openedWindow + if (!this.waitSessionsByWindow.has(openedWindow)) { + this.waitSessionsByWindow.set(openedWindow, []) + } + this.waitSessionsByWindow.get(openedWindow).push({ + pid: pidToKillWhenClosed, + remainingPaths: new Set(pathsToOpen) + }) } - openedWindow.browserWindow.once('closed', () => this.killProcessForWindow(openedWindow)) + openedWindow.browserWindow.once('closed', () => this.killProcessesForWindow(openedWindow)) return openedWindow } // Kill all processes associated with opened windows. killAllProcesses () { - for (let pid in this.pidsToOpenWindows) { - this.killProcess(pid) + for (let window of this.waitSessionsByWindow.keys()) { + this.killProcessesForWindow(window) } } - // Kill process associated with the given opened window. - killProcessForWindow (openedWindow) { - for (let pid in this.pidsToOpenWindows) { - const trackedWindow = this.pidsToOpenWindows[pid] - if (trackedWindow === openedWindow) { - this.killProcess(pid) + killProcessesForWindow (window) { + const sessions = this.waitSessionsByWindow.get(window) + if (!sessions) return + for (const session of sessions) { + this.killProcess(session.pid) + } + this.waitSessionsByWindow.delete(window) + } + + windowDidCloseInitialPath (window, initialPath) { + const waitSessions = this.waitSessionsByWindow.get(window) + for (let i = waitSessions.length - 1; i >= 0; i--) { + const session = waitSessions[i] + session.remainingPaths.delete(initialPath) + if (session.remainingPaths.size === 0) { + this.killProcess(session.pid) + waitSessions.splice(i, 1) } } } @@ -887,7 +908,6 @@ class AtomApplication extends EventEmitter { console.log(`Killing process ${pid} failed: ${error.code != null ? error.code : error.message}`) } } - delete this.pidsToOpenWindows[pid] } saveState (allowEmpty = false) { @@ -1193,7 +1213,7 @@ class AtomApplication extends EventEmitter { } } - locationForPathToOpen (pathToOpen, executedFrom = '', forceAddToWindow) { + parsePathToOpen (pathToOpen, executedFrom = '') { let initialColumn, initialLine if (!pathToOpen) { return {pathToOpen} @@ -1218,7 +1238,7 @@ class AtomApplication extends EventEmitter { pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen)) } - return {pathToOpen, initialLine, initialColumn, forceAddToWindow} + return {pathToOpen, initialLine, initialColumn} } // Opens a native dialog to prompt the user for a path. diff --git a/src/main-process/atom-window.js b/src/main-process/atom-window.js index 582852ad4..77dd09b31 100644 --- a/src/main-process/atom-window.js +++ b/src/main-process/atom-window.js @@ -411,6 +411,10 @@ class AtomWindow extends EventEmitter { return this.atomApplication.saveState() } + didCloseInitialPath (path) { + this.atomApplication.windowDidCloseInitialPath(this, path) + } + copy () { return this.browserWindow.copy() } From 386b786d93b68d9b72e7a483a39d46ede7bcab0d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2018 17:43:51 -0800 Subject: [PATCH 172/176] Let 'atom --wait -a folder' exit due to removing the project folder --- spec/main-process/atom-application.test.js | 29 +++++++++++++++++++++- src/atom-environment.js | 11 ++++++-- src/main-process/atom-application.js | 1 + 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index e9775d225..7818314db 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -465,6 +465,33 @@ describe('AtomApplication', function () { await processKillPromise assert.deepEqual(killedPids, [102, 101]) }) + + it('kills the specified pid after a newly-opened directory in an existing window is closed', async () => { + const window = atomApplication.launch(parseCommandLine([])) + await focusWindow(window) + + const dirPath1 = makeTempDir() + const reusedWindow = atomApplication.launch(parseCommandLine(['--wait', '--pid', '101', dirPath1])) + assert.equal(reusedWindow, window) + assert.deepEqual(await getTreeViewRootDirectories(window), [dirPath1]) + assert.deepEqual(killedPids, []) + + const dirPath2 = makeTempDir() + await evalInWebContents(window.browserWindow.webContents, (send, dirPath1, dirPath2) => { + atom.project.setPaths([dirPath1, dirPath2]) + send() + }, dirPath1, dirPath2) + await timeoutPromise(100) + assert.deepEqual(killedPids, []) + + let processKillPromise = new Promise(resolve => { onDidKillProcess = resolve }) + await evalInWebContents(window.browserWindow.webContents, (send, dirPath2) => { + atom.project.setPaths([dirPath2]) + send() + }, dirPath2) + await processKillPromise + assert.deepEqual(killedPids, [101]) + }) }) describe('when closing the last window', () => { @@ -662,7 +689,7 @@ describe('AtomApplication', function () { function sendBackToMainProcess (result) { require('electron').ipcRenderer.send('${channelId}', result) } - (${source})(sendBackToMainProcess) + (${source})(sendBackToMainProcess, ${args.map(JSON.stringify).join(', ')}) `) }) } diff --git a/src/atom-environment.js b/src/atom-environment.js index b629e96d2..6e42f88a0 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -824,8 +824,15 @@ class AtomEnvironment { this.document.body.appendChild(this.workspace.getElement()) if (this.backgroundStylesheet) this.backgroundStylesheet.remove() - this.disposables.add(this.project.onDidChangePaths(() => { - this.applicationDelegate.setRepresentedDirectoryPaths(this.project.getPaths()) + let previousProjectPaths = this.project.getPaths() + this.disposables.add(this.project.onDidChangePaths(newPaths => { + for (let path of previousProjectPaths) { + if (this.pathsToNotifyWhenClosed.has(path) && !newPaths.includes(path)) { + this.applicationDelegate.didCloseInitialPath(path) + } + } + previousProjectPaths = newPaths + this.applicationDelegate.setRepresentedDirectoryPaths(newPaths) })) this.disposables.add(this.workspace.onDidDestroyPaneItem(({item}) => { const path = item.getPath && item.getPath() diff --git a/src/main-process/atom-application.js b/src/main-process/atom-application.js index 46a5f8afa..52bc1287b 100644 --- a/src/main-process/atom-application.js +++ b/src/main-process/atom-application.js @@ -888,6 +888,7 @@ class AtomApplication extends EventEmitter { windowDidCloseInitialPath (window, initialPath) { const waitSessions = this.waitSessionsByWindow.get(window) + if (!waitSessions) return for (let i = waitSessions.length - 1; i >= 0; i--) { const session = waitSessions[i] session.remainingPaths.delete(initialPath) From 6f50f32116c21eca0dc47abc6f3ea464127b422f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2018 17:49:44 -0800 Subject: [PATCH 173/176] Rename pathsToNotifyWhenClosed -> pathsWithWaitSessions --- src/application-delegate.js | 4 ++-- src/atom-environment.js | 14 +++++++------- src/main-process/atom-application.js | 4 ++-- src/main-process/atom-window.js | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/application-delegate.js b/src/application-delegate.js index 6d6d892ca..1b1dd1e9c 100644 --- a/src/application-delegate.js +++ b/src/application-delegate.js @@ -139,8 +139,8 @@ class ApplicationDelegate { return ipcRenderer.send('execute-javascript-in-dev-tools', code) } - didCloseInitialPath (path) { - return ipcHelpers.call('window-method', 'didCloseInitialPath', path) + didClosePathWithWaitSession (path) { + return ipcHelpers.call('window-method', 'didClosePathWithWaitSession', path) } setWindowDocumentEdited (edited) { diff --git a/src/atom-environment.js b/src/atom-environment.js index 6e42f88a0..70fb352e2 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -70,7 +70,7 @@ class AtomEnvironment { this.loadTime = null this.emitter = new Emitter() this.disposables = new CompositeDisposable() - this.pathsToNotifyWhenClosed = new Set() + this.pathsWithWaitSessions = new Set() // Public: A {DeserializerManager} instance this.deserializers = new DeserializerManager(this) @@ -360,7 +360,7 @@ class AtomEnvironment { this.grammars.clear() this.textEditors.clear() this.views.clear() - this.pathsToNotifyWhenClosed.clear() + this.pathsWithWaitSessions.clear() } destroy () { @@ -827,8 +827,8 @@ class AtomEnvironment { let previousProjectPaths = this.project.getPaths() this.disposables.add(this.project.onDidChangePaths(newPaths => { for (let path of previousProjectPaths) { - if (this.pathsToNotifyWhenClosed.has(path) && !newPaths.includes(path)) { - this.applicationDelegate.didCloseInitialPath(path) + if (this.pathsWithWaitSessions.has(path) && !newPaths.includes(path)) { + this.applicationDelegate.didClosePathWithWaitSession(path) } } previousProjectPaths = newPaths @@ -836,8 +836,8 @@ class AtomEnvironment { })) this.disposables.add(this.workspace.onDidDestroyPaneItem(({item}) => { const path = item.getPath && item.getPath() - if (this.pathsToNotifyWhenClosed.has(path)) { - this.applicationDelegate.didCloseInitialPath(path) + if (this.pathsWithWaitSessions.has(path)) { + this.applicationDelegate.didClosePathWithWaitSession(path) } })) @@ -1326,7 +1326,7 @@ class AtomEnvironment { fileLocationsToOpen.push(location) } - if (location.notifyWhenClosed) this.pathsToNotifyWhenClosed.add(pathToOpen) + if (location.hasWaitSession) this.pathsWithWaitSessions.add(pathToOpen) } let restoredState = false diff --git a/src/main-process/atom-application.js b/src/main-process/atom-application.js index 52bc1287b..372bd537c 100644 --- a/src/main-process/atom-application.js +++ b/src/main-process/atom-application.js @@ -793,7 +793,7 @@ class AtomApplication extends EventEmitter { for (let i = 0; i < pathsToOpen.length; i++) { const location = this.parsePathToOpen(pathsToOpen[i], executedFrom, addToLastWindow) location.forceAddToWindow = addToLastWindow - location.notifyWhenClosed = pidToKillWhenClosed != null + location.hasWaitSession = pidToKillWhenClosed != null locationsToOpen.push(location) pathsToOpen[i] = location.pathToOpen } @@ -886,7 +886,7 @@ class AtomApplication extends EventEmitter { this.waitSessionsByWindow.delete(window) } - windowDidCloseInitialPath (window, initialPath) { + windowDidClosePathWithWaitSession (window, initialPath) { const waitSessions = this.waitSessionsByWindow.get(window) if (!waitSessions) return for (let i = waitSessions.length - 1; i >= 0; i--) { diff --git a/src/main-process/atom-window.js b/src/main-process/atom-window.js index 77dd09b31..0492b5f8f 100644 --- a/src/main-process/atom-window.js +++ b/src/main-process/atom-window.js @@ -411,8 +411,8 @@ class AtomWindow extends EventEmitter { return this.atomApplication.saveState() } - didCloseInitialPath (path) { - this.atomApplication.windowDidCloseInitialPath(this, path) + didClosePathWithWaitSession (path) { + this.atomApplication.windowDidClosePathWithWaitSession(this, path) } copy () { From 3f11fa57ee7022b5383070427a91f82238cfa1ed Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2018 20:26:41 -0800 Subject: [PATCH 174/176] Make tree-sitter indent methods delegate to textmate ones for now --- src/text-mate-language-mode.js | 27 +++++++++++-------- src/tree-sitter-language-mode.js | 45 +++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/src/text-mate-language-mode.js b/src/text-mate-language-mode.js index 123e39f58..1a7cb6d2e 100644 --- a/src/text-mate-language-mode.js +++ b/src/text-mate-language-mode.js @@ -74,10 +74,15 @@ class TextMateLanguageMode { // // Returns a {Number}. suggestedIndentForBufferRow (bufferRow, tabLength, options) { - return this._suggestedIndentForTokenizedLineAtBufferRow( + const line = this.buffer.lineForRow(bufferRow) + const tokenizedLine = this.tokenizedLineForRow(bufferRow) + const iterator = tokenizedLine.getTokenIterator() + iterator.next() + const scopeDescriptor = new ScopeDescriptor({scopes: iterator.getScopes()}) + return this._suggestedIndentForLineWithScopeAtBufferRow( bufferRow, - this.buffer.lineForRow(bufferRow), - this.tokenizedLineForRow(bufferRow), + line, + scopeDescriptor, tabLength, options ) @@ -90,10 +95,14 @@ class TextMateLanguageMode { // // Returns a {Number}. suggestedIndentForLineAtBufferRow (bufferRow, line, tabLength) { - return this._suggestedIndentForTokenizedLineAtBufferRow( + const tokenizedLine = this.buildTokenizedLineForRowWithText(bufferRow, line) + const iterator = tokenizedLine.getTokenIterator() + iterator.next() + const scopeDescriptor = new ScopeDescriptor({scopes: iterator.getScopes()}) + return this._suggestedIndentForLineWithScopeAtBufferRow( bufferRow, line, - this.buildTokenizedLineForRowWithText(bufferRow, line), + scopeDescriptor, tabLength ) } @@ -111,7 +120,7 @@ class TextMateLanguageMode { const currentIndentLevel = this.indentLevelForLine(line, tabLength) if (currentIndentLevel === 0) return - const scopeDescriptor = this.scopeDescriptorForPosition([bufferRow, 0]) + const scopeDescriptor = this.scopeDescriptorForPosition(new Point(bufferRow, 0)) const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor) if (!decreaseIndentRegex) return @@ -138,11 +147,7 @@ class TextMateLanguageMode { return desiredIndentLevel } - _suggestedIndentForTokenizedLineAtBufferRow (bufferRow, line, tokenizedLine, tabLength, options) { - const iterator = tokenizedLine.getTokenIterator() - iterator.next() - const scopeDescriptor = new ScopeDescriptor({scopes: iterator.getScopes()}) - + _suggestedIndentForLineWithScopeAtBufferRow (bufferRow, line, scopeDescriptor, tabLength, options) { const increaseIndentRegex = this.increaseIndentRegexForScopeDescriptor(scopeDescriptor) const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor) const decreaseNextIndentRegex = this.decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor) diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 313c3574d..2ab023b86 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -2,6 +2,7 @@ const {Document} = require('tree-sitter') const {Point, Range, Emitter} = require('atom') const ScopeDescriptor = require('./scope-descriptor') const TokenizedLine = require('./tokenized-line') +const TextMateLanguageMode = require('./text-mate-language-mode') let nextId = 0 @@ -19,6 +20,10 @@ class TreeSitterLanguageMode { this.rootScopeDescriptor = new ScopeDescriptor({scopes: [this.grammar.id]}) this.emitter = new Emitter() this.isFoldableCache = [] + + // TODO: Remove this once TreeSitterLanguageMode implements its own auto-indentation system. This + // is temporarily needed in order to delegate to the TextMateLanguageMode's auto-indent system. + this.regexesByPattern = {} } getLanguageId () { @@ -83,24 +88,22 @@ class TreeSitterLanguageMode { */ suggestedIndentForLineAtBufferRow (row, line, tabLength) { - return this.suggestedIndentForBufferRow(row, tabLength) + return this._suggestedIndentForLineWithScopeAtBufferRow( + row, + line, + this.rootScopeDescriptor, + tabLength + ) } suggestedIndentForBufferRow (row, tabLength, options) { - let precedingRow - if (!options || options.skipBlankLines !== false) { - precedingRow = this.buffer.previousNonBlankRow(row) - if (precedingRow == null) return 0 - } else { - precedingRow = row - 1 - if (precedingRow < 0) return 0 - } - - return this.indentLevelForLine(this.buffer.lineForRow(precedingRow), tabLength) - } - - suggestedIndentForEditedBufferRow (row) { - return null + return this._suggestedIndentForLineWithScopeAtBufferRow( + row, + this.buffer.lineForRow(row), + this.rootScopeDescriptor, + tabLength, + options + ) } indentLevelForLine (line, tabLength = tabLength) { @@ -508,3 +511,15 @@ class TreeSitterTextBufferInput { function last (array) { return array[array.length - 1] } + +// TODO: Remove this once TreeSitterLanguageMode implements its own auto-indent system. +[ + '_suggestedIndentForLineWithScopeAtBufferRow', + 'suggestedIndentForEditedBufferRow', + 'increaseIndentRegexForScopeDescriptor', + 'decreaseIndentRegexForScopeDescriptor', + 'decreaseNextIndentRegexForScopeDescriptor', + 'regexForPattern' +].forEach(methodName => { + module.exports.prototype[methodName] = TextMateLanguageMode.prototype[methodName] +}) From 84c9524403d115c7a0a3505880655c3befe52103 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2018 20:26:41 -0800 Subject: [PATCH 175/176] Omit anonymous token types in tree-sitter scope descriptors for now --- src/tree-sitter-language-mode.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 2ab023b86..41c87ba00 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -298,6 +298,13 @@ class TreeSitterLanguageMode { scopeDescriptorForPosition (point) { const result = [] let node = this.document.rootNode.descendantForPosition(point) + + // Don't include anonymous token types like '(' because they prevent scope chains + // from being parsed as CSS selectors by the `slick` parser. Other css selector + // parsers like `postcss-selector-parser` do allow arbitrary quoted strings in + // selectors. + if (!node.isNamed) node = node.parent + while (node) { result.push(node.type) node = node.parent From 390ab7449a77f42c554afef03b86d5ddf2c9278e Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sun, 7 Jan 2018 20:44:02 -0500 Subject: [PATCH 176/176] :arrow_up: markdown-preview@0.159.19 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 77ecd06fd..12236bcd4 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "keybinding-resolver": "0.38.1", "line-ending-selector": "0.7.5", "link": "0.31.4", - "markdown-preview": "0.159.18", + "markdown-preview": "0.159.19", "metrics": "1.2.6", "notifications": "0.70.2", "open-on-github": "1.3.1",