diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 693e7358c7b..dceaecddb04 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).
diff --git a/LICENSE.md b/LICENSE.md
index 5bdf03cdecf..58684e68332 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
diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md
index a578c38ce18..a3356809dcd 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
diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson
index 7161a847832..6d576f1020c 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'
diff --git a/package.json b/package.json
index 3330769cbb1..b5974667694 100644
--- a/package.json
+++ b/package.json
@@ -70,7 +70,8 @@
"service-hub": "^0.7.4",
"sinon": "1.17.4",
"temp": "^0.8.3",
- "text-buffer": "13.10.1",
+ "text-buffer": "13.11.0",
+ "tree-sitter": "^0.8.4",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"winreg": "^1.2.1",
@@ -118,13 +119,13 @@
"keybinding-resolver": "0.38.1",
"line-ending-selector": "0.7.5",
"link": "0.31.4",
- "markdown-preview": "0.159.18",
+ "markdown-preview": "0.159.20",
"metrics": "1.2.6",
"notifications": "0.70.2",
"open-on-github": "1.3.1",
"package-generator": "1.3.0",
- "settings-view": "0.253.2",
- "snippets": "1.2.0",
+ "settings-view": "0.253.4",
+ "snippets": "1.3.0",
"spell-check": "0.72.5",
"status-bar": "1.8.15",
"styleguide": "0.49.10",
@@ -136,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",
"language-clojure": "0.22.5",
"language-coffee-script": "0.49.3",
"language-csharp": "0.14.4",
"language-css": "0.42.8",
"language-gfm": "0.90.3",
"language-git": "0.19.1",
- "language-go": "0.44.4",
+ "language-go": "0.45.0",
"language-html": "0.48.5",
"language-hyperlink": "0.16.3",
"language-java": "0.27.6",
- "language-javascript": "0.127.7",
+ "language-javascript": "0.128.0",
"language-json": "0.19.1",
"language-less": "0.34.1",
"language-make": "0.22.3",
@@ -156,17 +157,17 @@
"language-perl": "0.38.1",
"language-php": "0.43.0",
"language-property-list": "0.9.1",
- "language-python": "0.45.6",
+ "language-python": "0.47.0",
"language-ruby": "0.71.4",
"language-ruby-on-rails": "0.25.3",
"language-sass": "0.61.4",
- "language-shellscript": "0.25.4",
+ "language-shellscript": "0.26.0",
"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.2.3",
+ "language-typescript": "0.3.0",
"language-xml": "0.35.2",
"language-yaml": "0.31.1"
},
diff --git a/script/build b/script/build
index acc54cdac0b..55cebe96d46 100755
--- a/script/build
+++ b/script/build
@@ -28,6 +28,7 @@ const argv = yargs
const checkChromedriverVersion = require('./lib/check-chromedriver-version')
const cleanOutputDirectory = require('./lib/clean-output-directory')
+const cleanPackageLock = require('./lib/clean-package-lock')
const codeSignOnMac = require('./lib/code-sign-on-mac')
const codeSignOnWindows = require('./lib/code-sign-on-windows')
const compressArtifacts = require('./lib/compress-artifacts')
@@ -58,6 +59,7 @@ const CONFIG = require('./config')
let binariesPromise = Promise.resolve()
if (!argv.existingBinaries) {
+ cleanPackageLock()
checkChromedriverVersion()
cleanOutputDirectory()
copyAssets()
diff --git a/script/lib/clean-package-lock.js b/script/lib/clean-package-lock.js
new file mode 100644
index 00000000000..01376c9c52e
--- /dev/null
+++ b/script/lib/clean-package-lock.js
@@ -0,0 +1,18 @@
+// This module exports a function that deletes all `package-lock.json` files that do
+// not exist under a `node_modules` directory.
+
+'use strict'
+
+const CONFIG = require('../config')
+const fs = require('fs-extra')
+const glob = require('glob')
+const path = require('path')
+
+module.exports = function () {
+ console.log('Deleting problematic package-lock.json files')
+ let paths = glob.sync(path.join(CONFIG.repositoryRootPath, '**', 'package-lock.json'), {ignore: path.join('**', 'node_modules', '**')})
+
+ for (let path of paths) {
+ fs.unlinkSync(path)
+ }
+}
diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js
index 4c074a2b589..89812a76210 100644
--- a/script/lib/generate-startup-snapshot.js
+++ b/script/lib/generate-startup-snapshot.js
@@ -57,7 +57,9 @@ 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') ||
+ relativePath === path.join('..', 'node_modules', 'winreg', 'lib', 'registry.js')
)
}
}).then((snapshotScript) => {
diff --git a/spec/atom-environment-spec.js b/spec/atom-environment-spec.js
index 70ca9c30993..7dc07dafde4 100644
--- a/spec/atom-environment-spec.js
+++ b/spec/atom-environment-spec.js
@@ -1,4 +1,4 @@
-const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
+const {it, fit, ffit, beforeEach, afterEach, conditionPromise} = require('./async-spec-helpers')
const _ = require('underscore-plus')
const path = require('path')
const temp = require('temp').track()
@@ -518,27 +518,31 @@ describe('AtomEnvironment', () => {
})
})
- it('prompts the user to restore the state in a new window, discarding it and adding folder to current window', () => {
- spyOn(atom, 'confirm').andReturn(1)
+ it('prompts the user to restore the state in a new window, discarding it and adding folder to current window', async () => {
+ jasmine.useRealClock()
+ spyOn(atom, 'confirm').andCallFake((options, callback) => callback(1))
spyOn(atom.project, 'addPath')
spyOn(atom.workspace, 'open')
const state = Symbol()
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.confirm).toHaveBeenCalled()
- expect(atom.project.addPath.callCount).toBe(1)
+ await conditionPromise(() => atom.project.addPath.callCount === 1)
+
expect(atom.project.addPath).toHaveBeenCalledWith(__dirname)
expect(atom.workspace.open.callCount).toBe(1)
expect(atom.workspace.open).toHaveBeenCalledWith(__filename)
})
- it('prompts the user to restore the state in a new window, opening a new window', () => {
- spyOn(atom, 'confirm').andReturn(0)
+ it('prompts the user to restore the state in a new window, opening a new window', async () => {
+ jasmine.useRealClock()
+ spyOn(atom, 'confirm').andCallFake((options, callback) => callback(0))
spyOn(atom, 'open')
const state = Symbol()
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.confirm).toHaveBeenCalled()
+ await conditionPromise(() => atom.open.callCount === 1)
expect(atom.open).toHaveBeenCalledWith({
pathsToOpen: [__dirname, __filename],
newWindow: true,
diff --git a/spec/command-installer-spec.js b/spec/command-installer-spec.js
index a2ecb6743e2..6a2a31e77f2 100644
--- a/spec/command-installer-spec.js
+++ b/spec/command-installer-spec.js
@@ -35,9 +35,9 @@ describe('CommandInstaller on #darwin', () => {
installer.installShellCommandsInteractively()
- expect(appDelegate.confirm).toHaveBeenCalledWith({
+ expect(appDelegate.confirm.mostRecentCall.args[0]).toEqual({
message: 'Failed to install shell commands',
- detailedMessage: 'an error'
+ detail: 'an error'
})
appDelegate.confirm.reset()
@@ -46,9 +46,9 @@ describe('CommandInstaller on #darwin', () => {
installer.installShellCommandsInteractively()
- expect(appDelegate.confirm).toHaveBeenCalledWith({
+ expect(appDelegate.confirm.mostRecentCall.args[0]).toEqual({
message: 'Failed to install shell commands',
- detailedMessage: 'another error'
+ detail: 'another error'
})
})
@@ -61,9 +61,9 @@ describe('CommandInstaller on #darwin', () => {
installer.installShellCommandsInteractively()
- expect(appDelegate.confirm).toHaveBeenCalledWith({
+ expect(appDelegate.confirm.mostRecentCall.args[0]).toEqual({
message: 'Commands installed.',
- detailedMessage: 'The shell commands `atom` and `apm` are installed.'
+ detail: 'The shell commands `atom` and `apm` are installed.'
})
})
diff --git a/spec/command-registry-spec.js b/spec/command-registry-spec.js
index a0ac86c0856..03ef0cc3486 100644
--- a/spec/command-registry-spec.js
+++ b/spec/command-registry-spec.js
@@ -1,5 +1,6 @@
const CommandRegistry = require('../src/command-registry');
const _ = require('underscore-plus');
+const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers');
describe("CommandRegistry", () => {
let registry, parent, child, grandchild;
@@ -357,12 +358,41 @@ describe("CommandRegistry", () => {
expect(called).toBe(true);
});
- it("returns a boolean indicating whether any listeners matched the command", () => {
+ it("returns a promise if any listeners matched the command", () => {
registry.add('.grandchild', 'command', () => {});
- expect(registry.dispatch(grandchild, 'command')).toBe(true);
- expect(registry.dispatch(grandchild, 'bogus')).toBe(false);
- expect(registry.dispatch(parent, 'command')).toBe(false);
+ expect(registry.dispatch(grandchild, 'command').constructor.name).toBe("Promise");
+ expect(registry.dispatch(grandchild, 'bogus')).toBe(null);
+ expect(registry.dispatch(parent, 'command')).toBe(null);
+ });
+
+ it("returns a promise that resolves when the listeners resolve", async () => {
+ jasmine.useRealClock();
+ registry.add('.grandchild', 'command', () => 1);
+ registry.add('.grandchild', 'command', () => Promise.resolve(2));
+ registry.add('.grandchild', 'command', () => new Promise((resolve) => {
+ setTimeout(() => { resolve(3); }, 1);
+ }));
+
+ const values = await registry.dispatch(grandchild, 'command');
+ expect(values).toEqual([3, 2, 1]);
+ });
+
+ it("returns a promise that rejects when a listener is rejected", async () => {
+ jasmine.useRealClock();
+ registry.add('.grandchild', 'command', () => 1);
+ registry.add('.grandchild', 'command', () => Promise.resolve(2));
+ registry.add('.grandchild', 'command', () => new Promise((resolve, reject) => {
+ setTimeout(() => { reject(3); }, 1);
+ }));
+
+ let value;
+ try {
+ value = await registry.dispatch(grandchild, 'command');
+ } catch (err) {
+ value = err;
+ }
+ expect(value).toBe(3);
});
});
diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee
index bcf50c2685c..090bc7a29fd 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/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 00000000000..028ee5135ac
--- /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 00000000000..5eb47345660
--- /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 c51ea03b91a..e6d815f8d0e 100644
--- a/spec/grammar-registry-spec.js
+++ b/spec/grammar-registry-spec.js
@@ -1,10 +1,13 @@
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()
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
@@ -13,8 +16,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 +37,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'))
@@ -48,6 +51,36 @@ 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')
+
+ 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', () => {
+ 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')
+
+ grammarRegistry.removeGrammar(grammar)
+ expect(grammarRegistry.grammarForId('source.js') instanceof FirstMate.Grammar).toBe(true)
+ })
+ })
+
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 +111,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 +122,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', () => {
@@ -226,6 +280,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')
@@ -335,14 +415,38 @@ 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)', () => {
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)
})
})
diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js
index 7c19efb9c31..90a51269220 100644
--- a/spec/main-process/atom-application.test.js
+++ b/spec/main-process/atom-application.test.js
@@ -1,14 +1,13 @@
-/** @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 temp = require('temp').track()
+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 +16,7 @@ describe('AtomApplication', function () {
let originalAppQuit, originalShowMessageBox, originalAtomHome, atomApplicationsToDestroy
- beforeEach(function () {
+ beforeEach(() => {
originalAppQuit = electron.app.quit
originalShowMessageBox = electron.dialog.showMessageBox
mockElectronAppQuit()
@@ -34,7 +33,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 +43,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 +52,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 +61,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 +69,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 +78,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 +86,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 +96,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 +114,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 +126,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 +138,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 +155,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 +166,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 +178,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 +197,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 +216,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 +230,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 +249,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 +260,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 +279,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 +293,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 +314,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 +337,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 +371,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,30 +390,136 @@ describe('AtomApplication', function () {
assert.deepEqual(app2Window.representedDirectoryPaths, [])
})
- describe('when closing the last window', function () {
+ describe('when the `--wait` flag is passed', () => {
+ let killedPids, atomApplication, onDidKillProcess
+
+ beforeEach(() => {
+ killedPids = []
+ onDidKillProcess = null
+ atomApplication = buildAtomApplication({
+ 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(['--wait', '--pid', '101']))
+ await focusWindow(window1)
+
+ 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 processKillPromise
+ assert.deepEqual(killedPids, [101])
+
+ processKillPromise = new Promise(resolve => { onDidKillProcess = resolve })
+ window2.close()
+ 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])
+ })
+
+ 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', () => {
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)
window.close()
await window.closedPromise
- assert(electron.app.hasQuitted())
+ await atomApplication.lastBeforeQuitPromise
+ assert(electron.app.didQuit())
})
} 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)
window.close()
await window.closedPromise
- assert(!electron.app.hasQuitted())
+ await timeoutPromise(1000)
+ assert(!electron.app.didQuit())
})
}
})
- 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 +546,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 +559,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 +593,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()
@@ -497,9 +602,12 @@ describe('AtomApplication', function () {
const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath, 'file-b')]))
await focusWindow(window2)
electron.app.quit()
- assert(!electron.app.hasQuitted())
+ await new Promise(process.nextTick)
+ assert(!electron.app.didQuit())
+
await Promise.all([window1.lastPrepareToUnloadPromise, window2.lastPrepareToUnloadPromise])
- assert(electron.app.hasQuitted())
+ await new Promise(process.nextTick)
+ assert(electron.app.didQuit())
})
it('prevents quitting if user cancels when prompted to save an item', async () => {
@@ -507,30 +615,30 @@ 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()
})
// Choosing "Cancel"
- mockElectronShowMessageBox({choice: 1})
+ mockElectronShowMessageBox({response: 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})
+ mockElectronShowMessageBox({response: 2})
electron.app.quit()
await atomApplication.lastBeforeQuitPromise
- assert(electron.app.hasQuitted())
+ 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
}
@@ -542,40 +650,34 @@ describe('AtomApplication', function () {
}
function mockElectronAppQuit () {
- let quitted = false
- electron.app.quit = function () {
- if (electron.app.quit.callCount) {
- electron.app.quit.callCount++
- } else {
- electron.app.quit.callCount = 1
- }
+ let didQuit = false
- let shouldQuit = true
- electron.app.emit('before-quit', {preventDefault: () => { shouldQuit = false }})
- if (shouldQuit) {
- quitted = true
- }
- }
- electron.app.hasQuitted = function () {
- 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}) {
- electron.dialog.showMessageBox = function () {
- return choice
+ function mockElectronShowMessageBox ({response}) {
+ electron.dialog.showMessageBox = (window, options, callback) => {
+ callback(response)
}
}
function makeTempDir (name) {
- const temp = require('temp').track()
return fs.realpathSync(temp.mkdirSync(name))
}
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) {
@@ -587,13 +689,13 @@ describe('AtomApplication', function () {
function sendBackToMainProcess (result) {
require('electron').ipcRenderer.send('${channelId}', result)
}
- (${source})(sendBackToMainProcess)
+ (${source})(sendBackToMainProcess, ${args.map(JSON.stringify).join(', ')})
`)
})
}
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 +709,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.
diff --git a/spec/package-manager-spec.js b/spec/package-manager-spec.js
index 0b26bf8392f..b1ecf834d9a 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/pane-container-spec.js b/spec/pane-container-spec.js
index 1918364f9eb..060808d0b61 100644
--- a/spec/pane-container-spec.js
+++ b/spec/pane-container-spec.js
@@ -5,7 +5,7 @@ describe('PaneContainer', () => {
let confirm, params
beforeEach(() => {
- confirm = spyOn(atom.applicationDelegate, 'confirm').andReturn(0)
+ confirm = spyOn(atom.applicationDelegate, 'confirm').andCallFake((options, callback) => callback(0))
params = {
location: 'center',
config: atom.config,
@@ -280,14 +280,14 @@ describe('PaneContainer', () => {
})
it('returns true if the user saves all modified files when prompted', async () => {
- confirm.andReturn(0)
+ confirm.andCallFake((options, callback) => callback(0))
const saved = await container.confirmClose()
expect(confirm).toHaveBeenCalled()
expect(saved).toBeTruthy()
})
it('returns false if the user cancels saving any modified file', async () => {
- confirm.andReturn(1)
+ confirm.andCallFake((options, callback) => callback(1))
const saved = await container.confirmClose()
expect(confirm).toHaveBeenCalled()
expect(saved).toBeFalsy()
diff --git a/spec/pane-spec.js b/spec/pane-spec.js
index e448f992ffe..8ef274c2ddd 100644
--- a/spec/pane-spec.js
+++ b/spec/pane-spec.js
@@ -3,7 +3,7 @@ const {Emitter} = require('event-kit')
const Grim = require('grim')
const Pane = require('../src/pane')
const PaneContainer = require('../src/pane-container')
-const {it, fit, ffit, fffit, beforeEach, timeoutPromise} = require('./async-spec-helpers')
+const {it, fit, ffit, fffit, beforeEach, conditionPromise, timeoutPromise} = require('./async-spec-helpers')
describe('Pane', () => {
let confirm, showSaveDialog, deserializerDisposable
@@ -564,7 +564,7 @@ describe('Pane', () => {
describe('when the item has a uri', () => {
it('saves the item before destroying it', async () => {
itemURI = 'test'
- confirm.andReturn(0)
+ confirm.andCallFake((options, callback) => callback(0))
const success = await pane.destroyItem(item1)
expect(item1.save).toHaveBeenCalled()
@@ -576,13 +576,17 @@ describe('Pane', () => {
describe('when the item has no uri', () => {
it('presents a save-as dialog, then saves the item with the given uri before removing and destroying it', async () => {
+ jasmine.useRealClock()
+
itemURI = null
- showSaveDialog.andReturn('/selected/path')
- confirm.andReturn(0)
+ showSaveDialog.andCallFake((options, callback) => callback('/selected/path'))
+ confirm.andCallFake((options, callback) => callback(0))
const success = await pane.destroyItem(item1)
- expect(showSaveDialog).toHaveBeenCalledWith({})
+ expect(showSaveDialog.mostRecentCall.args[0]).toEqual({})
+
+ await conditionPromise(() => item1.saveAs.callCount === 1)
expect(item1.saveAs).toHaveBeenCalledWith('/selected/path')
expect(pane.getItems().includes(item1)).toBe(false)
expect(item1.isDestroyed()).toBe(true)
@@ -593,7 +597,7 @@ describe('Pane', () => {
describe("if the [Don't Save] option is selected", () => {
it('removes and destroys the item without saving it', async () => {
- confirm.andReturn(2)
+ confirm.andCallFake((options, callback) => callback(2))
const success = await pane.destroyItem(item1)
expect(item1.save).not.toHaveBeenCalled()
@@ -605,7 +609,7 @@ describe('Pane', () => {
describe('if the [Cancel] option is selected', () => {
it('does not save, remove, or destroy the item', async () => {
- confirm.andReturn(1)
+ confirm.andCallFake((options, callback) => callback(1))
const success = await pane.destroyItem(item1)
expect(item1.save).not.toHaveBeenCalled()
@@ -735,7 +739,7 @@ describe('Pane', () => {
beforeEach(() => {
pane = new Pane(paneParams({items: [new Item('A')]}))
- showSaveDialog.andReturn('/selected/path')
+ showSaveDialog.andCallFake((options, callback) => callback('/selected/path'))
})
describe('when the active item has a uri', () => {
@@ -764,7 +768,7 @@ describe('Pane', () => {
it('opens a save dialog and saves the current item as the selected path', async () => {
pane.getActiveItem().saveAs = jasmine.createSpy('saveAs')
await pane.saveActiveItem()
- expect(showSaveDialog).toHaveBeenCalledWith({})
+ expect(showSaveDialog.mostRecentCall.args[0]).toEqual({})
expect(pane.getActiveItem().saveAs).toHaveBeenCalledWith('/selected/path')
})
})
@@ -779,7 +783,7 @@ describe('Pane', () => {
it('does nothing if the user cancels choosing a path', async () => {
pane.getActiveItem().saveAs = jasmine.createSpy('saveAs')
- showSaveDialog.andReturn(undefined)
+ showSaveDialog.andCallFake((options, callback) => callback(undefined))
await pane.saveActiveItem()
expect(pane.getActiveItem().saveAs).not.toHaveBeenCalled()
})
@@ -835,15 +839,19 @@ describe('Pane', () => {
beforeEach(() => {
pane = new Pane(paneParams({items: [new Item('A')]}))
- showSaveDialog.andReturn('/selected/path')
+ showSaveDialog.andCallFake((options, callback) => callback('/selected/path'))
})
describe('when the current item has a saveAs method', () => {
- it('opens the save dialog and calls saveAs on the item with the selected path', () => {
+ it('opens the save dialog and calls saveAs on the item with the selected path', async () => {
+ jasmine.useRealClock()
+
pane.getActiveItem().path = __filename
pane.getActiveItem().saveAs = jasmine.createSpy('saveAs')
pane.saveActiveItemAs()
- expect(showSaveDialog).toHaveBeenCalledWith({defaultPath: __filename})
+ expect(showSaveDialog.mostRecentCall.args[0]).toEqual({defaultPath: __filename})
+
+ await conditionPromise(() => pane.getActiveItem().saveAs.callCount === 1)
expect(pane.getActiveItem().saveAs).toHaveBeenCalledWith('/selected/path')
})
})
@@ -1210,7 +1218,7 @@ describe('Pane', () => {
item1.getURI = () => '/test/path'
item1.save = jasmine.createSpy('save')
- confirm.andReturn(0)
+ confirm.andCallFake((options, callback) => callback(0))
await pane.close()
expect(confirm).toHaveBeenCalled()
expect(item1.save).toHaveBeenCalled()
@@ -1225,7 +1233,7 @@ describe('Pane', () => {
item1.getURI = () => '/test/path'
item1.save = jasmine.createSpy('save')
- confirm.andReturn(1)
+ confirm.andCallFake((options, callback) => callback(1))
await pane.close()
expect(confirm).toHaveBeenCalled()
@@ -1240,8 +1248,8 @@ describe('Pane', () => {
item1.shouldPromptToSave = () => true
item1.saveAs = jasmine.createSpy('saveAs')
- confirm.andReturn(0)
- showSaveDialog.andReturn(undefined)
+ confirm.andCallFake((options, callback) => callback(0))
+ showSaveDialog.andCallFake((options, callback) => callback(undefined))
await pane.close()
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
@@ -1270,12 +1278,12 @@ describe('Pane', () => {
it('does not destroy the pane if save fails and user clicks cancel', async () => {
let confirmations = 0
- confirm.andCallFake(() => {
+ confirm.andCallFake((options, callback) => {
confirmations++
if (confirmations === 1) {
- return 0 // click save
+ callback(0) // click save
} else {
- return 1
+ callback(1)
}
}) // click cancel
@@ -1290,17 +1298,17 @@ describe('Pane', () => {
item1.saveAs = jasmine.createSpy('saveAs').andReturn(true)
let confirmations = 0
- confirm.andCallFake(() => {
+ confirm.andCallFake((options, callback) => {
confirmations++
- return 0
+ callback(0)
}) // save and then save as
- showSaveDialog.andReturn('new/path')
+ showSaveDialog.andCallFake((options, callback) => callback('new/path'))
await pane.close()
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(confirmations).toBe(2)
- expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalledWith({})
+ expect(atom.applicationDelegate.showSaveDialog.mostRecentCall.args[0]).toEqual({})
expect(item1.save).toHaveBeenCalled()
expect(item1.saveAs).toHaveBeenCalled()
expect(pane.isDestroyed()).toBe(true)
@@ -1315,20 +1323,21 @@ describe('Pane', () => {
})
let confirmations = 0
- confirm.andCallFake(() => {
+ confirm.andCallFake((options, callback) => {
confirmations++
if (confirmations < 3) {
- return 0 // save, save as, save as
+ callback(0) // save, save as, save as
+ } else {
+ callback(2) // don't save
}
- return 2
- }) // don't save
+ })
- showSaveDialog.andReturn('new/path')
+ showSaveDialog.andCallFake((options, callback) => callback('new/path'))
await pane.close()
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(confirmations).toBe(3)
- expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalledWith({})
+ expect(atom.applicationDelegate.showSaveDialog.mostRecentCall.args[0]).toEqual({})
expect(item1.save).toHaveBeenCalled()
expect(item1.saveAs).toHaveBeenCalled()
expect(pane.isDestroyed()).toBe(true)
diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee
index b5ce2914bed..f4f1f356863 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/spec/syntax-scope-map-spec.js b/spec/syntax-scope-map-spec.js
new file mode 100644
index 00000000000..61b1bdc7d09
--- /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/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js
index 0b888f47cef..97210dd413f 100644
--- a/spec/text-editor-component-spec.js
+++ b/spec/text-editor-component-spec.js
@@ -263,13 +263,13 @@ describe('TextEditorComponent', () => {
it('keeps the number of tiles stable when the visible line count changes during vertical scrolling', async () => {
const {component, element, editor} = buildComponent({rowsPerTile: 3, autoHeight: false})
await setEditorHeightInLines(component, 5.5)
- expect(component.refs.lineTiles.children.length).toBe(3 + 1) // account for cursors container
+ expect(component.refs.lineTiles.children.length).toBe(3 + 2) // account for cursors and highlights containers
await setScrollTop(component, 0.5 * component.getLineHeight())
- expect(component.refs.lineTiles.children.length).toBe(3 + 1) // account for cursors container
+ expect(component.refs.lineTiles.children.length).toBe(3 + 2) // account for cursors and highlights containers
await setScrollTop(component, 1 * component.getLineHeight())
- expect(component.refs.lineTiles.children.length).toBe(3 + 1) // account for cursors container
+ expect(component.refs.lineTiles.children.length).toBe(3 + 2) // account for cursors and highlights containers
})
it('recycles tiles on resize', async () => {
diff --git a/spec/text-editor-element-spec.js b/spec/text-editor-element-spec.js
index b7181fa91b3..7ffdf374de4 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()
diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js
index 78daf510500..32883a01d71 100644
--- a/spec/text-editor-spec.js
+++ b/spec/text-editor-spec.js
@@ -2376,6 +2376,19 @@ describe('TextEditor', () => {
])
})
})
+
+ it('does not create a new selection if it would be fully contained within another selection', () => {
+ editor.setText('abc\ndef\nghi\njkl\nmno')
+ editor.setCursorBufferPosition([0, 1])
+
+ let addedSelectionCount = 0
+ editor.onDidAddSelection(() => { addedSelectionCount++ })
+
+ editor.addSelectionBelow()
+ editor.addSelectionBelow()
+ editor.addSelectionBelow()
+ expect(addedSelectionCount).toBe(3)
+ })
})
describe('.addSelectionAbove()', () => {
@@ -2498,6 +2511,19 @@ describe('TextEditor', () => {
])
})
})
+
+ it('does not create a new selection if it would be fully contained within another selection', () => {
+ editor.setText('abc\ndef\nghi\njkl\nmno')
+ editor.setCursorBufferPosition([4, 1])
+
+ let addedSelectionCount = 0
+ editor.onDidAddSelection(() => { addedSelectionCount++ })
+
+ editor.addSelectionAbove()
+ editor.addSelectionAbove()
+ editor.addSelectionAbove()
+ expect(addedSelectionCount).toBe(3)
+ })
})
describe('.splitSelectionsIntoLines()', () => {
@@ -5401,6 +5427,34 @@ describe('TextEditor', () => {
expect(buffer.getLineCount()).toBe(count - 2)
})
+ it("restores cursor position for multiple cursors", () => {
+ const line = '0123456789'.repeat(8)
+ editor.setText((line + '\n').repeat(5))
+ editor.setCursorScreenPosition([0, 5])
+ editor.addCursorAtScreenPosition([2, 8])
+ editor.deleteLine()
+
+ const cursors = editor.getCursors()
+ expect(cursors.length).toBe(2)
+ expect(cursors[0].getScreenPosition()).toEqual([0, 5])
+ expect(cursors[1].getScreenPosition()).toEqual([1, 8])
+ })
+
+ it("restores cursor position for multiple selections", () => {
+ const line = '0123456789'.repeat(8)
+ editor.setText((line + '\n').repeat(5))
+ editor.setSelectedBufferRanges([
+ [[0, 5], [0, 8]],
+ [[2, 4], [2, 15]]
+ ])
+ editor.deleteLine()
+
+ const cursors = editor.getCursors()
+ expect(cursors.length).toBe(2)
+ expect(cursors[0].getScreenPosition()).toEqual([0, 5])
+ expect(cursors[1].getScreenPosition()).toEqual([1, 4])
+ })
+
it('deletes a line only once when multiple selections are on the same line', () => {
const line1 = buffer.lineForRow(1)
const count = buffer.getLineCount()
diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js
new file mode 100644
index 00000000000..ec38c1a067b
--- /dev/null
+++ b/spec/tree-sitter-language-mode-spec.js
@@ -0,0 +1,560 @@
+const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
+
+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')
+
+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', () => {
+ let editor, buffer
+
+ beforeEach(async () => {
+ editor = await atom.workspace.open('')
+ buffer = editor.getBuffer()
+ })
+
+ describe('highlighting', () => {
+ 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',
+ '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());')
+ expectTokensToEqual(editor, [[
+ {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']}
+ ]])
+ })
+
+ 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']}
+ ]])
+ })
+
+ 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: []}
+ ]
+ ])
+ })
+
+ 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', () => {
+ beforeEach(() => {
+ editor.displayLayer.reset({foldCharacter: '…'})
+ })
+
+ it('can fold nodes that start and end with specified tokens', () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ parser: 'tree-sitter-javascript',
+ folds: [
+ {
+ start: {type: '{', index: 0},
+ end: {type: '}', index: -1}
+ },
+ {
+ start: {type: '(', index: 0},
+ end: {type: ')', index: -1}
+ }
+ ]
+ })
+
+ buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
+ buffer.setText(dedent `
+ module.exports =
+ class A {
+ getB (c,
+ d,
+ e) {
+ return this.f(g)
+ }
+ }
+ `)
+
+ 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)
+ expect(editor.isFoldableAtBufferRow(5)).toBe(false)
+
+ editor.foldBufferRow(2)
+ expect(getDisplayText(editor)).toBe(dedent `
+ module.exports =
+ class A {
+ getB (…) {
+ return this.f(g)
+ }
+ }
+ `)
+
+ editor.foldBufferRow(4)
+ expect(getDisplayText(editor)).toBe(dedent `
+ module.exports =
+ class A {
+ getB (…) {…}
+ }
+ `)
+ })
+
+ 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},
+ 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: {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 entire nodes when no start or end parameters are specified', () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ parser: 'tree-sitter-javascript',
+ folds: [
+ // 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'}
+ ]
+ })
+
+ buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
+ buffer.setText(dedent `
+ /**
+ * Important
+ */
+ const x = 1 /*
+ Also important
+ */
+ `)
+
+ 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 = 1 /*
+ Also important
+ */
+ `)
+
+ editor.foldBufferRow(3)
+ expect(getDisplayText(editor)).toBe(dedent `
+ /**… */
+ 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', 'preproc_elif'],
+ start: {index: 1},
+ end: {type: ['preproc_else', 'preproc_elif']}
+ },
+
+ // 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 = "\\";
+
+ #elif defined MACOS
+
+ #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…
+ #elif defined MACOS
+
+ #include
+ const char *path_separator = "/";
+
+ #else
+
+ #include
+ const char *path_separator = "/";
+
+ #endif
+
+ #endif
+ `)
+
+ editor.foldBufferRow(8)
+ expect(getDisplayText(editor)).toBe(dedent `
+ #ifndef FOO_H_
+ #define FOO_H_
+
+ #ifdef _WIN32…
+ #elif defined MACOS…
+ #else
+
+ #include
+ const char *path_separator = "/";
+
+ #endif
+
+ #endif
+ `)
+
+ editor.foldBufferRow(0)
+ expect(getDisplayText(editor)).toBe(dedent `
+ #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', () => {
+ 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('.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, {
+ 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) {
+ return editor.displayLayer.getText()
+}
+
+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(' '))
+ }))
+ }
+
+ 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/spec/window-event-handler-spec.js b/spec/window-event-handler-spec.js
index a03e168fabe..2891aa2db2d 100644
--- a/spec/window-event-handler-spec.js
+++ b/spec/window-event-handler-spec.js
@@ -44,7 +44,15 @@ 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')
diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js
index 6bc3199bac4..4b115e594e8 100644
--- a/spec/workspace-spec.js
+++ b/spec/workspace-spec.js
@@ -10,7 +10,7 @@ const _ = require('underscore-plus')
const fstream = require('fstream')
const fs = require('fs-plus')
const AtomEnvironment = require('../src/atom-environment')
-const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
+const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise} = require('./async-spec-helpers')
describe('Workspace', () => {
let workspace
@@ -659,47 +659,42 @@ describe('Workspace', () => {
})
})
- describe('when the file is over user-defined limit', () => {
- const shouldPromptForFileOfSize = (size, shouldPrompt) => {
+ describe('when the file size is over the limit defined in `core.warnOnLargeFileLimit`', () => {
+ const shouldPromptForFileOfSize = async (size, shouldPrompt) => {
spyOn(fs, 'getSizeSync').andReturn(size * 1048577)
- atom.applicationDelegate.confirm.andCallFake(() => selectedButtonIndex)
- atom.applicationDelegate.confirm()
- var selectedButtonIndex = 1 // cancel
- let editor = null
- waitsForPromise(() => workspace.open('sample.js').then(e => { editor = e }))
+ let selectedButtonIndex = 1 // cancel
+ atom.applicationDelegate.confirm.andCallFake((options, callback) => callback(selectedButtonIndex))
+
+ let editor = await workspace.open('sample.js')
if (shouldPrompt) {
- runs(() => {
- expect(editor).toBeUndefined()
- expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
+ expect(editor).toBeUndefined()
+ expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
- atom.applicationDelegate.confirm.reset()
- selectedButtonIndex = 0
- }) // open the file
+ atom.applicationDelegate.confirm.reset()
+ selectedButtonIndex = 0 // open the file
- waitsForPromise(() => workspace.open('sample.js').then(e => { editor = e }))
+ editor = await workspace.open('sample.js')
- runs(() => {
- expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
- })
+ expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
} else {
- runs(() => expect(editor).not.toBeUndefined())
+ expect(editor).not.toBeUndefined()
}
}
- it('prompts the user to make sure they want to open a file this big', () => {
+ it('prompts before opening the file', async () => {
atom.config.set('core.warnOnLargeFileLimit', 20)
- shouldPromptForFileOfSize(20, true)
+ await shouldPromptForFileOfSize(20, true)
})
- it("doesn't prompt on files below the limit", () => {
+ it("doesn't prompt on files below the limit", async () => {
atom.config.set('core.warnOnLargeFileLimit', 30)
- shouldPromptForFileOfSize(20, false)
+ await shouldPromptForFileOfSize(20, false)
})
- it('prompts for smaller files with a lower limit', () => {
+ it('prompts for smaller files with a lower limit', async () => {
atom.config.set('core.warnOnLargeFileLimit', 5)
- shouldPromptForFileOfSize(10, true)
+ await shouldPromptForFileOfSize(10, true)
})
})
@@ -2809,29 +2804,30 @@ describe('Workspace', () => {
describe('.checkoutHeadRevision()', () => {
let editor = null
- beforeEach(() => {
+ beforeEach(async () => {
+ jasmine.useRealClock()
atom.config.set('editor.confirmCheckoutHeadRevision', false)
- waitsForPromise(() => atom.workspace.open('sample-with-comments.js').then(o => { editor = o }))
+ editor = await atom.workspace.open('sample-with-comments.js')
})
- it('reverts to the version of its file checked into the project repository', () => {
+ it('reverts to the version of its file checked into the project repository', async () => {
editor.setCursorBufferPosition([0, 0])
editor.insertText('---\n')
expect(editor.lineTextForBufferRow(0)).toBe('---')
- waitsForPromise(() => atom.workspace.checkoutHeadRevision(editor))
+ atom.workspace.checkoutHeadRevision(editor)
- runs(() => expect(editor.lineTextForBufferRow(0)).toBe(''))
+ await conditionPromise(() => editor.lineTextForBufferRow(0) === '')
})
describe("when there's no repository for the editor's file", () => {
- it("doesn't do anything", () => {
+ it("doesn't do anything", async () => {
editor = new TextEditor()
editor.setText('stuff')
atom.workspace.checkoutHeadRevision(editor)
- waitsForPromise(() => atom.workspace.checkoutHeadRevision(editor))
+ atom.workspace.checkoutHeadRevision(editor)
})
})
})
diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee
deleted file mode 100644
index 70b0f91bcde..00000000000
--- 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 00000000000..a6d7010780a
--- /dev/null
+++ b/src/application-delegate.js
@@ -0,0 +1,374 @@
+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)
+ }
+
+ didClosePathWithWaitSession (path) {
+ return ipcHelpers.call('window-method', 'didClosePathWithWaitSession', path)
+ }
+
+ 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 (options, callback) {
+ if (typeof callback === 'function') {
+ // Async version: pass options directly to Electron but set sane defaults
+ options = Object.assign({type: 'info', normalizeAccessKeys: true}, options)
+ remote.dialog.showMessageBox(remote.getCurrentWindow(), options, callback)
+ } else {
+ // Legacy sync version: options can only have `message`,
+ // `detailedMessage` (optional), and buttons array or object (optional)
+ let {message, detailedMessage, buttons} = options
+
+ 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]]
+ if (typeof callback === 'function') callback()
+ }
+ }
+ }
+
+ showMessageDialog (params) {}
+
+ showSaveDialog (options, callback) {
+ if (typeof callback === 'function') {
+ // Async
+ this.getCurrentWindow().showSaveDialog(options, callback)
+ } else {
+ // Sync
+ if (typeof params === 'string') {
+ options = {defaultPath: options}
+ }
+ return this.getCurrentWindow().showSaveDialog(options)
+ }
+ }
+
+ 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))
+ }
+}
diff --git a/src/atom-environment.js b/src/atom-environment.js
index fc3201dfcef..1594645343a 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.pathsWithWaitSessions = 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.pathsWithWaitSessions.clear()
}
destroy () {
@@ -822,7 +824,22 @@ class AtomEnvironment {
this.document.body.appendChild(this.workspace.getElement())
if (this.backgroundStylesheet) this.backgroundStylesheet.remove()
- this.watchProjectPaths()
+ let previousProjectPaths = this.project.getPaths()
+ this.disposables.add(this.project.onDidChangePaths(newPaths => {
+ for (let path of previousProjectPaths) {
+ if (this.pathsWithWaitSessions.has(path) && !newPaths.includes(path)) {
+ this.applicationDelegate.didClosePathWithWaitSession(path)
+ }
+ }
+ previousProjectPaths = newPaths
+ this.applicationDelegate.setRepresentedDirectoryPaths(newPaths)
+ }))
+ this.disposables.add(this.workspace.onDidDestroyPaneItem(({item}) => {
+ const path = item.getPath && item.getPath()
+ if (this.pathsWithWaitSessions.has(path)) {
+ this.applicationDelegate.didClosePathWithWaitSession(path)
+ }
+ }))
this.packages.activate()
this.keymaps.loadUserKeymap()
@@ -948,29 +965,63 @@ class AtomEnvironment {
// Essential: A flexible way to open a dialog akin to an alert dialog.
//
+ // While both async and sync versions are provided, it is recommended to use the async version
+ // such that the renderer process is not blocked while the dialog box is open.
+ //
+ // The async version accepts the same options as Electron's `dialog.showMessageBox`.
+ // For convenience, it sets `type` to `'info'` and `normalizeAccessKeys` to `true` by default.
+ //
// If the dialog is closed (via `Esc` key or `X` in the top corner) without selecting a button
// the first button will be clicked unless a "Cancel" or "No" button is provided.
//
// ## Examples
//
- // ```coffee
- // atom.confirm
- // message: 'How you feeling?'
- // detailedMessage: 'Be honest.'
- // buttons:
- // Good: -> window.alert('good to hear')
- // Bad: -> window.alert('bummer')
+ // ```js
+ // // Async version (recommended)
+ // atom.confirm({
+ // message: 'How you feeling?',
+ // detail: 'Be honest.',
+ // buttons: ['Good', 'Bad']
+ // }, response => {
+ // if (response === 0) {
+ // window.alert('good to hear')
+ // } else {
+ // window.alert('bummer')
+ // }
+ // })
+ //
+ // ```js
+ // // Legacy sync version
+ // const chosen = atom.confirm({
+ // message: 'How you feeling?',
+ // detailedMessage: 'Be honest.',
+ // buttons: {
+ // Good: () => window.alert('good to hear'),
+ // Bad: () => window.alert('bummer')
+ // }
+ // })
// ```
//
- // * `options` An {Object} with the following keys:
+ // * `options` An options {Object}. If the callback argument is also supplied, see the documentation at
+ // https://electronjs.org/docs/api/dialog#dialogshowmessageboxbrowserwindow-options-callback for the list of
+ // available options. Otherwise, only the following keys are accepted:
// * `message` The {String} message to display.
// * `detailedMessage` (optional) The {String} detailed message to display.
- // * `buttons` (optional) Either an array of strings or an object where keys are
- // button names and the values are callbacks to invoke when clicked.
+ // * `buttons` (optional) Either an {Array} of {String}s or an {Object} where keys are
+ // button names and the values are callback {Function}s to invoke when clicked.
+ // * `callback` (optional) A {Function} that will be called with the index of the chosen option.
+ // If a callback is supplied, the dialog will be non-blocking. This argument is recommended.
//
- // Returns the chosen button index {Number} if the buttons option is an array or the return value of the callback if the buttons option is an object.
- confirm (params = {}) {
- return this.applicationDelegate.confirm(params)
+ // Returns the chosen button index {Number} if the buttons option is an array
+ // or the return value of the callback if the buttons option is an object.
+ // If a callback function is supplied, returns `undefined`.
+ confirm (options = {}, callback) {
+ if (callback) {
+ // Async: no return value
+ this.applicationDelegate.confirm(options, callback)
+ } else {
+ return this.applicationDelegate.confirm(options)
+ }
}
/*
@@ -1025,13 +1076,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)
@@ -1061,7 +1105,7 @@ class AtomEnvironment {
}
}
- attemptRestoreProjectStateForPaths (state, projectPaths, filesToOpen = []) {
+ async attemptRestoreProjectStateForPaths (state, projectPaths, filesToOpen = []) {
const center = this.workspace.getCenter()
const windowIsUnused = () => {
for (let container of this.workspace.getPaneContainers()) {
@@ -1080,30 +1124,38 @@ class AtomEnvironment {
this.restoreStateIntoThisEnvironment(state)
return Promise.all(filesToOpen.map(file => this.workspace.open(file)))
} else {
+ let resolveDiscardStatePromise = null
+ const discardStatePromise = new Promise((resolve) => {
+ resolveDiscardStatePromise = resolve
+ })
const nouns = projectPaths.length === 1 ? 'folder' : 'folders'
- const choice = this.confirm({
+ this.confirm({
message: 'Previous automatically-saved project state detected',
- detailedMessage: `There is previously saved state for the selected ${nouns}. ` +
+ detail: `There is previously saved state for the selected ${nouns}. ` +
`Would you like to add the ${nouns} to this window, permanently discarding the saved state, ` +
`or open the ${nouns} in a new window, restoring the saved state?`,
buttons: [
'&Open in new window and recover state',
'&Add to this window and discard state'
- ]})
- if (choice === 0) {
- this.open({
- pathsToOpen: projectPaths.concat(filesToOpen),
- newWindow: true,
- devMode: this.inDevMode(),
- safeMode: this.inSafeMode()
- })
- return Promise.resolve(null)
- } else if (choice === 1) {
- for (let selectedPath of projectPaths) {
- this.project.addPath(selectedPath)
+ ]
+ }, response => {
+ if (response === 0) {
+ this.open({
+ pathsToOpen: projectPaths.concat(filesToOpen),
+ newWindow: true,
+ devMode: this.inDevMode(),
+ safeMode: this.inSafeMode()
+ })
+ resolveDiscardStatePromise(Promise.resolve(null))
+ } else if (response === 1) {
+ for (let selectedPath of projectPaths) {
+ this.project.addPath(selectedPath)
+ }
+ resolveDiscardStatePromise(Promise.all(filesToOpen.map(file => this.workspace.open(file))))
}
- return Promise.all(filesToOpen.map(file => this.workspace.open(file)))
- }
+ })
+
+ return discardStatePromise
}
}
@@ -1115,12 +1167,11 @@ class AtomEnvironment {
return this.deserialize(state)
}
- showSaveDialog (callback) {
- callback(this.showSaveDialogSync())
- }
-
showSaveDialogSync (options = {}) {
- this.applicationDelegate.showSaveDialog(options)
+ deprecate(`atom.showSaveDialogSync is deprecated and will be removed soon.
+Please, implement ::saveAs and ::getSaveDialogOptions instead for pane items
+or use Pane::saveItemAs for programmatic saving.`)
+ return this.applicationDelegate.showSaveDialog(options)
}
async saveState (options, storageKey) {
@@ -1300,8 +1351,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 +1364,10 @@ class AtomEnvironment {
}
if (!fs.isDirectorySync(pathToOpen)) {
- fileLocationsToOpen.push({pathToOpen, initialLine, initialColumn})
+ fileLocationsToOpen.push(location)
}
+
+ if (location.hasWaitSession) this.pathsWithWaitSessions.add(pathToOpen)
}
let restoredState = false
@@ -1334,7 +1388,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/command-installer.js b/src/command-installer.js
index 2c032d6c54f..85360da17ac 100644
--- a/src/command-installer.js
+++ b/src/command-installer.js
@@ -23,8 +23,8 @@ class CommandInstaller {
const showErrorDialog = (error) => {
this.applicationDelegate.confirm({
message: 'Failed to install shell commands',
- detailedMessage: error.message
- })
+ detail: error.message
+ }, () => {})
}
this.installAtomCommand(true, error => {
@@ -33,8 +33,8 @@ class CommandInstaller {
if (error) return showErrorDialog(error)
this.applicationDelegate.confirm({
message: 'Commands installed.',
- detailedMessage: 'The shell commands `atom` and `apm` are installed.'
- })
+ detail: 'The shell commands `atom` and `apm` are installed.'
+ }, () => {})
})
})
}
diff --git a/src/command-registry.js b/src/command-registry.js
index 9e6d8c2e194..e503691db58 100644
--- a/src/command-registry.js
+++ b/src/command-registry.js
@@ -309,7 +309,7 @@ module.exports = class CommandRegistry {
handleCommandEvent (event) {
let propagationStopped = false
let immediatePropagationStopped = false
- let matched = false
+ let matched = []
let currentTarget = event.target
const dispatchedEvent = new CustomEvent(event.type, {
@@ -373,10 +373,6 @@ module.exports = class CommandRegistry {
listeners = selectorBasedListeners.concat(listeners)
}
- if (listeners.length > 0) {
- matched = true
- }
-
// Call inline listeners first in reverse registration order,
// and selector-based listeners by specificity and reverse
// registration order.
@@ -385,7 +381,7 @@ module.exports = class CommandRegistry {
if (immediatePropagationStopped) {
break
}
- listener.didDispatch.call(currentTarget, dispatchedEvent)
+ matched.push(listener.didDispatch.call(currentTarget, dispatchedEvent))
}
if (currentTarget === window) {
@@ -399,7 +395,7 @@ module.exports = class CommandRegistry {
this.emitter.emit('did-dispatch', dispatchedEvent)
- return matched
+ return (matched.length > 0 ? Promise.all(matched) : null)
}
commandRegistered (commandName) {
diff --git a/src/config-schema.js b/src/config-schema.js
index 2ff68be8665..18dc3d77405 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/config.coffee b/src/config.coffee
index b8bf8a76fa0..84e7267006d 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 db86958fda9..b316bdbb0cb 100644
--- a/src/grammar-registry.js
+++ b/src/grammar-registry.js
@@ -1,13 +1,16 @@
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')
-const GRAMMAR_SELECTION_RANGE = Range(Point.ZERO, Point(10, 0)).freeze()
+const GRAMMAR_TYPE_BONUS = 1000
const PATH_SPLIT_REGEX = new RegExp('[/.]')
// Extended: This class holds the grammars used for tokenizing.
@@ -24,10 +27,13 @@ class GrammarRegistry {
clear () {
this.textmateRegistry.clear()
+ this.treeSitterGrammarsById = {}
if (this.subscriptions) this.subscriptions.dispose()
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)
@@ -102,17 +108,18 @@ 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
// found.
assignLanguageMode (buffer, languageId) {
if (buffer.getBuffer) buffer = buffer.getBuffer()
+ languageId = this.normalizeLanguageId(languageId)
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 {
@@ -136,7 +143,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)
@@ -146,7 +153,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,39 +176,44 @@ 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)) {
score += 0.25
}
+
+ if (score > 0 && this.isGrammarPreferredType(grammar)) {
+ score += GRAMMAR_TYPE_BONUS
+ }
+
return score
}
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)
- let pathScore = -1
+ let pathScore = 0
let customFileTypes
if (this.config.get('core.customFileTypes')) {
@@ -225,25 +241,48 @@ class GrammarRegistry {
}
grammarMatchesContents (grammar, contents) {
- if ((contents == null) || (grammar.firstLineRegex == 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 (contents == null) return 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'))
+ } else {
+ return false
}
- const lines = contents.split('\n')
- 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) {
+ languageId = this.normalizeLanguageId(languageId)
+
+ return (
+ this.textmateRegistry.grammarForScopeName(languageId) ||
+ this.treeSitterGrammarsById[languageId]
+ )
}
// Deprecated: Get the grammar override for the given file path.
@@ -284,6 +323,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) {
@@ -295,16 +336,11 @@ 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(
- 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))
@@ -348,15 +384,35 @@ class GrammarRegistry {
}
grammarForScopeName (scopeName) {
- return this.textmateRegistry.grammarForScopeName(scopeName)
+ return this.grammarForId(scopeName)
}
addGrammar (grammar) {
- return this.textmateRegistry.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(() => 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) {
@@ -370,7 +426,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.
@@ -379,7 +439,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.
@@ -391,7 +453,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 +470,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.
@@ -417,4 +494,25 @@ 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')) {
+ return this.treeSitterLanguageIdsByTextMateScopeName.get(languageId) || languageId
+ } else {
+ return this.textMateScopeNamesByTreeSitterLanguageId.get(languageId) || languageId
+ }
+ }
+}
+
+function getGrammarSelectionContent (buffer) {
+ return buffer.getTextInRange(Range(
+ Point(0, 0),
+ buffer.positionForCharacterIndex(1024)
+ ))
}
diff --git a/src/main-process/application-menu.coffee b/src/main-process/application-menu.coffee
deleted file mode 100644
index 35bc7d66c77..00000000000
--- 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 00000000000..26dcd19419c
--- /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)
+ }
+}
diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee
deleted file mode 100644
index 328da7fc55f..00000000000
--- a/src/main-process/atom-application.coffee
+++ /dev/null
@@ -1,925 +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, clipboard, 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
-
- @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 00000000000..3696a3b4083
--- /dev/null
+++ b/src/main-process/atom-application.js
@@ -0,0 +1,1375 @@
+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, clipboard, 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
+ this._killProcess = options.killProcess || process.kill.bind(process)
+ if (options.test || options.benchmark || options.benchmarkTest) this.socketPath = null
+
+ this.waitSessionsByWindow = new Map()
+ 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', async event => {
+ let resolveBeforeQuitPromise
+ this.lastBeforeQuitPromise = new Promise(resolve => { resolveBeforeQuitPromise = resolve })
+
+ 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()
+ }
+
+ 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 {
+ this.addWindow(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
+ }))
+
+ 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 = []
+ for (let i = 0; i < pathsToOpen.length; i++) {
+ const location = this.parsePathToOpen(pathsToOpen[i], executedFrom, addToLastWindow)
+ location.forceAddToWindow = addToLastWindow
+ location.hasWaitSession = pidToKillWhenClosed != null
+ locationsToOpen.push(location)
+ pathsToOpen[i] = location.pathToOpen
+ }
+
+ let existingWindow
+ if (!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
+ })
+ this.addWindow(openedWindow)
+ openedWindow.focus()
+ }
+
+ if (pidToKillWhenClosed != null) {
+ 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.killProcessesForWindow(openedWindow))
+ return openedWindow
+ }
+
+ // Kill all processes associated with opened windows.
+ killAllProcesses () {
+ for (let window of this.waitSessionsByWindow.keys()) {
+ this.killProcessesForWindow(window)
+ }
+ }
+
+ killProcessesForWindow (window) {
+ const sessions = this.waitSessionsByWindow.get(window)
+ if (!sessions) return
+ for (const session of sessions) {
+ this.killProcess(session.pid)
+ }
+ this.waitSessionsByWindow.delete(window)
+ }
+
+ windowDidClosePathWithWaitSession (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)
+ if (session.remainingPaths.size === 0) {
+ this.killProcess(session.pid)
+ waitSessions.splice(i, 1)
+ }
+ }
+ }
+
+ // Kill the process with the given pid.
+ killProcess (pid) {
+ try {
+ const parsedPid = parseInt(pid)
+ 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}`)
+ }
+ }
+ }
+
+ 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.addWindow(window)
+ window.on('window:loaded', () => window.sendURIMessage(url))
+ return window
+ }
+ }
+
+ 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()
+ const window = new AtomWindow(this, this.fileRecoveryService, {
+ windowInitializationScript,
+ resourcePath: this.resourcePath,
+ devMode,
+ safeMode,
+ urlToOpen,
+ windowDimensions,
+ env
+ })
+ this.addWindow(window)
+ return window
+ }
+
+ 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
+ }
+ const window = new AtomWindow(this, this.fileRecoveryService, {
+ windowInitializationScript,
+ resourcePath,
+ headless,
+ isSpec,
+ devMode,
+ testRunnerPath,
+ legacyTestRunnerPath,
+ testPaths,
+ logFile,
+ safeMode,
+ env
+ })
+ this.addWindow(window)
+ return window
+ }
+
+ 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
+ const window = new AtomWindow(this, this.fileRecoveryService, {
+ windowInitializationScript,
+ resourcePath,
+ headless,
+ test,
+ isSpec,
+ devMode,
+ benchmarkPaths,
+ safeMode,
+ env
+ })
+ this.addWindow(window)
+ return window
+ }
+
+ 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'))
+ }
+ }
+
+ parsePathToOpen (pathToOpen, executedFrom = '') {
+ 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}
+ }
+
+ // 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
+ }
+}
diff --git a/src/main-process/atom-protocol-handler.coffee b/src/main-process/atom-protocol-handler.coffee
deleted file mode 100644
index db385b4b783..00000000000
--- 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 00000000000..1affba02a65
--- /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)
+ })
+ }
+}
diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee
deleted file mode 100644
index ca3995c055a..00000000000
--- 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 00000000000..0268cc1cf5f
--- /dev/null
+++ b/src/main-process/atom-window.js
@@ -0,0 +1,432 @@
+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)
+ }
+
+ 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 (options, callback) {
+ options = Object.assign({
+ title: 'Save File',
+ defaultPath: this.representedDirectoryPaths[0]
+ }, options)
+
+ if (typeof callback === 'function') {
+ // Async
+ dialog.showSaveDialog(this.browserWindow, options, callback)
+ } else {
+ // Sync
+ return dialog.showSaveDialog(this.browserWindow, options)
+ }
+ }
+
+ 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()
+ }
+
+ didClosePathWithWaitSession (path) {
+ this.atomApplication.windowDidClosePathWithWaitSession(this, path)
+ }
+
+ copy () {
+ return this.browserWindow.copy()
+ }
+
+ disableZoom () {
+ return this.browserWindow.webContents.setVisualZoomLevelLimits(1, 1)
+ }
+}
diff --git a/src/pane.js b/src/pane.js
index 0305b39dd89..af93f8e1e1b 100644
--- a/src/pane.js
+++ b/src/pane.js
@@ -790,57 +790,53 @@ class Pane {
}
promptToSaveItem (item, options = {}) {
- if (typeof item.shouldPromptToSave !== 'function' || !item.shouldPromptToSave(options)) {
- return Promise.resolve(true)
- }
-
- let uri
- if (typeof item.getURI === 'function') {
- uri = item.getURI()
- } else if (typeof item.getUri === 'function') {
- uri = item.getUri()
- } else {
- return Promise.resolve(true)
- }
-
- const title = (typeof item.getTitle === 'function' && item.getTitle()) || uri
+ return new Promise((resolve, reject) => {
+ if (typeof item.shouldPromptToSave !== 'function' || !item.shouldPromptToSave(options)) {
+ return resolve(true)
+ }
- const saveDialog = (saveButtonText, saveFn, message) => {
- const chosen = this.applicationDelegate.confirm({
- message,
- detailedMessage: 'Your changes will be lost if you close this item without saving.',
- buttons: [saveButtonText, 'Cancel', "&Don't Save"]}
- )
+ let uri
+ if (typeof item.getURI === 'function') {
+ uri = item.getURI()
+ } else if (typeof item.getUri === 'function') {
+ uri = item.getUri()
+ } else {
+ return resolve(true)
+ }
- switch (chosen) {
- case 0:
- return new Promise(resolve => {
- return saveFn(item, error => {
- if (error instanceof SaveCancelledError) {
- resolve(false)
- } else if (error) {
- saveDialog(
- 'Save as',
- this.saveItemAs,
- `'${title}' could not be saved.\nError: ${this.getMessageForErrorCode(error.code)}`
- ).then(resolve)
- } else {
- resolve(true)
- }
- })
- })
- case 1:
- return Promise.resolve(false)
- case 2:
- return Promise.resolve(true)
+ const title = (typeof item.getTitle === 'function' && item.getTitle()) || uri
+
+ const saveDialog = (saveButtonText, saveFn, message) => {
+ this.applicationDelegate.confirm({
+ message,
+ detail: 'Your changes will be lost if you close this item without saving.',
+ buttons: [saveButtonText, 'Cancel', "&Don't Save"]
+ }, response => {
+ switch (response) {
+ case 0:
+ return saveFn(item, error => {
+ if (error instanceof SaveCancelledError) {
+ resolve(false)
+ } else if (error) {
+ saveDialog(
+ 'Save as',
+ this.saveItemAs,
+ `'${title}' could not be saved.\nError: ${this.getMessageForErrorCode(error.code)}`
+ )
+ } else {
+ resolve(true)
+ }
+ })
+ case 1:
+ return resolve(false)
+ case 2:
+ return resolve(true)
+ }
+ })
}
- }
- return saveDialog(
- 'Save',
- this.saveItem,
- `'${title}' has changes, do you want to save them?`
- )
+ saveDialog('Save', this.saveItem, `'${title}' has changes, do you want to save them?`)
+ })
}
// Public: Save the active item.
@@ -908,7 +904,7 @@ class Pane {
// after the item is successfully saved, or with the error if it failed.
// The return value will be that of `nextAction` or `undefined` if it was not
// provided
- saveItemAs (item, nextAction) {
+ async saveItemAs (item, nextAction) {
if (!item) return
if (typeof item.saveAs !== 'function') return
@@ -919,22 +915,34 @@ class Pane {
const itemPath = item.getPath()
if (itemPath && !saveOptions.defaultPath) saveOptions.defaultPath = itemPath
- const newItemPath = this.applicationDelegate.showSaveDialog(saveOptions)
- if (newItemPath) {
- return promisify(() => item.saveAs(newItemPath))
- .then(() => {
- if (nextAction) nextAction()
- })
- .catch(error => {
- if (nextAction) {
- nextAction(error)
- } else {
- this.handleSaveError(error, item)
- }
- })
- } else if (nextAction) {
- return nextAction(new SaveCancelledError('Save Cancelled'))
- }
+ let resolveSaveDialogPromise = null
+ const saveDialogPromise = new Promise(resolve => { resolveSaveDialogPromise = resolve })
+ this.applicationDelegate.showSaveDialog(saveOptions, newItemPath => {
+ if (newItemPath) {
+ promisify(() => item.saveAs(newItemPath))
+ .then(() => {
+ if (nextAction) {
+ resolveSaveDialogPromise(nextAction())
+ } else {
+ resolveSaveDialogPromise()
+ }
+ })
+ .catch(error => {
+ if (nextAction) {
+ resolveSaveDialogPromise(nextAction(error))
+ } else {
+ this.handleSaveError(error, item)
+ resolveSaveDialogPromise()
+ }
+ })
+ } else if (nextAction) {
+ resolveSaveDialogPromise(nextAction(new SaveCancelledError('Save Cancelled')))
+ } else {
+ resolveSaveDialogPromise()
+ }
+ })
+
+ return await saveDialogPromise
}
// Public: Save all items.
diff --git a/src/protocol-handler-installer.js b/src/protocol-handler-installer.js
index 37df6838972..27a272ea0d9 100644
--- a/src/protocol-handler-installer.js
+++ b/src/protocol-handler-installer.js
@@ -12,13 +12,13 @@ class ProtocolHandlerInstaller {
}
isDefaultProtocolClient () {
- return remote.app.isDefaultProtocolClient('atom', process.execPath, ['--uri-handler'])
+ return remote.app.isDefaultProtocolClient('atom', process.execPath, ['--uri-handler', '--'])
}
setAsDefaultProtocolClient () {
// This Electron API is only available on Windows and macOS. There might be some
// hacks to make it work on Linux; see https://github.com/electron/electron/issues/6440
- return this.isSupported() && remote.app.setAsDefaultProtocolClient('atom', process.execPath, ['--uri-handler'])
+ return this.isSupported() && remote.app.setAsDefaultProtocolClient('atom', process.execPath, ['--uri-handler', '--'])
}
initialize (config, notifications) {
@@ -26,19 +26,28 @@ class ProtocolHandlerInstaller {
return
}
- if (!this.isDefaultProtocolClient()) {
- const behaviorWhenNotProtocolClient = config.get(SETTING)
- switch (behaviorWhenNotProtocolClient) {
- case PROMPT:
+ const behaviorWhenNotProtocolClient = config.get(SETTING)
+ switch (behaviorWhenNotProtocolClient) {
+ case PROMPT:
+ if (!this.isDefaultProtocolClient()) {
this.promptToBecomeProtocolClient(config, notifications)
- break
- case ALWAYS:
+ }
+ break
+ case ALWAYS:
+ if (!this.isDefaultProtocolClient()) {
this.setAsDefaultProtocolClient()
- break
- case NEVER:
- default:
- // Do nothing
- }
+ }
+ break
+ case NEVER:
+ if (process.platform === 'win32') {
+ // Only win32 supports deregistration
+ const Registry = require('winreg')
+ const commandKey = new Registry({hive: 'HKCR', key: `\\atom`})
+ commandKey.destroy((_err, _val) => { /* no op */ })
+ }
+ break
+ default:
+ // Do nothing
}
}
diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee
index 0bacfbb8e6e..a367e6188c2 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/scope-descriptor.coffee b/src/scope-descriptor.coffee
index 95539cc692d..2085bd6b25b 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/selection.js b/src/selection.js
index 99c1ea95ebc..2c64fa1265c 100644
--- a/src/selection.js
+++ b/src/selection.js
@@ -585,7 +585,8 @@ class Selection {
// is empty unless the selection spans multiple lines in which case all lines
// are removed.
deleteLine () {
- if (this.isEmpty()) {
+ const range = this.getBufferRange()
+ if (range.isEmpty()) {
const start = this.cursor.getScreenRow()
const range = this.editor.bufferRowsForScreenRows(start, start + 1)
if (range[1] > range[0]) {
@@ -594,12 +595,12 @@ class Selection {
this.editor.buffer.deleteRow(range[0])
}
} else {
- const range = this.getBufferRange()
const start = range.start.row
let end = range.end.row
if (end !== this.editor.buffer.getLastRow() && range.end.column === 0) end--
this.editor.buffer.deleteRows(start, end)
}
+ this.cursor.setBufferPosition({row: this.cursor.getBufferRow(), column: range.start.column})
}
// Public: Joins the current line with the one below it. Lines will
@@ -831,8 +832,12 @@ class Selection {
if (clippedRange.isEmpty()) continue
}
- const selection = this.editor.addSelectionForScreenRange(clippedRange)
- selection.setGoalScreenRange(range)
+ const containingSelections = this.editor.selectionsMarkerLayer.findMarkers({containsScreenRange: clippedRange})
+ if (containingSelections.length === 0) {
+ const selection = this.editor.addSelectionForScreenRange(clippedRange)
+ selection.setGoalScreenRange(range)
+ }
+
break
}
}
@@ -853,8 +858,12 @@ class Selection {
if (clippedRange.isEmpty()) continue
}
- const selection = this.editor.addSelectionForScreenRange(clippedRange)
- selection.setGoalScreenRange(range)
+ const containingSelections = this.editor.selectionsMarkerLayer.findMarkers({containsScreenRange: clippedRange})
+ if (containingSelections.length === 0) {
+ const selection = this.editor.addSelectionForScreenRange(clippedRange)
+ selection.setGoalScreenRange(range)
+ }
+
break
}
}
diff --git a/src/syntax-scope-map.js b/src/syntax-scope-map.js
new file mode 100644
index 00000000000..e000fb64717
--- /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/text-editor-component.js b/src/text-editor-component.js
index aa52468643d..48cb919d0d0 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()
@@ -576,7 +577,6 @@ class TextEditorComponent {
on: {mousedown: this.didMouseDownOnContent},
style
},
- this.renderHighlightDecorations(),
this.renderLineTiles(),
this.renderBlockDecorationMeasurementArea(),
this.renderCharacterMeasurementLine()
@@ -594,13 +594,15 @@ class TextEditorComponent {
}
renderLineTiles () {
- const children = []
const style = {
position: 'absolute',
contain: 'strict',
overflow: 'hidden'
}
+ const children = []
+ children.push(this.renderHighlightDecorations())
+
if (this.hasInitialMeasurements) {
const {lineComponentsByScreenLineId} = this
@@ -681,7 +683,8 @@ class TextEditorComponent {
scrollWidth: this.getScrollWidth(),
decorationsToRender: this.decorationsToRender,
cursorsBlinkedOff: this.cursorsBlinkedOff,
- hiddenInputPosition: this.hiddenInputPosition
+ hiddenInputPosition: this.hiddenInputPosition,
+ tabIndex: this.tabIndex
})
}
@@ -3533,7 +3536,8 @@ class CursorsAndInputComponent {
zIndex: 1,
width: scrollWidth + 'px',
height: scrollHeight + 'px',
- pointerEvents: 'none'
+ pointerEvents: 'none',
+ userSelect: 'none'
}
}, children)
}
@@ -3546,7 +3550,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
@@ -3574,7 +3578,7 @@ class CursorsAndInputComponent {
compositionupdate: didCompositionUpdate,
compositionend: didCompositionEnd
},
- tabIndex: -1,
+ tabIndex: tabIndex,
style: {
position: 'absolute',
width: '1px',
@@ -4009,6 +4013,7 @@ class HighlightsComponent {
this.element.style.contain = 'strict'
this.element.style.position = 'absolute'
this.element.style.overflow = 'hidden'
+ this.element.style.userSelect = 'none'
this.highlightComponentsByKey = new Map()
this.update(props)
}
diff --git a/src/text-editor-element.js b/src/text-editor-element.js
index 7218b7f0597..926f7af4480 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))
}
diff --git a/src/text-editor.js b/src/text-editor.js
index 3964323e179..47fb9f48551 100644
--- a/src/text-editor.js
+++ b/src/text-editor.js
@@ -3083,6 +3083,36 @@ 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 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)
+ }
+ }
+ })
+ }
+
// Extended: Select the range of the given marker if it is valid.
//
// * `marker` A {DisplayMarker}
@@ -3869,7 +3899,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/text-mate-language-mode.js b/src/text-mate-language-mode.js
index 123e39f58fc..1a7cb6d2e46 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-grammar.js b/src/tree-sitter-grammar.js
new file mode 100644
index 00000000000..d00344fb110
--- /dev/null
+++ b/src/tree-sitter-grammar.js
@@ -0,0 +1,72 @@
+const path = require('path')
+const SyntaxScopeMap = require('./syntax-scope-map')
+const Module = require('module')
+
+module.exports =
+class TreeSitterGrammar {
+ constructor (registry, filePath, params) {
+ 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 || []
+
+ this.commentStrings = {
+ commentStartString: params.comments && params.comments.start,
+ commentEndString: params.comments && params.comments.end
+ }
+
+ const scopeSelectors = {}
+ for (const key in 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.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 00000000000..41c87ba004a
--- /dev/null
+++ b/src/tree-sitter-language-mode.js
@@ -0,0 +1,532 @@
+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
+
+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()
+ 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 () {
+ return this.grammar.id
+ }
+
+ bufferDidChange ({oldRange, newRange, oldText, newText}) {
+ 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,
+ 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++) {
+ 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)
+ }
+
+ 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._suggestedIndentForLineWithScopeAtBufferRow(
+ row,
+ line,
+ this.rootScopeDescriptor,
+ tabLength
+ )
+ }
+
+ suggestedIndentForBufferRow (row, tabLength, options) {
+ return this._suggestedIndentForLineWithScopeAtBufferRow(
+ row,
+ this.buffer.lineForRow(row),
+ this.rootScopeDescriptor,
+ tabLength,
+ options
+ )
+ }
+
+ 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) {
+ 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 () {
+ 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 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
+ break
+ }
+ }
+ if (!updatedExistingRange) result.push(range)
+ }
+ }
+
+ 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 {startPosition: childStart, endPosition: childEnd} = child
+ if (childEnd.row > childStart.row) {
+ if (childStart.row === parentStartRow && childEnd.row === parentEndRow) {
+ stack.push({node: child, level: level})
+ } else {
+ const childLevel = range && range.containsPoint(childStart) && range.containsPoint(childEnd)
+ ? level + 1
+ : level
+ if (childLevel <= goalLevel || goalLevel == null) {
+ stack.push({node: child, level: childLevel})
+ }
+ }
+ }
+ }
+ }
+
+ return result.sort((a, b) => a.start.row - b.start.row)
+ }
+
+ getFoldableRangeContainingPoint (point, tabLength, existenceOnly = false) {
+ let node = this.document.rootNode.descendantForPosition(this.buffer.clipPosition(point))
+ while (node) {
+ if (existenceOnly && node.startPosition.row < point.row) break
+ if (node.endPosition.row > point.row) {
+ const range = this.getFoldableRangeForNode(node, existenceOnly)
+ if (range) return range
+ }
+ node = node.parent
+ }
+ }
+
+ getFoldableRangeForNode (node, existenceOnly) {
+ const {children, type: nodeType} = node
+ const childCount = children.length
+ let childTypes
+
+ for (var i = 0, {length} = this.grammar.folds; i < length; i++) {
+ const foldEntry = this.grammar.folds[i]
+
+ if (foldEntry.type) {
+ if (typeof foldEntry.type === 'string') {
+ if (foldEntry.type !== nodeType) continue
+ } else {
+ if (!foldEntry.type.includes(nodeType)) continue
+ }
+ }
+
+ let foldStart
+ const startEntry = foldEntry.start
+ if (startEntry) {
+ if (startEntry.index != null) {
+ 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)
+ 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
+ }
+ } else {
+ foldStart = new Point(node.startPosition.row, Infinity)
+ }
+
+ let foldEnd
+ const endEntry = foldEntry.end
+ if (endEntry) {
+ let foldEndNode
+ if (endEntry.index != null) {
+ const index = endEntry.index < 0 ? childCount + endEntry.index : endEntry.index
+ foldEndNode = children[index]
+ if (!foldEndNode || (endEntry.type && endEntry.type !== foldEndNode.type)) continue
+ } else {
+ if (!childTypes) childTypes = children.map(foldEndNode => foldEndNode.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]
+ }
+
+ if (foldEndNode.endIndex - foldEndNode.startIndex > 1 && foldEndNode.startPosition.row > foldStart.row) {
+ foldEnd = new Point(foldEndNode.startPosition.row - 1, Infinity)
+ } else {
+ foldEnd = foldEndNode.startPosition
+ }
+ } else {
+ const {endPosition} = node
+ if (endPosition.column === 0) {
+ foldEnd = Point(endPosition.row - 1, Infinity)
+ } else if (childCount > 0) {
+ foldEnd = endPosition
+ } else {
+ foldEnd = Point(endPosition.row, 0)
+ }
+ }
+
+ return existenceOnly ? true : new Range(foldStart, foldEnd)
+ }
+ }
+
+ /*
+ 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
+ */
+
+ 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) {
+ 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
+ }
+ result.push(this.grammar.id)
+ return new ScopeDescriptor({scopes: result.reverse()})
+ }
+
+ hasTokenForSelector (scopeSelector) {
+ return false
+ }
+
+ getGrammar () {
+ return this.grammar
+ }
+}
+
+class TreeSitterHighlightIterator {
+ constructor (layer, document) {
+ this.layer = layer
+
+ // 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.length = 0
+ this.openTags.length = 0
+ this.containingNodeTypes.length = 0
+ this.containingNodeChildIndices.length = 0
+ this.currentPosition = targetPosition
+ this.currentIndex = this.layer.buffer.characterIndexForPosition(targetPosition)
+
+ var node = this.layer.document.rootNode
+ var childIndex = -1
+ var done = false
+ var nodeContainsTarget = true
+ do {
+ this.currentNode = node
+ this.currentChildIndex = childIndex
+ if (!nodeContainsTarget) break
+ this.containingNodeTypes.push(node.type)
+ this.containingNodeChildIndices.push(childIndex)
+
+ const scopeName = this.currentScopeName()
+ if (scopeName) {
+ const id = this.layer.grammar.idForScope(scopeName)
+ if (this.currentIndex === node.startIndex) {
+ this.openTags.push(id)
+ } else {
+ containingTags.push(id)
+ }
+ }
+
+ done = true
+ for (var i = 0, {children} = node, childCount = children.length; i < childCount; i++) {
+ const child = children[i]
+ if (child.endIndex > this.currentIndex) {
+ node = child
+ childIndex = i
+ done = false
+ if (child.startIndex > this.currentIndex) nodeContainsTarget = false
+ break
+ }
+ }
+ } while (!done)
+
+ return containingTags
+ }
+
+ moveToSuccessor () {
+ this.closeTags.length = 0
+ this.openTags.length = 0
+
+ if (!this.currentNode) {
+ this.currentPosition = {row: Infinity, column: Infinity}
+ return false
+ }
+
+ do {
+ 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.currentIndex = this.currentNode.endIndex
+ this.currentPosition = this.currentNode.endPosition
+ this.pushCloseTag()
+
+ const {nextSibling} = this.currentNode
+ if (nextSibling) {
+ this.currentNode = nextSibling
+ this.currentChildIndex++
+ if (this.currentIndex === nextSibling.startIndex) {
+ this.pushOpenTag()
+ this.descendLeft()
+ }
+ break
+ } else {
+ this.currentNode = this.currentNode.parent
+ this.currentChildIndex = last(this.containingNodeChildIndices)
+ if (!this.currentNode) break
+ }
+ }
+ } else if (this.currentNode.startIndex < this.currentNode.endIndex) {
+ this.currentNode = this.currentNode.nextSibling
+ if (this.currentNode) {
+ this.currentChildIndex++
+ this.currentPosition = this.currentNode.startPosition
+ this.currentIndex = this.currentNode.startIndex
+ 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)
+
+ 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.currentIndex === child.startIndex) {
+ 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]
+}
+
+// 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]
+})
diff --git a/src/window-event-handler.js b/src/window-event-handler.js
index 6d380819b56..da735294e59 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')
}
diff --git a/src/workspace.js b/src/workspace.js
index 5e85401eff1..12716874800 100644
--- a/src/workspace.js
+++ b/src/workspace.js
@@ -1160,16 +1160,17 @@ module.exports = class Workspace extends Model {
// * `uri` A {String} containing a URI.
//
// Returns a {Promise} that resolves to the {TextEditor} (or other item) for the given URI.
- createItemForURI (uri, options) {
+ async createItemForURI (uri, options) {
if (uri != null) {
- for (let opener of this.getOpeners()) {
+ for (const opener of this.getOpeners()) {
const item = opener(uri, options)
- if (item != null) return Promise.resolve(item)
+ if (item != null) return item
}
}
try {
- return this.openTextFile(uri, options)
+ const item = await this.openTextFile(uri, options)
+ return item
} catch (error) {
switch (error.code) {
case 'CANCELLED':
@@ -1199,7 +1200,7 @@ module.exports = class Workspace extends Model {
}
}
- openTextFile (uri, options) {
+ async openTextFile (uri, options) {
const filePath = this.project.resolvePath(uri)
if (filePath != null) {
@@ -1214,23 +1215,38 @@ module.exports = class Workspace extends Model {
}
const fileSize = fs.getSizeSync(filePath)
- if (fileSize >= (this.config.get('core.warnOnLargeFileLimit') * 1048576)) {
- const choice = this.applicationDelegate.confirm({
+
+ let [resolveConfirmFileOpenPromise, rejectConfirmFileOpenPromise] = []
+ const confirmFileOpenPromise = new Promise((resolve, reject) => {
+ resolveConfirmFileOpenPromise = resolve
+ rejectConfirmFileOpenPromise = reject
+ })
+
+ if (fileSize >= (this.config.get('core.warnOnLargeFileLimit') * 1048576)) { // 40MB by default
+ this.applicationDelegate.confirm({
message: 'Atom will be unresponsive during the loading of very large files.',
- detailedMessage: 'Do you still want to load this file?',
+ detail: 'Do you still want to load this file?',
buttons: ['Proceed', 'Cancel']
+ }, response => {
+ if (response === 1) {
+ rejectConfirmFileOpenPromise()
+ } else {
+ resolveConfirmFileOpenPromise()
+ }
})
- if (choice === 1) {
- const error = new Error()
- error.code = 'CANCELLED'
- throw error
- }
+ } else {
+ resolveConfirmFileOpenPromise()
}
- return this.project.bufferForPath(filePath, options)
- .then(buffer => {
- return this.textEditorRegistry.build(Object.assign({buffer, autoHeight: false}, options))
- })
+ try {
+ await confirmFileOpenPromise
+ const buffer = await this.project.bufferForPath(filePath, options)
+ return this.textEditorRegistry.build(Object.assign({buffer, autoHeight: false}, options))
+ } catch (e) {
+ const error = new Error()
+ error.code = 'CANCELLED'
+ throw error
+ }
}
handleGrammarUsed (grammar) {
@@ -1987,25 +2003,22 @@ module.exports = class Workspace extends Model {
checkoutHeadRevision (editor) {
if (editor.getPath()) {
- const checkoutHead = () => {
- return this.project.repositoryForDirectory(new Directory(editor.getDirectoryPath()))
- .then(repository => repository && repository.checkoutHeadForEditor(editor))
+ const checkoutHead = async () => {
+ const repository = await this.project.repositoryForDirectory(new Directory(editor.getDirectoryPath()))
+ if (repository) repository.checkoutHeadForEditor(editor)
}
if (this.config.get('editor.confirmCheckoutHeadRevision')) {
this.applicationDelegate.confirm({
message: 'Confirm Checkout HEAD Revision',
- detailedMessage: `Are you sure you want to discard all changes to "${editor.getFileName()}" since the last Git commit?`,
- buttons: {
- OK: checkoutHead,
- Cancel: null
- }
+ detail: `Are you sure you want to discard all changes to "${editor.getFileName()}" since the last Git commit?`,
+ buttons: ['OK', 'Cancel']
+ }, response => {
+ if (response === 0) checkoutHead()
})
} else {
- return checkoutHead()
+ checkoutHead()
}
- } else {
- return Promise.resolve(false)
}
}
}