+
+ class Noun {
+ public:
+ void verb();
+ };
+ `)
+ expect(grammar.name).toBe('C++')
+
+ // The word `class` only indicates C++ in `.h` files, not in all files.
+ grammar = grammarRegistry.selectGrammar('test.coffee', dedent `
+ module.exports =
+ class Noun
+ verb: -> true
+ `)
+ expect(grammar.name).toBe('CoffeeScript')
+ })
+
+ it('recognizes C++ files that do not match the content regex (regression)', () => {
+ atom.config.set('core.useTreeSitterParsers', true)
+ grammarRegistry.loadGrammarSync(require.resolve('language-c/grammars/tree-sitter-c.cson'))
+ grammarRegistry.loadGrammarSync(require.resolve('language-c/grammars/c++.cson'))
+ grammarRegistry.loadGrammarSync(require.resolve('language-c/grammars/tree-sitter-cpp.cson'))
+
+ let grammar = grammarRegistry.selectGrammar('test.cc', dedent `
+ int a();
+ `)
+ expect(grammar.name).toBe('C++')
+ })
+
+ it('does not apply content regexes from grammars without filetype or first line matches', () => {
+ atom.config.set('core.useTreeSitterParsers', true)
+ grammarRegistry.loadGrammarSync(require.resolve('language-c/grammars/tree-sitter-cpp.cson'))
+
+ let grammar = grammarRegistry.selectGrammar('', dedent `
+ class Foo
+ # this is ruby, not C++
+ end
+ `)
+
+ expect(grammar.name).toBe('Null Grammar')
+ })
+
+ it('recognizes shell scripts with shebang lines', () => {
+ atom.config.set('core.useTreeSitterParsers', true)
+ grammarRegistry.loadGrammarSync(require.resolve('language-shellscript/grammars/shell-unix-bash.cson'))
+ grammarRegistry.loadGrammarSync(require.resolve('language-shellscript/grammars/tree-sitter-bash.cson'))
+
+ let grammar = grammarRegistry.selectGrammar('test.h', dedent `
+ #!/bin/bash
+
+ echo "hi"
+ `)
+ expect(grammar.name).toBe('Shell Script')
+ expect(grammar instanceof TreeSitterGrammar).toBeTruthy()
+
+ grammar = grammarRegistry.selectGrammar('test.h', dedent `
+ # vim: set ft=bash
+
+ echo "hi"
+ `)
+ expect(grammar.name).toBe('Shell Script')
+ expect(grammar instanceof TreeSitterGrammar).toBeTruthy()
+
+ atom.config.set('core.useTreeSitterParsers', false)
+ grammar = grammarRegistry.selectGrammar('test.h', dedent `
+ #!/bin/bash
+
+ echo "hi"
+ `)
+ expect(grammar.name).toBe('Shell Script')
+ expect(grammar instanceof TreeSitterGrammar).toBeFalsy()
+ })
+
+ it('recognizes JavaScript files that use Flow', () => {
+ atom.config.set('core.useTreeSitterParsers', true)
+ grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson'))
+ grammarRegistry.loadGrammarSync(require.resolve('language-typescript/grammars/tree-sitter-flow.cson'))
+
+ let grammar = grammarRegistry.selectGrammar('test.js', dedent`
+ // Copyright something
+ // @flow
+
+ module.exports = function () { return 1 + 1 }
+ `)
+ expect(grammar.name).toBe('Flow JavaScript')
+
+ grammar = grammarRegistry.selectGrammar('test.js', dedent`
+ module.exports = function () { return 1 + 1 }
+ `)
+ expect(grammar.name).toBe('JavaScript')
+ })
})
})
@@ -452,6 +545,34 @@ describe('GrammarRegistry', () => {
})
})
+ describe('.addInjectionPoint(languageId, {type, language, content})', () => {
+ const injectionPoint = {
+ type: 'some_node_type',
+ language() { return 'some_language_name' },
+ content(node) { return node }
+ }
+
+ beforeEach(() => {
+ atom.config.set('core.useTreeSitterParsers', true)
+ })
+
+ it('adds an injection point to the grammar with the given id', async () => {
+ await atom.packages.activatePackage('language-javascript')
+ atom.grammars.addInjectionPoint('javascript', injectionPoint)
+ const grammar = atom.grammars.grammarForId('javascript')
+ expect(grammar.injectionPoints).toContain(injectionPoint)
+ })
+
+ describe('when called before a grammar with the given id is loaded', () => {
+ it('adds the injection point once the grammar is loaded', async () => {
+ atom.grammars.addInjectionPoint('javascript', injectionPoint)
+ await atom.packages.activatePackage('language-javascript')
+ const grammar = atom.grammars.grammarForId('javascript')
+ expect(grammar.injectionPoints).toContain(injectionPoint)
+ })
+ })
+ })
+
describe('serialization', () => {
it('persists editors\' grammar overrides', async () => {
const buffer1 = new TextBuffer()
diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js
index 16aef8e27..c49e36b5d 100644
--- a/spec/main-process/atom-application.test.js
+++ b/spec/main-process/atom-application.test.js
@@ -606,6 +606,8 @@ describe('AtomApplication', function () {
assert(!electron.app.didQuit())
await Promise.all([window1.lastPrepareToUnloadPromise, window2.lastPrepareToUnloadPromise])
+ assert(!electron.app.didQuit())
+ await atomApplication.lastBeforeQuitPromise
await new Promise(process.nextTick)
assert(electron.app.didQuit())
})
@@ -634,6 +636,29 @@ describe('AtomApplication', function () {
assert(electron.app.didQuit())
})
+ it('closes successfully unloaded windows when quitting', async () => {
+ const atomApplication = buildAtomApplication()
+ const [window1] = await atomApplication.launch(parseCommandLine([]))
+ const [window2] = await atomApplication.launch(parseCommandLine([]))
+ await Promise.all([window1.loadedPromise, window2.loadedPromise])
+ await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
+ atom.workspace.getActiveTextEditor().insertText('unsaved text')
+ sendBackToMainProcess()
+ })
+
+ // Choosing "Cancel"
+ mockElectronShowMessageBox({response: 1})
+ electron.app.quit()
+ await atomApplication.lastBeforeQuitPromise
+ assert(atomApplication.getAllWindows().length === 1)
+
+ // Choosing "Don't save"
+ mockElectronShowMessageBox({response: 2})
+ electron.app.quit()
+ await atomApplication.lastBeforeQuitPromise
+ assert(atomApplication.getAllWindows().length === 0)
+ })
+
function buildAtomApplication (params = {}) {
const atomApplication = new AtomApplication(Object.assign({
resourcePath: ATOM_RESOURCE_PATH,
diff --git a/spec/main-process/file-recovery-service.test.js b/spec/main-process/file-recovery-service.test.js
index 2a8f2088c..45c10c25b 100644
--- a/spec/main-process/file-recovery-service.test.js
+++ b/spec/main-process/file-recovery-service.test.js
@@ -1,6 +1,8 @@
const {dialog} = require('electron')
const FileRecoveryService = require('../../src/main-process/file-recovery-service')
const fs = require('fs-plus')
+const fsreal = require('fs')
+const EventEmitter = require('events').EventEmitter
const sinon = require('sinon')
const {escapeRegExp} = require('underscore-plus')
const temp = require('temp').track()
@@ -116,13 +118,22 @@ describe("FileRecoveryService", () => {
const mockWindow = {}
const filePath = temp.path()
fs.writeFileSync(filePath, "content")
- fs.chmodSync(filePath, 0444)
let logs = []
spies.stub(console, 'log', (message) => logs.push(message))
spies.stub(dialog, 'showMessageBox')
+ // Copy files to be recovered before mocking fs.createWriteStream
await recoveryService.willSavePath(mockWindow, filePath)
+
+ // Stub out fs.createWriteStream so that we can return a fake error when
+ // attempting to copy the recovered file to its original location
+ var fakeEmitter = new EventEmitter()
+ var onStub = spies.stub(fakeEmitter, 'on')
+ onStub.withArgs('error').yields(new Error('Nope')).returns(fakeEmitter)
+ onStub.withArgs('open').returns(fakeEmitter)
+ spies.stub(fsreal, 'createWriteStream').withArgs(filePath).returns(fakeEmitter)
+
await recoveryService.didCrashWindow(mockWindow)
let recoveryFiles = fs.listTreeSync(recoveryDirectory)
assert.equal(recoveryFiles.length, 1)
diff --git a/spec/main-process/mocha-test-runner.js b/spec/main-process/mocha-test-runner.js
index 433727c56..61d533417 100644
--- a/spec/main-process/mocha-test-runner.js
+++ b/spec/main-process/mocha-test-runner.js
@@ -1,10 +1,9 @@
-"use babel"
+const Mocha = require('mocha')
+const fs = require('fs-plus')
+const {assert} = require('chai')
-import Mocha from 'mocha'
-import fs from 'fs-plus'
-import {assert} from 'chai'
-
-export default function (testPaths) {
+module.exports =
+function (testPaths) {
global.assert = assert
let reporterOptions = {
@@ -24,6 +23,7 @@ export default function (testPaths) {
reporter: 'mocha-multi-reporters',
reporterOptions
})
+
for (let testPath of testPaths) {
if (fs.isDirectorySync(testPath)) {
for (let testFilePath of fs.listTreeSync(testPath)) {
@@ -36,7 +36,7 @@ export default function (testPaths) {
}
}
- mocha.run(function (failures) {
+ mocha.run(failures => {
if (failures === 0) {
process.exit(0)
} else {
diff --git a/spec/main-process/parse-command-line.test.js b/spec/main-process/parse-command-line.test.js
index 0cd1f5b13..bb5d625a9 100644
--- a/spec/main-process/parse-command-line.test.js
+++ b/spec/main-process/parse-command-line.test.js
@@ -1,10 +1,8 @@
-/** @babel */
+const parseCommandLine = require('../../src/main-process/parse-command-line')
-import parseCommandLine from '../../src/main-process/parse-command-line'
-
-describe('parseCommandLine', function () {
- describe('when --uri-handler is not passed', function () {
- it('parses arguments as normal', function () {
+describe('parseCommandLine', () => {
+ describe('when --uri-handler is not passed', () => {
+ it('parses arguments as normal', () => {
const args = parseCommandLine(['-d', '--safe', '--test', '/some/path', 'atom://test/url', 'atom://other/url'])
assert.isTrue(args.devMode)
assert.isTrue(args.safeMode)
@@ -14,8 +12,8 @@ describe('parseCommandLine', function () {
})
})
- describe('when --uri-handler is passed', function () {
- it('ignores other arguments and limits to one URL', function () {
+ describe('when --uri-handler is passed', () => {
+ it('ignores other arguments and limits to one URL', () => {
const args = parseCommandLine(['-d', '--uri-handler', '--safe', '--test', '/some/path', 'atom://test/url', 'atom://other/url'])
assert.isUndefined(args.devMode)
assert.isUndefined(args.safeMode)
diff --git a/spec/menu-manager-spec.coffee b/spec/menu-manager-spec.coffee
index 3bbd8b9da..c7b15aae6 100644
--- a/spec/menu-manager-spec.coffee
+++ b/spec/menu-manager-spec.coffee
@@ -58,10 +58,8 @@ describe "MenuManager", ->
menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}]
atom.keymaps.add 'test', 'atom-workspace': 'ctrl-b': 'b'
menu.update()
-
- waits 50
-
- runs -> expect(menu.sendToBrowserProcess.argsForCall[0][1]['b']).toEqual ['ctrl-b']
+ advanceClock(1)
+ expect(menu.sendToBrowserProcess.argsForCall[0][1]['b']).toEqual ['ctrl-b']
it "omits key bindings that are mapped to unset! in any context", ->
# it would be nice to be smarter about omitting, but that would require a much
@@ -69,10 +67,8 @@ describe "MenuManager", ->
menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}]
atom.keymaps.add 'test', 'atom-workspace': 'ctrl-b': 'b'
atom.keymaps.add 'test', 'atom-text-editor': 'ctrl-b': 'unset!'
-
- waits 50
-
- runs -> expect(menu.sendToBrowserProcess.argsForCall[0][1]['b']).toBeUndefined()
+ advanceClock(1)
+ expect(menu.sendToBrowserProcess.argsForCall[0][1]['b']).toBeUndefined()
it "omits key bindings that could conflict with AltGraph characters on macOS", ->
Object.defineProperty process, 'platform', value: 'darwin'
@@ -87,12 +83,10 @@ describe "MenuManager", ->
'alt-shift-C': 'c'
'alt-cmd-d': 'd'
- waits 50
-
- runs ->
- expect(menu.sendToBrowserProcess.argsForCall[0][1]['b']).toBeUndefined()
- expect(menu.sendToBrowserProcess.argsForCall[0][1]['c']).toBeUndefined()
- expect(menu.sendToBrowserProcess.argsForCall[0][1]['d']).toEqual(['alt-cmd-d'])
+ advanceClock(1)
+ expect(menu.sendToBrowserProcess.argsForCall[0][1]['b']).toBeUndefined()
+ expect(menu.sendToBrowserProcess.argsForCall[0][1]['c']).toBeUndefined()
+ expect(menu.sendToBrowserProcess.argsForCall[0][1]['d']).toEqual(['alt-cmd-d'])
it "omits key bindings that could conflict with AltGraph characters on Windows", ->
Object.defineProperty process, 'platform', value: 'win32'
@@ -107,12 +101,10 @@ describe "MenuManager", ->
'ctrl-alt-shift-C': 'c'
'ctrl-alt-cmd-d': 'd'
- waits 50
-
- runs ->
- expect(menu.sendToBrowserProcess.argsForCall[0][1]['b']).toBeUndefined()
- expect(menu.sendToBrowserProcess.argsForCall[0][1]['c']).toBeUndefined()
- expect(menu.sendToBrowserProcess.argsForCall[0][1]['d']).toEqual(['ctrl-alt-cmd-d'])
+ advanceClock(1)
+ expect(menu.sendToBrowserProcess.argsForCall[0][1]['b']).toBeUndefined()
+ expect(menu.sendToBrowserProcess.argsForCall[0][1]['c']).toBeUndefined()
+ expect(menu.sendToBrowserProcess.argsForCall[0][1]['d']).toEqual(['ctrl-alt-cmd-d'])
it "updates the application menu when a keymap is reloaded", ->
spyOn(menu, 'update')
diff --git a/spec/package-manager-spec.js b/spec/package-manager-spec.js
index b1ecf834d..dd87f85fa 100644
--- a/spec/package-manager-spec.js
+++ b/spec/package-manager-spec.js
@@ -30,13 +30,15 @@ describe('PackageManager', () => {
expect(packageManger.packageDirPaths[0]).toBe(path.join(configDirPath, 'packages'))
})
- it('adds regular package path and dev package path in dev mode', () => {
+ it('adds regular package path, dev package path, and Atom repo package path in dev mode and dev resource path is set', () => {
const packageManger = new PackageManager({})
const configDirPath = path.join('~', 'someConfig')
- packageManger.initialize({configDirPath, devMode: true})
- expect(packageManger.packageDirPaths.length).toBe(2)
+ const resourcePath = path.join('~', '/atom')
+ packageManger.initialize({configDirPath, resourcePath, devMode: true})
+ expect(packageManger.packageDirPaths.length).toBe(3)
expect(packageManger.packageDirPaths).toContain(path.join(configDirPath, 'packages'))
expect(packageManger.packageDirPaths).toContain(path.join(configDirPath, 'dev', 'packages'))
+ expect(packageManger.packageDirPaths).toContain(path.join(resourcePath, 'packages'))
})
})
@@ -1032,6 +1034,7 @@ describe('PackageManager', () => {
})
it('loads any tree-sitter grammars defined in the package', async () => {
+ atom.config.set('core.useTreeSitterParsers', true)
await atom.packages.activatePackage('package-with-tree-sitter-grammar')
const grammar = atom.grammars.selectGrammar('test.somelang')
expect(grammar.name).toBe('Some Language')
diff --git a/spec/pane-spec.js b/spec/pane-spec.js
index 8ef274c2d..ddb92b96e 100644
--- a/spec/pane-spec.js
+++ b/spec/pane-spec.js
@@ -219,6 +219,34 @@ describe('Pane', () => {
runs(() => expect(eventOrder).toEqual(['add', 'remove']))
})
+ it('subscribes to be notified when item terminates its pending state', () => {
+ const fakeDisposable = { dispose: () => {} }
+ const spy = jasmine.createSpy('onDidTerminatePendingState').andReturn((fakeDisposable))
+
+ const pane = new Pane(paneParams({items: []}))
+ const item = {
+ getTitle: () => '',
+ onDidTerminatePendingState: spy
+ }
+ pane.addItem(item)
+
+ expect(spy).toHaveBeenCalled()
+ })
+
+ it('subscribes to be notified when item is destroyed', () => {
+ const fakeDisposable = { dispose: () => {} }
+ const spy = jasmine.createSpy('onDidDestroy').andReturn((fakeDisposable))
+
+ const pane = new Pane(paneParams({items: []}))
+ const item = {
+ getTitle: () => '',
+ onDidDestroy: spy
+ }
+ pane.addItem(item)
+
+ expect(spy).toHaveBeenCalled()
+ })
+
describe('when using the old API of ::addItem(item, index)', () => {
beforeEach(() => spyOn(Grim, 'deprecate'))
diff --git a/spec/project-spec.js b/spec/project-spec.js
index 2d38058fe..861a0f53a 100644
--- a/spec/project-spec.js
+++ b/spec/project-spec.js
@@ -969,6 +969,77 @@ describe('Project', () => {
})
})
+ describe('.observeRepositories()', () => {
+ it('invokes the observer with current and future repositories', () => {
+ const observed = []
+
+ const directory1 = temp.mkdirSync('git-repo1')
+ const gitDirPath1 = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git'))
+ fs.copySync(gitDirPath1, path.join(directory1, '.git'))
+
+ const directory2 = temp.mkdirSync('git-repo2')
+ const gitDirPath2 = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'repo-with-submodules', 'git.git'))
+ fs.copySync(gitDirPath2, path.join(directory2, '.git'))
+
+ atom.project.setPaths([directory1])
+
+ const disposable = atom.project.observeRepositories((repo) => observed.push(repo))
+ expect(observed.length).toBe(1)
+ expect(observed[0].getReferenceTarget('refs/heads/master')).toBe('ef046e9eecaa5255ea5e9817132d4001724d6ae1')
+
+ atom.project.addPath(directory2)
+ expect(observed.length).toBe(2)
+ expect(observed[1].getReferenceTarget('refs/heads/master')).toBe('d2b0ad9cbc6f6c4372e8956e5cc5af771b2342e5')
+
+ disposable.dispose()
+ })
+ })
+
+ describe('.onDidAddRepository()', () => {
+ it('invokes callback when a path is added and the path is the root of a repository', () => {
+ const observed = []
+ const disposable = atom.project.onDidAddRepository((repo) => observed.push(repo))
+
+ const projectRootPath = temp.mkdirSync()
+ const fixtureRepoPath = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git'))
+ fs.copySync(fixtureRepoPath, path.join(projectRootPath, '.git'))
+
+ atom.project.addPath(projectRootPath)
+ expect(observed.length).toBe(1)
+ expect(observed[0].getOriginURL()).toEqual('https://github.com/example-user/example-repo.git')
+
+ disposable.dispose()
+ })
+
+ it('invokes callback when a path is added and the path is subdirectory of a repository', () => {
+ const observed = []
+ const disposable = atom.project.onDidAddRepository((repo) => observed.push(repo))
+
+ const projectRootPath = temp.mkdirSync()
+ const fixtureRepoPath = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git'))
+ fs.copySync(fixtureRepoPath, path.join(projectRootPath, '.git'))
+
+ const projectSubDirPath = path.join(projectRootPath, 'sub-dir')
+ fs.mkdirSync(projectSubDirPath)
+
+ atom.project.addPath(projectSubDirPath)
+ expect(observed.length).toBe(1)
+ expect(observed[0].getOriginURL()).toEqual('https://github.com/example-user/example-repo.git')
+
+ disposable.dispose()
+ })
+
+ it('does not invoke callback when a path is added and the path is not part of a repository', () => {
+ const observed = []
+ const disposable = atom.project.onDidAddRepository((repo) => observed.push(repo))
+
+ atom.project.addPath(temp.mkdirSync('not-a-repository'))
+ expect(observed.length).toBe(0)
+
+ disposable.dispose()
+ })
+ })
+
describe('.relativize(path)', () => {
it('returns the path, relative to whichever root directory it is inside of', () => {
atom.project.addPath(temp.mkdirSync('another-path'))
diff --git a/spec/sample-with-comments.js b/spec/sample-with-comments.js
deleted file mode 100644
index 66dc9051d..000000000
--- a/spec/sample-with-comments.js
+++ /dev/null
@@ -1 +0,0 @@
-undefined
\ No newline at end of file
diff --git a/spec/selection-spec.js b/spec/selection-spec.js
index cb586da26..e9cf1c617 100644
--- a/spec/selection-spec.js
+++ b/spec/selection-spec.js
@@ -85,6 +85,17 @@ describe('Selection', () => {
})
})
+ describe("when the selection's range is moved", () => {
+ it('notifies ::onDidChangeRange observers', () => {
+ selection.setBufferRange([[2, 0], [2, 10]])
+ const changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler')
+ selection.onDidChangeRange(changeScreenRangeHandler)
+ buffer.insert([2, 5], 'abc')
+ expect(changeScreenRangeHandler).toHaveBeenCalled()
+ expect(changeScreenRangeHandler.mostRecentCall.args[0]).not.toBeUndefined()
+ });
+ });
+
describe("when only the selection's tail is moved (regression)", () => {
it('notifies ::onDidChangeRange observers', () => {
selection.setBufferRange([[2, 0], [2, 10]], {reversed: true})
@@ -93,6 +104,7 @@ describe('Selection', () => {
buffer.insert([2, 5], 'abc')
expect(changeScreenRangeHandler).toHaveBeenCalled()
+ expect(changeScreenRangeHandler.mostRecentCall.args[0]).not.toBeUndefined()
})
})
@@ -154,4 +166,118 @@ describe('Selection', () => {
expect(editor.isFoldedAtBufferRow(0)).toBe(false)
})
})
+
+ describe('within a read-only editor', () => {
+ beforeEach(() => {
+ editor.setReadOnly(true)
+ selection.setBufferRange([[0, 0], [0, 13]])
+ })
+
+ const modifications = [
+ {
+ name: 'insertText',
+ op: opts => selection.insertText('yes', opts)
+ },
+ {
+ name: 'backspace',
+ op: opts => selection.backspace(opts)
+ },
+ {
+ name: 'deleteToPreviousWordBoundary',
+ op: opts => selection.deleteToPreviousWordBoundary(opts)
+ },
+ {
+ name: 'deleteToNextWordBoundary',
+ op: opts => selection.deleteToNextWordBoundary(opts)
+ },
+ {
+ name: 'deleteToBeginningOfWord',
+ op: opts => selection.deleteToBeginningOfWord(opts)
+ },
+ {
+ name: 'deleteToBeginningOfLine',
+ op: opts => selection.deleteToBeginningOfLine(opts)
+ },
+ {
+ name: 'delete',
+ op: opts => selection.delete(opts)
+ },
+ {
+ name: 'deleteToEndOfLine',
+ op: opts => selection.deleteToEndOfLine(opts)
+ },
+ {
+ name: 'deleteToEndOfWord',
+ op: opts => selection.deleteToEndOfWord(opts)
+ },
+ {
+ name: 'deleteToBeginningOfSubword',
+ op: opts => selection.deleteToBeginningOfSubword(opts)
+ },
+ {
+ name: 'deleteToEndOfSubword',
+ op: opts => selection.deleteToEndOfSubword(opts)
+ },
+ {
+ name: 'deleteSelectedText',
+ op: opts => selection.deleteSelectedText(opts)
+ },
+ {
+ name: 'deleteLine',
+ op: opts => selection.deleteLine(opts)
+ },
+ {
+ name: 'joinLines',
+ op: opts => selection.joinLines(opts)
+ },
+ {
+ name: 'outdentSelectedRows',
+ op: opts => selection.outdentSelectedRows(opts)
+ },
+ {
+ name: 'autoIndentSelectedRows',
+ op: opts => selection.autoIndentSelectedRows(opts)
+ },
+ {
+ name: 'toggleLineComments',
+ op: opts => selection.toggleLineComments(opts)
+ },
+ {
+ name: 'cutToEndOfLine',
+ op: opts => selection.cutToEndOfLine(false, opts)
+ },
+ {
+ name: 'cutToEndOfBufferLine',
+ op: opts => selection.cutToEndOfBufferLine(false, opts)
+ },
+ {
+ name: 'cut',
+ op: opts => selection.cut(false, false, opts.bypassReadOnly)
+ },
+ {
+ name: 'indent',
+ op: opts => selection.indent(opts)
+ },
+ {
+ name: 'indentSelectedRows',
+ op: opts => selection.indentSelectedRows(opts)
+ },
+ ]
+
+ describe('without bypassReadOnly', () => {
+ for (const {name, op} of modifications) {
+ it(`throws an error on ${name}`, () => {
+ expect(op).toThrow()
+ })
+ }
+ })
+
+ describe('with bypassReadOnly', () => {
+ for (const {name, op} of modifications) {
+ it(`permits ${name}`, () => {
+ op({bypassReadOnly: true})
+ })
+ }
+ })
+ })
})
diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee
index dcc3c6641..f4f1f3568 100644
--- a/spec/spec-helper.coffee
+++ b/spec/spec-helper.coffee
@@ -12,7 +12,7 @@ FindParentDir = require 'find-parent-dir'
TextEditor = require '../src/text-editor'
TextEditorElement = require '../src/text-editor-element'
TextMateLanguageMode = require '../src/text-mate-language-mode'
-clipboard = require '../src/safe-clipboard'
+{clipboard} = require 'electron'
jasmineStyle = document.createElement('style')
jasmineStyle.textContent = atom.themes.loadStylesheet(atom.themes.resolveStylesheet('../static/jasmine'))
diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js
index f2031258f..66274a99a 100644
--- a/spec/text-editor-component-spec.js
+++ b/spec/text-editor-component-spec.js
@@ -11,7 +11,7 @@ const fs = require('fs')
const path = require('path')
const Grim = require('grim')
const electron = require('electron')
-const clipboard = require('../src/safe-clipboard')
+const clipboard = electron.clipboard
const SAMPLE_TEXT = fs.readFileSync(path.join(__dirname, 'fixtures', 'sample.js'), 'utf8')
@@ -26,6 +26,7 @@ document.registerElement('text-editor-component-test-element', {
})
const editors = []
+let verticalScrollbarWidth, horizontalScrollbarHeight
describe('TextEditorComponent', () => {
beforeEach(() => {
@@ -33,8 +34,15 @@ describe('TextEditorComponent', () => {
// Force scrollbars to be visible regardless of local system configuration
const scrollbarStyle = document.createElement('style')
- scrollbarStyle.textContent = '::-webkit-scrollbar { -webkit-appearance: none }'
+ scrollbarStyle.textContent = 'atom-text-editor ::-webkit-scrollbar { -webkit-appearance: none }'
jasmine.attachToDOM(scrollbarStyle)
+
+ if (verticalScrollbarWidth == null) {
+ const {component, element} = buildComponent({text: 'abcdefgh\n'.repeat(10), width: 30, height: 30})
+ verticalScrollbarWidth = getVerticalScrollbarWidth(component)
+ horizontalScrollbarHeight = getHorizontalScrollbarHeight(component)
+ element.remove()
+ }
})
afterEach(() => {
@@ -184,8 +192,8 @@ describe('TextEditorComponent', () => {
})
it('makes the content at least as tall as the scroll container client height', async () => {
- const {component, element, editor} = buildComponent({text: 'a', height: 100})
- expect(component.refs.content.offsetHeight).toBe(100)
+ const {component, element, editor} = buildComponent({text: 'a'.repeat(100), width: 50, height: 100})
+ expect(component.refs.content.offsetHeight).toBe(100 - getHorizontalScrollbarHeight(component))
editor.setText('a\n'.repeat(30))
await component.getNextUpdatePromise()
@@ -201,7 +209,7 @@ describe('TextEditorComponent', () => {
await setEditorHeightInLines(component, 6)
// scroll to end
- await setScrollTop(component, scrollContainer.scrollHeight - scrollContainer.clientHeight)
+ await setScrollTop(component, Infinity)
expect(component.getFirstVisibleRow()).toBe(editor.getScreenLineCount() - 3)
editor.update({scrollPastEnd: false})
@@ -211,7 +219,7 @@ describe('TextEditorComponent', () => {
// Always allows at least 3 lines worth of overscroll if the editor is short
await setEditorHeightInLines(component, 2)
await editor.update({scrollPastEnd: true})
- await setScrollTop(component, scrollContainer.scrollHeight - scrollContainer.clientHeight)
+ await setScrollTop(component, Infinity)
expect(component.getFirstVisibleRow()).toBe(editor.getScreenLineCount() + 1)
})
@@ -296,31 +304,6 @@ describe('TextEditorComponent', () => {
expect(lineNumberNodeForScreenRow(component, 0).querySelector('.foldable')).toBeNull()
})
- it('gracefully handles folds that change the soft-wrap boundary by causing the vertical scrollbar to disappear (regression)', async () => {
- const text = ('x'.repeat(100) + '\n') + 'y\n'.repeat(28) + ' z\n'.repeat(50)
- const {component, element, editor} = buildComponent({text, height: 1000, width: 500})
-
- element.addEventListener('scroll', (event) => {
- event.stopPropagation()
- }, true)
-
- editor.setSoftWrapped(true)
- jasmine.attachToDOM(element)
- await component.getNextUpdatePromise()
-
- const firstScreenLineLengthWithVerticalScrollbar = element.querySelector('.line').textContent.length
-
- setScrollTop(component, 620)
- await component.getNextUpdatePromise()
-
- editor.foldBufferRow(28)
- await component.getNextUpdatePromise()
-
- const firstLineElement = element.querySelector('.line')
- expect(firstLineElement.dataset.screenRow).toBe('0')
- expect(firstLineElement.textContent.length).toBeGreaterThan(firstScreenLineLengthWithVerticalScrollbar)
- })
-
it('shows the foldable icon on the last screen row of a buffer row that can be folded', async () => {
const {component, element, editor} = buildComponent({text: 'abc\n de\nfghijklm\n no', softWrapped: true})
await setEditorWidthInCharacters(component, 5)
@@ -361,18 +344,14 @@ describe('TextEditorComponent', () => {
expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(0)
expect(getHorizontalScrollbarHeight(component)).toBe(0)
expect(verticalScrollbar.style.visibility).toBe('')
- expect(verticalScrollbar.style.bottom).toBe('0px')
expect(horizontalScrollbar.style.visibility).toBe('hidden')
- expect(component.refs.scrollbarCorner).toBeUndefined()
editor.setText('a'.repeat(100))
await component.getNextUpdatePromise()
expect(getVerticalScrollbarWidth(component)).toBe(0)
expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(0)
expect(verticalScrollbar.style.visibility).toBe('hidden')
- expect(horizontalScrollbar.style.right).toBe('0px')
expect(horizontalScrollbar.style.visibility).toBe('')
- expect(component.refs.scrollbarCorner).toBeUndefined()
editor.setText('')
await component.getNextUpdatePromise()
@@ -380,37 +359,6 @@ describe('TextEditorComponent', () => {
expect(getHorizontalScrollbarHeight(component)).toBe(0)
expect(verticalScrollbar.style.visibility).toBe('hidden')
expect(horizontalScrollbar.style.visibility).toBe('hidden')
- expect(component.refs.scrollbarCorner).toBeUndefined()
-
- editor.setText(SAMPLE_TEXT)
- await component.getNextUpdatePromise()
-
- // Does not show scrollbars if the content perfectly fits
- element.style.width = component.getGutterContainerWidth() + component.getContentWidth() + 'px'
- element.style.height = component.getContentHeight() + 'px'
- await component.getNextUpdatePromise()
- expect(getVerticalScrollbarWidth(component)).toBe(0)
- expect(getHorizontalScrollbarHeight(component)).toBe(0)
- expect(verticalScrollbar.style.visibility).toBe('hidden')
- expect(horizontalScrollbar.style.visibility).toBe('hidden')
-
- // Shows scrollbars if the only reason we overflow is the presence of the
- // scrollbar for the opposite axis.
- element.style.width = component.getGutterContainerWidth() + component.getContentWidth() - 1 + 'px'
- element.style.height = component.getContentHeight() + component.getHorizontalScrollbarHeight() - 1 + 'px'
- await component.getNextUpdatePromise()
- expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(0)
- expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(0)
- expect(verticalScrollbar.style.visibility).toBe('')
- expect(horizontalScrollbar.style.visibility).toBe('')
-
- element.style.width = component.getGutterContainerWidth() + component.getContentWidth() + component.getVerticalScrollbarWidth() - 1 + 'px'
- element.style.height = component.getContentHeight() - 1 + 'px'
- await component.getNextUpdatePromise()
- expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(0)
- expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(0)
- expect(verticalScrollbar.style.visibility).toBe('')
- expect(horizontalScrollbar.style.visibility).toBe('')
})
describe('when scrollbar styles change or the editor element is detached and then reattached', () => {
@@ -564,9 +512,20 @@ describe('TextEditorComponent', () => {
it('gives cursors at the end of lines the width of an "x" character', async () => {
const {component, element, editor} = buildComponent()
+ editor.setText('abcde')
+ await setEditorWidthInCharacters(component, 5.5)
+
editor.setCursorScreenPosition([0, Infinity])
await component.getNextUpdatePromise()
expect(element.querySelector('.cursor').offsetWidth).toBe(Math.round(component.getBaseCharacterWidth()))
+
+ // Clip cursor width when soft-wrap is on and the cursor is at the end of
+ // the line. This prevents the parent tile from disabling sub-pixel
+ // anti-aliasing. For some reason, adding overflow: hidden to the cursor
+ // container doesn't solve this issue so we're adding this workaround instead.
+ editor.setSoftWrapped(true)
+ await component.getNextUpdatePromise()
+ expect(element.querySelector('.cursor').offsetWidth).toBeLessThan(Math.round(component.getBaseCharacterWidth()))
})
it('positions and sizes cursors correctly when they are located next to a fold marker', async () => {
@@ -672,17 +631,6 @@ describe('TextEditorComponent', () => {
expect(scrollContainer.clientWidth).toBe(scrollContainer.scrollWidth)
})
- it('accounts for the width of the vertical scrollbar when soft-wrapping lines', async () => {
- const {component, element, editor} = buildComponent({
- height: 200,
- text: 'a'.repeat(300),
- softWrapped: true
- })
- await setEditorWidthInCharacters(component, 23)
- expect(Math.floor(component.getScrollContainerClientWidth() / component.getBaseCharacterWidth())).toBe(20)
- expect(editor.lineLengthForScreenRow(0)).toBe(20)
- })
-
it('correctly forces the display layer to index visible rows when resizing (regression)', async () => {
const text = 'a'.repeat(30) + '\n' + 'b'.repeat(1000)
const {component, element, editor} = buildComponent({height: 300, width: 800, attach: false, text})
@@ -707,7 +655,7 @@ describe('TextEditorComponent', () => {
editor.setText('a')
await component.getNextUpdatePromise()
- expect(element.querySelector('.line').offsetWidth).toBe(scrollContainer.offsetWidth)
+ expect(element.querySelector('.line').offsetWidth).toBe(scrollContainer.offsetWidth - verticalScrollbarWidth)
})
it('resizes based on the content when the autoHeight and/or autoWidth options are true', async () => {
@@ -717,44 +665,39 @@ describe('TextEditorComponent', () => {
const {gutterContainer, scrollContainer} = component.refs
const initialWidth = element.offsetWidth
const initialHeight = element.offsetHeight
- expect(initialWidth).toBe(component.getGutterContainerWidth() + component.getContentWidth() + 2 * editorPadding)
- expect(initialHeight).toBe(component.getContentHeight() + 2 * editorPadding)
+ expect(initialWidth).toBe(
+ component.getGutterContainerWidth() +
+ component.getContentWidth() +
+ verticalScrollbarWidth +
+ 2 * editorPadding
+ )
+ expect(initialHeight).toBe(
+ component.getContentHeight() +
+ horizontalScrollbarHeight +
+ 2 * editorPadding
+ )
// When autoWidth is enabled, width adjusts to content
editor.setCursorScreenPosition([6, Infinity])
editor.insertText('x'.repeat(50))
await component.getNextUpdatePromise()
- expect(element.offsetWidth).toBe(component.getGutterContainerWidth() + component.getContentWidth() + 2 * editorPadding)
+ expect(element.offsetWidth).toBe(
+ component.getGutterContainerWidth() +
+ component.getContentWidth() +
+ verticalScrollbarWidth +
+ 2 * editorPadding
+ )
expect(element.offsetWidth).toBeGreaterThan(initialWidth)
// When autoHeight is enabled, height adjusts to content
editor.insertText('\n'.repeat(5))
await component.getNextUpdatePromise()
- expect(element.offsetHeight).toBe(component.getContentHeight() + 2 * editorPadding)
- expect(element.offsetHeight).toBeGreaterThan(initialHeight)
-
- // When a horizontal scrollbar is visible, autoHeight accounts for it
- editor.update({autoWidth: false})
- await component.getNextUpdatePromise()
- element.style.width = component.getGutterContainerWidth() + component.getContentHeight() - 20 + 'px'
- await component.getNextUpdatePromise()
- expect(component.canScrollHorizontally()).toBe(true)
- expect(component.canScrollVertically()).toBe(false)
- expect(element.offsetHeight).toBe(component.getContentHeight() + component.getHorizontalScrollbarHeight() + 2 * editorPadding)
-
- // When a vertical scrollbar is visible, autoWidth accounts for it
- editor.update({autoWidth: true, autoHeight: false})
- await component.getNextUpdatePromise()
- element.style.height = component.getContentHeight() - 20
- await component.getNextUpdatePromise()
- expect(component.canScrollHorizontally()).toBe(false)
- expect(component.canScrollVertically()).toBe(true)
- expect(element.offsetWidth).toBe(
- component.getGutterContainerWidth() +
- component.getContentWidth() +
- component.getVerticalScrollbarWidth() +
+ expect(element.offsetHeight).toBe(
+ component.getContentHeight() +
+ horizontalScrollbarHeight +
2 * editorPadding
)
+ expect(element.offsetHeight).toBeGreaterThan(initialHeight)
})
it('does not render the line number gutter at all if the isLineNumberGutterVisible parameter is false', () => {
@@ -875,6 +818,18 @@ describe('TextEditorComponent', () => {
expect(element.className).toBe('editor a b')
})
+ it('does not blow away class names managed by the component when packages change the element class name', async () => {
+ assertDocumentFocused()
+ const {component, element, editor} = buildComponent({mini: true})
+ element.classList.add('a', 'b')
+ element.focus()
+ await component.getNextUpdatePromise()
+ expect(element.className).toBe('editor mini a b is-focused')
+ element.className = 'a c d';
+ await component.getNextUpdatePromise()
+ expect(element.className).toBe('a c d editor is-focused mini')
+ })
+
it('ignores resize events when the editor is hidden', async () => {
const {component, element, editor} = buildComponent({autoHeight: false})
element.style.height = 5 * component.getLineHeight() + 'px'
@@ -1040,7 +995,6 @@ describe('TextEditorComponent', () => {
it('does not render scrollbars', async () => {
const {component, element, editor} = buildComponent({mini: true, autoHeight: false})
await setEditorWidthInCharacters(component, 10)
- await setEditorHeightInLines(component, 1)
editor.setText('x'.repeat(20) + 'y'.repeat(20))
await component.getNextUpdatePromise()
@@ -1125,7 +1079,7 @@ describe('TextEditorComponent', () => {
describe('autoscroll', () => {
it('automatically scrolls vertically when the requested range is within the vertical scroll margin of the top or bottom', async () => {
- const {component, editor} = buildComponent({height: 120})
+ const {component, editor} = buildComponent({height: 120 + horizontalScrollbarHeight})
expect(component.getLastVisibleRow()).toBe(7)
editor.scrollToScreenRange([[4, 0], [6, 0]])
@@ -1147,7 +1101,7 @@ describe('TextEditorComponent', () => {
it('does not vertically autoscroll by more than half of the visible lines if the editor is shorter than twice the scroll margin', async () => {
const {component, element, editor} = buildComponent({autoHeight: false})
- element.style.height = 5.5 * component.measurements.lineHeight + 'px'
+ element.style.height = 5.5 * component.measurements.lineHeight + horizontalScrollbarHeight + 'px'
await component.getNextUpdatePromise()
expect(component.getLastVisibleRow()).toBe(5)
const scrollMarginInLines = 2
@@ -1177,8 +1131,8 @@ describe('TextEditorComponent', () => {
await component.getNextUpdatePromise()
const actualScrollCenter = (component.getScrollTop() + component.getScrollBottom()) / 2
- const expectedScrollCenter = Math.round((4 + 7) / 2 * component.getLineHeight())
- expect(actualScrollCenter).toBe(expectedScrollCenter)
+ const expectedScrollCenter = (4 + 7) / 2 * component.getLineHeight()
+ expect(actualScrollCenter).toBeCloseTo(expectedScrollCenter, 0)
})
it('automatically scrolls horizontally when the requested range is within the horizontal scroll margin of the right edge of the gutter or right edge of the scroll container', async () => {
@@ -1191,29 +1145,27 @@ describe('TextEditorComponent', () => {
editor.scrollToScreenRange([[1, 12], [2, 28]])
await component.getNextUpdatePromise()
- let expectedScrollLeft = Math.round(
+ let expectedScrollLeft =
clientLeftForCharacter(component, 1, 12) -
lineNodeForScreenRow(component, 1).getBoundingClientRect().left -
(editor.horizontalScrollMargin * component.measurements.baseCharacterWidth)
- )
- expect(component.getScrollLeft()).toBe(expectedScrollLeft)
+ expect(component.getScrollLeft()).toBeCloseTo(expectedScrollLeft, 0)
editor.scrollToScreenRange([[1, 12], [2, 28]], {reversed: false})
await component.getNextUpdatePromise()
- expectedScrollLeft = Math.round(
+ expectedScrollLeft =
component.getGutterContainerWidth() +
clientLeftForCharacter(component, 2, 28) -
lineNodeForScreenRow(component, 2).getBoundingClientRect().left +
(editor.horizontalScrollMargin * component.measurements.baseCharacterWidth) -
component.getScrollContainerClientWidth()
- )
- expect(component.getScrollLeft()).toBe(expectedScrollLeft)
+ expect(component.getScrollLeft()).toBeCloseTo(expectedScrollLeft, 0)
})
it('does not horizontally autoscroll by more than half of the visible "base-width" characters if the editor is narrower than twice the scroll margin', async () => {
const {component, editor} = buildComponent({autoHeight: false})
await setEditorWidthInCharacters(component, 1.5 * editor.horizontalScrollMargin)
- const editorWidthInChars = component.getScrollContainerWidth() / component.getBaseCharacterWidth()
+ const editorWidthInChars = component.getScrollContainerClientWidth() / component.getBaseCharacterWidth()
expect(Math.round(editorWidthInChars)).toBe(9)
editor.scrollToScreenRange([[6, 10], [6, 15]])
@@ -1313,22 +1265,22 @@ describe('TextEditorComponent', () => {
// Assigns the scrollTop based on the logical position when attached
jasmine.attachToDOM(element)
- expect(component.getScrollLeft()).toBe(Math.round(2 * component.getBaseCharacterWidth()))
+ expect(component.getScrollLeft()).toBeCloseTo(2 * component.getBaseCharacterWidth(), 0)
// Allows the scrollTopRow to be updated while attached
component.setScrollLeftColumn(4)
- expect(component.getScrollLeft()).toBe(Math.round(4 * component.getBaseCharacterWidth()))
+ expect(component.getScrollLeft()).toBeCloseTo(4 * component.getBaseCharacterWidth(), 0)
// Preserves the scrollTopRow when detached
element.remove()
- expect(component.getScrollLeft()).toBe(Math.round(4 * component.getBaseCharacterWidth()))
+ expect(component.getScrollLeft()).toBeCloseTo(4 * component.getBaseCharacterWidth(), 0)
component.setScrollLeftColumn(6)
- expect(component.getScrollLeft()).toBe(Math.round(6 * component.getBaseCharacterWidth()))
+ expect(component.getScrollLeft()).toBeCloseTo(6 * component.getBaseCharacterWidth(), 0)
jasmine.attachToDOM(element)
element.style.width = '60px'
- expect(component.getScrollLeft()).toBe(Math.round(6 * component.getBaseCharacterWidth()))
+ expect(component.getScrollLeft()).toBeCloseTo(6 * component.getBaseCharacterWidth(), 0)
})
})
@@ -2102,6 +2054,37 @@ describe('TextEditorComponent', () => {
expect(decorationNode2.firstChild).toBeNull()
expect(gutterB.getElement().firstChild.children.length).toBe(0)
})
+
+ it('renders custom line number gutters', async () => {
+ const {component, editor} = buildComponent()
+ const gutterA = editor.addGutter({
+ name: 'a',
+ priority: 1,
+ type: 'line-number',
+ class: 'a-number',
+ labelFn: ({bufferRow}) => `a - ${bufferRow}`
+ })
+ const gutterB = editor.addGutter({
+ name: 'b',
+ priority: 1,
+ type: 'line-number',
+ class: 'b-number',
+ labelFn: ({bufferRow}) => `b - ${bufferRow}`
+ })
+ editor.setText('0000\n0001\n0002\n0003\n0004\n')
+
+ await component.getNextUpdatePromise()
+
+ const gutterAElement = gutterA.getElement()
+ const aNumbers = gutterAElement.querySelectorAll('div.line-number[data-buffer-row]')
+ const aLabels = Array.from(aNumbers, e => e.textContent)
+ expect(aLabels).toEqual(['a - 0', 'a - 1', 'a - 2', 'a - 3', 'a - 4', 'a - 5'])
+
+ const gutterBElement = gutterB.getElement()
+ const bNumbers = gutterBElement.querySelectorAll('div.line-number[data-buffer-row]')
+ const bLabels = Array.from(bNumbers, e => e.textContent)
+ expect(bLabels).toEqual(['b - 0', 'b - 1', 'b - 2', 'b - 3', 'b - 4', 'b - 5'])
+ })
})
describe('block decorations', () => {
@@ -2112,7 +2095,8 @@ describe('TextEditorComponent', () => {
// render an editor that already contains some block decorations
const {component, element} = buildComponent({editor, rowsPerTile: 3})
- await setEditorHeightInLines(component, 4)
+ element.style.height = 4 * component.getLineHeight() + horizontalScrollbarHeight + 'px'
+ await component.getNextUpdatePromise()
expect(component.getRenderedStartRow()).toBe(0)
expect(component.getRenderedEndRow()).toBe(9)
expect(component.getScrollHeight()).toBe(
@@ -2327,7 +2311,7 @@ describe('TextEditorComponent', () => {
component.element.style.width = (
component.getGutterContainerWidth() +
component.getScrollContainerClientWidth() * 2 +
- component.getVerticalScrollbarWidth()
+ verticalScrollbarWidth
) + 'px'
await component.getNextUpdatePromise()
expect(component.getRenderedStartRow()).toBe(0)
@@ -3557,12 +3541,12 @@ describe('TextEditorComponent', () => {
describe('on the scrollbars', () => {
it('delegates the mousedown events to the parent component unless the mousedown was on the actual scrollbar', async () => {
const {component, element, editor} = buildComponent({height: 100})
- await setEditorWidthInCharacters(component, 8.5)
+ await setEditorWidthInCharacters(component, 6)
const verticalScrollbar = component.refs.verticalScrollbar
const horizontalScrollbar = component.refs.horizontalScrollbar
- const leftEdgeOfVerticalScrollbar = verticalScrollbar.element.getBoundingClientRect().right - getVerticalScrollbarWidth(component)
- const topEdgeOfHorizontalScrollbar = horizontalScrollbar.element.getBoundingClientRect().bottom - getHorizontalScrollbarHeight(component)
+ const leftEdgeOfVerticalScrollbar = verticalScrollbar.element.getBoundingClientRect().right - verticalScrollbarWidth
+ const topEdgeOfHorizontalScrollbar = horizontalScrollbar.element.getBoundingClientRect().bottom - horizontalScrollbarHeight
verticalScrollbar.didMouseDown({
button: 0,
@@ -4124,7 +4108,7 @@ describe('TextEditorComponent', () => {
it('assigns scrollTop on the component when calling setFirstVisibleScreenRow', async () => {
const {component, element, editor} = buildComponent({rowsPerTile: 3, autoHeight: false})
- element.style.height = 4 * component.measurements.lineHeight + 'px'
+ element.style.height = 4 * component.measurements.lineHeight + horizontalScrollbarHeight + 'px'
await component.getNextUpdatePromise()
expect(component.getMaxScrollTop() / component.getLineHeight()).toBe(9)
@@ -4151,17 +4135,17 @@ describe('TextEditorComponent', () => {
element.style.width = 30 * component.getBaseCharacterWidth() + 'px'
await component.getNextUpdatePromise()
expect(editor.getFirstVisibleScreenColumn()).toBe(0)
- expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(0 * component.getBaseCharacterWidth())
+ expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(0)
setScrollLeft(component, 5.5 * component.getBaseCharacterWidth())
expect(editor.getFirstVisibleScreenColumn()).toBe(5)
await component.getNextUpdatePromise()
- expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(Math.round(5.5 * component.getBaseCharacterWidth()))
+ expect(component.refs.horizontalScrollbar.element.scrollLeft).toBeCloseTo(5.5 * component.getBaseCharacterWidth(), -1)
editor.setFirstVisibleScreenColumn(12)
- expect(component.getScrollLeft()).toBe(Math.round(12 * component.getBaseCharacterWidth()))
+ expect(component.getScrollLeft()).toBeCloseTo(12 * component.getBaseCharacterWidth(), -1)
await component.getNextUpdatePromise()
- expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(Math.round(12 * component.getBaseCharacterWidth()))
+ expect(component.refs.horizontalScrollbar.element.scrollLeft).toBeCloseTo(12 * component.getBaseCharacterWidth(), -1)
})
})
@@ -4308,6 +4292,7 @@ async function setEditorWidthInCharacters (component, widthInCharacters) {
component.element.style.width =
component.getGutterContainerWidth() +
widthInCharacters * component.measurements.baseCharacterWidth +
+ verticalScrollbarWidth +
'px'
await component.getNextUpdatePromise()
}
diff --git a/spec/text-editor-element-spec.js b/spec/text-editor-element-spec.js
index 7ffdf374d..d6c33e7ad 100644
--- a/spec/text-editor-element-spec.js
+++ b/spec/text-editor-element-spec.js
@@ -9,7 +9,7 @@ describe('TextEditorElement', () => {
jasmineContent = document.body.querySelector('#jasmine-content')
// Force scrollbars to be visible regardless of local system configuration
const scrollbarStyle = document.createElement('style')
- scrollbarStyle.textContent = '::-webkit-scrollbar { -webkit-appearance: none }'
+ scrollbarStyle.textContent = 'atom-text-editor ::-webkit-scrollbar { -webkit-appearance: none }'
jasmine.attachToDOM(scrollbarStyle)
})
@@ -338,18 +338,20 @@ describe('TextEditorElement', () => {
element.style.width = '200px'
jasmine.attachToDOM(element)
+ const horizontalScrollbarHeight = element.component.getHorizontalScrollbarHeight()
+
expect(element.getMaxScrollTop()).toBe(0)
await editor.update({autoHeight: false})
- element.style.height = '100px'
+ element.style.height = 100 + horizontalScrollbarHeight + 'px'
await element.getNextUpdatePromise()
expect(element.getMaxScrollTop()).toBe(60)
- element.style.height = '120px'
+ element.style.height = 120 + horizontalScrollbarHeight + 'px'
await element.getNextUpdatePromise()
expect(element.getMaxScrollTop()).toBe(40)
- element.style.height = '200px'
+ element.style.height = 200 + horizontalScrollbarHeight + 'px'
await element.getNextUpdatePromise()
expect(element.getMaxScrollTop()).toBe(0)
})
@@ -392,10 +394,13 @@ describe('TextEditorElement', () => {
it('returns true if the given row range intersects the visible row range', async () => {
const element = buildTextEditorElement()
const editor = element.getModel()
+ const horizontalScrollbarHeight = element.component.getHorizontalScrollbarHeight()
+
editor.update({autoHeight: false})
element.getModel().setText('x\n'.repeat(20))
- element.style.height = '120px'
+ element.style.height = 120 + horizontalScrollbarHeight + 'px'
await element.getNextUpdatePromise()
+
element.setScrollTop(80)
await element.getNextUpdatePromise()
expect(element.getVisibleRowRange()).toEqual([4, 11])
@@ -412,9 +417,11 @@ describe('TextEditorElement', () => {
it('returns a {top/left/width/height} object describing the rectangle between two screen positions, even if they are not on screen', async () => {
const element = buildTextEditorElement()
const editor = element.getModel()
+ const horizontalScrollbarHeight = element.component.getHorizontalScrollbarHeight()
+
editor.update({autoHeight: false})
element.getModel().setText('xxxxxxxxxxxxxxxxxxxxxx\n'.repeat(20))
- element.style.height = '120px'
+ element.style.height = 120 + horizontalScrollbarHeight + 'px'
await element.getNextUpdatePromise()
element.setScrollTop(80)
await element.getNextUpdatePromise()
diff --git a/spec/text-editor-registry-spec.js b/spec/text-editor-registry-spec.js
index 4f4d1ee93..4c6d680eb 100644
--- a/spec/text-editor-registry-spec.js
+++ b/spec/text-editor-registry-spec.js
@@ -1,6 +1,7 @@
const TextEditorRegistry = require('../src/text-editor-registry')
const TextEditor = require('../src/text-editor')
const TextBuffer = require('text-buffer')
+const {Point, Range} = TextBuffer
const {it, fit, ffit, fffit} = require('./async-spec-helpers')
const dedent = require('dedent')
@@ -257,19 +258,19 @@ describe('TextEditorRegistry', function () {
describe('when the "tabType" config setting is "auto"', function () {
it('enables or disables soft tabs based on the editor\'s content', async function () {
+ await initialPackageActivation
await atom.packages.activatePackage('language-javascript')
atom.grammars.assignLanguageMode(editor, 'source.js')
atom.config.set('editor.tabType', 'auto')
-
- registry.maintainConfig(editor)
await initialPackageActivation
+ const languageMode = editor.getBuffer().getLanguageMode()
editor.setText(dedent`
{
hello;
}
`)
- editor.getBuffer().getLanguageMode().retokenizeLines()
+ let disposable = registry.maintainConfig(editor)
expect(editor.getSoftTabs()).toBe(true)
editor.setText(dedent`
@@ -277,18 +278,17 @@ describe('TextEditorRegistry', function () {
hello;
}
`)
- editor.getBuffer().getLanguageMode().retokenizeLines()
+ disposable.dispose()
+ disposable = registry.maintainConfig(editor)
expect(editor.getSoftTabs()).toBe(false)
- editor.setText(dedent`
+ editor.setTextInBufferRange(new Range(Point.ZERO, Point.ZERO), dedent`
/*
* Comment with a leading space.
*/
- {
- ${'\t'}hello;
- }
- ` + editor.getText())
- editor.getBuffer().getLanguageMode().retokenizeLines()
+ ` + '\n')
+ disposable.dispose()
+ disposable = registry.maintainConfig(editor)
expect(editor.getSoftTabs()).toBe(false)
editor.setText(dedent`
@@ -300,8 +300,8 @@ describe('TextEditorRegistry', function () {
hello;
}
`)
-
- editor.getBuffer().getLanguageMode().retokenizeLines()
+ disposable.dispose()
+ disposable = registry.maintainConfig(editor)
expect(editor.getSoftTabs()).toBe(false)
editor.setText(dedent`
@@ -313,7 +313,8 @@ describe('TextEditorRegistry', function () {
hello;
}
`)
- editor.getBuffer().getLanguageMode().retokenizeLines()
+ disposable.dispose()
+ disposable = registry.maintainConfig(editor)
expect(editor.getSoftTabs()).toBe(true)
})
})
diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js
index 69be6be32..9041d3528 100644
--- a/spec/text-editor-spec.js
+++ b/spec/text-editor-spec.js
@@ -4,7 +4,7 @@ const fs = require('fs')
const path = require('path')
const temp = require('temp').track()
const dedent = require('dedent')
-const clipboard = require('../src/safe-clipboard')
+const {clipboard} = require('electron')
const TextEditor = require('../src/text-editor')
const TextBuffer = require('text-buffer')
const TextMateLanguageMode = require('../src/text-mate-language-mode')
@@ -861,6 +861,15 @@ describe('TextEditor', () => {
})
})
})
+
+ it("clears the goal column", () => {
+ editor.setText('first\n\nthird')
+ editor.setCursorScreenPosition([0, 3])
+ editor.moveDown()
+ editor.moveToFirstCharacterOfLine()
+ editor.moveDown()
+ expect(editor.getCursorBufferPosition()).toEqual([2, 0])
+ })
})
describe('.moveToBeginningOfWord()', () => {
@@ -5193,6 +5202,111 @@ describe('TextEditor', () => {
})
})
+ describe('undo/redo restore selections of editor which initiated original change', () => {
+ let editor1, editor2
+
+ beforeEach(async () => {
+ editor1 = editor
+ editor2 = new TextEditor({buffer: editor1.buffer})
+
+ editor1.setText(dedent `
+ aaaaaa
+ bbbbbb
+ cccccc
+ dddddd
+ eeeeee
+ `)
+ })
+
+ it('[editor.transact] restore selection of change-initiated-editor', () => {
+ editor1.setCursorBufferPosition([0, 0]); editor1.transact(() => editor1.insertText('1'))
+ editor2.setCursorBufferPosition([1, 0]); editor2.transact(() => editor2.insertText('2'))
+ editor1.setCursorBufferPosition([2, 0]); editor1.transact(() => editor1.insertText('3'))
+ editor2.setCursorBufferPosition([3, 0]); editor2.transact(() => editor2.insertText('4'))
+
+ expect(editor1.getText()).toBe(dedent `
+ 1aaaaaa
+ 2bbbbbb
+ 3cccccc
+ 4dddddd
+ eeeeee
+ `)
+
+ editor2.setCursorBufferPosition([4, 0])
+ editor1.undo(); expect(editor1.getCursorBufferPosition()).toEqual([3, 0])
+ editor1.undo(); expect(editor1.getCursorBufferPosition()).toEqual([2, 0])
+ editor1.undo(); expect(editor1.getCursorBufferPosition()).toEqual([1, 0])
+ editor1.undo(); expect(editor1.getCursorBufferPosition()).toEqual([0, 0])
+ expect(editor2.getCursorBufferPosition()).toEqual([4, 0]) // remain unchanged
+
+ editor1.redo(); expect(editor1.getCursorBufferPosition()).toEqual([0, 1])
+ editor1.redo(); expect(editor1.getCursorBufferPosition()).toEqual([1, 1])
+ editor1.redo(); expect(editor1.getCursorBufferPosition()).toEqual([2, 1])
+ editor1.redo(); expect(editor1.getCursorBufferPosition()).toEqual([3, 1])
+ expect(editor2.getCursorBufferPosition()).toEqual([4, 0]) // remain unchanged
+
+ editor1.setCursorBufferPosition([4, 0])
+ editor2.undo(); expect(editor2.getCursorBufferPosition()).toEqual([3, 0])
+ editor2.undo(); expect(editor2.getCursorBufferPosition()).toEqual([2, 0])
+ editor2.undo(); expect(editor2.getCursorBufferPosition()).toEqual([1, 0])
+ editor2.undo(); expect(editor2.getCursorBufferPosition()).toEqual([0, 0])
+ expect(editor1.getCursorBufferPosition()).toEqual([4, 0]) // remain unchanged
+
+ editor2.redo(); expect(editor2.getCursorBufferPosition()).toEqual([0, 1])
+ editor2.redo(); expect(editor2.getCursorBufferPosition()).toEqual([1, 1])
+ editor2.redo(); expect(editor2.getCursorBufferPosition()).toEqual([2, 1])
+ editor2.redo(); expect(editor2.getCursorBufferPosition()).toEqual([3, 1])
+ expect(editor1.getCursorBufferPosition()).toEqual([4, 0]) // remain unchanged
+ })
+
+ it('[manually group checkpoint] restore selection of change-initiated-editor', () => {
+ const transact = (editor, fn) => {
+ const checkpoint = editor.createCheckpoint()
+ fn()
+ editor.groupChangesSinceCheckpoint(checkpoint)
+ }
+
+ editor1.setCursorBufferPosition([0, 0]); transact(editor1, () => editor1.insertText('1'))
+ editor2.setCursorBufferPosition([1, 0]); transact(editor2, () => editor2.insertText('2'))
+ editor1.setCursorBufferPosition([2, 0]); transact(editor1, () => editor1.insertText('3'))
+ editor2.setCursorBufferPosition([3, 0]); transact(editor2, () => editor2.insertText('4'))
+
+ expect(editor1.getText()).toBe(dedent `
+ 1aaaaaa
+ 2bbbbbb
+ 3cccccc
+ 4dddddd
+ eeeeee
+ `)
+
+ editor2.setCursorBufferPosition([4, 0])
+ editor1.undo(); expect(editor1.getCursorBufferPosition()).toEqual([3, 0])
+ editor1.undo(); expect(editor1.getCursorBufferPosition()).toEqual([2, 0])
+ editor1.undo(); expect(editor1.getCursorBufferPosition()).toEqual([1, 0])
+ editor1.undo(); expect(editor1.getCursorBufferPosition()).toEqual([0, 0])
+ expect(editor2.getCursorBufferPosition()).toEqual([4, 0]) // remain unchanged
+
+ editor1.redo(); expect(editor1.getCursorBufferPosition()).toEqual([0, 1])
+ editor1.redo(); expect(editor1.getCursorBufferPosition()).toEqual([1, 1])
+ editor1.redo(); expect(editor1.getCursorBufferPosition()).toEqual([2, 1])
+ editor1.redo(); expect(editor1.getCursorBufferPosition()).toEqual([3, 1])
+ expect(editor2.getCursorBufferPosition()).toEqual([4, 0]) // remain unchanged
+
+ editor1.setCursorBufferPosition([4, 0])
+ editor2.undo(); expect(editor2.getCursorBufferPosition()).toEqual([3, 0])
+ editor2.undo(); expect(editor2.getCursorBufferPosition()).toEqual([2, 0])
+ editor2.undo(); expect(editor2.getCursorBufferPosition()).toEqual([1, 0])
+ editor2.undo(); expect(editor2.getCursorBufferPosition()).toEqual([0, 0])
+ expect(editor1.getCursorBufferPosition()).toEqual([4, 0]) // remain unchanged
+
+ editor2.redo(); expect(editor2.getCursorBufferPosition()).toEqual([0, 1])
+ editor2.redo(); expect(editor2.getCursorBufferPosition()).toEqual([1, 1])
+ editor2.redo(); expect(editor2.getCursorBufferPosition()).toEqual([2, 1])
+ editor2.redo(); expect(editor2.getCursorBufferPosition()).toEqual([3, 1])
+ expect(editor1.getCursorBufferPosition()).toEqual([4, 0]) // remain unchanged
+ })
+ })
+
describe('when the buffer is changed (via its direct api, rather than via than edit session)', () => {
it('moves the cursor so it is in the same relative position of the buffer', () => {
expect(editor.getCursorScreenPosition()).toEqual([0, 0])
@@ -5383,6 +5497,195 @@ describe('TextEditor', () => {
})
})
})
+
+ describe('when readonly', () => {
+ beforeEach(() => {
+ editor.setReadOnly(true)
+ })
+
+ const modifications = [
+ {
+ name: 'moveLineUp',
+ op: (opts = {}) => {
+ editor.setCursorBufferPosition([1, 0])
+ editor.moveLineUp(opts)
+ }
+ },
+ {
+ name: 'moveLineDown',
+ op: (opts = {}) => {
+ editor.setCursorBufferPosition([0, 0])
+ editor.moveLineDown(opts)
+ }
+ },
+ {
+ name: 'insertText',
+ op: (opts = {}) => {
+ editor.setSelectedBufferRange([[1, 0], [1, 2]])
+ editor.insertText('xxx', opts)
+ }
+ },
+ {
+ name: 'insertNewline',
+ op: (opts = {}) => {
+ editor.setCursorScreenPosition({row: 1, column: 0})
+ editor.insertNewline(opts)
+ }
+ },
+ {
+ name: 'insertNewlineBelow',
+ op: (opts = {}) => {
+ editor.setCursorBufferPosition([0, 2])
+ editor.insertNewlineBelow(opts)
+ }
+ },
+ {
+ name: 'insertNewlineAbove',
+ op: (opts = {}) => {
+ editor.setCursorBufferPosition([0])
+ editor.insertNewlineAbove(opts)
+ }
+ },
+ {
+ name: 'backspace',
+ op: (opts = {}) => {
+ editor.setCursorScreenPosition({row: 1, column: 7})
+ editor.backspace(opts)
+ }
+ },
+ {
+ name: 'deleteToPreviousWordBoundary',
+ op: (opts = {}) => {
+ editor.setCursorBufferPosition([0, 16])
+ editor.deleteToPreviousWordBoundary(opts)
+ }
+ },
+ {
+ name: 'deleteToNextWordBoundary',
+ op: (opts = {}) => {
+ editor.setCursorBufferPosition([0, 15])
+ editor.deleteToNextWordBoundary(opts)
+ }
+ },
+ {
+ name: 'deleteToBeginningOfWord',
+ op: (opts = {}) => {
+ editor.setCursorBufferPosition([1, 24])
+ editor.deleteToBeginningOfWord(opts)
+ }
+ },
+ {
+ name: 'deleteToEndOfLine',
+ op: (opts = {}) => {
+ editor.setCursorBufferPosition([1, 24])
+ editor.deleteToEndOfLine(opts)
+ }
+ },
+ {
+ name: 'deleteToBeginningOfLine',
+ op: (opts = {}) => {
+ editor.setCursorBufferPosition([1, 24])
+ editor.deleteToBeginningOfLine(opts)
+ }
+ },
+ {
+ name: 'delete',
+ op: (opts = {}) => {
+ editor.setCursorScreenPosition([1, 6])
+ editor.delete(opts)
+ }
+ },
+ {
+ name: 'deleteToEndOfWord',
+ op: (opts = {}) => {
+ editor.setCursorBufferPosition([1, 24])
+ editor.deleteToEndOfWord(opts)
+ }
+ },
+ {
+ name: 'indent',
+ op: (opts = {}) => {
+ editor.indent(opts)
+ }
+ },
+ {
+ name: 'cutSelectedText',
+ op: (opts = {}) => {
+ editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]])
+ editor.cutSelectedText(opts)
+ }
+ },
+ {
+ name: 'cutToEndOfLine',
+ op: (opts = {}) => {
+ editor.setCursorBufferPosition([2, 20])
+ editor.cutToEndOfLine(opts)
+ }
+ },
+ {
+ name: 'cutToEndOfBufferLine',
+ op: (opts = {}) => {
+ editor.setCursorBufferPosition([2, 20])
+ editor.cutToEndOfBufferLine(opts)
+ }
+ },
+ {
+ name: 'pasteText',
+ op: (opts = {}) => {
+ editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]])
+ atom.clipboard.write('first')
+ editor.pasteText(opts)
+ }
+ },
+ {
+ name: 'indentSelectedRows',
+ op: (opts = {}) => {
+ editor.setSelectedBufferRange([[0, 3], [0, 3]])
+ editor.indentSelectedRows(opts)
+ }
+ },
+ {
+ name: 'outdentSelectedRows',
+ op: (opts = {}) => {
+ editor.setSelectedBufferRange([[1, 3], [1, 3]])
+ editor.outdentSelectedRows(opts)
+ }
+ },
+ {
+ name: 'autoIndentSelectedRows',
+ op: (opts = {}) => {
+ editor.setCursorBufferPosition([2, 0])
+ editor.insertText('function() {\ninside=true\n}\n i=1\n', opts)
+ editor.getLastSelection().setBufferRange([[2, 0], [6, 0]])
+ editor.autoIndentSelectedRows(opts)
+ }
+ },
+ {
+ name: 'undo/redo',
+ op: (opts = {}) => {
+ editor.insertText('foo', opts)
+ editor.undo(opts)
+ editor.redo(opts)
+ }
+ }
+ ]
+
+ describe('without bypassReadOnly', () => {
+ for (const {name, op} of modifications) {
+ it(`throws an error on ${name}`, () => {
+ expect(op).toThrow()
+ })
+ }
+ })
+
+ describe('with bypassReadOnly', () => {
+ for (const {name, op} of modifications) {
+ it(`permits ${name}`, () => {
+ op({bypassReadOnly: true})
+ })
+ }
+ })
+ })
})
describe('reading text', () => {
@@ -6413,6 +6716,20 @@ describe('TextEditor', () => {
const gutter = editor.addGutter(options)
expect(editor.getGutters().length).toBe(2)
expect(editor.getGutters()[1]).toBe(gutter)
+ expect(gutter.type).toBe('decorated')
+ })
+
+ it('can add a custom line-number gutter', () => {
+ expect(editor.getGutters().length).toBe(1)
+ const options = {
+ name: 'another-gutter',
+ priority: 2,
+ type: 'line-number'
+ }
+ const gutter = editor.addGutter(options)
+ expect(editor.getGutters().length).toBe(2)
+ expect(editor.getGutters()[1]).toBe(gutter)
+ expect(gutter.type).toBe('line-number')
})
it("does not allow a custom gutter with the 'line-number' name.", () => expect(editor.addGutter.bind(editor, {name: 'line-number'})).toThrow())
diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js
index 3a6b56a1b..0cf3c3e64 100644
--- a/spec/tooltip-manager-spec.js
+++ b/spec/tooltip-manager-spec.js
@@ -213,6 +213,18 @@ describe('TooltipManager', () => {
})
)
+ describe('when a user types', () =>
+ it('hides the tooltips', () => {
+ const disposable = manager.add(element, { title: 'Title' })
+ hover(element, function () {
+ expect(document.body.querySelector('.tooltip')).not.toBeNull()
+ window.dispatchEvent(new CustomEvent('keydown'))
+ expect(document.body.querySelector('.tooltip')).toBeNull()
+ disposable.dispose()
+ })
+ })
+ )
+
describe('findTooltips', () => {
it('adds and remove tooltips correctly', () => {
expect(manager.findTooltips(element).length).toBe(0)
diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js
index a788fac47..6bcd23ead 100644
--- a/spec/tree-sitter-language-mode-spec.js
+++ b/spec/tree-sitter-language-mode-spec.js
@@ -10,6 +10,9 @@ 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')
+const htmlGrammarPath = require.resolve('language-html/grammars/tree-sitter-html.cson')
+const ejsGrammarPath = require.resolve('language-html/grammars/tree-sitter-ejs.cson')
+const rubyGrammarPath = require.resolve('language-ruby/grammars/tree-sitter-ruby.cson')
describe('TreeSitterLanguageMode', () => {
let editor, buffer
@@ -17,10 +20,11 @@ describe('TreeSitterLanguageMode', () => {
beforeEach(async () => {
editor = await atom.workspace.open('')
buffer = editor.getBuffer()
+ editor.displayLayer.reset({foldCharacter: '…'})
})
describe('highlighting', () => {
- it('applies the most specific scope mapping to each node in the syntax tree', () => {
+ it('applies the most specific scope mapping to each node in the syntax tree', async () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
scopes: {
@@ -31,8 +35,11 @@ describe('TreeSitterLanguageMode', () => {
}
})
- buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText('aa.bbb = cc(d.eee());')
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+
expectTokensToEqual(editor, [[
{text: 'aa.', scopes: ['source']},
{text: 'bbb', scopes: ['source', 'property']},
@@ -44,7 +51,7 @@ describe('TreeSitterLanguageMode', () => {
]])
})
- it('can start or end multiple scopes at the same position', () => {
+ it('can start or end multiple scopes at the same position', async () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
scopes: {
@@ -57,8 +64,11 @@ describe('TreeSitterLanguageMode', () => {
}
})
- buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText('a = bb.ccc();')
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+
expectTokensToEqual(editor, [[
{text: 'a', scopes: ['source', 'variable']},
{text: ' = ', scopes: ['source']},
@@ -70,7 +80,7 @@ describe('TreeSitterLanguageMode', () => {
]])
})
- it('can resume highlighting on a line that starts with whitespace', () => {
+ it('can resume highlighting on a line that starts with whitespace', async () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
scopes: {
@@ -80,14 +90,17 @@ describe('TreeSitterLanguageMode', () => {
}
})
- buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText('a\n .b();')
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+
expectTokensToEqual(editor, [
[
{text: 'a', scopes: ['variable']},
],
[
- {text: ' ', scopes: ['whitespace']},
+ {text: ' ', scopes: ['leading-whitespace']},
{text: '.', scopes: []},
{text: 'b', scopes: ['function']},
{text: '();', scopes: []}
@@ -95,8 +108,8 @@ describe('TreeSitterLanguageMode', () => {
])
})
- it('correctly skips over tokens with zero size', () => {
- const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ it('correctly skips over tokens with zero size', async () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, cGrammarPath, {
parser: 'tree-sitter-c',
scopes: {
'primitive_type': 'type',
@@ -104,13 +117,13 @@ describe('TreeSitterLanguageMode', () => {
}
})
- const languageMode = new TreeSitterLanguageMode({buffer, grammar})
- buffer.setLanguageMode(languageMode)
buffer.setText('int main() {\n int a\n int b;\n}');
- editor.screenLineForScreenRow(0)
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+
expect(
- languageMode.document.rootNode.descendantForPosition(Point(1, 2), Point(1, 6)).toString()
+ languageMode.tree.rootNode.descendantForPosition(Point(1, 2), Point(1, 6)).toString()
).toBe('(declaration (primitive_type) (identifier) (MISSING))')
expectTokensToEqual(editor, [
@@ -121,13 +134,13 @@ describe('TreeSitterLanguageMode', () => {
{text: '() {', scopes: []}
],
[
- {text: ' ', scopes: ['whitespace']},
+ {text: ' ', scopes: ['leading-whitespace']},
{text: 'int', scopes: ['type']},
{text: ' ', scopes: []},
{text: 'a', scopes: ['variable']}
],
[
- {text: ' ', scopes: ['whitespace']},
+ {text: ' ', scopes: ['leading-whitespace']},
{text: 'int', scopes: ['type']},
{text: ' ', scopes: []},
{text: 'b', scopes: ['variable']},
@@ -139,7 +152,7 @@ describe('TreeSitterLanguageMode', () => {
])
})
- it('updates lines\' highlighting when they are affected by distant changes', () => {
+ it('updates lines\' highlighting when they are affected by distant changes', async () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
scopes: {
@@ -148,10 +161,12 @@ describe('TreeSitterLanguageMode', () => {
}
})
- buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
+ buffer.setText('a(\nb,\nc\n')
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
// missing closing paren
- buffer.setText('a(\nb,\nc\n')
expectTokensToEqual(editor, [
[{text: 'a(', scopes: []}],
[{text: 'b,', scopes: []}],
@@ -171,7 +186,36 @@ describe('TreeSitterLanguageMode', () => {
])
})
- it('handles edits after tokens that end between CR and LF characters (regression)', () => {
+ it('allows comma-separated selectors as scope mapping keys', async () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ parser: 'tree-sitter-javascript',
+ scopes: {
+ 'identifier, call_expression > identifier': [
+ {match: '^[A-Z]', scopes: 'constructor'}
+ ],
+
+ 'call_expression > identifier': 'function'
+ }
+ })
+
+ buffer.setText(`a(B(new C))`)
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+
+ expectTokensToEqual(editor, [
+ [
+ {text: 'a', scopes: ['function']},
+ {text: '(', scopes: []},
+ {text: 'B', scopes: ['constructor']},
+ {text: '(new ', scopes: []},
+ {text: 'C', scopes: ['constructor']},
+ {text: '))', scopes: []},
+ ]
+ ])
+ })
+
+ it('handles edits after tokens that end between CR and LF characters (regression)', async () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
scopes: {
@@ -181,14 +225,15 @@ describe('TreeSitterLanguageMode', () => {
}
})
- buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
-
buffer.setText([
'// abc',
'',
'a("b").c'
].join('\r\n'))
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+
expectTokensToEqual(editor, [
[{text: '// abc', scopes: ['comment']}],
[{text: '', scopes: []}],
@@ -205,7 +250,7 @@ describe('TreeSitterLanguageMode', () => {
[{text: '// abc', scopes: ['comment']}],
[{text: '', scopes: []}],
[
- {text: ' ', scopes: ['whitespace']},
+ {text: ' ', scopes: ['leading-whitespace']},
{text: 'a(', scopes: []},
{text: '"b"', scopes: ['string']},
{text: ').', scopes: []},
@@ -213,14 +258,539 @@ describe('TreeSitterLanguageMode', () => {
]
])
})
+
+ it('handles multi-line nodes with children on different lines (regression)', async () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ parser: 'tree-sitter-javascript',
+ scopes: {
+ 'template_string': 'string',
+ '"${"': 'interpolation',
+ '"}"': 'interpolation'
+ }
+ });
+
+ buffer.setText('`\na${1}\nb${2}\n`;')
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+
+ expectTokensToEqual(editor, [
+ [
+ {text: '`', scopes: ['string']}
+ ], [
+ {text: 'a', scopes: ['string']},
+ {text: '${', scopes: ['string', 'interpolation']},
+ {text: '1', scopes: ['string']},
+ {text: '}', scopes: ['string', 'interpolation']}
+ ], [
+ {text: 'b', scopes: ['string']},
+ {text: '${', scopes: ['string', 'interpolation']},
+ {text: '2', scopes: ['string']},
+ {text: '}', scopes: ['string', 'interpolation']}
+ ],
+ [
+ {text: '`', scopes: ['string']},
+ {text: ';', scopes: []}
+ ]
+ ])
+ })
+
+ it('handles folds inside of highlighted tokens', async () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ parser: 'tree-sitter-javascript',
+ scopes: {
+ 'comment': 'comment',
+ 'call_expression > identifier': 'function',
+ }
+ })
+
+ buffer.setText(dedent `
+ /*
+ * Hello
+ */
+
+ hello();
+ `)
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+
+ editor.foldBufferRange([[0, 2], [2, 0]])
+
+ expectTokensToEqual(editor, [
+ [
+ {text: '/*', scopes: ['comment']},
+ {text: '…', scopes: ['fold-marker']},
+ {text: ' */', scopes: ['comment']}
+ ],
+ [
+ {text: '', scopes: []}
+ ],
+ [
+ {text: 'hello', scopes: ['function']},
+ {text: '();', scopes: []},
+ ]
+ ])
+ })
+
+ it('applies regex match rules when specified', async () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ parser: 'tree-sitter-javascript',
+ scopes: {
+ 'identifier': [
+ {match: '^(exports|document|window|global)$', scopes: 'global'},
+ {match: '^[A-Z_]+$', scopes: 'constant'},
+ {match: '^[A-Z]', scopes: 'constructor'},
+ 'variable'
+ ],
+ }
+ })
+
+ buffer.setText(`exports.object = Class(SOME_CONSTANT, x)`)
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+
+ expectTokensToEqual(editor, [
+ [
+ {text: 'exports', scopes: ['global']},
+ {text: '.object = ', scopes: []},
+ {text: 'Class', scopes: ['constructor']},
+ {text: '(', scopes: []},
+ {text: 'SOME_CONSTANT', scopes: ['constant']},
+ {text: ', ', scopes: []},
+ {text: 'x', scopes: ['variable']},
+ {text: ')', scopes: []},
+ ]
+ ])
+ })
+
+ it('handles nodes that start before their first child and end after their last child', async () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, rubyGrammarPath, {
+ parser: 'tree-sitter-ruby',
+ scopes: {
+ 'bare_string': 'string',
+ 'interpolation': 'embedded',
+ '"#{"': 'punctuation',
+ '"}"': 'punctuation',
+ }
+ })
+
+ // The bare string node `bc#{d}ef` has one child: the interpolation, and that child
+ // starts later and ends earlier than the bare string.
+ buffer.setText('a = %W( bc#{d}ef )')
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+
+ expectTokensToEqual(editor, [
+ [
+ {text: 'a = %W( ', scopes: []},
+ {text: 'bc', scopes: ['string']},
+ {text: '#{', scopes: ['string', 'embedded', 'punctuation']},
+ {text: 'd', scopes: ['string', 'embedded']},
+ {text: '}', scopes: ['string', 'embedded', 'punctuation']},
+ {text: 'ef', scopes: ['string']},
+ {text: ' )', scopes: []},
+ ]
+ ])
+ })
+
+ describe('when the buffer changes during a parse', () => {
+ it('immediately parses again when the current parse completes', async () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ parser: 'tree-sitter-javascript',
+ scopes: {
+ 'identifier': 'variable',
+ 'call_expression > identifier': 'function',
+ 'new_expression > call_expression > identifier': 'constructor'
+ }
+ })
+
+ buffer.setText('abc;');
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar, syncOperationLimit: 0})
+ buffer.setLanguageMode(languageMode)
+ await nextHighlightingUpdate(languageMode)
+ await new Promise(process.nextTick)
+
+ expectTokensToEqual(editor, [
+ [
+ {text: 'abc', scopes: ['variable']},
+ {text: ';', scopes: []}
+ ],
+ ])
+
+ buffer.setTextInRange([[0, 3], [0, 3]], '()');
+ expectTokensToEqual(editor, [
+ [
+ {text: 'abc()', scopes: ['variable']},
+ {text: ';', scopes: []}
+ ],
+ ])
+
+ buffer.setTextInRange([[0, 0], [0, 0]], 'new ');
+ expectTokensToEqual(editor, [
+ [
+ {text: 'new ', scopes: []},
+ {text: 'abc()', scopes: ['variable']},
+ {text: ';', scopes: []}
+ ],
+ ])
+
+ await nextHighlightingUpdate(languageMode)
+ expectTokensToEqual(editor, [
+ [
+ {text: 'new ', scopes: []},
+ {text: 'abc', scopes: ['function']},
+ {text: '();', scopes: []}
+ ],
+ ])
+
+ await nextHighlightingUpdate(languageMode)
+ expectTokensToEqual(editor, [
+ [
+ {text: 'new ', scopes: []},
+ {text: 'abc', scopes: ['constructor']},
+ {text: '();', scopes: []}
+ ],
+ ])
+ })
+ })
+
+ describe('when changes are small enough to be re-parsed synchronously', () => {
+ it('can incorporate multiple consecutive synchronous updates', () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ parser: 'tree-sitter-javascript',
+ scopes: {
+ 'property_identifier': 'property',
+ 'call_expression > identifier': 'function',
+ 'call_expression > member_expression > property_identifier': 'method',
+ }
+ })
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+ buffer.setText('a');
+ expectTokensToEqual(editor, [[
+ {text: 'a', scopes: []},
+ ]])
+
+ buffer.append('.')
+ expectTokensToEqual(editor, [[
+ {text: 'a.', scopes: []},
+ ]])
+
+ buffer.append('b')
+ expectTokensToEqual(editor, [[
+ {text: 'a.', scopes: []},
+ {text: 'b', scopes: ['property']},
+ ]])
+
+ buffer.append('()')
+ expectTokensToEqual(editor, [[
+ {text: 'a.', scopes: []},
+ {text: 'b', scopes: ['method']},
+ {text: '()', scopes: []},
+ ]])
+
+ buffer.delete([[0, 1], [0, 2]])
+ expectTokensToEqual(editor, [[
+ {text: 'ab', scopes: ['function']},
+ {text: '()', scopes: []},
+ ]])
+ })
+ })
+
+ describe('injectionPoints and injectionPatterns', () => {
+ let jsGrammar, htmlGrammar
+
+ beforeEach(() => {
+ jsGrammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ scopeName: 'javascript',
+ parser: 'tree-sitter-javascript',
+ scopes: {
+ 'property_identifier': 'property',
+ 'call_expression > identifier': 'function',
+ 'template_string': 'string',
+ 'template_substitution > "${"': 'interpolation',
+ 'template_substitution > "}"': 'interpolation'
+ },
+ injectionRegExp: 'javascript',
+ injectionPoints: [HTML_TEMPLATE_LITERAL_INJECTION_POINT]
+ })
+
+ htmlGrammar = new TreeSitterGrammar(atom.grammars, htmlGrammarPath, {
+ scopeName: 'html',
+ parser: 'tree-sitter-html',
+ scopes: {
+ fragment: 'html',
+ tag_name: 'tag',
+ attribute_name: 'attr'
+ },
+ injectionRegExp: 'html',
+ injectionPoints: [SCRIPT_TAG_INJECTION_POINT]
+ })
+ })
+
+ it('highlights code inside of injection points', async () => {
+ atom.grammars.addGrammar(jsGrammar)
+ atom.grammars.addGrammar(htmlGrammar)
+ buffer.setText('node.innerHTML = html `\na ${b}
\n`;')
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar: jsGrammar, grammars: atom.grammars})
+ buffer.setLanguageMode(languageMode)
+
+ expectTokensToEqual(editor, [
+ [
+ {text: 'node.', scopes: []},
+ {text: 'innerHTML', scopes: ['property']},
+ {text: ' = ', scopes: []},
+ {text: 'html', scopes: ['function']},
+ {text: ' ', scopes: []},
+ {text: '`', scopes: ['string']},
+ {text: '', scopes: ['string', 'html']}
+ ], [
+ {text: 'a ', scopes: ['string', 'html']},
+ {text: '${', scopes: ['string', 'html', 'interpolation']},
+ {text: 'b', scopes: ['string', 'html']},
+ {text: '}', scopes: ['string', 'html', 'interpolation']},
+ {text: '<', scopes: ['string', 'html']},
+ {text: 'img', scopes: ['string', 'html', 'tag']},
+ {text: ' ', scopes: ['string', 'html']},
+ {text: 'src', scopes: ['string', 'html', 'attr']},
+ {text: '="d">', scopes: ['string', 'html']}
+ ], [
+ {text: '`', scopes: ['string']},
+ {text: ';', scopes: []},
+ ],
+ ])
+
+ const range = buffer.findSync('html')
+ buffer.setTextInRange(range, 'xml')
+ await nextHighlightingUpdate(languageMode)
+
+ expectTokensToEqual(editor, [
+ [
+ {text: 'node.', scopes: []},
+ {text: 'innerHTML', scopes: ['property']},
+ {text: ' = ', scopes: []},
+ {text: 'xml', scopes: ['function']},
+ {text: ' ', scopes: []},
+ {text: '`', scopes: ['string']}
+ ], [
+ {text: 'a ', scopes: ['string']},
+ {text: '${', scopes: ['string', 'interpolation']},
+ {text: 'b', scopes: ['string']},
+ {text: '}', scopes: ['string', 'interpolation']},
+ {text: '
', scopes: ['string']},
+ ], [
+ {text: '`', scopes: ['string']},
+ {text: ';', scopes: []},
+ ],
+ ])
+ })
+
+ it('highlights the content after injections', async () => {
+ atom.grammars.addGrammar(jsGrammar)
+ atom.grammars.addGrammar(htmlGrammar)
+ buffer.setText('\n\n
')
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar: htmlGrammar, grammars: atom.grammars})
+ buffer.setLanguageMode(languageMode)
+
+ expectTokensToEqual(editor, [
+ [
+ {text: '<', scopes: ['html']},
+ {text: 'script', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']},
+ ],
+ [
+ {text: 'hello', scopes: ['html', 'function']},
+ {text: '();', scopes: ['html']},
+ ],
+ [
+ {text: '', scopes: ['html']},
+ {text: 'script', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']},
+ ],
+ [
+ {text: '<', scopes: ['html']},
+ {text: 'div', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']},
+ ],
+ [
+ {text: '', scopes: ['html']},
+ {text: 'div', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']},
+ ]
+ ])
+ })
+
+ it('updates buffers highlighting when a grammar with injectionRegExp is added', async () => {
+ atom.grammars.addGrammar(jsGrammar)
+
+ buffer.setText('node.innerHTML = html `\na ${b}
\n`;')
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar: jsGrammar, grammars: atom.grammars})
+ buffer.setLanguageMode(languageMode)
+
+ expectTokensToEqual(editor, [
+ [
+ {text: 'node.', scopes: []},
+ {text: 'innerHTML', scopes: ['property']},
+ {text: ' = ', scopes: []},
+ {text: 'html', scopes: ['function']},
+ {text: ' ', scopes: []},
+ {text: '`', scopes: ['string']}
+ ], [
+ {text: 'a ', scopes: ['string']},
+ {text: '${', scopes: ['string', 'interpolation']},
+ {text: 'b', scopes: ['string']},
+ {text: '}', scopes: ['string', 'interpolation']},
+ {text: '
', scopes: ['string']},
+ ], [
+ {text: '`', scopes: ['string']},
+ {text: ';', scopes: []},
+ ],
+ ])
+
+ atom.grammars.addGrammar(htmlGrammar)
+ await nextHighlightingUpdate(languageMode)
+ expectTokensToEqual(editor, [
+ [
+ {text: 'node.', scopes: []},
+ {text: 'innerHTML', scopes: ['property']},
+ {text: ' = ', scopes: []},
+ {text: 'html', scopes: ['function']},
+ {text: ' ', scopes: []},
+ {text: '`', scopes: ['string']},
+ {text: '', scopes: ['string', 'html']}
+ ], [
+ {text: 'a ', scopes: ['string', 'html']},
+ {text: '${', scopes: ['string', 'html', 'interpolation']},
+ {text: 'b', scopes: ['string', 'html']},
+ {text: '}', scopes: ['string', 'html', 'interpolation']},
+ {text: '<', scopes: ['string', 'html']},
+ {text: 'img', scopes: ['string', 'html', 'tag']},
+ {text: ' ', scopes: ['string', 'html']},
+ {text: 'src', scopes: ['string', 'html', 'attr']},
+ {text: '="d">', scopes: ['string', 'html']}
+ ], [
+ {text: '`', scopes: ['string']},
+ {text: ';', scopes: []},
+ ],
+ ])
+ })
+
+ it('handles injections that intersect', async () => {
+ const ejsGrammar = new TreeSitterGrammar(atom.grammars, ejsGrammarPath, {
+ id: 'ejs',
+ parser: 'tree-sitter-embedded-template',
+ scopes: {
+ '"<%="': 'directive',
+ '"%>"': 'directive',
+ },
+ injectionPoints: [
+ {
+ type: 'template',
+ language (node) { return 'javascript' },
+ content (node) { return node.descendantsOfType('code') }
+ },
+ {
+ type: 'template',
+ language (node) { return 'html' },
+ content (node) { return node.descendantsOfType('content') }
+ }
+ ]
+ })
+
+ atom.grammars.addGrammar(jsGrammar)
+ atom.grammars.addGrammar(htmlGrammar)
+
+ buffer.setText('\n\n')
+ const languageMode = new TreeSitterLanguageMode({
+ buffer,
+ grammar: ejsGrammar,
+ grammars: atom.grammars,
+ })
+ buffer.setLanguageMode(languageMode)
+
+ expectTokensToEqual(editor, [
+ [
+ {text: '<', scopes: ['html']},
+ {text: 'body', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']}
+ ],
+ [
+ {text: '<', scopes: ['html']},
+ {text: 'script', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']}
+ ],
+ [
+ {text: 'b', scopes: ['html', 'function']},
+ {text: '(', scopes: ['html']},
+ {text: '<%=', scopes: ['html', 'directive']},
+ {text: ' c.', scopes: ['html']},
+ {text: 'd', scopes: ['html', 'property']},
+ {text: ' ', scopes: ['html']},
+ {text: '%>', scopes: ['html', 'directive']},
+ {text: ')', scopes: ['html']},
+ ],
+ [
+ {text: '', scopes: ['html']},
+ {text: 'script', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']}
+ ],
+ [
+ {text: '', scopes: ['html']},
+ {text: 'body', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']}
+ ],
+ ])
+ })
+
+ it('notifies onDidTokenize listeners the first time all syntax highlighting is done', async () => {
+ const promise = new Promise(resolve => {
+ editor.onDidTokenize(event => {
+ expectTokensToEqual(editor, [
+ [
+ {text: '<', scopes: ['html']},
+ {text: 'script', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']},
+ ],
+ [
+ {text: 'hello', scopes: ['html', 'function']},
+ {text: '();', scopes: ['html']},
+ ],
+ [
+ {text: '', scopes: ['html']},
+ {text: 'script', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']},
+ ]
+ ])
+ resolve()
+ })
+ })
+
+ atom.grammars.addGrammar(jsGrammar)
+ atom.grammars.addGrammar(htmlGrammar)
+ buffer.setText('')
+
+ const languageMode = new TreeSitterLanguageMode({
+ buffer,
+ grammar: htmlGrammar,
+ grammars: atom.grammars,
+ syncOperationLimit: 0
+ })
+ buffer.setLanguageMode(languageMode)
+
+ await promise
+ })
+ })
})
describe('folding', () => {
- beforeEach(() => {
- editor.displayLayer.reset({foldCharacter: '…'})
- })
-
- it('can fold nodes that start and end with specified tokens', () => {
+ it('can fold nodes that start and end with specified tokens', async () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
folds: [
@@ -235,7 +805,6 @@ describe('TreeSitterLanguageMode', () => {
]
})
- buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText(dedent `
module.exports =
class A {
@@ -247,7 +816,8 @@ describe('TreeSitterLanguageMode', () => {
}
`)
- editor.screenLineForScreenRow(0)
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
expect(editor.isFoldableAtBufferRow(0)).toBe(false)
expect(editor.isFoldableAtBufferRow(1)).toBe(true)
@@ -260,7 +830,7 @@ describe('TreeSitterLanguageMode', () => {
expect(getDisplayText(editor)).toBe(dedent `
module.exports =
class A {
- getB (…) {
+ getB (c,…) {
return this.f(g)
}
}
@@ -270,12 +840,60 @@ describe('TreeSitterLanguageMode', () => {
expect(getDisplayText(editor)).toBe(dedent `
module.exports =
class A {
- getB (…) {…}
+ getB (c,…) {…}
}
`)
})
- it('can fold nodes of specified types', () => {
+ it('folds entire buffer rows when necessary to keep words on separate lines', async () => {
+ 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.setText(dedent `
+ if (a) {
+ b
+ } else if (c) {
+ d
+ } else {
+ e
+ }
+ `)
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+
+ // Avoid bringing the `else if...` up onto the same screen line as the preceding `if`.
+ editor.foldBufferRow(1)
+ editor.foldBufferRow(3)
+ expect(getDisplayText(editor)).toBe(dedent `
+ if (a) {…
+ } else if (c) {…
+ } else {
+ e
+ }
+ `)
+
+ // It's ok to bring the final `}` onto the same screen line as the preceding `else`.
+ editor.foldBufferRow(5)
+ expect(getDisplayText(editor)).toBe(dedent `
+ if (a) {…
+ } else if (c) {…
+ } else {…}
+ `)
+ })
+
+ it('can fold nodes of specified types', async () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
folds: [
@@ -296,7 +914,6 @@ describe('TreeSitterLanguageMode', () => {
]
})
- buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText(dedent `
const element1 = {
`)
- editor.screenLineForScreenRow(0)
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
expect(editor.isFoldableAtBufferRow(0)).toBe(true)
expect(editor.isFoldableAtBufferRow(1)).toBe(false)
@@ -336,7 +954,7 @@ describe('TreeSitterLanguageMode', () => {
`)
})
- it('can fold entire nodes when no start or end parameters are specified', () => {
+ it('can fold entire nodes when no start or end parameters are specified', async () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
folds: [
@@ -346,7 +964,6 @@ describe('TreeSitterLanguageMode', () => {
]
})
- buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText(dedent `
/**
* Important
@@ -356,7 +973,8 @@ describe('TreeSitterLanguageMode', () => {
*/
`)
- editor.screenLineForScreenRow(0)
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
expect(editor.isFoldableAtBufferRow(0)).toBe(true)
expect(editor.isFoldableAtBufferRow(1)).toBe(false)
@@ -379,7 +997,7 @@ describe('TreeSitterLanguageMode', () => {
`)
})
- it('tries each folding strategy for a given node in the order specified', () => {
+ it('tries each folding strategy for a given node in the order specified', async () => {
const grammar = new TreeSitterGrammar(atom.grammars, cGrammarPath, {
parser: 'tree-sitter-c',
folds: [
@@ -405,8 +1023,6 @@ describe('TreeSitterLanguageMode', () => {
]
})
- buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
-
buffer.setText(dedent `
#ifndef FOO_H_
#define FOO_H_
@@ -431,7 +1047,8 @@ describe('TreeSitterLanguageMode', () => {
#endif
`)
- editor.screenLineForScreenRow(0)
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
editor.foldBufferRow(3)
expect(getDisplayText(editor)).toBe(dedent `
@@ -492,8 +1109,127 @@ describe('TreeSitterLanguageMode', () => {
`)
})
+ it('does not fold when the start and end parameters match the same child', async () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, htmlGrammarPath, {
+ parser: 'tree-sitter-html',
+ folds: [
+ {
+ type: 'element',
+ start: {index: 0},
+ end: {index: -1}
+ }
+ ]
+ })
+
+ buffer.setText(dedent `
+
+
+
+
+ `)
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+
+ // Void elements have only one child
+ expect(editor.isFoldableAtBufferRow(1)).toBe(false)
+ expect(editor.isFoldableAtBufferRow(2)).toBe(false)
+
+ editor.foldBufferRow(0)
+ expect(getDisplayText(editor)).toBe(dedent `
+ …
+
+ `)
+ })
+
+ it('can target named vs anonymous nodes as fold boundaries', async () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, rubyGrammarPath, {
+ parser: 'tree-sitter-ruby',
+ folds: [
+ {
+ type: 'elsif',
+ start: {index: 1},
+
+ // There are no double quotes around the `elsif` type. This indicates
+ // that we're targeting a *named* node in the syntax tree. The fold
+ // should end at the nested `elsif` node, not at the token that represents
+ // the literal string "elsif".
+ end: {type: ['else', 'elsif']}
+ },
+ {
+ type: 'else',
+
+ // There are double quotes around the `else` type. This indicates that
+ // we're targetting an *anonymous* node in the syntax tree. The fold
+ // should start at the token representing the literal string "else",
+ // not at an `else` node.
+ start: {type: '"else"'}
+ }
+ ]
+ })
+
+ buffer.setText(dedent `
+ if a
+ b
+ elsif c
+ d
+ elsif e
+ f
+ else
+ g
+ end
+ `)
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+
+ expect(languageMode.tree.rootNode.toString()).toBe(
+ "(program (if (identifier) " +
+ "(identifier) " +
+ "(elsif (identifier) " +
+ "(identifier) " +
+ "(elsif (identifier) " +
+ "(identifier) " +
+ "(else " +
+ "(identifier))))))"
+ )
+
+ editor.foldBufferRow(2)
+ expect(getDisplayText(editor)).toBe(dedent `
+ if a
+ b
+ elsif c…
+ elsif e
+ f
+ else
+ g
+ end
+ `)
+
+ editor.foldBufferRow(4)
+ expect(getDisplayText(editor)).toBe(dedent `
+ if a
+ b
+ elsif c…
+ elsif e…
+ else
+ g
+ end
+ `)
+
+ editor.foldBufferRow(6)
+ expect(getDisplayText(editor)).toBe(dedent `
+ if a
+ b
+ elsif c…
+ elsif e…
+ else…
+ end
+ `)
+ })
+
describe('when folding a node that ends with a line break', () => {
- it('ends the fold at the end of the previous line', () => {
+ it('ends the fold at the end of the previous line', async () => {
const grammar = new TreeSitterGrammar(atom.grammars, pythonGrammarPath, {
parser: 'tree-sitter-python',
folds: [
@@ -504,8 +1240,6 @@ describe('TreeSitterLanguageMode', () => {
]
})
- buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
-
buffer.setText(dedent `
def ab():
print 'a'
@@ -516,7 +1250,7 @@ describe('TreeSitterLanguageMode', () => {
print 'd'
`)
- editor.screenLineForScreenRow(0)
+ buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
editor.foldBufferRow(0)
expect(getDisplayText(editor)).toBe(dedent `
@@ -528,41 +1262,371 @@ describe('TreeSitterLanguageMode', () => {
`)
})
})
+
+ it('folds code in injected languages', async () => {
+ const htmlGrammar = new TreeSitterGrammar(atom.grammars, htmlGrammarPath, {
+ scopeName: 'html',
+ parser: 'tree-sitter-html',
+ scopes: {},
+ folds: [{
+ type: ['element', 'raw_element'],
+ start: {index: 0},
+ end: {index: -1}
+ }],
+ injectionRegExp: 'html'
+ })
+
+ const jsGrammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ scopeName: 'javascript',
+ parser: 'tree-sitter-javascript',
+ scopes: {},
+ folds: [{
+ type: ['template_string'],
+ start: {index: 0},
+ end: {index: -1},
+ },
+ {
+ start: {index: 0, type: '('},
+ end: {index: -1, type: ')'}
+ }],
+ injectionRegExp: 'javascript',
+ injectionPoints: [HTML_TEMPLATE_LITERAL_INJECTION_POINT]
+ })
+
+ atom.grammars.addGrammar(htmlGrammar)
+
+ buffer.setText(
+ `a = html \`
+
+ c\${def(
+ 1,
+ 2,
+ 3,
+ )}e\${f}g
+
+ \`
+ `
+ )
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar: jsGrammar, grammars: atom.grammars})
+ buffer.setLanguageMode(languageMode)
+
+ editor.foldBufferRow(2)
+ expect(getDisplayText(editor)).toBe(
+ `a = html \`
+
+ c\${def(…
+ )}e\${f}g
+
+ \`
+ `
+ )
+
+ editor.foldBufferRow(1)
+ expect(getDisplayText(editor)).toBe(
+ `a = html \`
+ …
+
+ \`
+ `
+ )
+
+ editor.foldBufferRow(0)
+ expect(getDisplayText(editor)).toBe(
+ `a = html \`…\`
+ `
+ )
+ })
})
describe('.scopeDescriptorForPosition', () => {
- it('returns a scope descriptor representing the given position in the syntax tree', () => {
+ it('returns a scope descriptor representing the given position in the syntax tree', async () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
- id: 'javascript',
- parser: 'tree-sitter-javascript'
+ scopeName: 'source.js',
+ parser: 'tree-sitter-javascript',
+ scopes: {
+ program: 'source.js',
+ property_identifier: 'property.name'
+ }
})
- buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
-
buffer.setText('foo({bar: baz});')
- editor.screenLineForScreenRow(0)
- expect(editor.scopeDescriptorForBufferPosition([0, 6]).getScopesArray()).toEqual([
- 'javascript',
- 'program',
- 'expression_statement',
- 'call_expression',
- 'arguments',
- 'object',
- 'pair',
- 'property_identifier'
+ buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
+ expect(editor.scopeDescriptorForBufferPosition([0, 'foo({b'.length]).getScopesArray()).toEqual([
+ 'source.js',
+ 'property.name'
+ ])
+ expect(editor.scopeDescriptorForBufferPosition([0, 'foo({'.length]).getScopesArray()).toEqual([
+ 'source.js',
+ 'property.name'
+ ])
+ })
+
+ it('includes nodes in injected syntax trees', async () => {
+ const jsGrammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ scopeName: 'source.js',
+ parser: 'tree-sitter-javascript',
+ scopes: {
+ program: 'source.js',
+ template_string: 'string.quoted',
+ interpolation: 'meta.embedded',
+ property_identifier: 'property.name'
+ },
+ injectionRegExp: 'javascript',
+ injectionPoints: [HTML_TEMPLATE_LITERAL_INJECTION_POINT]
+ })
+
+ const htmlGrammar = new TreeSitterGrammar(atom.grammars, htmlGrammarPath, {
+ scopeName: 'text.html',
+ parser: 'tree-sitter-html',
+ scopes: {
+ fragment: 'text.html',
+ raw_element: 'script.tag'
+ },
+ injectionRegExp: 'html',
+ injectionPoints: [SCRIPT_TAG_INJECTION_POINT]
+ })
+
+ atom.grammars.addGrammar(jsGrammar)
+ atom.grammars.addGrammar(htmlGrammar)
+
+ buffer.setText(`
+
+
+
+ `)
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar: htmlGrammar, grammars: atom.grammars})
+ buffer.setLanguageMode(languageMode)
+
+ const position = buffer.findSync('name').start
+ expect(languageMode.scopeDescriptorForPosition(position).getScopesArray()).toEqual([
+ 'text.html',
+ 'script.tag',
+ 'source.js',
+ 'string.quoted',
+ 'text.html',
+ 'property.name'
+ ])
+ })
+
+ it('includes the root scope name even when the given position is in trailing whitespace at EOF', () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ scopeName: 'source.js',
+ parser: 'tree-sitter-javascript',
+ scopes: {
+ program: 'source.js',
+ property_identifier: 'property.name'
+ }
+ })
+
+ buffer.setText('a; ')
+ buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
+ expect(editor.scopeDescriptorForBufferPosition([0, 3]).getScopesArray()).toEqual([
+ 'source.js'
])
})
})
+ describe('.bufferRangeForScopeAtPosition(selector?, position)', () => {
+ describe('when selector = null', () => {
+ it('returns the range of the smallest node at position', async () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ scopeName: 'javascript',
+ parser: 'tree-sitter-javascript'
+ })
+
+ buffer.setText('foo({bar: baz});')
+
+ buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
+ expect(editor.bufferRangeForScopeAtPosition(null, [0, 6])).toEqual(
+ [[0, 5], [0, 8]]
+ )
+ expect(editor.bufferRangeForScopeAtPosition(null, [0, 9])).toEqual(
+ [[0, 8], [0, 9]]
+ )
+ })
+
+ it('includes nodes in injected syntax trees', async () => {
+ const jsGrammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ scopeName: 'javascript',
+ parser: 'tree-sitter-javascript',
+ scopes: {},
+ injectionRegExp: 'javascript',
+ injectionPoints: [HTML_TEMPLATE_LITERAL_INJECTION_POINT]
+ })
+
+ const htmlGrammar = new TreeSitterGrammar(atom.grammars, htmlGrammarPath, {
+ scopeName: 'html',
+ parser: 'tree-sitter-html',
+ scopes: {},
+ injectionRegExp: 'html',
+ injectionPoints: [SCRIPT_TAG_INJECTION_POINT]
+ })
+
+ atom.grammars.addGrammar(jsGrammar)
+ atom.grammars.addGrammar(htmlGrammar)
+
+ buffer.setText(`
+
+
+
+ `)
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar: htmlGrammar, grammars: atom.grammars})
+ buffer.setLanguageMode(languageMode)
+
+ const nameProperty = buffer.findSync('name')
+ const {start} = nameProperty
+ const position = Object.assign({}, start, {column: start.column + 2})
+ expect(languageMode.bufferRangeForScopeAtPosition(null, position))
+ .toEqual(nameProperty)
+ })
+ })
+
+ describe('with a selector', () => {
+ it('returns the range of the smallest matching node at position', async () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ scopeName: 'javascript',
+ parser: 'tree-sitter-javascript'
+ })
+
+ buffer.setText('foo({bar: baz});')
+
+ buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
+ expect(editor.bufferRangeForScopeAtPosition('.property_identifier', [0, 6])).toEqual(
+ buffer.findSync('bar')
+ )
+ expect(editor.bufferRangeForScopeAtPosition('.call_expression', [0, 6])).toEqual(
+ [[0, 0], [0, buffer.getText().length - 1]]
+ )
+ expect(editor.bufferRangeForScopeAtPosition('.object', [0, 9])).toEqual(
+ buffer.findSync('{bar: baz}')
+ )
+ })
+
+ it('includes nodes in injected syntax trees', async () => {
+ const jsGrammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ scopeName: 'javascript',
+ parser: 'tree-sitter-javascript',
+ scopes: {},
+ injectionRegExp: 'javascript',
+ injectionPoints: [HTML_TEMPLATE_LITERAL_INJECTION_POINT]
+ })
+
+ const htmlGrammar = new TreeSitterGrammar(atom.grammars, htmlGrammarPath, {
+ scopeName: 'html',
+ parser: 'tree-sitter-html',
+ scopes: {},
+ injectionRegExp: 'html',
+ injectionPoints: [SCRIPT_TAG_INJECTION_POINT]
+ })
+
+ atom.grammars.addGrammar(jsGrammar)
+ atom.grammars.addGrammar(htmlGrammar)
+
+ buffer.setText(`
+
+
+
+ `)
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar: htmlGrammar, grammars: atom.grammars})
+ buffer.setLanguageMode(languageMode)
+
+ const nameProperty = buffer.findSync('name')
+ const {start} = nameProperty
+ const position = Object.assign({}, start, {column: start.column + 2})
+ expect(languageMode.bufferRangeForScopeAtPosition('.property_identifier', position))
+ .toEqual(nameProperty)
+ expect(languageMode.bufferRangeForScopeAtPosition('.element', position))
+ .toEqual(buffer.findSync('\\${person\\.name}'))
+ })
+
+ it('accepts node-matching functions as selectors', async () => {
+ const jsGrammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ scopeName: 'javascript',
+ parser: 'tree-sitter-javascript',
+ scopes: {},
+ injectionRegExp: 'javascript',
+ injectionPoints: [HTML_TEMPLATE_LITERAL_INJECTION_POINT]
+ })
+
+ const htmlGrammar = new TreeSitterGrammar(atom.grammars, htmlGrammarPath, {
+ scopeName: 'html',
+ parser: 'tree-sitter-html',
+ scopes: {},
+ injectionRegExp: 'html',
+ injectionPoints: [SCRIPT_TAG_INJECTION_POINT]
+ })
+
+ atom.grammars.addGrammar(jsGrammar)
+ atom.grammars.addGrammar(htmlGrammar)
+
+ buffer.setText(`
+
+
+
+ `)
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar: htmlGrammar, grammars: atom.grammars})
+ buffer.setLanguageMode(languageMode)
+
+ const nameProperty = buffer.findSync('name')
+ const {start} = nameProperty
+ const position = Object.assign({}, start, {column: start.column + 2})
+ const templateStringInCallExpression = node =>
+ node.type === 'template_string' && node.parent.type === 'call_expression'
+ expect(languageMode.bufferRangeForScopeAtPosition(templateStringInCallExpression, position))
+ .toEqual([[3, 19], [5, 15]])
+ })
+ })
+ })
+
+ describe('.getSyntaxNodeAtPosition(position, where?)', () => {
+ it('returns the range of the smallest matching node at position', async () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ scopeName: 'javascript',
+ parser: 'tree-sitter-javascript'
+ })
+
+ buffer.setText('foo(bar({x: 2}));')
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+ expect(languageMode.getSyntaxNodeAtPosition([0, 6]).range).toEqual(
+ buffer.findSync('bar')
+ )
+ const findFoo = node =>
+ node.type === 'call_expression' && node.firstChild.text === 'foo'
+ expect(languageMode.getSyntaxNodeAtPosition([0, 6], findFoo).range).toEqual(
+ [[0, 0], [0, buffer.getText().length - 1]]
+ )
+ })
+ })
+
describe('TextEditor.selectLargerSyntaxNode and .selectSmallerSyntaxNode', () => {
- it('expands and contract the selection based on the syntax tree', () => {
+ it('expands and contracts the selection based on the syntax tree', async () => {
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()
@@ -570,7 +1634,7 @@ describe('TreeSitterLanguageMode', () => {
}
`)
- editor.screenLineForScreenRow(0)
+ buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
editor.setCursorBufferPosition([1, 3])
editor.selectLargerSyntaxNode()
@@ -595,9 +1659,69 @@ describe('TreeSitterLanguageMode', () => {
editor.selectSmallerSyntaxNode()
expect(editor.getSelectedBufferRange()).toEqual([[1, 3], [1, 3]])
})
+
+ it('handles injected languages', async () => {
+ const jsGrammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ scopeName: 'javascript',
+ parser: 'tree-sitter-javascript',
+ scopes: {
+ 'property_identifier': 'property',
+ 'call_expression > identifier': 'function',
+ 'template_string': 'string',
+ 'template_substitution > "${"': 'interpolation',
+ 'template_substitution > "}"': 'interpolation'
+ },
+ injectionRegExp: 'javascript',
+ injectionPoints: [HTML_TEMPLATE_LITERAL_INJECTION_POINT]
+ })
+
+ const htmlGrammar = new TreeSitterGrammar(atom.grammars, htmlGrammarPath, {
+ scopeName: 'html',
+ parser: 'tree-sitter-html',
+ scopes: {
+ fragment: 'html',
+ tag_name: 'tag',
+ attribute_name: 'attr'
+ },
+ injectionRegExp: 'html'
+ })
+
+ atom.grammars.addGrammar(htmlGrammar)
+
+ buffer.setText('a = html ` c${def()}e${f}g `')
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar: jsGrammar, grammars: atom.grammars})
+ buffer.setLanguageMode(languageMode)
+
+ editor.setCursorBufferPosition({row: 0, column: buffer.getText().indexOf('ef()')})
+ editor.selectLargerSyntaxNode()
+ expect(editor.getSelectedText()).toBe('def')
+ editor.selectLargerSyntaxNode()
+ expect(editor.getSelectedText()).toBe('def()')
+ editor.selectLargerSyntaxNode()
+ expect(editor.getSelectedText()).toBe('${def()}')
+ editor.selectLargerSyntaxNode()
+ expect(editor.getSelectedText()).toBe('c${def()}e${f}g')
+ editor.selectLargerSyntaxNode()
+ expect(editor.getSelectedText()).toBe('c${def()}e${f}g')
+ editor.selectLargerSyntaxNode()
+ expect(editor.getSelectedText()).toBe(' c${def()}e${f}g ')
+ editor.selectLargerSyntaxNode()
+ expect(editor.getSelectedText()).toBe('` c${def()}e${f}g `')
+ editor.selectLargerSyntaxNode()
+ expect(editor.getSelectedText()).toBe('html ` c${def()}e${f}g `')
+ })
})
})
+function nextHighlightingUpdate (languageMode) {
+ return new Promise(resolve => {
+ const subscription = languageMode.onDidChangeHighlighting(() => {
+ subscription.dispose()
+ resolve()
+ })
+ })
+}
+
function getDisplayText (editor) {
return editor.displayLayer.getText()
}
@@ -624,7 +1748,7 @@ function expectTokensToEqual (editor, expectedTokenLines) {
text,
scopes: scopes.map(scope => scope
.split(' ')
- .map(className => className.slice('syntax--'.length))
+ .map(className => className.replace('syntax--', ''))
.join(' '))
}))
}
@@ -644,3 +1768,21 @@ function expectTokensToEqual (editor, expectedTokenLines) {
// due to subsequent edits can be tested.
editor.displayLayer.getScreenLines(0, Infinity)
}
+
+const HTML_TEMPLATE_LITERAL_INJECTION_POINT = {
+ type: 'call_expression',
+ language (node) {
+ if (node.lastChild.type === 'template_string' && node.firstChild.type === 'identifier') {
+ return node.firstChild.text
+ }
+ },
+ content (node) {
+ return node.lastChild
+ }
+}
+
+const SCRIPT_TAG_INJECTION_POINT = {
+ type: 'raw_element',
+ language () { return 'javascript' },
+ content (node) { return node.child(1) }
+}
diff --git a/spec/update-process-env-spec.js b/spec/update-process-env-spec.js
index e5a1cfd9c..f7948d998 100644
--- a/spec/update-process-env-spec.js
+++ b/spec/update-process-env-spec.js
@@ -36,7 +36,7 @@ describe('updateProcessEnv(launchEnv)', function () {
})
describe('when the launch environment appears to come from a shell', function () {
- it('updates process.env to match the launch environment', async function () {
+ it('updates process.env to match the launch environment because PWD is set', async function () {
process.env = {
WILL_BE_DELETED: 'hi',
NODE_ENV: 'the-node-env',
@@ -64,6 +64,65 @@ describe('updateProcessEnv(launchEnv)', function () {
expect(process.env).toBe(initialProcessEnv)
})
+ it('updates process.env to match the launch environment because PROMPT is set', async function () {
+ process.env = {
+ WILL_BE_DELETED: 'hi',
+ NODE_ENV: 'the-node-env',
+ NODE_PATH: '/the/node/path',
+ ATOM_HOME: '/the/atom/home'
+ }
+
+ const initialProcessEnv = process.env
+
+ await updateProcessEnv({ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PROMPT: '$P$G', KEY1: 'value1', KEY2: 'value2'})
+ expect(process.env).toEqual({
+ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true',
+ PROMPT: '$P$G',
+ KEY1: 'value1',
+ KEY2: 'value2',
+ NODE_ENV: 'the-node-env',
+ NODE_PATH: '/the/node/path',
+ ATOM_HOME: '/the/atom/home'
+ })
+
+ // See #11302. On Windows, `process.env` is a magic object that offers
+ // case-insensitive environment variable matching, so we cannot replace it
+ // with another object.
+ expect(process.env).toBe(initialProcessEnv)
+ })
+
+ it('updates process.env to match the launch environment because PSModulePath is set', async function () {
+ process.env = {
+ WILL_BE_DELETED: 'hi',
+ NODE_ENV: 'the-node-env',
+ NODE_PATH: '/the/node/path',
+ ATOM_HOME: '/the/atom/home'
+ }
+
+ const initialProcessEnv = process.env
+
+ await updateProcessEnv({
+ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true',
+ PSModulePath: 'C:\\Program Files\\WindowsPowerShell\\Modules;C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\Modules\\',
+ KEY1: 'value1',
+ KEY2: 'value2'
+ })
+ expect(process.env).toEqual({
+ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true',
+ PSModulePath: 'C:\\Program Files\\WindowsPowerShell\\Modules;C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\Modules\\',
+ KEY1: 'value1',
+ KEY2: 'value2',
+ NODE_ENV: 'the-node-env',
+ NODE_PATH: '/the/node/path',
+ ATOM_HOME: '/the/atom/home'
+ })
+
+ // See #11302. On Windows, `process.env` is a magic object that offers
+ // case-insensitive environment variable matching, so we cannot replace it
+ // with another object.
+ expect(process.env).toBe(initialProcessEnv)
+ })
+
it('allows ATOM_HOME to be overwritten only if the new value is a valid path', async function () {
let newAtomHomePath = temp.mkdirSync('atom-home')
diff --git a/spec/workspace-element-spec.js b/spec/workspace-element-spec.js
index 90d973773..7564f6931 100644
--- a/spec/workspace-element-spec.js
+++ b/spec/workspace-element-spec.js
@@ -1,11 +1,14 @@
/** @babel */
const {ipcRenderer} = require('electron')
+const etch = require('etch')
const path = require('path')
const temp = require('temp').track()
const {Disposable} = require('event-kit')
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
+const getNextUpdatePromise = () => etch.getScheduler().nextUpdatePromise
+
describe('WorkspaceElement', () => {
afterEach(() => {
try {
@@ -565,35 +568,42 @@ describe('WorkspaceElement', () => {
// Mouse over where the toggle button would be if the dock were hovered
moveMouse({clientX: 440, clientY: 150})
+ await getNextUpdatePromise()
expectToggleButtonHidden(leftDock)
expectToggleButtonHidden(rightDock)
expectToggleButtonHidden(bottomDock)
// Mouse over the dock
moveMouse({clientX: 460, clientY: 150})
+ await getNextUpdatePromise()
expectToggleButtonHidden(leftDock)
expectToggleButtonVisible(rightDock, 'icon-chevron-right')
expectToggleButtonHidden(bottomDock)
// Mouse over the toggle button
moveMouse({clientX: 440, clientY: 150})
+ await getNextUpdatePromise()
expectToggleButtonHidden(leftDock)
expectToggleButtonVisible(rightDock, 'icon-chevron-right')
expectToggleButtonHidden(bottomDock)
// Click the toggle button
- rightDock.toggleButton.innerElement.click()
+ rightDock.refs.toggleButton.refs.innerElement.click()
+ await getNextUpdatePromise()
expect(rightDock.isVisible()).toBe(false)
expectToggleButtonHidden(rightDock)
// Mouse to edge of the window
moveMouse({clientX: 575, clientY: 150})
+ await getNextUpdatePromise()
expectToggleButtonHidden(rightDock)
moveMouse({clientX: 598, clientY: 150})
+ await getNextUpdatePromise()
expectToggleButtonVisible(rightDock, 'icon-chevron-left')
// Click the toggle button again
- rightDock.toggleButton.innerElement.click()
+ rightDock.refs.toggleButton.refs.innerElement.click()
+ await getNextUpdatePromise()
expect(rightDock.isVisible()).toBe(true)
expectToggleButtonVisible(rightDock, 'icon-chevron-right')
@@ -601,35 +611,42 @@ describe('WorkspaceElement', () => {
// Mouse over where the toggle button would be if the dock were hovered
moveMouse({clientX: 160, clientY: 150})
+ await getNextUpdatePromise()
expectToggleButtonHidden(leftDock)
expectToggleButtonHidden(rightDock)
expectToggleButtonHidden(bottomDock)
// Mouse over the dock
moveMouse({clientX: 140, clientY: 150})
+ await getNextUpdatePromise()
expectToggleButtonVisible(leftDock, 'icon-chevron-left')
expectToggleButtonHidden(rightDock)
expectToggleButtonHidden(bottomDock)
// Mouse over the toggle button
moveMouse({clientX: 160, clientY: 150})
+ await getNextUpdatePromise()
expectToggleButtonVisible(leftDock, 'icon-chevron-left')
expectToggleButtonHidden(rightDock)
expectToggleButtonHidden(bottomDock)
// Click the toggle button
- leftDock.toggleButton.innerElement.click()
+ leftDock.refs.toggleButton.refs.innerElement.click()
+ await getNextUpdatePromise()
expect(leftDock.isVisible()).toBe(false)
expectToggleButtonHidden(leftDock)
// Mouse to edge of the window
moveMouse({clientX: 25, clientY: 150})
+ await getNextUpdatePromise()
expectToggleButtonHidden(leftDock)
moveMouse({clientX: 2, clientY: 150})
+ await getNextUpdatePromise()
expectToggleButtonVisible(leftDock, 'icon-chevron-right')
// Click the toggle button again
- leftDock.toggleButton.innerElement.click()
+ leftDock.refs.toggleButton.refs.innerElement.click()
+ await getNextUpdatePromise()
expect(leftDock.isVisible()).toBe(true)
expectToggleButtonVisible(leftDock, 'icon-chevron-left')
@@ -637,51 +654,58 @@ describe('WorkspaceElement', () => {
// Mouse over where the toggle button would be if the dock were hovered
moveMouse({clientX: 300, clientY: 190})
+ await getNextUpdatePromise()
expectToggleButtonHidden(leftDock)
expectToggleButtonHidden(rightDock)
expectToggleButtonHidden(bottomDock)
// Mouse over the dock
moveMouse({clientX: 300, clientY: 210})
+ await getNextUpdatePromise()
expectToggleButtonHidden(leftDock)
expectToggleButtonHidden(rightDock)
expectToggleButtonVisible(bottomDock, 'icon-chevron-down')
// Mouse over the toggle button
moveMouse({clientX: 300, clientY: 195})
+ await getNextUpdatePromise()
expectToggleButtonHidden(leftDock)
expectToggleButtonHidden(rightDock)
expectToggleButtonVisible(bottomDock, 'icon-chevron-down')
// Click the toggle button
- bottomDock.toggleButton.innerElement.click()
+ bottomDock.refs.toggleButton.refs.innerElement.click()
+ await getNextUpdatePromise()
expect(bottomDock.isVisible()).toBe(false)
expectToggleButtonHidden(bottomDock)
// Mouse to edge of the window
moveMouse({clientX: 300, clientY: 290})
+ await getNextUpdatePromise()
expectToggleButtonHidden(leftDock)
moveMouse({clientX: 300, clientY: 299})
+ await getNextUpdatePromise()
expectToggleButtonVisible(bottomDock, 'icon-chevron-up')
// Click the toggle button again
- bottomDock.toggleButton.innerElement.click()
+ bottomDock.refs.toggleButton.refs.innerElement.click()
+ await getNextUpdatePromise()
expect(bottomDock.isVisible()).toBe(true)
expectToggleButtonVisible(bottomDock, 'icon-chevron-down')
})
- function moveMouse(coordinates) {
+ function moveMouse (coordinates) {
window.dispatchEvent(new MouseEvent('mousemove', coordinates))
advanceClock(100)
}
function expectToggleButtonHidden(dock) {
- expect(dock.toggleButton.element).not.toHaveClass('atom-dock-toggle-button-visible')
+ expect(dock.refs.toggleButton.element).not.toHaveClass('atom-dock-toggle-button-visible')
}
function expectToggleButtonVisible(dock, iconClass) {
- expect(dock.toggleButton.element).toHaveClass('atom-dock-toggle-button-visible')
- expect(dock.toggleButton.iconElement).toHaveClass(iconClass)
+ expect(dock.refs.toggleButton.element).toHaveClass('atom-dock-toggle-button-visible')
+ expect(dock.refs.toggleButton.refs.iconElement).toHaveClass(iconClass)
}
})
@@ -871,27 +895,39 @@ describe('WorkspaceElement', () => {
// No active item. Use first project directory.
atom.commands.dispatch(workspaceElement, 'window:run-package-specs')
- expect(ipcRenderer.send).toHaveBeenCalledWith('run-package-specs', path.join(projectPaths[0], 'spec'))
+ expect(ipcRenderer.send).toHaveBeenCalledWith('run-package-specs', path.join(projectPaths[0], 'spec'), {})
ipcRenderer.send.reset()
// Active item doesn't implement ::getPath(). Use first project directory.
const item = document.createElement('div')
atom.workspace.getActivePane().activateItem(item)
atom.commands.dispatch(workspaceElement, 'window:run-package-specs')
- expect(ipcRenderer.send).toHaveBeenCalledWith('run-package-specs', path.join(projectPaths[0], 'spec'))
+ expect(ipcRenderer.send).toHaveBeenCalledWith('run-package-specs', path.join(projectPaths[0], 'spec'), {})
ipcRenderer.send.reset()
// Active item has no path. Use first project directory.
item.getPath = () => null
atom.commands.dispatch(workspaceElement, 'window:run-package-specs')
- expect(ipcRenderer.send).toHaveBeenCalledWith('run-package-specs', path.join(projectPaths[0], 'spec'))
+ expect(ipcRenderer.send).toHaveBeenCalledWith('run-package-specs', path.join(projectPaths[0], 'spec'), {})
ipcRenderer.send.reset()
// Active item has path. Use project path for item path.
item.getPath = () => path.join(projectPaths[1], 'a-file.txt')
atom.commands.dispatch(workspaceElement, 'window:run-package-specs')
- expect(ipcRenderer.send).toHaveBeenCalledWith('run-package-specs', path.join(projectPaths[1], 'spec'))
+ expect(ipcRenderer.send).toHaveBeenCalledWith('run-package-specs', path.join(projectPaths[1], 'spec'), {})
ipcRenderer.send.reset()
})
+
+ it("passes additional options to the spec window", () => {
+ const workspaceElement = atom.workspace.getElement()
+ spyOn(ipcRenderer, 'send')
+
+ const projectPath = temp.mkdirSync('dir1-')
+ atom.project.setPaths([projectPath])
+ workspaceElement.runPackageSpecs({env: {ATOM_GITHUB_BABEL_ENV: 'coverage'}})
+
+ expect(ipcRenderer.send).toHaveBeenCalledWith(
+ 'run-package-specs', path.join(projectPath, 'spec'), {env: {ATOM_GITHUB_BABEL_ENV: 'coverage'}})
+ })
})
})
diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js
index 4b115e594..091588a70 100644
--- a/spec/workspace-spec.js
+++ b/spec/workspace-spec.js
@@ -274,6 +274,21 @@ describe('Workspace', () => {
})
})
+ it('discovers existing editors that are still opening', () => {
+ let editor0 = null
+ let editor1 = null
+
+ waitsForPromise(() => Promise.all([
+ workspace.open('spartacus.txt').then(o0 => { editor0 = o0 }),
+ workspace.open('spartacus.txt').then(o1 => { editor1 = o1 }),
+ ]))
+
+ runs(() => {
+ expect(editor0).toEqual(editor1)
+ expect(workspace.getActivePane().items).toEqual([editor0])
+ })
+ })
+
it("uses the location specified by the model's `getDefaultLocation()` method", () => {
const item = {
getDefaultLocation: jasmine.createSpy().andReturn('right'),
@@ -361,6 +376,28 @@ describe('Workspace', () => {
})
})
+ it('discovers existing editors that are still opening in an inactive pane', () => {
+ let editor0 = null
+ let editor1 = null
+ const pane0 = workspace.getActivePane()
+ const pane1 = workspace.getActivePane().splitRight()
+
+ pane0.activate()
+ const promise0 = workspace.open('spartacus.txt', {searchAllPanes: true}).then(o0 => { editor0 = o0 })
+ pane1.activate()
+ const promise1 = workspace.open('spartacus.txt', {searchAllPanes: true}).then(o1 => { editor1 = o1 })
+
+ waitsForPromise(() => Promise.all([promise0, promise1]))
+
+ runs(() => {
+ expect(editor0).toBeDefined()
+ expect(editor1).toBeDefined()
+
+ expect(editor0).toEqual(editor1)
+ expect(workspace.getActivePane().items).toEqual([editor0])
+ })
+ })
+
it('activates the pane in the dock with the matching item', () => {
const dock = atom.workspace.getRightDock()
const ITEM_URI = 'atom://test'
@@ -1236,21 +1273,57 @@ describe('Workspace', () => {
describe('the grammar-used hook', () => {
it('fires when opening a file or changing the grammar of an open file', async () => {
- let resolveJavascriptGrammarUsed, resolveCoffeeScriptGrammarUsed
- const javascriptGrammarUsed = new Promise(resolve => { resolveJavascriptGrammarUsed = resolve })
- const coffeescriptGrammarUsed = new Promise(resolve => { resolveCoffeeScriptGrammarUsed = resolve })
+ await atom.packages.activatePackage('language-javascript')
+ await atom.packages.activatePackage('language-coffee-script')
+
+ const observeTextEditorsSpy = jasmine.createSpy('observeTextEditors')
+ const javascriptGrammarUsed = jasmine.createSpy('javascript')
+ const coffeeScriptGrammarUsed = jasmine.createSpy('coffeescript')
atom.packages.triggerDeferredActivationHooks()
- atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', resolveJavascriptGrammarUsed)
- atom.packages.onDidTriggerActivationHook('language-coffee-script:grammar-used', resolveCoffeeScriptGrammarUsed)
+ atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', () => {
+ atom.workspace.observeTextEditors(observeTextEditorsSpy)
+ javascriptGrammarUsed()
+ })
+ atom.packages.onDidTriggerActivationHook('language-coffee-script:grammar-used', coffeeScriptGrammarUsed)
+ expect(javascriptGrammarUsed).not.toHaveBeenCalled()
+ expect(observeTextEditorsSpy).not.toHaveBeenCalled()
const editor = await atom.workspace.open('sample.js', {autoIndent: false})
- await atom.packages.activatePackage('language-javascript')
- await javascriptGrammarUsed
+ expect(javascriptGrammarUsed).toHaveBeenCalled()
+ expect(observeTextEditorsSpy.callCount).toBe(1)
- await atom.packages.activatePackage('language-coffee-script')
+ expect(coffeeScriptGrammarUsed).not.toHaveBeenCalled()
atom.grammars.assignLanguageMode(editor, 'source.coffee')
- await coffeescriptGrammarUsed
+ expect(coffeeScriptGrammarUsed).toHaveBeenCalled()
+ })
+ })
+
+ describe('the root-scope-used hook', () => {
+ it('fires when opening a file or changing the grammar of an open file', async () => {
+ await atom.packages.activatePackage('language-javascript')
+ await atom.packages.activatePackage('language-coffee-script')
+
+ const observeTextEditorsSpy = jasmine.createSpy('observeTextEditors')
+ const javascriptGrammarUsed = jasmine.createSpy('javascript')
+ const coffeeScriptGrammarUsed = jasmine.createSpy('coffeescript')
+
+ atom.packages.triggerDeferredActivationHooks()
+ atom.packages.onDidTriggerActivationHook('source.js:root-scope-used', () => {
+ atom.workspace.observeTextEditors(observeTextEditorsSpy)
+ javascriptGrammarUsed()
+ })
+ atom.packages.onDidTriggerActivationHook('source.coffee:root-scope-used', coffeeScriptGrammarUsed)
+
+ expect(javascriptGrammarUsed).not.toHaveBeenCalled()
+ expect(observeTextEditorsSpy).not.toHaveBeenCalled()
+ const editor = await atom.workspace.open('sample.js', {autoIndent: false})
+ expect(javascriptGrammarUsed).toHaveBeenCalled()
+ expect(observeTextEditorsSpy.callCount).toBe(1)
+
+ expect(coffeeScriptGrammarUsed).not.toHaveBeenCalled()
+ atom.grammars.assignLanguageMode(editor, 'source.coffee')
+ expect(coffeeScriptGrammarUsed).toHaveBeenCalled()
})
})
diff --git a/src/application-delegate.js b/src/application-delegate.js
index fcf8441b6..8d7981edb 100644
--- a/src/application-delegate.js
+++ b/src/application-delegate.js
@@ -1,12 +1,23 @@
const {ipcRenderer, remote, shell} = require('electron')
const ipcHelpers = require('./ipc-helpers')
-const {Disposable} = require('event-kit')
+const {Emitter, Disposable} = require('event-kit')
const getWindowLoadSettings = require('./get-window-load-settings')
module.exports =
class ApplicationDelegate {
constructor () {
this.pendingSettingsUpdateCount = 0
+ this._ipcMessageEmitter = null
+ }
+
+ ipcMessageEmitter () {
+ if (!this._ipcMessageEmitter) {
+ this._ipcMessageEmitter = new Emitter()
+ ipcRenderer.on('message', (event, message, detail) => {
+ this._ipcMessageEmitter.emit(message, detail)
+ })
+ }
+ return this._ipcMessageEmitter
}
getWindowLoadSettings () { return getWindowLoadSettings() }
@@ -179,31 +190,23 @@ class ApplicationDelegate {
return remote.systemPreferences.getUserDefault(key, type)
}
- async setUserSettings (config) {
+ async setUserSettings (config, configFilePath) {
this.pendingSettingsUpdateCount++
try {
- await ipcHelpers.call('set-user-settings', JSON.stringify(config))
+ await ipcHelpers.call('set-user-settings', JSON.stringify(config), configFilePath)
} finally {
this.pendingSettingsUpdateCount--
}
}
onDidChangeUserSettings (callback) {
- const outerCallback = (event, message, detail) => {
- if (message === 'did-change-user-settings') {
- if (this.pendingSettingsUpdateCount === 0) callback(detail)
- }
- }
- ipcRenderer.on('message', outerCallback)
- return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
+ return this.ipcMessageEmitter().on('did-change-user-settings', detail => {
+ if (this.pendingSettingsUpdateCount === 0) callback(detail)
+ })
}
onDidFailToReadUserSettings (callback) {
- const outerCallback = (event, message, detail) => {
- if (message === 'did-fail-to-read-user-settings') callback(detail)
- }
- ipcRenderer.on('message', outerCallback)
- return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
+ return this.ipcMessageEmitter().on('did-fail-to-read-user-setting', callback)
}
confirm (options, callback) {
@@ -261,24 +264,14 @@ class ApplicationDelegate {
}
onDidOpenLocations (callback) {
- const outerCallback = (event, message, detail) => {
- if (message === 'open-locations') callback(detail)
- }
-
- ipcRenderer.on('message', outerCallback)
- return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
+ return this.ipcMessageEmitter().on('open-locations', callback)
}
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))
+ // 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.
+ return this.ipcMessageEmitter().on('did-begin-downloading-update', callback)
}
onDidBeginDownloadingUpdate (callback) {
@@ -286,40 +279,19 @@ class ApplicationDelegate {
}
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))
+ return this.ipcMessageEmitter().on('checking-for-update', callback)
}
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))
+ return this.ipcMessageEmitter().on('update-available', callback)
}
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))
+ return this.ipcMessageEmitter().on('update-not-available', callback)
}
onUpdateError (callback) {
- const outerCallback = (event, message, detail) => {
- if (message === 'update-error') callback(detail)
- }
-
- ipcRenderer.on('message', outerCallback)
- return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
+ return this.ipcMessageEmitter().on('update-error', callback)
}
onApplicationMenuCommand (handler) {
diff --git a/src/atom-environment.js b/src/atom-environment.js
index 5a5264637..915ff78f1 100644
--- a/src/atom-environment.js
+++ b/src/atom-environment.js
@@ -64,7 +64,7 @@ class AtomEnvironment {
this.applicationDelegate = params.applicationDelegate
this.nextProxyRequestId = 0
- this.unloaded = false
+ this.unloading = false
this.loadTime = null
this.emitter = new Emitter()
this.disposables = new CompositeDisposable()
@@ -86,7 +86,7 @@ class AtomEnvironment {
this.config = new Config({
saveCallback: settings => {
if (this.enablePersistence) {
- this.applicationDelegate.setUserSettings(settings)
+ this.applicationDelegate.setUserSettings(settings, this.config.getUserConfigPath())
}
}
})
@@ -280,7 +280,7 @@ class AtomEnvironment {
attachSaveStateListeners () {
const saveState = _.debounce(() => {
this.window.requestIdleCallback(() => {
- if (!this.unloaded) this.saveState({isUnloading: false})
+ if (!this.unloading) this.saveState({isUnloading: false})
})
}, this.saveStateDebounceInterval)
this.document.addEventListener('mousedown', saveState, true)
@@ -487,21 +487,25 @@ class AtomEnvironment {
// Public: Gets the release channel of the Atom application.
//
- // Returns the release channel as a {String}. Will return one of `dev`, `beta`, or `stable`.
+ // Returns the release channel as a {String}. Will return a specific release channel
+ // name like 'beta' or 'nightly' if one is found in the Atom version or 'stable'
+ // otherwise.
getReleaseChannel () {
- const version = this.getVersion()
- if (version.includes('beta')) {
- return 'beta'
- } else if (version.includes('dev')) {
- return 'dev'
- } else {
- return 'stable'
+ // This matches stable, dev (with or without commit hash) and any other
+ // release channel following the pattern '1.00.0-channel0'
+ const match = this.getVersion().match(/\d+\.\d+\.\d+(-([a-z]+)(\d+|-\w{4,})?)?$/)
+ if (!match) {
+ return 'unrecognized'
+ } else if (match[2]) {
+ return match[2]
}
+
+ return 'stable'
}
// Public: Returns a {Boolean} that is `true` if the current version is an official release.
isReleasedVersion () {
- return !/\w{7}/.test(this.getVersion()) // Check if the release is a 7-character SHA prefix
+ return this.getReleaseChannel().match(/stable|beta|nightly/) != null
}
// Public: Get the time taken to completely load the current window.
@@ -771,7 +775,7 @@ class AtomEnvironment {
await this.stateStore.clear()
}
- this.unloaded = false
+ this.unloading = false
const updateProcessEnvPromise = this.updateProcessEnvAndTriggerHooks()
@@ -796,21 +800,7 @@ class AtomEnvironment {
this.disposables.add(this.applicationDelegate.onApplicationMenuCommand(this.dispatchApplicationMenuCommand.bind(this)))
this.disposables.add(this.applicationDelegate.onContextMenuCommand(this.dispatchContextMenuCommand.bind(this)))
this.disposables.add(this.applicationDelegate.onURIMessage(this.dispatchURIMessage.bind(this)))
- this.disposables.add(this.applicationDelegate.onDidRequestUnload(async () => {
- try {
- await this.saveState({isUnloading: true})
- } catch (error) {
- console.error(error)
- }
-
- const closing = !this.workspace || await this.workspace.confirmClose({
- windowCloseRequested: true,
- projectHasPaths: this.project.getPaths().length > 0
- })
-
- if (closing) await this.packages.deactivatePackages()
- return closing
- }))
+ this.disposables.add(this.applicationDelegate.onDidRequestUnload(this.prepareToUnloadEditorWindow.bind(this)))
this.listenForUpdates()
@@ -889,12 +879,30 @@ class AtomEnvironment {
}
}
+ async prepareToUnloadEditorWindow () {
+ try {
+ await this.saveState({isUnloading: true})
+ } catch (error) {
+ console.error(error)
+ }
+
+ const closing = !this.workspace || await this.workspace.confirmClose({
+ windowCloseRequested: true,
+ projectHasPaths: this.project.getPaths().length > 0
+ })
+
+ if (closing) {
+ this.unloading = true
+ await this.packages.deactivatePackages()
+ }
+ return closing
+ }
+
unloadEditorWindow () {
if (!this.project) return
this.storeWindowBackground()
this.saveBlobStoreSync()
- this.unloaded = true
}
saveBlobStoreSync () {
diff --git a/src/babel.js b/src/babel.js
index a944f2e8c..8476a33c0 100644
--- a/src/babel.js
+++ b/src/babel.js
@@ -11,7 +11,8 @@ var PREFIXES = [
'/** @babel */',
'"use babel"',
'\'use babel\'',
- '/* @flow */'
+ '/* @flow */',
+ '// @flow'
]
var PREFIX_LENGTH = Math.max.apply(Math, PREFIXES.map(function (prefix) {
diff --git a/src/clipboard.js b/src/clipboard.js
index d36bb1018..eafc04d6f 100644
--- a/src/clipboard.js
+++ b/src/clipboard.js
@@ -1,5 +1,5 @@
const crypto = require('crypto')
-const clipboard = require('./safe-clipboard')
+const {clipboard} = require('electron')
// Extended: Represents the clipboard used for copying and pasting in Atom.
//
@@ -7,10 +7,10 @@ const clipboard = require('./safe-clipboard')
//
// ## Examples
//
-// ```coffee
+// ```js
// atom.clipboard.write('hello')
//
-// console.log(atom.clipboard.read()) # 'hello'
+// console.log(atom.clipboard.read()) // 'hello'
// ```
module.exports =
class Clipboard {
@@ -59,7 +59,7 @@ class Clipboard {
// * `text` The {String} clipboard text.
// * `metadata` The metadata stored by an earlier call to {::write}.
readWithMetadata () {
- let text = this.read()
+ const text = this.read()
if (this.signatureForMetadata === this.md5(text)) {
return {text, metadata: this.metadata}
} else {
diff --git a/src/command-installer.js b/src/command-installer.js
index 85360da17..b432023ba 100644
--- a/src/command-installer.js
+++ b/src/command-installer.js
@@ -27,22 +27,36 @@ class CommandInstaller {
}, () => {})
}
- this.installAtomCommand(true, error => {
+ this.installAtomCommand(true, (error, atomCommandName) => {
if (error) return showErrorDialog(error)
- this.installApmCommand(true, error => {
+ this.installApmCommand(true, (error, apmCommandName) => {
if (error) return showErrorDialog(error)
this.applicationDelegate.confirm({
message: 'Commands installed.',
- detail: 'The shell commands `atom` and `apm` are installed.'
+ detail: `The shell commands \`${atomCommandName}\` and \`${apmCommandName}\` are installed.`
}, () => {})
})
})
}
+ getCommandNameForChannel (commandName) {
+ let channelMatch = this.appVersion.match(/beta|nightly/)
+ let channel = channelMatch ? channelMatch[0] : ''
+
+ switch (channel) {
+ case 'beta':
+ return `${commandName}-beta`
+ case 'nightly':
+ return `${commandName}-nightly`
+ default:
+ return commandName
+ }
+ }
+
installAtomCommand (askForPrivilege, callback) {
this.installCommand(
path.join(this.getResourcesDirectory(), 'app', 'atom.sh'),
- this.appVersion.includes('beta') ? 'atom-beta' : 'atom',
+ this.getCommandNameForChannel('atom'),
askForPrivilege,
callback
)
@@ -51,7 +65,7 @@ class CommandInstaller {
installApmCommand (askForPrivilege, callback) {
this.installCommand(
path.join(this.getResourcesDirectory(), 'app', 'apm', 'node_modules', '.bin', 'apm'),
- this.appVersion.includes('beta') ? 'apm-beta' : 'apm',
+ this.getCommandNameForChannel('apm'),
askForPrivilege,
callback
)
@@ -64,11 +78,11 @@ class CommandInstaller {
fs.readlink(destinationPath, (error, realpath) => {
if (error && error.code !== 'ENOENT') return callback(error)
- if (realpath === commandPath) return callback()
+ if (realpath === commandPath) return callback(null, commandName)
this.createSymlink(fs, commandPath, destinationPath, error => {
if (error && error.code === 'EACCES' && askForPrivilege) {
const fsAdmin = require('fs-admin')
- this.createSymlink(fsAdmin, commandPath, destinationPath, callback)
+ this.createSymlink(fsAdmin, commandPath, destinationPath, (error) => { callback(error, commandName) })
} else {
callback(error)
}
diff --git a/src/compile-cache.js b/src/compile-cache.js
index a4f9ded1e..ea387a631 100644
--- a/src/compile-cache.js
+++ b/src/compile-cache.js
@@ -17,6 +17,7 @@ var packageTranspilationRegistry = new PackageTranspilationRegistry()
var COMPILERS = {
'.js': packageTranspilationRegistry.wrapTranspiler(require('./babel')),
'.ts': packageTranspilationRegistry.wrapTranspiler(require('./typescript')),
+ '.tsx': packageTranspilationRegistry.wrapTranspiler(require('./typescript')),
'.coffee': packageTranspilationRegistry.wrapTranspiler(require('./coffee-script'))
}
diff --git a/src/config-file.js b/src/config-file.js
index 38912beb1..f8cba37ef 100644
--- a/src/config-file.js
+++ b/src/config-file.js
@@ -6,6 +6,7 @@ const {watchPath} = require('./path-watcher')
const CSON = require('season')
const Path = require('path')
const async = require('async')
+const temp = require('temp')
const EVENT_TYPES = new Set([
'created',
@@ -15,6 +16,21 @@ const EVENT_TYPES = new Set([
module.exports =
class ConfigFile {
+ static at (path) {
+ if (!this._known) {
+ this._known = new Map()
+ }
+
+ const existing = this._known.get(path)
+ if (existing) {
+ return existing
+ }
+
+ const created = new ConfigFile(path)
+ this._known.set(path, created)
+ return created
+ }
+
constructor (path) {
this.path = path
this.emitter = new Emitter()
@@ -22,9 +38,11 @@ class ConfigFile {
this.reloadCallbacks = []
// Use a queue to prevent multiple concurrent write to the same file.
- const writeQueue = async.queue((data, callback) =>
- CSON.writeFile(this.path, data, error => {
- if (error) {
+ const writeQueue = async.queue((data, callback) => {
+ (async () => {
+ try {
+ await writeCSONFileAtomically(this.path, data)
+ } catch (error) {
this.emitter.emit('did-error', dedent `
Failed to write \`${Path.basename(this.path)}\`.
@@ -32,8 +50,8 @@ class ConfigFile {
`)
}
callback()
- })
- )
+ })()
+ })
this.requestLoad = _.debounce(() => this.reload(), 200)
this.requestSave = _.debounce((data) => writeQueue.push(data), 200)
@@ -101,3 +119,27 @@ class ConfigFile {
})
}
}
+
+function writeCSONFile (path, data) {
+ return new Promise((resolve, reject) => {
+ CSON.writeFile(path, data, error => {
+ if (error) reject(error)
+ else resolve()
+ })
+ })
+}
+
+async function writeCSONFileAtomically (path, data) {
+ const tempPath = temp.path()
+ await writeCSONFile(tempPath, data)
+ await rename(tempPath, path)
+}
+
+function rename (oldPath, newPath) {
+ return new Promise((resolve, reject) => {
+ fs.rename(oldPath, newPath, error => {
+ if (error) reject(error)
+ else resolve()
+ })
+ })
+}
diff --git a/src/config-schema.js b/src/config-schema.js
index 97a6d16f3..343726d2c 100644
--- a/src/config-schema.js
+++ b/src/config-schema.js
@@ -355,6 +355,21 @@ const configSchema = {
type: 'boolean',
default: false,
description: 'Experimental: Use the new Tree-sitter parsing system for supported languages.'
+ },
+ colorProfile: {
+ description: "Specify whether Atom should use the operating system's color profile (recommended) or an alternative color profile.
Changing this setting will require a relaunch of Atom to take effect.",
+ type: 'string',
+ default: 'default',
+ enum: [
+ {
+ value: 'default',
+ description: 'Use color profile configured in the operating system'
+ },
+ {
+ value: 'srgb',
+ description: 'Use sRGB color profile'
+ }
+ ]
}
}
},
diff --git a/src/config.js b/src/config.js
index b3636aa10..ce20db30c 100644
--- a/src/config.js
+++ b/src/config.js
@@ -8,6 +8,8 @@ const Color = require('./color')
const ScopedPropertyStore = require('scoped-property-store')
const ScopeDescriptor = require('./scope-descriptor')
+const schemaEnforcers = {}
+
// Essential: Used to access all of Atom's configuration details.
//
// An instance of this class is always available as the `atom.config` global.
@@ -41,7 +43,7 @@ const ScopeDescriptor = require('./scope-descriptor')
// ### Value Coercion
//
// Config settings each have a type specified by way of a
-// [schema](json-schema.org). For example we might an integer setting that only
+// [schema](json-schema.org). For example we might want an integer setting that only
// allows integers greater than `0`:
//
// ```coffee
@@ -359,8 +361,6 @@ const ScopeDescriptor = require('./scope-descriptor')
//
// * Don't depend on (or write to) configuration keys outside of your keypath.
//
-const schemaEnforcers = {}
-
class Config {
static addSchemaEnforcer (typeName, enforcerFunction) {
if (schemaEnforcers[typeName] == null) { schemaEnforcers[typeName] = [] }
@@ -823,21 +823,7 @@ class Config {
}
getLegacyScopeDescriptorForNewScopeDescriptor (scopeDescriptor) {
- scopeDescriptor = ScopeDescriptor.fromObject(scopeDescriptor)
- const legacyAlias = this.legacyScopeAliases.get(scopeDescriptor.scopes[0])
- if (legacyAlias) {
- const scopes = scopeDescriptor.scopes.slice()
- scopes[0] = legacyAlias
- return new ScopeDescriptor({scopes})
- }
- }
-
- setLegacyScopeAliasForNewScope (languageId, legacyScopeName) {
- this.legacyScopeAliases.set(languageId, legacyScopeName)
- }
-
- removeLegacyScopeAliasForNewScope (languageId) {
- this.legacyScopeAliases.delete(languageId)
+ return null
}
/*
diff --git a/src/cursor.js b/src/cursor.js
index 929dc741f..f75f94709 100644
--- a/src/cursor.js
+++ b/src/cursor.js
@@ -326,7 +326,9 @@ class Cursor extends Model {
// Public: Moves the cursor to the bottom of the buffer.
moveToBottom () {
+ const column = this.goalColumn
this.setBufferPosition(this.editor.getEofBufferPosition())
+ this.goalColumn = column
}
// Public: Moves the cursor to the beginning of the line.
@@ -664,7 +666,7 @@ class Cursor extends Model {
// Returns a {RegExp}.
wordRegExp (options) {
const nonWordCharacters = _.escapeRegExp(this.getNonWordCharacters())
- let source = `^[\t\r ]*$|[^\\s${nonWordCharacters}]+`
+ let source = `^[\t ]*$|[^\\s${nonWordCharacters}]+`
if (!options || options.includeNonWordCharacters !== false) {
source += `|${`[${nonWordCharacters}]+`}`
}
@@ -711,6 +713,7 @@ class Cursor extends Model {
changePosition (options, fn) {
this.clearSelection({autoscroll: false})
fn()
+ this.goalColumn = null
const autoscroll = (options && options.autoscroll != null)
? options.autoscroll
: this.isLastCursor()
diff --git a/src/decoration.js b/src/decoration.js
index f1126f7a9..69bbcaa19 100644
--- a/src/decoration.js
+++ b/src/decoration.js
@@ -157,7 +157,7 @@ class Decoration {
// ## Examples
//
// ```coffee
- // decoration.update({type: 'line-number', class: 'my-new-class'})
+ // decoration.setProperties({type: 'line-number', class: 'my-new-class'})
// ```
//
// * `newProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}`
diff --git a/src/deprecated-packages.coffee b/src/deprecated-packages.coffee
deleted file mode 100644
index 2eeea435c..000000000
--- a/src/deprecated-packages.coffee
+++ /dev/null
@@ -1,43 +0,0 @@
-semver = require 'semver'
-
-deprecatedPackages = require('../package.json')?._deprecatedPackages ? {}
-ranges = {}
-
-exports.getDeprecatedPackageMetadata = (name) ->
- metadata = null
- if deprecatedPackages.hasOwnProperty(name)
- metadata = deprecatedPackages[name]
- Object.freeze(metadata) if metadata
- metadata
-
-exports.isDeprecatedPackage = (name, version) ->
- return false unless deprecatedPackages.hasOwnProperty(name)
-
- deprecatedVersionRange = deprecatedPackages[name].version
- return true unless deprecatedVersionRange
-
- semver.valid(version) and satisfies(version, deprecatedVersionRange)
-
-satisfies = (version, rawRange) ->
- unless parsedRange = ranges[rawRange]
- parsedRange = new Range(rawRange)
- ranges[rawRange] = parsedRange
- parsedRange.test(version)
-
-# Extend semver.Range to memoize matched versions for speed
-class Range extends semver.Range
- constructor: ->
- super
- @matchedVersions = new Set()
- @unmatchedVersions = new Set()
-
- test: (version) ->
- return true if @matchedVersions.has(version)
- return false if @unmatchedVersions.has(version)
-
- matches = super
- if matches
- @matchedVersions.add(version)
- else
- @unmatchedVersions.add(version)
- matches
diff --git a/src/dock.js b/src/dock.js
index a9a97b43d..dc77365fb 100644
--- a/src/dock.js
+++ b/src/dock.js
@@ -1,11 +1,11 @@
-'use strict'
-
+const etch = require('etch')
const _ = require('underscore-plus')
const {CompositeDisposable, Emitter} = require('event-kit')
const PaneContainer = require('./pane-container')
const TextEditor = require('./text-editor')
const Grim = require('grim')
+const $ = etch.dom
const MINIMUM_SIZE = 100
const DEFAULT_INITIAL_SIZE = 300
const SHOULD_ANIMATE_CLASS = 'atom-dock-should-animate'
@@ -26,6 +26,8 @@ module.exports = class Dock {
this.handleMouseUp = this.handleMouseUp.bind(this)
this.handleDrag = _.throttle(this.handleDrag.bind(this), 30)
this.handleDragEnd = this.handleDragEnd.bind(this)
+ this.handleToggleButtonDragEnter = this.handleToggleButtonDragEnter.bind(this)
+ this.toggle = this.toggle.bind(this)
this.location = params.location
this.widthOrHeight = getWidthOrHeight(this.location)
@@ -72,11 +74,16 @@ module.exports = class Dock {
// This method is called explicitly by the object which adds the Dock to the document.
elementAttached () {
// Re-render when the dock is attached to make sure we remeasure sizes defined in CSS.
- this.render(this.state)
+ etch.updateSync(this)
}
getElement () {
- if (!this.element) this.render(this.state)
+ // Because this code is included in the snapshot, we have to make sure we don't touch the DOM
+ // during initialization. Therefore, we defer initialization of the component (which creates a
+ // DOM element) until somebody asks for the element.
+ if (this.element == null) {
+ etch.initialize(this)
+ }
return this.element
}
@@ -151,9 +158,16 @@ module.exports = class Dock {
}
this.state = nextState
- this.render(this.state)
const {hovered, visible} = this.state
+
+ // Render immediately if the dock becomes visible or the size changes in case people are
+ // measuring after opening, for example.
+ if (this.element != null) {
+ if ((visible && !prevState.visible) || (this.state.size !== prevState.size)) etch.updateSync(this)
+ else etch.update(this)
+ }
+
if (hovered !== prevState.hovered) {
this.emitter.emit('did-change-hovered', hovered)
}
@@ -162,80 +176,76 @@ module.exports = class Dock {
}
}
- render (state) {
- if (this.element == null) {
- this.element = document.createElement('atom-dock')
- this.element.classList.add(this.location)
- this.innerElement = document.createElement('div')
- this.innerElement.classList.add('atom-dock-inner', this.location)
- this.maskElement = document.createElement('div')
- this.maskElement.classList.add('atom-dock-mask')
- this.wrapperElement = document.createElement('div')
- this.wrapperElement.classList.add('atom-dock-content-wrapper', this.location)
- this.resizeHandle = new DockResizeHandle({
- location: this.location,
- onResizeStart: this.handleResizeHandleDragStart,
- onResizeToFit: this.handleResizeToFit
- })
- this.toggleButton = new DockToggleButton({
- onDragEnter: this.handleToggleButtonDragEnter.bind(this),
- location: this.location,
- toggle: this.toggle.bind(this)
- })
- this.cursorOverlayElement = document.createElement('div')
- this.cursorOverlayElement.classList.add('atom-dock-cursor-overlay', this.location)
+ render () {
+ const innerElementClassList = ['atom-dock-inner', this.location]
+ if (this.state.visible) innerElementClassList.push(VISIBLE_CLASS)
- // Add the children to the DOM tree
- this.element.appendChild(this.innerElement)
- this.innerElement.appendChild(this.maskElement)
- this.maskElement.appendChild(this.wrapperElement)
- this.wrapperElement.appendChild(this.resizeHandle.getElement())
- this.wrapperElement.appendChild(this.paneContainer.getElement())
- this.wrapperElement.appendChild(this.cursorOverlayElement)
- // The toggle button must be rendered outside the mask because (1) it shouldn't be masked and
- // (2) if we made the mask larger to avoid masking it, the mask would block mouse events.
- this.innerElement.appendChild(this.toggleButton.getElement())
- }
+ const maskElementClassList = ['atom-dock-mask']
+ if (this.state.shouldAnimate) maskElementClassList.push(SHOULD_ANIMATE_CLASS)
- if (state.visible) {
- this.innerElement.classList.add(VISIBLE_CLASS)
- } else {
- this.innerElement.classList.remove(VISIBLE_CLASS)
- }
+ const cursorOverlayElementClassList = ['atom-dock-cursor-overlay', this.location]
+ if (this.state.resizing) cursorOverlayElementClassList.push(CURSOR_OVERLAY_VISIBLE_CLASS)
- if (state.shouldAnimate) {
- this.maskElement.classList.add(SHOULD_ANIMATE_CLASS)
- } else {
- this.maskElement.classList.remove(SHOULD_ANIMATE_CLASS)
- }
-
- if (state.resizing) {
- this.cursorOverlayElement.classList.add(CURSOR_OVERLAY_VISIBLE_CLASS)
- } else {
- this.cursorOverlayElement.classList.remove(CURSOR_OVERLAY_VISIBLE_CLASS)
- }
-
- const shouldBeVisible = state.visible || state.showDropTarget
+ const shouldBeVisible = this.state.visible || this.state.showDropTarget
const size = Math.max(MINIMUM_SIZE,
- state.size ||
- (state.draggingItem && getPreferredSize(state.draggingItem, this.location)) ||
+ this.state.size ||
+ (this.state.draggingItem && getPreferredSize(this.state.draggingItem, this.location)) ||
DEFAULT_INITIAL_SIZE
)
// We need to change the size of the mask...
- this.maskElement.style[this.widthOrHeight] = `${shouldBeVisible ? size : 0}px`
+ const maskStyle = {[this.widthOrHeight]: `${shouldBeVisible ? size : 0}px`}
// ...but the content needs to maintain a constant size.
- this.wrapperElement.style[this.widthOrHeight] = `${size}px`
+ const wrapperStyle = {[this.widthOrHeight]: `${size}px`}
- this.resizeHandle.update({dockIsVisible: this.state.visible})
- this.toggleButton.update({
- dockIsVisible: shouldBeVisible,
- visible:
- // Don't show the toggle button if the dock is closed and empty...
- (state.hovered && (this.state.visible || this.getPaneItems().length > 0)) ||
- // ...or if the item can't be dropped in that dock.
- (!shouldBeVisible && state.draggingItem && isItemAllowed(state.draggingItem, this.location))
- })
+ return $(
+ 'atom-dock',
+ {className: this.location},
+ $.div(
+ {ref: 'innerElement', className: innerElementClassList.join(' ')},
+ $.div(
+ {
+ className: maskElementClassList.join(' '),
+ style: maskStyle
+ },
+ $.div(
+ {
+ ref: 'wrapperElement',
+ className: `atom-dock-content-wrapper ${this.location}`,
+ style: wrapperStyle
+ },
+ $(DockResizeHandle, {
+ location: this.location,
+ onResizeStart: this.handleResizeHandleDragStart,
+ onResizeToFit: this.handleResizeToFit,
+ dockIsVisible: this.state.visible
+ }),
+ $(ElementComponent, {element: this.paneContainer.getElement()}),
+ $.div({className: cursorOverlayElementClassList.join(' ')})
+ )
+ ),
+ $(DockToggleButton, {
+ ref: 'toggleButton',
+ onDragEnter: this.state.draggingItem ? this.handleToggleButtonDragEnter : null,
+ location: this.location,
+ toggle: this.toggle,
+ dockIsVisible: shouldBeVisible,
+ visible:
+ // Don't show the toggle button if the dock is closed and empty...
+ (this.state.hovered &&
+ (this.state.visible || this.getPaneItems().length > 0)) ||
+ // ...or if the item can't be dropped in that dock.
+ (!shouldBeVisible &&
+ this.state.draggingItem &&
+ isItemAllowed(this.state.draggingItem, this.location))
+ })
+ )
+ )
+ }
+
+ update (props) {
+ // Since we're interopping with non-etch stuff, this method's actually never called.
+ return etch.update(this)
}
handleDidAddPaneItem () {
@@ -321,7 +331,7 @@ module.exports = class Dock {
// area considered when detecting exit MUST fully encompass the area considered when detecting
// entry.
pointWithinHoverArea (point, detectingExit) {
- const dockBounds = this.innerElement.getBoundingClientRect()
+ const dockBounds = this.refs.innerElement.getBoundingClientRect()
// Copy the bounds object since we can't mutate it.
const bounds = {
@@ -370,7 +380,7 @@ module.exports = class Dock {
// remove it as an argument and determine whether we're inside the toggle button using
// mouseenter/leave events on it. This class would still need to keep track of the mouse
// position (via a mousemove listener) for the other measurements, though.
- const toggleButtonBounds = this.toggleButton.getBounds()
+ const toggleButtonBounds = this.refs.toggleButton.getBounds()
if (rectContainsPoint(toggleButtonBounds, point)) return true
// The area used when detecting exit is actually larger than when detecting entrances. Expand
@@ -707,13 +717,18 @@ module.exports = class Dock {
class DockResizeHandle {
constructor (props) {
- this.handleMouseDown = this.handleMouseDown.bind(this)
-
- this.element = document.createElement('div')
- this.element.classList.add('atom-dock-resize-handle', props.location)
- this.element.addEventListener('mousedown', this.handleMouseDown)
this.props = props
- this.update(props)
+ etch.initialize(this)
+ }
+
+ render () {
+ const classList = ['atom-dock-resize-handle', this.props.location]
+ if (this.props.dockIsVisible) classList.push(RESIZE_HANDLE_RESIZABLE_CLASS)
+
+ return $.div({
+ className: classList.join(' '),
+ on: {mousedown: this.handleMouseDown}
+ })
}
getElement () {
@@ -729,12 +744,7 @@ class DockResizeHandle {
update (newProps) {
this.props = Object.assign({}, this.props, newProps)
-
- if (this.props.dockIsVisible) {
- this.element.classList.add(RESIZE_HANDLE_RESIZABLE_CLASS)
- } else {
- this.element.classList.remove(RESIZE_HANDLE_RESIZABLE_CLASS)
- }
+ return etch.update(this)
}
handleMouseDown (event) {
@@ -748,22 +758,34 @@ class DockResizeHandle {
class DockToggleButton {
constructor (props) {
- this.handleClick = this.handleClick.bind(this)
- this.handleDragEnter = this.handleDragEnter.bind(this)
-
- this.element = document.createElement('div')
- this.element.classList.add('atom-dock-toggle-button', props.location)
- this.element.classList.add(props.location)
- this.innerElement = document.createElement('div')
- this.innerElement.classList.add('atom-dock-toggle-button-inner', props.location)
- this.innerElement.addEventListener('click', this.handleClick)
- this.innerElement.addEventListener('dragenter', this.handleDragEnter)
- this.iconElement = document.createElement('span')
- this.innerElement.appendChild(this.iconElement)
- this.element.appendChild(this.innerElement)
-
this.props = props
- this.update(props)
+ etch.initialize(this)
+ }
+
+ render () {
+ const classList = ['atom-dock-toggle-button', this.props.location]
+ if (this.props.visible) classList.push(TOGGLE_BUTTON_VISIBLE_CLASS)
+
+ return $.div(
+ {className: classList.join(' ')},
+ $.div(
+ {
+ ref: 'innerElement',
+ className: `atom-dock-toggle-button-inner ${this.props.location}`,
+ on: {
+ click: this.handleClick,
+ dragenter: this.props.onDragEnter
+ }
+ },
+ $.span({
+ ref: 'iconElement',
+ className: `icon ${getIconName(
+ this.props.location,
+ this.props.dockIsVisible
+ )}`
+ })
+ )
+ )
}
getElement () {
@@ -771,27 +793,28 @@ class DockToggleButton {
}
getBounds () {
- return this.innerElement.getBoundingClientRect()
+ return this.refs.innerElement.getBoundingClientRect()
}
update (newProps) {
this.props = Object.assign({}, this.props, newProps)
-
- if (this.props.visible) {
- this.element.classList.add(TOGGLE_BUTTON_VISIBLE_CLASS)
- } else {
- this.element.classList.remove(TOGGLE_BUTTON_VISIBLE_CLASS)
- }
-
- this.iconElement.className = 'icon ' + getIconName(this.props.location, this.props.dockIsVisible)
+ return etch.update(this)
}
handleClick () {
this.props.toggle()
}
+}
- handleDragEnter () {
- this.props.onDragEnter()
+// An etch component that doesn't use etch, this component provides a gateway from JSX back into
+// the mutable DOM world.
+class ElementComponent {
+ constructor (props) {
+ this.element = props.element
+ }
+
+ update (props) {
+ this.element = props.element
}
}
diff --git a/src/git-repository.js b/src/git-repository.js
index 55d70c12c..80f76e40a 100644
--- a/src/git-repository.js
+++ b/src/git-repository.js
@@ -163,7 +163,7 @@ class GitRepository {
// Public: Invoke the given callback when a multiple files' statuses have
// changed. For example, on window focus, the status of all the paths in the
// repo is checked. If any of them have changed, this will be fired. Call
- // {::getPathStatus(path)} to get the status for your path of choice.
+ // {::getPathStatus} to get the status for your path of choice.
//
// * `callback` {Function}
//
diff --git a/src/grammar-registry.js b/src/grammar-registry.js
index 20757fb0b..0c79d5b1e 100644
--- a/src/grammar-registry.js
+++ b/src/grammar-registry.js
@@ -10,7 +10,6 @@ const Token = require('./token')
const fs = require('fs-plus')
const {Point, Range} = require('text-buffer')
-const GRAMMAR_TYPE_BONUS = 1000
const PATH_SPLIT_REGEX = new RegExp('[/.]')
// Extended: This class holds the grammars used for tokenizing.
@@ -38,6 +37,14 @@ class GrammarRegistry {
const grammarAddedOrUpdated = this.grammarAddedOrUpdated.bind(this)
this.textmateRegistry.onDidAddGrammar(grammarAddedOrUpdated)
this.textmateRegistry.onDidUpdateGrammar(grammarAddedOrUpdated)
+
+ this.subscriptions.add(this.config.onDidChange('core.useTreeSitterParsers', () => {
+ this.grammarScoresByBuffer.forEach((score, buffer) => {
+ if (!this.languageOverridesByBufferId.has(buffer.id)) {
+ this.autoAssignLanguageMode(buffer)
+ }
+ })
+ }))
}
serialize () {
@@ -115,7 +122,6 @@ class GrammarRegistry {
// found.
assignLanguageMode (buffer, languageId) {
if (buffer.getBuffer) buffer = buffer.getBuffer()
- languageId = this.normalizeLanguageId(languageId)
let grammar = null
if (languageId != null) {
@@ -128,7 +134,7 @@ class GrammarRegistry {
}
this.grammarScoresByBuffer.set(buffer, null)
- if (grammar.scopeName !== buffer.getLanguageMode().getLanguageId()) {
+ if (grammar !== buffer.getLanguageMode().grammar) {
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
}
@@ -155,14 +161,14 @@ class GrammarRegistry {
)
this.languageOverridesByBufferId.delete(buffer.id)
this.grammarScoresByBuffer.set(buffer, result.score)
- if (result.grammar.scopeName !== buffer.getLanguageMode().getLanguageId()) {
+ if (result.grammar !== buffer.getLanguageMode().grammar) {
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(result.grammar, buffer))
}
}
languageModeForGrammarAndBuffer (grammar, buffer) {
if (grammar instanceof TreeSitterGrammar) {
- return new TreeSitterLanguageMode({grammar, buffer, config: this.config})
+ return new TreeSitterLanguageMode({grammar, buffer, config: this.config, grammars: this})
} else {
return new TextMateLanguageMode({grammar, buffer, config: this.config})
}
@@ -201,16 +207,34 @@ class GrammarRegistry {
contents = fs.readFileSync(filePath, 'utf8')
}
+ // Initially identify matching grammars based on the filename and the first
+ // line of the file.
let score = this.getGrammarPathScore(grammar, filePath)
- if (score > 0 && !grammar.bundledPackage) {
- score += 0.125
- }
- if (this.grammarMatchesContents(grammar, contents)) {
- score += 0.25
- }
+ if (this.grammarMatchesPrefix(grammar, contents)) score += 0.5
- if (score > 0 && this.isGrammarPreferredType(grammar)) {
- score += GRAMMAR_TYPE_BONUS
+ // If multiple grammars match by one of the above criteria, break ties.
+ if (score > 0) {
+ // Prefer either TextMate or Tree-sitter grammars based on the user's settings.
+ if (grammar instanceof TreeSitterGrammar) {
+ if (this.config.get('core.useTreeSitterParsers')) {
+ score += 0.1
+ } else {
+ return -Infinity
+ }
+ }
+
+ // Prefer grammars with matching content regexes. Prefer a grammar with no content regex
+ // over one with a non-matching content regex.
+ if (grammar.contentRegex) {
+ if (grammar.contentRegex.test(contents)) {
+ score += 0.05
+ } else {
+ score -= 0.05
+ }
+ }
+
+ // Prefer grammars that the user has manually installed over bundled grammars.
+ if (!grammar.bundledPackage) score += 0.01
}
return score
@@ -248,12 +272,8 @@ class GrammarRegistry {
return pathScore
}
- grammarMatchesContents (grammar, contents) {
- if (contents == null) return false
-
- if (grammar.contentRegExp) { // TreeSitter grammars
- return grammar.contentRegExp.test(contents)
- } else if (grammar.firstLineRegex) { // FirstMate grammars
+ grammarMatchesPrefix (grammar, contents) {
+ if (contents && grammar.firstLineRegex) {
let escaped = false
let numberOfNewlinesInRegex = 0
for (let character of grammar.firstLineRegex.source) {
@@ -270,8 +290,12 @@ class GrammarRegistry {
}
}
- const lines = contents.split('\n')
- return grammar.firstLineRegex.testSync(lines.slice(0, numberOfNewlinesInRegex + 1).join('\n'))
+ const prefix = contents.split('\n').slice(0, numberOfNewlinesInRegex + 1).join('\n')
+ if (grammar.firstLineRegex.testSync) {
+ return grammar.firstLineRegex.testSync(prefix)
+ } else {
+ return grammar.firstLineRegex.test(prefix)
+ }
} else {
return false
}
@@ -279,18 +303,25 @@ class GrammarRegistry {
forEachGrammar (callback) {
this.textmateRegistry.grammars.forEach(callback)
- for (let grammarId in this.treeSitterGrammarsById) {
- callback(this.treeSitterGrammarsById[grammarId])
+ for (const grammarId in this.treeSitterGrammarsById) {
+ const grammar = this.treeSitterGrammarsById[grammarId]
+ if (grammar.scopeName) callback(grammar)
}
}
grammarForId (languageId) {
- languageId = this.normalizeLanguageId(languageId)
-
- return (
- this.textmateRegistry.grammarForScopeName(languageId) ||
- this.treeSitterGrammarsById[languageId]
- )
+ if (!languageId) return null
+ if (this.config.get('core.useTreeSitterParsers')) {
+ return (
+ this.treeSitterGrammarsById[languageId] ||
+ this.textmateRegistry.grammarForScopeName(languageId)
+ )
+ } else {
+ return (
+ this.textmateRegistry.grammarForScopeName(languageId) ||
+ this.treeSitterGrammarsById[languageId]
+ )
+ }
}
// Deprecated: Get the grammar override for the given file path.
@@ -335,26 +366,23 @@ class GrammarRegistry {
this.grammarScoresByBuffer.forEach((score, buffer) => {
const languageMode = buffer.getLanguageMode()
- if (grammar.injectionSelector) {
- if (languageMode.hasTokenForSelector(grammar.injectionSelector)) {
- languageMode.retokenizeLines()
- }
- return
- }
-
const languageOverride = this.languageOverridesByBufferId.get(buffer.id)
- if ((grammar.id === buffer.getLanguageMode().getLanguageId() ||
- grammar.id === languageOverride)) {
+ if (grammar === buffer.getLanguageMode().grammar ||
+ grammar === this.grammarForId(languageOverride)) {
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
+ return
} else if (!languageOverride) {
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))
this.grammarScoresByBuffer.set(buffer, score)
+ return
}
}
+
+ languageMode.updateForInjection(grammar)
})
}
@@ -379,6 +407,32 @@ class GrammarRegistry {
return this.textmateRegistry.onDidUpdateGrammar(callback)
}
+ // Experimental: Specify a type of syntax node that may embed other languages.
+ //
+ // * `grammarId` The {String} id of the parent language
+ // * `injectionPoint` An {Object} with the following keys:
+ // * `type` The {String} type of syntax node that may embed other languages
+ // * `language` A {Function} that is called with syntax nodes of the specified `type` and
+ // returns a {String} that will be tested against other grammars' `injectionRegex` in
+ // order to determine what language should be embedded.
+ // * `content` A {Function} that is called with syntax nodes of the specified `type` and
+ // returns another syntax node or array of syntax nodes that contain the embedded source code.
+ addInjectionPoint (grammarId, injectionPoint) {
+ const grammar = this.treeSitterGrammarsById[grammarId]
+ if (grammar) {
+ grammar.injectionPoints.push(injectionPoint)
+ } else {
+ this.treeSitterGrammarsById[grammarId] = {
+ injectionPoints: [injectionPoint]
+ }
+ }
+ return new Disposable(() => {
+ const grammar = this.treeSitterGrammarsById[grammarId]
+ const index = grammar.injectionPoints.indexOf(injectionPoint)
+ if (index !== -1) grammar.injectionPoints.splice(index, 1)
+ })
+ }
+
get nullGrammar () {
return this.textmateRegistry.nullGrammar
}
@@ -397,12 +451,9 @@ class GrammarRegistry {
addGrammar (grammar) {
if (grammar instanceof TreeSitterGrammar) {
- this.treeSitterGrammarsById[grammar.id] = grammar
- if (grammar.legacyScopeName) {
- this.config.setLegacyScopeAliasForNewScope(grammar.id, grammar.legacyScopeName)
- this.textMateScopeNamesByTreeSitterLanguageId.set(grammar.id, grammar.legacyScopeName)
- this.treeSitterLanguageIdsByTextMateScopeName.set(grammar.legacyScopeName, grammar.id)
- }
+ const existingParams = this.treeSitterGrammarsById[grammar.scopeName] || {}
+ if (grammar.scopeName) this.treeSitterGrammarsById[grammar.scopeName] = grammar
+ if (existingParams.injectionPoints) grammar.injectionPoints.push(...existingParams.injectionPoints)
this.grammarAddedOrUpdated(grammar)
return new Disposable(() => this.removeGrammar(grammar))
} else {
@@ -412,12 +463,7 @@ class GrammarRegistry {
removeGrammar (grammar) {
if (grammar instanceof TreeSitterGrammar) {
- delete this.treeSitterGrammarsById[grammar.id]
- if (grammar.legacyScopeName) {
- this.config.removeLegacyScopeAliasForNewScope(grammar.id)
- this.textMateScopeNamesByTreeSitterLanguageId.delete(grammar.id)
- this.treeSitterLanguageIdsByTextMateScopeName.delete(grammar.legacyScopeName)
- }
+ delete this.treeSitterGrammarsById[grammar.scopeName]
} else {
return this.textmateRegistry.removeGrammar(grammar)
}
@@ -503,10 +549,23 @@ class GrammarRegistry {
return this.textmateRegistry.scopeForId(id)
}
- isGrammarPreferredType (grammar) {
- return this.config.get('core.useTreeSitterParsers')
- ? grammar instanceof TreeSitterGrammar
- : grammar instanceof FirstMate.Grammar
+ treeSitterGrammarForLanguageString (languageString) {
+ let longestMatchLength = 0
+ let grammarWithLongestMatch = null
+ for (const id in this.treeSitterGrammarsById) {
+ const grammar = this.treeSitterGrammarsById[id]
+ if (grammar.injectionRegex) {
+ const match = languageString.match(grammar.injectionRegex)
+ if (match) {
+ const {length} = match[0]
+ if (length > longestMatchLength) {
+ grammarWithLongestMatch = grammar
+ longestMatchLength = length
+ }
+ }
+ }
+ }
+ return grammarWithLongestMatch
}
normalizeLanguageId (languageId) {
diff --git a/src/gutter-container.js b/src/gutter-container.js
index 3faece073..cd0c796b2 100644
--- a/src/gutter-container.js
+++ b/src/gutter-container.js
@@ -97,7 +97,7 @@ module.exports = class GutterContainer {
// The public interface is Gutter::decorateMarker or TextEditor::decorateMarker.
addGutterDecoration (gutter, marker, options) {
- if (gutter.name === 'line-number') {
+ if (gutter.type === 'line-number') {
options.type = 'line-number'
} else {
options.type = 'gutter'
diff --git a/src/gutter.js b/src/gutter.js
index 3bf7a72ea..bd5955b78 100644
--- a/src/gutter.js
+++ b/src/gutter.js
@@ -11,6 +11,12 @@ module.exports = class Gutter {
this.name = options && options.name
this.priority = (options && options.priority != null) ? options.priority : DefaultPriority
this.visible = (options && options.visible != null) ? options.visible : true
+ this.type = (options && options.type != null) ? options.type : 'decorated'
+ this.labelFn = options && options.labelFn
+ this.className = options && options.class
+
+ this.onMouseDown = options && options.onMouseDown
+ this.onMouseMove = options && options.onMouseMove
this.emitter = new Emitter()
}
diff --git a/src/initialize-application-window.coffee b/src/initialize-application-window.coffee
index f8f670cf5..913c83977 100644
--- a/src/initialize-application-window.coffee
+++ b/src/initialize-application-window.coffee
@@ -36,6 +36,9 @@ if global.isGeneratingSnapshot
require('image-view')
require('incompatible-packages')
require('keybinding-resolver')
+ require('language-html')
+ require('language-javascript')
+ require('language-ruby')
require('line-ending-selector')
require('link')
require('markdown-preview')
diff --git a/src/initialize-test-window.coffee b/src/initialize-test-window.coffee
index c6aaada0e..4cbd02bfd 100644
--- a/src/initialize-test-window.coffee
+++ b/src/initialize-test-window.coffee
@@ -24,9 +24,13 @@ module.exports = ({blobStore}) ->
ApplicationDelegate = require '../src/application-delegate'
Clipboard = require '../src/clipboard'
TextEditor = require '../src/text-editor'
+ {updateProcessEnv} = require('./update-process-env')
require './electron-shims'
- {testRunnerPath, legacyTestRunnerPath, headless, logFile, testPaths} = getWindowLoadSettings()
+ ipcRenderer.on 'environment', (event, env) ->
+ updateProcessEnv(env)
+
+ {testRunnerPath, legacyTestRunnerPath, headless, logFile, testPaths, env} = getWindowLoadSettings()
unless headless
# Show window synchronously so a focusout doesn't fire on input elements
@@ -59,6 +63,8 @@ module.exports = ({blobStore}) ->
require('module').globalPaths.push(exportsPath)
process.env.NODE_PATH = exportsPath # Set NODE_PATH env variable since tasks may need it.
+ updateProcessEnv(env)
+
# Set up optional transpilation for packages under test if any
FindParentDir = require 'find-parent-dir'
if packageRoot = FindParentDir.sync(testPaths[0], 'package.json')
diff --git a/src/main-process/atom-application.js b/src/main-process/atom-application.js
index 10b791761..c10895650 100644
--- a/src/main-process/atom-application.js
+++ b/src/main-process/atom-application.js
@@ -7,7 +7,7 @@ const Config = require('../config')
const ConfigFile = require('../config-file')
const FileRecoveryService = require('./file-recovery-service')
const ipcHelpers = require('../ipc-helpers')
-const {BrowserWindow, Menu, app, dialog, ipcMain, shell, screen} = require('electron')
+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')
@@ -113,10 +113,12 @@ class AtomApplication extends EventEmitter {
? path.join(process.env.ATOM_HOME, 'config.json')
: path.join(process.env.ATOM_HOME, 'config.cson')
- this.configFile = new ConfigFile(configFilePath)
+ this.configFile = ConfigFile.at(configFilePath)
this.config = new Config({
saveCallback: settings => {
- if (!this.quitting) return this.configFile.update(settings)
+ if (!this.quitting) {
+ return this.configFile.update(settings)
+ }
}
})
this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)})
@@ -137,7 +139,7 @@ class AtomApplication extends EventEmitter {
// 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) {
+ async initialize (options) {
global.atomApplication = this
// DEPRECATED: This can be removed at some point (added in 1.13)
@@ -147,14 +149,15 @@ class AtomApplication extends EventEmitter {
this.config.set('core.titleBar', 'custom')
}
- 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)
+ const result = await this.launch(options)
+ this.autoUpdateManager.initialize()
+ return result
}
async destroy () {
@@ -170,7 +173,8 @@ class AtomApplication extends EventEmitter {
if (!this.configFilePromise) {
this.configFilePromise = this.configFile.watch()
this.disposable.add(await this.configFilePromise)
- this.config.onDidChange('core.titleBar', this.promptForRestart.bind(this))
+ this.config.onDidChange('core.titleBar', () => this.promptForRestart())
+ this.config.onDidChange('core.colorProfile', () => this.promptForRestart())
}
const optionsForWindowsToOpen = []
@@ -202,7 +206,6 @@ class AtomApplication extends EventEmitter {
openWithOptions (options) {
const {
- projectSpecification,
initialPaths,
pathsToOpen,
executedFrom,
@@ -257,7 +260,6 @@ class AtomApplication extends EventEmitter {
profileStartup,
clearWindowState,
addToLastWindow,
- projectSpecification,
env
})
} else if (urlsToOpen.length > 0) {
@@ -435,9 +437,20 @@ class AtomApplication extends EventEmitter {
if (!this.quitting) {
this.quitting = true
event.preventDefault()
- const windowUnloadPromises = this.getAllWindows().map(window => window.prepareToUnload())
+ const windowUnloadPromises = this.getAllWindows().map(async window => {
+ const unloaded = await window.prepareToUnload()
+ if (unloaded) {
+ window.close()
+ await window.closedPromise
+ }
+ return unloaded
+ })
const windowUnloadedResults = await Promise.all(windowUnloadPromises)
- if (windowUnloadedResults.every(Boolean)) app.quit()
+ if (windowUnloadedResults.every(Boolean)) {
+ app.quit()
+ } else {
+ this.quitting = false
+ }
}
resolveBeforeQuitPromise()
@@ -505,12 +518,12 @@ class AtomApplication extends EventEmitter {
if (this.applicationMenu) this.applicationMenu.update(window, template, menu)
}))
- this.disposable.add(ipcHelpers.on(ipcMain, 'run-package-specs', (event, packageSpecPath) => {
- this.runTests({
+ this.disposable.add(ipcHelpers.on(ipcMain, 'run-package-specs', (event, packageSpecPath, options = {}) => {
+ this.runTests(Object.assign({
resourcePath: this.devResourcePath,
pathsToOpen: [packageSpecPath],
headless: false
- })
+ }, options))
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'run-benchmarks', (event, benchmarksPath) => {
@@ -561,9 +574,11 @@ class AtomApplication extends EventEmitter {
window.setPosition(x, y)
}))
- this.disposable.add(ipcHelpers.respondTo('set-user-settings', (window, settings) =>
- this.configFile.update(JSON.parse(settings))
- ))
+ this.disposable.add(ipcHelpers.respondTo('set-user-settings', (window, settings, filePath) => {
+ if (!this.quitting) {
+ ConfigFile.at(filePath || this.configFilePath).update(JSON.parse(settings))
+ }
+ }))
this.disposable.add(ipcHelpers.respondTo('center-window', window => window.center()))
this.disposable.add(ipcHelpers.respondTo('focus-window', window => window.focus()))
@@ -575,7 +590,6 @@ class AtomApplication extends EventEmitter {
win.temporaryState = state
}))
- const clipboard = require('../safe-clipboard')
this.disposable.add(ipcHelpers.on(ipcMain, 'write-text-to-selection-clipboard', (event, text) =>
clipboard.writeText(text, 'selection')
))
@@ -821,7 +835,6 @@ class AtomApplication extends EventEmitter {
window,
clearWindowState,
addToLastWindow,
- projectSpecification,
env
} = {}) {
if (!pathsToOpen || pathsToOpen.length === 0) return
@@ -855,7 +868,7 @@ class AtomApplication extends EventEmitter {
}
let openedWindow
- if (existingWindow && (projectSpecification == null || projectSpecification.config == null)) {
+ if (existingWindow) {
openedWindow = existingWindow
openedWindow.openLocations(locationsToOpen)
if (openedWindow.isMinimized()) {
@@ -891,7 +904,6 @@ class AtomApplication extends EventEmitter {
windowDimensions,
profileStartup,
clearWindowState,
- projectSpecification,
env
})
this.addWindow(openedWindow)
@@ -1162,6 +1174,7 @@ class AtomApplication extends EventEmitter {
env
})
this.addWindow(window)
+ if (env) window.replaceEnvironment(env)
return window
}
diff --git a/src/main-process/atom-protocol-handler.js b/src/main-process/atom-protocol-handler.js
index 1affba02a..47c3da14f 100644
--- a/src/main-process/atom-protocol-handler.js
+++ b/src/main-process/atom-protocol-handler.js
@@ -20,6 +20,7 @@ class AtomProtocolHandler {
if (!safeMode) {
this.loadPaths.push(path.join(process.env.ATOM_HOME, 'dev', 'packages'))
+ this.loadPaths.push(path.join(resourcePath, 'packages'))
}
this.loadPaths.push(path.join(process.env.ATOM_HOME, 'packages'))
diff --git a/src/main-process/atom-window.js b/src/main-process/atom-window.js
index 7c4151403..a56679143 100644
--- a/src/main-process/atom-window.js
+++ b/src/main-process/atom-window.js
@@ -22,7 +22,6 @@ class AtomWindow extends EventEmitter {
this.safeMode = settings.safeMode
this.devMode = settings.devMode
this.resourcePath = settings.resourcePath
- this.projectSpecification = settings.projectSpecification
let {pathToOpen, locationsToOpen} = settings
if (!locationsToOpen && pathToOpen) locationsToOpen = [{pathToOpen}]
@@ -52,7 +51,7 @@ class AtomWindow extends EventEmitter {
// 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.shouldAddCustomInsetTitleBar()) options.titleBarStyle = 'hiddenInset'
if (this.shouldHideTitleBar()) options.frame = false
this.browserWindow = new BrowserWindow(options)
@@ -60,8 +59,7 @@ class AtomWindow extends EventEmitter {
get: () => JSON.stringify(Object.assign({
userSettings: !this.isSpec
? this.atomApplication.configFile.get()
- : null,
- projectSpecification: this.projectSpecification
+ : null
}, this.loadSettings))
})
@@ -187,6 +185,7 @@ class AtomWindow extends EventEmitter {
dialog.showMessageBox(this.browserWindow, {
type: 'warning',
buttons: ['Force Close', 'Keep Waiting'],
+ cancelId: 1, // Canceling should be the least destructive action
message: 'Editor is not responding',
detail:
'The editor is not responding. Would you like to force close it or just keep waiting?'
@@ -204,6 +203,7 @@ class AtomWindow extends EventEmitter {
dialog.showMessageBox(this.browserWindow, {
type: 'warning',
buttons: ['Close Window', 'Reload', 'Keep It Open'],
+ cancelId: 2, // Canceling should be the least destructive action
message: 'The editor has crashed',
detail: 'Please report this issue to https://github.com/atom/atom'
}, response => {
diff --git a/src/main-process/auto-update-manager.coffee b/src/main-process/auto-update-manager.coffee
deleted file mode 100644
index bc81d425d..000000000
--- a/src/main-process/auto-update-manager.coffee
+++ /dev/null
@@ -1,143 +0,0 @@
-autoUpdater = null
-{EventEmitter} = require 'events'
-path = require 'path'
-
-IdleState = 'idle'
-CheckingState = 'checking'
-DownloadingState = 'downloading'
-UpdateAvailableState = 'update-available'
-NoUpdateAvailableState = 'no-update-available'
-UnsupportedState = 'unsupported'
-ErrorState = 'error'
-
-module.exports =
-class AutoUpdateManager
- Object.assign @prototype, EventEmitter.prototype
-
- constructor: (@version, @testMode, @config) ->
- @state = IdleState
- @iconPath = path.resolve(__dirname, '..', '..', 'resources', 'atom.png')
-
- initialize: ->
- if process.platform is 'win32'
- archSuffix = if process.arch is 'ia32' then '' else '-' + process.arch
- @feedUrl = "https://atom.io/api/updates#{archSuffix}?version=#{@version}"
- autoUpdater = require './auto-updater-win32'
- else
- @feedUrl = "https://atom.io/api/updates?version=#{@version}"
- {autoUpdater} = require 'electron'
-
- autoUpdater.on 'error', (event, message) =>
- @setState(ErrorState, message)
- @emitWindowEvent('update-error')
- console.error "Error Downloading Update: #{message}"
-
- autoUpdater.setFeedURL @feedUrl
-
- autoUpdater.on 'checking-for-update', =>
- @setState(CheckingState)
- @emitWindowEvent('checking-for-update')
-
- autoUpdater.on 'update-not-available', =>
- @setState(NoUpdateAvailableState)
- @emitWindowEvent('update-not-available')
-
- autoUpdater.on 'update-available', =>
- @setState(DownloadingState)
- # We use sendMessage to send an event called 'update-available' in 'update-downloaded'
- # once the update download is complete. This mismatch between the electron
- # autoUpdater events is unfortunate but in the interest of not changing the
- # one existing event handled by applicationDelegate
- @emitWindowEvent('did-begin-downloading-update')
- @emit('did-begin-download')
-
- autoUpdater.on 'update-downloaded', (event, releaseNotes, @releaseVersion) =>
- @setState(UpdateAvailableState)
- @emitUpdateAvailableEvent()
-
- @config.onDidChange 'core.automaticallyUpdate', ({newValue}) =>
- if newValue
- @scheduleUpdateCheck()
- else
- @cancelScheduledUpdateCheck()
-
- @scheduleUpdateCheck() if @config.get 'core.automaticallyUpdate'
-
- switch process.platform
- when 'win32'
- @setState(UnsupportedState) unless autoUpdater.supportsUpdates()
- when 'linux'
- @setState(UnsupportedState)
-
- emitUpdateAvailableEvent: ->
- return unless @releaseVersion?
- @emitWindowEvent('update-available', {@releaseVersion})
- return
-
- emitWindowEvent: (eventName, payload) ->
- for atomWindow in @getWindows()
- atomWindow.sendMessage(eventName, payload)
- return
-
- setState: (state, errorMessage) ->
- return if @state is state
- @state = state
- @errorMessage = errorMessage
- @emit 'state-changed', @state
-
- getState: ->
- @state
-
- getErrorMessage: ->
- @errorMessage
-
- scheduleUpdateCheck: ->
- # Only schedule update check periodically if running in release version and
- # and there is no existing scheduled update check.
- unless /\w{7}/.test(@version) or @checkForUpdatesIntervalID
- checkForUpdates = => @check(hidePopups: true)
- fourHours = 1000 * 60 * 60 * 4
- @checkForUpdatesIntervalID = setInterval(checkForUpdates, fourHours)
- checkForUpdates()
-
- cancelScheduledUpdateCheck: ->
- if @checkForUpdatesIntervalID
- clearInterval(@checkForUpdatesIntervalID)
- @checkForUpdatesIntervalID = null
-
- check: ({hidePopups}={}) ->
- unless hidePopups
- autoUpdater.once 'update-not-available', @onUpdateNotAvailable
- autoUpdater.once 'error', @onUpdateError
-
- autoUpdater.checkForUpdates() unless @testMode
-
- install: ->
- autoUpdater.quitAndInstall() unless @testMode
-
- onUpdateNotAvailable: =>
- autoUpdater.removeListener 'error', @onUpdateError
- {dialog} = require 'electron'
- dialog.showMessageBox {
- type: 'info'
- buttons: ['OK']
- icon: @iconPath
- message: 'No update available.'
- title: 'No Update Available'
- detail: "Version #{@version} is the latest version."
- }, -> # noop callback to get async behavior
-
- onUpdateError: (event, message) =>
- autoUpdater.removeListener 'update-not-available', @onUpdateNotAvailable
- {dialog} = require 'electron'
- dialog.showMessageBox {
- type: 'warning'
- buttons: ['OK']
- icon: @iconPath
- message: 'There was an error checking for updates.'
- title: 'Update Error'
- detail: message
- }, -> # noop callback to get async behavior
-
- getWindows: ->
- global.atomApplication.getAllWindows()
diff --git a/src/main-process/auto-update-manager.js b/src/main-process/auto-update-manager.js
new file mode 100644
index 000000000..d9082b6c5
--- /dev/null
+++ b/src/main-process/auto-update-manager.js
@@ -0,0 +1,178 @@
+const {EventEmitter} = require('events')
+const path = require('path')
+
+const IdleState = 'idle'
+const CheckingState = 'checking'
+const DownloadingState = 'downloading'
+const UpdateAvailableState = 'update-available'
+const NoUpdateAvailableState = 'no-update-available'
+const UnsupportedState = 'unsupported'
+const ErrorState = 'error'
+
+let autoUpdater = null
+
+module.exports =
+class AutoUpdateManager extends EventEmitter {
+ constructor (version, testMode, config) {
+ super()
+ this.onUpdateNotAvailable = this.onUpdateNotAvailable.bind(this)
+ this.onUpdateError = this.onUpdateError.bind(this)
+ this.version = version
+ this.testMode = testMode
+ this.config = config
+ this.state = IdleState
+ this.iconPath = path.resolve(__dirname, '..', '..', 'resources', 'atom.png')
+ }
+
+ initialize () {
+ if (process.platform === 'win32') {
+ const archSuffix = process.arch === 'ia32' ? '' : `-${process.arch}`
+ this.feedUrl = `https://atom.io/api/updates${archSuffix}?version=${this.version}`
+ autoUpdater = require('./auto-updater-win32')
+ } else {
+ this.feedUrl = `https://atom.io/api/updates?version=${this.version}`;
+ ({autoUpdater} = require('electron'))
+ }
+
+ autoUpdater.on('error', (event, message) => {
+ this.setState(ErrorState, message)
+ this.emitWindowEvent('update-error')
+ console.error(`Error Downloading Update: ${message}`)
+ })
+
+ autoUpdater.setFeedURL(this.feedUrl)
+
+ autoUpdater.on('checking-for-update', () => {
+ this.setState(CheckingState)
+ this.emitWindowEvent('checking-for-update')
+ })
+
+ autoUpdater.on('update-not-available', () => {
+ this.setState(NoUpdateAvailableState)
+ this.emitWindowEvent('update-not-available')
+ })
+
+ autoUpdater.on('update-available', () => {
+ this.setState(DownloadingState)
+ // We use sendMessage to send an event called 'update-available' in 'update-downloaded'
+ // once the update download is complete. This mismatch between the electron
+ // autoUpdater events is unfortunate but in the interest of not changing the
+ // one existing event handled by applicationDelegate
+ this.emitWindowEvent('did-begin-downloading-update')
+ this.emit('did-begin-download')
+ })
+
+ autoUpdater.on('update-downloaded', (event, releaseNotes, releaseVersion) => {
+ this.releaseVersion = releaseVersion
+ this.setState(UpdateAvailableState)
+ this.emitUpdateAvailableEvent()
+ })
+
+ this.config.onDidChange('core.automaticallyUpdate', ({newValue}) => {
+ if (newValue) {
+ this.scheduleUpdateCheck()
+ } else {
+ this.cancelScheduledUpdateCheck()
+ }
+ })
+
+ if (this.config.get('core.automaticallyUpdate')) this.scheduleUpdateCheck()
+
+ switch (process.platform) {
+ case 'win32':
+ if (!autoUpdater.supportsUpdates()) {
+ this.setState(UnsupportedState)
+ }
+ break
+ case 'linux':
+ this.setState(UnsupportedState)
+ }
+ }
+
+ emitUpdateAvailableEvent () {
+ if (this.releaseVersion == null) return
+ this.emitWindowEvent('update-available', {releaseVersion: this.releaseVersion})
+ }
+
+ emitWindowEvent (eventName, payload) {
+ for (let atomWindow of this.getWindows()) {
+ atomWindow.sendMessage(eventName, payload)
+ }
+ }
+
+ setState (state, errorMessage) {
+ if (this.state === state) return
+ this.state = state
+ this.errorMessage = errorMessage
+ this.emit('state-changed', this.state)
+ }
+
+ getState () {
+ return this.state
+ }
+
+ getErrorMessage () {
+ return this.errorMessage
+ }
+
+ scheduleUpdateCheck () {
+ // Only schedule update check periodically if running in release version and
+ // and there is no existing scheduled update check.
+ if (!/-dev/.test(this.version) && !this.checkForUpdatesIntervalID) {
+ const checkForUpdates = () => this.check({hidePopups: true})
+ const fourHours = 1000 * 60 * 60 * 4
+ this.checkForUpdatesIntervalID = setInterval(checkForUpdates, fourHours)
+ checkForUpdates()
+ }
+ }
+
+ cancelScheduledUpdateCheck () {
+ if (this.checkForUpdatesIntervalID) {
+ clearInterval(this.checkForUpdatesIntervalID)
+ this.checkForUpdatesIntervalID = null
+ }
+ }
+
+ check ({hidePopups} = {}) {
+ if (!hidePopups) {
+ autoUpdater.once('update-not-available', this.onUpdateNotAvailable)
+ autoUpdater.once('error', this.onUpdateError)
+ }
+
+ if (!this.testMode) autoUpdater.checkForUpdates()
+ }
+
+ install () {
+ if (!this.testMode) autoUpdater.quitAndInstall()
+ }
+
+ onUpdateNotAvailable () {
+ autoUpdater.removeListener('error', this.onUpdateError)
+ const {dialog} = require('electron')
+ dialog.showMessageBox({
+ type: 'info',
+ buttons: ['OK'],
+ icon: this.iconPath,
+ message: 'No update available.',
+ title: 'No Update Available',
+ detail: `Version ${this.version} is the latest version.`
+ }, () => {}) // noop callback to get async behavior
+ }
+
+ onUpdateError (event, message) {
+ autoUpdater.removeListener('update-not-available', this.onUpdateNotAvailable)
+ const {dialog} = require('electron')
+ dialog.showMessageBox({
+ type: 'warning',
+ buttons: ['OK'],
+ icon: this.iconPath,
+ message: 'There was an error checking for updates.',
+ title: 'Update Error',
+ detail: message
+ }, () => {}) // noop callback to get async behavior
+ }
+
+ getWindows () {
+ return global.atomApplication.getAllWindows()
+ }
+}
diff --git a/src/main-process/auto-updater-win32.coffee b/src/main-process/auto-updater-win32.coffee
deleted file mode 100644
index ee49e5096..000000000
--- a/src/main-process/auto-updater-win32.coffee
+++ /dev/null
@@ -1,62 +0,0 @@
-{EventEmitter} = require 'events'
-SquirrelUpdate = require './squirrel-update'
-
-class AutoUpdater
- Object.assign @prototype, EventEmitter.prototype
-
- setFeedURL: (@updateUrl) ->
-
- quitAndInstall: ->
- if SquirrelUpdate.existsSync()
- SquirrelUpdate.restartAtom(require('electron').app)
- else
- require('electron').autoUpdater.quitAndInstall()
-
- downloadUpdate: (callback) ->
- SquirrelUpdate.spawn ['--download', @updateUrl], (error, stdout) ->
- return callback(error) if error?
-
- try
- # Last line of output is the JSON details about the releases
- json = stdout.trim().split('\n').pop()
- update = JSON.parse(json)?.releasesToApply?.pop?()
- catch error
- error.stdout = stdout
- return callback(error)
-
- callback(null, update)
-
- installUpdate: (callback) ->
- SquirrelUpdate.spawn(['--update', @updateUrl], callback)
-
- supportsUpdates: ->
- SquirrelUpdate.existsSync()
-
- checkForUpdates: ->
- throw new Error('Update URL is not set') unless @updateUrl
-
- @emit 'checking-for-update'
-
- unless SquirrelUpdate.existsSync()
- @emit 'update-not-available'
- return
-
- @downloadUpdate (error, update) =>
- if error?
- @emit 'update-not-available'
- return
-
- unless update?
- @emit 'update-not-available'
- return
-
- @emit 'update-available'
-
- @installUpdate (error) =>
- if error?
- @emit 'update-not-available'
- return
-
- @emit 'update-downloaded', {}, update.releaseNotes, update.version, new Date(), 'https://atom.io', => @quitAndInstall()
-
-module.exports = new AutoUpdater()
diff --git a/src/main-process/auto-updater-win32.js b/src/main-process/auto-updater-win32.js
new file mode 100644
index 000000000..062d290b9
--- /dev/null
+++ b/src/main-process/auto-updater-win32.js
@@ -0,0 +1,88 @@
+const {EventEmitter} = require('events')
+const SquirrelUpdate = require('./squirrel-update')
+
+class AutoUpdater extends EventEmitter {
+ setFeedURL (updateUrl) {
+ this.updateUrl = updateUrl
+ }
+
+ quitAndInstall () {
+ if (SquirrelUpdate.existsSync()) {
+ SquirrelUpdate.restartAtom(require('electron').app)
+ } else {
+ require('electron').autoUpdater.quitAndInstall()
+ }
+ }
+
+ downloadUpdate (callback) {
+ SquirrelUpdate.spawn(['--download', this.updateUrl], function (error, stdout) {
+ let update
+ if (error != null) return callback(error)
+
+ try {
+ // Last line of output is the JSON details about the releases
+ const json = stdout.trim().split('\n').pop()
+ const data = JSON.parse(json)
+ const releasesToApply = data && data.releasesToApply
+ if (releasesToApply.pop) update = releasesToApply.pop()
+ } catch (error) {
+ error.stdout = stdout
+ return callback(error)
+ }
+
+ callback(null, update)
+ })
+ }
+
+ installUpdate (callback) {
+ SquirrelUpdate.spawn(['--update', this.updateUrl], callback)
+ }
+
+ supportsUpdates () {
+ SquirrelUpdate.existsSync()
+ }
+
+ checkForUpdates () {
+ if (!this.updateUrl) throw new Error('Update URL is not set')
+
+ this.emit('checking-for-update')
+
+ if (!SquirrelUpdate.existsSync()) {
+ this.emit('update-not-available')
+ return
+ }
+
+ this.downloadUpdate((error, update) => {
+ if (error != null) {
+ this.emit('update-not-available')
+ return
+ }
+
+ if (update == null) {
+ this.emit('update-not-available')
+ return
+ }
+
+ this.emit('update-available')
+
+ this.installUpdate(error => {
+ if (error != null) {
+ this.emit('update-not-available')
+ return
+ }
+
+ this.emit(
+ 'update-downloaded',
+ {},
+ update.releaseNotes,
+ update.version,
+ new Date(),
+ 'https://atom.io',
+ () => this.quitAndInstall()
+ )
+ })
+ })
+ }
+}
+
+module.exports = new AutoUpdater()
diff --git a/src/main-process/context-menu.coffee b/src/main-process/context-menu.coffee
deleted file mode 100644
index ce1faf82d..000000000
--- a/src/main-process/context-menu.coffee
+++ /dev/null
@@ -1,24 +0,0 @@
-{Menu} = require 'electron'
-
-module.exports =
-class ContextMenu
- constructor: (template, @atomWindow) ->
- template = @createClickHandlers(template)
- menu = Menu.buildFromTemplate(template)
- menu.popup(@atomWindow.browserWindow, {async: true})
-
- # It's necessary to build the event handlers in this process, otherwise
- # closures are dragged across processes and failed to be garbage collected
- # appropriately.
- createClickHandlers: (template) ->
- for item in template
- if item.command
- item.commandDetail ?= {}
- item.commandDetail.contextCommand = true
- item.commandDetail.atomWindow = @atomWindow
- do (item) =>
- item.click = =>
- global.atomApplication.sendCommandToWindow(item.command, @atomWindow, item.commandDetail)
- else if item.submenu
- @createClickHandlers(item.submenu)
- item
diff --git a/src/main-process/context-menu.js b/src/main-process/context-menu.js
new file mode 100644
index 000000000..6726bbe12
--- /dev/null
+++ b/src/main-process/context-menu.js
@@ -0,0 +1,33 @@
+const {Menu} = require('electron')
+
+module.exports =
+class ContextMenu {
+ constructor (template, atomWindow) {
+ this.atomWindow = atomWindow
+ this.createClickHandlers(template)
+ const menu = Menu.buildFromTemplate(template)
+ menu.popup(this.atomWindow.browserWindow, {async: true})
+ }
+
+ // It's necessary to build the event handlers in this process, otherwise
+ // closures are dragged across processes and failed to be garbage collected
+ // appropriately.
+ createClickHandlers (template) {
+ template.forEach(item => {
+ if (item.command) {
+ if (!item.commandDetail) item.commandDetail = {}
+ item.commandDetail.contextCommand = true
+ item.commandDetail.atomWindow = this.atomWindow
+ item.click = () => {
+ global.atomApplication.sendCommandToWindow(
+ item.command,
+ this.atomWindow,
+ item.commandDetail
+ )
+ }
+ } else if (item.submenu) {
+ this.createClickHandlers(item.submenu)
+ }
+ })
+ }
+}
diff --git a/src/main-process/file-recovery-service.js b/src/main-process/file-recovery-service.js
index eef84089d..abe2df84e 100644
--- a/src/main-process/file-recovery-service.js
+++ b/src/main-process/file-recovery-service.js
@@ -2,6 +2,7 @@ const {dialog} = require('electron')
const crypto = require('crypto')
const Path = require('path')
const fs = require('fs-plus')
+const mkdirp = require('mkdirp')
module.exports =
class FileRecoveryService {
@@ -147,15 +148,18 @@ async function tryStatFile (path) {
async function copyFile (source, destination, mode) {
return new Promise((resolve, reject) => {
- const readStream = fs.createReadStream(source)
- readStream
- .on('error', reject)
- .once('open', () => {
- const writeStream = fs.createWriteStream(destination, {mode})
- writeStream
- .on('error', reject)
- .on('open', () => readStream.pipe(writeStream))
- .once('close', () => resolve())
- })
+ mkdirp(Path.dirname(destination), (error) => {
+ if (error) return reject(error)
+ const readStream = fs.createReadStream(source)
+ readStream
+ .on('error', reject)
+ .once('open', () => {
+ const writeStream = fs.createWriteStream(destination, {mode})
+ writeStream
+ .on('error', reject)
+ .on('open', () => readStream.pipe(writeStream))
+ .once('close', () => resolve())
+ })
+ })
})
}
diff --git a/src/main-process/main.js b/src/main-process/main.js
index ee7b96232..11703bf3f 100644
--- a/src/main-process/main.js
+++ b/src/main-process/main.js
@@ -4,37 +4,55 @@ if (typeof snapshotResult !== 'undefined') {
const startTime = Date.now()
-const electron = require('electron')
-const fs = require('fs')
const path = require('path')
+const fs = require('fs-plus')
+const CSON = require('season')
const yargs = require('yargs')
+const electron = require('electron')
const args =
yargs(process.argv)
.alias('d', 'dev')
.alias('t', 'test')
+ .alias('r', 'resource-path')
.argv
+function isAtomRepoPath (repoPath) {
+ let packageJsonPath = path.join(repoPath, 'package.json')
+ if (fs.statSyncNoException(packageJsonPath)) {
+ let packageJson = CSON.readFileSync(packageJsonPath)
+ return packageJson.name === 'atom'
+ }
+
+ return false
+}
+
let resourcePath
+let devResourcePath
if (args.resourcePath) {
resourcePath = args.resourcePath
+ devResourcePath = resourcePath
} else {
const stableResourcePath = path.dirname(path.dirname(__dirname))
const defaultRepositoryPath = path.join(electron.app.getPath('home'), 'github', 'atom')
+ if (process.env.ATOM_DEV_RESOURCE_PATH) {
+ devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH
+ } else if (isAtomRepoPath(process.cwd())) {
+ devResourcePath = process.cwd()
+ } else if (fs.statSyncNoException(defaultRepositoryPath)) {
+ devResourcePath = defaultRepositoryPath
+ } else {
+ devResourcePath = stableResourcePath
+ }
+
if (args.dev || args.test || args.benchmark || args.benchmarkTest) {
- if (process.env.ATOM_DEV_RESOURCE_PATH) {
- resourcePath = process.env.ATOM_DEV_RESOURCE_PATH
- } else if (fs.statSyncNoException(defaultRepositoryPath)) {
- resourcePath = defaultRepositoryPath
- } else {
- resourcePath = stableResourcePath
- }
+ resourcePath = devResourcePath
} else {
resourcePath = stableResourcePath
}
}
const start = require(path.join(resourcePath, 'src', 'main-process', 'start'))
-start(resourcePath, startTime)
+start(resourcePath, devResourcePath, startTime)
diff --git a/src/main-process/parse-command-line.js b/src/main-process/parse-command-line.js
index d844b4033..5d7849eac 100644
--- a/src/main-process/parse-command-line.js
+++ b/src/main-process/parse-command-line.js
@@ -3,9 +3,6 @@
const dedent = require('dedent')
const yargs = require('yargs')
const {app} = require('electron')
-const path = require('path')
-const fs = require('fs-plus')
-const CSON = require('season')
module.exports = function parseCommandLine (processArgs) {
const options = yargs(processArgs).wrap(yargs.terminalWidth())
@@ -13,13 +10,18 @@ module.exports = function parseCommandLine (processArgs) {
options.usage(
dedent`Atom Editor v${version}
- Usage: atom [options] [path ...]
+ Usage:
+ atom [options] [path ...]
+ atom file[:line[:column]]
One or more paths to files or folders may be specified. If there is an
existing Atom window that contains all of the given folders, the paths
will be opened in that window. Otherwise, they will be opened in a new
window.
+ A file may be opened at the desired line (and optionally column) by
+ appending the numbers right after the file name, e.g. \`atom file:5:8\`.
+
Paths that start with \`atom://\` will be interpreted as URLs.
Environment Variables:
@@ -53,7 +55,6 @@ module.exports = function parseCommandLine (processArgs) {
'When in test mode, waits until the specified time (in minutes) and kills the process (exit code: 130).'
)
options.alias('v', 'version').boolean('v').describe('v', 'Print the version information.')
- options.alias('p', 'project').describe('p', 'Start Atom with a project specification file.')
options.alias('w', 'wait').boolean('w').describe('w', 'Wait for window to be closed before returning.')
options.alias('a', 'add').boolean('a').describe('add', 'Open path as a new project in last used window.')
options.string('socket-path')
@@ -93,7 +94,6 @@ module.exports = function parseCommandLine (processArgs) {
const benchmark = args['benchmark']
const benchmarkTest = args['benchmark-test']
const test = args['test']
- const projectSpecificationFile = args['project']
const mainProcess = args['main-process']
const timeout = args['timeout']
const newWindow = args['new-window']
@@ -117,8 +117,6 @@ module.exports = function parseCommandLine (processArgs) {
let pathsToOpen = []
let urlsToOpen = []
let devMode = args['dev']
- let devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH || path.join(app.getPath('home'), 'github', 'atom')
- let resourcePath = null
for (const path of args._) {
if (path.startsWith('atom://')) {
@@ -128,44 +126,8 @@ module.exports = function parseCommandLine (processArgs) {
}
}
- // Check to see if project flag is set, then add all paths from the .atomproject.
- if (args['resource-path']) {
+ if (args.resourcePath || test) {
devMode = true
- devResourcePath = args['resource-path']
- }
-
- if (test) {
- devMode = true
- }
-
- let projectSpecification = {}
- if (projectSpecificationFile) {
- const readPath = path.isAbsolute(projectSpecificationFile)
- ? projectSpecificationFile
- : path.join(executedFrom, projectSpecificationFile)
-
- const contents = Object.assign({}, readProjectSpecificationSync(readPath, executedFrom))
- const pathToProjectFile = path.join(executedFrom, projectSpecificationFile)
-
- const base = path.dirname(pathToProjectFile)
- pathsToOpen.push(path.dirname(projectSpecificationFile))
- const paths = (contents.paths == null)
- ? undefined
- : contents.paths.map(curPath => path.resolve(base, curPath))
-
- projectSpecification = {
- originPath: pathToProjectFile,
- paths,
- config: contents.config
- }
- }
-
- if (devMode) {
- resourcePath = devResourcePath
- }
-
- if (!fs.statSyncNoException(resourcePath)) {
- resourcePath = path.dirname(path.dirname(__dirname))
}
if (args['path-environment']) {
@@ -174,13 +136,7 @@ module.exports = function parseCommandLine (processArgs) {
process.env.PATH = args['path-environment']
}
- resourcePath = normalizeDriveLetterName(resourcePath)
- devResourcePath = normalizeDriveLetterName(devResourcePath)
-
return {
- projectSpecification,
- resourcePath,
- devResourcePath,
pathsToOpen,
urlsToOpen,
executedFrom,
@@ -203,23 +159,3 @@ module.exports = function parseCommandLine (processArgs) {
env: process.env
}
}
-
-function readProjectSpecificationSync (filepath, executedFrom) {
- let contents
- try {
- contents = CSON.readFileSync(filepath)
- } catch (e) {
- throw new Error('Unable to read supplied project specification file.')
- }
-
- contents.config = (contents.config == null) ? {} : contents.config
- return contents
-}
-
-function normalizeDriveLetterName (filePath) {
- if (process.platform === 'win32') {
- return filePath.replace(/^([a-z]):/, ([driveLetter]) => driveLetter.toUpperCase() + ':')
- } else {
- return filePath
- }
-}
diff --git a/src/main-process/spawner.coffee b/src/main-process/spawner.coffee
deleted file mode 100644
index edf93182e..000000000
--- a/src/main-process/spawner.coffee
+++ /dev/null
@@ -1,36 +0,0 @@
-ChildProcess = require 'child_process'
-
-# Spawn a command and invoke the callback when it completes with an error
-# and the output from standard out.
-#
-# * `command` The underlying OS command {String} to execute.
-# * `args` (optional) The {Array} with arguments to be passed to command.
-# * `callback` (optional) The {Function} to call after the command has run. It will be invoked with arguments:
-# * `error` (optional) An {Error} object returned by the command, `null` if no error was thrown.
-# * `code` Error code returned by the command.
-# * `stdout` The {String} output text generated by the command.
-# * `stdout` The {String} output text generated by the command.
-#
-# Returns `undefined`.
-exports.spawn = (command, args, callback) ->
- stdout = ''
-
- try
- spawnedProcess = ChildProcess.spawn(command, args)
- catch error
- # Spawn can throw an error
- process.nextTick -> callback?(error, stdout)
- return
-
- spawnedProcess.stdout.on 'data', (data) -> stdout += data
-
- error = null
- spawnedProcess.on 'error', (processError) -> error ?= processError
- spawnedProcess.on 'close', (code, signal) ->
- error ?= new Error("Command failed: #{signal ? code}") if code isnt 0
- error?.code ?= code
- error?.stdout ?= stdout
- callback?(error, stdout)
- # This is necessary if using Powershell 2 on Windows 7 to get the events to raise
- # http://stackoverflow.com/questions/9155289/calling-powershell-from-nodejs
- spawnedProcess.stdin.end()
diff --git a/src/main-process/spawner.js b/src/main-process/spawner.js
new file mode 100644
index 000000000..e39e0ff4e
--- /dev/null
+++ b/src/main-process/spawner.js
@@ -0,0 +1,43 @@
+const ChildProcess = require('child_process')
+
+// Spawn a command and invoke the callback when it completes with an error
+// and the output from standard out.
+//
+// * `command` The underlying OS command {String} to execute.
+// * `args` (optional) The {Array} with arguments to be passed to command.
+// * `callback` (optional) The {Function} to call after the command has run. It will be invoked with arguments:
+// * `error` (optional) An {Error} object returned by the command, `null` if no error was thrown.
+// * `code` Error code returned by the command.
+// * `stdout` The {String} output text generated by the command.
+// * `stdout` The {String} output text generated by the command.
+exports.spawn = function (command, args, callback) {
+ let error
+ let spawnedProcess
+ let stdout = ''
+
+ try {
+ spawnedProcess = ChildProcess.spawn(command, args)
+ } catch (error) {
+ process.nextTick(() => callback && callback(error, stdout))
+ return
+ }
+
+ spawnedProcess.stdout.on('data', data => { stdout += data })
+ spawnedProcess.on('error', processError => { error = processError })
+ spawnedProcess.on('close', (code, signal) => {
+ if (!error && code !== 0) {
+ error = new Error(`Command failed: ${signal != null ? signal : code}`)
+ }
+
+ if (error) {
+ if (error.code == null) error.code = code
+ if (error.stdout == null) error.stdout = stdout
+ }
+
+ callback && callback(error, stdout)
+ })
+
+ // This is necessary if using Powershell 2 on Windows 7 to get the events to raise
+ // http://stackoverflow.com/questions/9155289/calling-powershell-from-nodejs
+ return spawnedProcess.stdin.end()
+}
diff --git a/src/main-process/squirrel-update.coffee b/src/main-process/squirrel-update.coffee
deleted file mode 100644
index 9bee339a2..000000000
--- a/src/main-process/squirrel-update.coffee
+++ /dev/null
@@ -1,162 +0,0 @@
-fs = require 'fs-plus'
-path = require 'path'
-Spawner = require './spawner'
-WinShell = require './win-shell'
-WinPowerShell = require './win-powershell'
-
-appFolder = path.resolve(process.execPath, '..')
-rootAtomFolder = path.resolve(appFolder, '..')
-binFolder = path.join(rootAtomFolder, 'bin')
-updateDotExe = path.join(rootAtomFolder, 'Update.exe')
-exeName = path.basename(process.execPath)
-
-if process.env.SystemRoot
- system32Path = path.join(process.env.SystemRoot, 'System32')
- setxPath = path.join(system32Path, 'setx.exe')
-else
- setxPath = 'setx.exe'
-
-# Spawn setx.exe and callback when it completes
-spawnSetx = (args, callback) ->
- Spawner.spawn(setxPath, args, callback)
-
-# Spawn the Update.exe with the given arguments and invoke the callback when
-# the command completes.
-spawnUpdate = (args, callback) ->
- Spawner.spawn(updateDotExe, args, callback)
-
-# Add atom and apm to the PATH
-#
-# This is done by adding .cmd shims to the root bin folder in the Atom
-# install directory that point to the newly installed versions inside
-# the versioned app directories.
-addCommandsToPath = (callback) ->
- installCommands = (callback) ->
- atomCommandPath = path.join(binFolder, 'atom.cmd')
- relativeAtomPath = path.relative(binFolder, path.join(appFolder, 'resources', 'cli', 'atom.cmd'))
- atomCommand = "@echo off\r\n\"%~dp0\\#{relativeAtomPath}\" %*"
-
- atomShCommandPath = path.join(binFolder, 'atom')
- relativeAtomShPath = path.relative(binFolder, path.join(appFolder, 'resources', 'cli', 'atom.sh'))
- atomShCommand = "#!/bin/sh\r\n\"$(dirname \"$0\")/#{relativeAtomShPath.replace(/\\/g, '/')}\" \"$@\"\r\necho"
-
- apmCommandPath = path.join(binFolder, 'apm.cmd')
- relativeApmPath = path.relative(binFolder, path.join(process.resourcesPath, 'app', 'apm', 'bin', 'apm.cmd'))
- apmCommand = "@echo off\r\n\"%~dp0\\#{relativeApmPath}\" %*"
-
- apmShCommandPath = path.join(binFolder, 'apm')
- relativeApmShPath = path.relative(binFolder, path.join(appFolder, 'resources', 'cli', 'apm.sh'))
- apmShCommand = "#!/bin/sh\r\n\"$(dirname \"$0\")/#{relativeApmShPath.replace(/\\/g, '/')}\" \"$@\""
-
- fs.writeFile atomCommandPath, atomCommand, ->
- fs.writeFile atomShCommandPath, atomShCommand, ->
- fs.writeFile apmCommandPath, apmCommand, ->
- fs.writeFile apmShCommandPath, apmShCommand, ->
- callback()
-
- addBinToPath = (pathSegments, callback) ->
- pathSegments.push(binFolder)
- newPathEnv = pathSegments.join(';')
- spawnSetx(['Path', newPathEnv], callback)
-
- installCommands (error) ->
- return callback(error) if error?
-
- WinPowerShell.getPath (error, pathEnv) ->
- return callback(error) if error?
-
- pathSegments = pathEnv.split(/;+/).filter (pathSegment) -> pathSegment
- if pathSegments.indexOf(binFolder) is -1
- addBinToPath(pathSegments, callback)
- else
- callback()
-
-# Remove atom and apm from the PATH
-removeCommandsFromPath = (callback) ->
- WinPowerShell.getPath (error, pathEnv) ->
- return callback(error) if error?
-
- pathSegments = pathEnv.split(/;+/).filter (pathSegment) ->
- pathSegment and pathSegment isnt binFolder
- newPathEnv = pathSegments.join(';')
-
- if pathEnv isnt newPathEnv
- spawnSetx(['Path', newPathEnv], callback)
- else
- callback()
-
-# Create a desktop and start menu shortcut by using the command line API
-# provided by Squirrel's Update.exe
-createShortcuts = (locations, callback) ->
- spawnUpdate(['--createShortcut', exeName, '-l', locations.join(',')], callback)
-
-# Update the desktop and start menu shortcuts by using the command line API
-# provided by Squirrel's Update.exe
-updateShortcuts = (callback) ->
- if homeDirectory = fs.getHomeDirectory()
- desktopShortcutPath = path.join(homeDirectory, 'Desktop', 'Atom.lnk')
- # Check if the desktop shortcut has been previously deleted and
- # and keep it deleted if it was
- fs.exists desktopShortcutPath, (desktopShortcutExists) ->
- locations = ['StartMenu']
- locations.push 'Desktop' if desktopShortcutExists
-
- createShortcuts locations, callback
- else
- createShortcuts ['Desktop', 'StartMenu'], callback
-
-# Remove the desktop and start menu shortcuts by using the command line API
-# provided by Squirrel's Update.exe
-removeShortcuts = (callback) ->
- spawnUpdate(['--removeShortcut', exeName], callback)
-
-exports.spawn = spawnUpdate
-
-# Is the Update.exe installed with Atom?
-exports.existsSync = ->
- fs.existsSync(updateDotExe)
-
-# Restart Atom using the version pointed to by the atom.cmd shim
-exports.restartAtom = (app) ->
- if projectPath = global.atomApplication?.lastFocusedWindow?.projectPath
- args = [projectPath]
- app.once 'will-quit', -> Spawner.spawn(path.join(binFolder, 'atom.cmd'), args)
- app.quit()
-
-updateContextMenus = (callback) ->
- WinShell.fileContextMenu.update ->
- WinShell.folderContextMenu.update ->
- WinShell.folderBackgroundContextMenu.update ->
- callback()
-
-# Handle squirrel events denoted by --squirrel-* command line arguments.
-exports.handleStartupEvent = (app, squirrelCommand) ->
- switch squirrelCommand
- when '--squirrel-install'
- createShortcuts ['Desktop', 'StartMenu'], ->
- addCommandsToPath ->
- WinShell.fileHandler.register ->
- updateContextMenus ->
- app.quit()
- true
- when '--squirrel-updated'
- updateShortcuts ->
- addCommandsToPath ->
- WinShell.fileHandler.update ->
- updateContextMenus ->
- app.quit()
- true
- when '--squirrel-uninstall'
- removeShortcuts ->
- removeCommandsFromPath ->
- WinShell.fileHandler.deregister ->
- WinShell.fileContextMenu.deregister ->
- WinShell.folderContextMenu.deregister ->
- WinShell.folderBackgroundContextMenu.deregister ->
- app.quit()
- true
- when '--squirrel-obsolete'
- app.quit()
- true
- else
- false
diff --git a/src/main-process/squirrel-update.js b/src/main-process/squirrel-update.js
new file mode 100644
index 000000000..0d8174f47
--- /dev/null
+++ b/src/main-process/squirrel-update.js
@@ -0,0 +1,187 @@
+let setxPath
+const fs = require('fs-plus')
+const path = require('path')
+const Spawner = require('./spawner')
+const WinShell = require('./win-shell')
+const WinPowerShell = require('./win-powershell')
+
+const appFolder = path.resolve(process.execPath, '..')
+const rootAtomFolder = path.resolve(appFolder, '..')
+const binFolder = path.join(rootAtomFolder, 'bin')
+const updateDotExe = path.join(rootAtomFolder, 'Update.exe')
+const exeName = path.basename(process.execPath)
+
+if (process.env.SystemRoot) {
+ const system32Path = path.join(process.env.SystemRoot, 'System32')
+ setxPath = path.join(system32Path, 'setx.exe')
+} else {
+ setxPath = 'setx.exe'
+}
+
+// Spawn setx.exe and callback when it completes
+const spawnSetx = (args, callback) => Spawner.spawn(setxPath, args, callback)
+
+// Spawn the Update.exe with the given arguments and invoke the callback when
+// the command completes.
+const spawnUpdate = (args, callback) => Spawner.spawn(updateDotExe, args, callback)
+
+// Add atom and apm to the PATH
+//
+// This is done by adding .cmd shims to the root bin folder in the Atom
+// install directory that point to the newly installed versions inside
+// the versioned app directories.
+const addCommandsToPath = callback => {
+ const installCommands = callback => {
+ const atomCommandPath = path.join(binFolder, 'atom.cmd')
+ const relativeAtomPath = path.relative(binFolder, path.join(appFolder, 'resources', 'cli', 'atom.cmd'))
+ const atomCommand = `@echo off\r\n\"%~dp0\\${relativeAtomPath}\" %*`
+
+ const atomShCommandPath = path.join(binFolder, 'atom')
+ const relativeAtomShPath = path.relative(binFolder, path.join(appFolder, 'resources', 'cli', 'atom.sh'))
+ const atomShCommand = `#!/bin/sh\r\n\"$(dirname \"$0\")/${relativeAtomShPath.replace(/\\/g, '/')}\" \"$@\"\r\necho`
+
+ const apmCommandPath = path.join(binFolder, 'apm.cmd')
+ const relativeApmPath = path.relative(binFolder, path.join(process.resourcesPath, 'app', 'apm', 'bin', 'apm.cmd'))
+ const apmCommand = `@echo off\r\n\"%~dp0\\${relativeApmPath}\" %*`
+
+ const apmShCommandPath = path.join(binFolder, 'apm')
+ const relativeApmShPath = path.relative(binFolder, path.join(appFolder, 'resources', 'cli', 'apm.sh'))
+ const apmShCommand = `#!/bin/sh\r\n\"$(dirname \"$0\")/${relativeApmShPath.replace(/\\/g, '/')}\" \"$@\"`
+
+ fs.writeFile(atomCommandPath, atomCommand, () =>
+ fs.writeFile(atomShCommandPath, atomShCommand, () =>
+ fs.writeFile(apmCommandPath, apmCommand, () =>
+ fs.writeFile(apmShCommandPath, apmShCommand, () => callback())
+ )
+ )
+ )
+ }
+
+ const addBinToPath = (pathSegments, callback) => {
+ pathSegments.push(binFolder)
+ const newPathEnv = pathSegments.join(';')
+ spawnSetx(['Path', newPathEnv], callback)
+ }
+
+ installCommands(error => {
+ if (error) return callback(error)
+
+ WinPowerShell.getPath((error, pathEnv) => {
+ if (error) return callback(error)
+
+ const pathSegments = pathEnv.split(/;+/).filter(pathSegment => pathSegment)
+ if (pathSegments.indexOf(binFolder) === -1) {
+ addBinToPath(pathSegments, callback)
+ } else {
+ callback()
+ }
+ })
+ })
+}
+
+// Remove atom and apm from the PATH
+const removeCommandsFromPath = callback =>
+ WinPowerShell.getPath((error, pathEnv) => {
+ if (error != null) { return callback(error) }
+
+ const pathSegments = pathEnv.split(/;+/).filter(pathSegment => pathSegment && (pathSegment !== binFolder))
+ const newPathEnv = pathSegments.join(';')
+
+ if (pathEnv !== newPathEnv) {
+ return spawnSetx(['Path', newPathEnv], callback)
+ } else {
+ return callback()
+ }
+ })
+
+// Create a desktop and start menu shortcut by using the command line API
+// provided by Squirrel's Update.exe
+const createShortcuts = (locations, callback) => spawnUpdate(['--createShortcut', exeName, '-l', locations.join(',')], callback)
+
+// Update the desktop and start menu shortcuts by using the command line API
+// provided by Squirrel's Update.exe
+const updateShortcuts = (callback) => {
+ const homeDirectory = fs.getHomeDirectory()
+ if (homeDirectory) {
+ const desktopShortcutPath = path.join(homeDirectory, 'Desktop', 'Atom.lnk')
+ // Check if the desktop shortcut has been previously deleted and
+ // and keep it deleted if it was
+ fs.exists(desktopShortcutPath, (desktopShortcutExists) => {
+ const locations = ['StartMenu']
+ if (desktopShortcutExists) { locations.push('Desktop') }
+
+ createShortcuts(locations, callback)
+ })
+ } else {
+ createShortcuts(['Desktop', 'StartMenu'], callback)
+ }
+}
+
+// Remove the desktop and start menu shortcuts by using the command line API
+// provided by Squirrel's Update.exe
+const removeShortcuts = callback => spawnUpdate(['--removeShortcut', exeName], callback)
+
+exports.spawn = spawnUpdate
+
+// Is the Update.exe installed with Atom?
+exports.existsSync = () => fs.existsSync(updateDotExe)
+
+// Restart Atom using the version pointed to by the atom.cmd shim
+exports.restartAtom = (app) => {
+ let args
+ if (global.atomApplication && global.atomApplication.lastFocusedWindow) {
+ const {projectPath} = global.atomApplication.lastFocusedWindow
+ if (projectPath) args = [projectPath]
+ }
+ app.once('will-quit', () => Spawner.spawn(path.join(binFolder, 'atom.cmd'), args))
+ app.quit()
+}
+
+const updateContextMenus = callback =>
+ WinShell.fileContextMenu.update(() =>
+ WinShell.folderContextMenu.update(() =>
+ WinShell.folderBackgroundContextMenu.update(() => callback())
+ )
+ )
+
+// Handle squirrel events denoted by --squirrel-* command line arguments.
+exports.handleStartupEvent = (app, squirrelCommand) => {
+ switch (squirrelCommand) {
+ case '--squirrel-install':
+ createShortcuts(['Desktop', 'StartMenu'], () =>
+ addCommandsToPath(() =>
+ WinShell.fileHandler.register(() =>
+ updateContextMenus(() => app.quit())
+ )
+ )
+ )
+ return true
+ case '--squirrel-updated':
+ updateShortcuts(() =>
+ addCommandsToPath(() =>
+ WinShell.fileHandler.update(() =>
+ updateContextMenus(() => app.quit())
+ )
+ )
+ )
+ return true
+ case '--squirrel-uninstall':
+ removeShortcuts(() =>
+ removeCommandsFromPath(() =>
+ WinShell.fileHandler.deregister(() =>
+ WinShell.fileContextMenu.deregister(() =>
+ WinShell.folderContextMenu.deregister(() =>
+ WinShell.folderBackgroundContextMenu.deregister(() => app.quit())
+ )
+ )
+ )
+ )
+ )
+ return true
+ case '--squirrel-obsolete':
+ app.quit()
+ return true
+ default:
+ return false
+ }
+}
diff --git a/src/main-process/start.js b/src/main-process/start.js
index 23b4df594..7fddf37ec 100644
--- a/src/main-process/start.js
+++ b/src/main-process/start.js
@@ -5,8 +5,11 @@ const temp = require('temp').track()
const parseCommandLine = require('./parse-command-line')
const startCrashReporter = require('../crash-reporter-start')
const atomPaths = require('../atom-paths')
+const fs = require('fs')
+const CSON = require('season')
+const Config = require('../config')
-module.exports = function start (resourcePath, startTime) {
+module.exports = function start (resourcePath, devResourcePath, startTime) {
global.shellStartTime = startTime
process.on('uncaughtException', function (error = {}) {
@@ -35,9 +38,17 @@ module.exports = function start (resourcePath, startTime) {
app.commandLine.appendSwitch('enable-experimental-web-platform-features')
const args = parseCommandLine(process.argv.slice(1))
+ args.resourcePath = normalizeDriveLetterName(resourcePath)
+ args.devResourcePath = normalizeDriveLetterName(devResourcePath)
+
atomPaths.setAtomHome(app.getPath('home'))
atomPaths.setUserData(app)
- setupCompileCache()
+
+ const config = getConfig()
+ const colorProfile = config.get('core.colorProfile')
+ if (colorProfile && colorProfile !== 'default') {
+ app.commandLine.appendSwitch('force-color-profile', colorProfile)
+ }
if (handleStartupEventWithSquirrel()) {
return
@@ -92,8 +103,28 @@ function handleStartupEventWithSquirrel () {
return SquirrelUpdate.handleStartupEvent(app, squirrelCommand)
}
-function setupCompileCache () {
- const CompileCache = require('../compile-cache')
- CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME)
- CompileCache.install(process.resourcesPath, require)
+function getConfig () {
+ const config = new Config()
+
+ let configFilePath
+ if (fs.existsSync(path.join(process.env.ATOM_HOME, 'config.json'))) {
+ configFilePath = path.join(process.env.ATOM_HOME, 'config.json')
+ } else if (fs.existsSync(path.join(process.env.ATOM_HOME, 'config.cson'))) {
+ configFilePath = path.join(process.env.ATOM_HOME, 'config.cson')
+ }
+
+ if (configFilePath) {
+ const configFileData = CSON.readFileSync(configFilePath)
+ config.resetUserSettings(configFileData)
+ }
+
+ return config
+}
+
+function normalizeDriveLetterName (filePath) {
+ if (process.platform === 'win32' && filePath) {
+ return filePath.replace(/^([a-z]):/, ([driveLetter]) => driveLetter.toUpperCase() + ':')
+ } else {
+ return filePath
+ }
}
diff --git a/src/main-process/win-powershell.js b/src/main-process/win-powershell.js
new file mode 100644
index 000000000..f8e404d07
--- /dev/null
+++ b/src/main-process/win-powershell.js
@@ -0,0 +1,44 @@
+let powershellPath
+const path = require('path')
+const Spawner = require('./spawner')
+
+if (process.env.SystemRoot) {
+ const system32Path = path.join(process.env.SystemRoot, 'System32')
+ powershellPath = path.join(system32Path, 'WindowsPowerShell', 'v1.0', 'powershell.exe')
+} else {
+ powershellPath = 'powershell.exe'
+}
+
+// Spawn powershell.exe and callback when it completes
+const spawnPowershell = function (args, callback) {
+ // Set encoding and execute the command, capture the output, and return it
+ // via .NET's console in order to have consistent UTF-8 encoding.
+ // See http://stackoverflow.com/questions/22349139/utf-8-output-from-powershell
+ // to address https://github.com/atom/atom/issues/5063
+ args[0] = `\
+[Console]::OutputEncoding=[System.Text.Encoding]::UTF8
+$output=${args[0]}
+[Console]::WriteLine($output)\
+`
+ args.unshift('-command')
+ args.unshift('RemoteSigned')
+ args.unshift('-ExecutionPolicy')
+ args.unshift('-noprofile')
+ Spawner.spawn(powershellPath, args, callback)
+}
+
+// Get the user's PATH environment variable registry value.
+//
+// * `callback` The {Function} to call after registry operation is done.
+// It will be invoked with the same arguments provided by {Spawner.spawn}.
+//
+// Returns the user's path {String}.
+exports.getPath = callback =>
+ spawnPowershell(['[environment]::GetEnvironmentVariable(\'Path\',\'User\')'], function (error, stdout) {
+ if (error != null) {
+ return callback(error)
+ }
+
+ const pathOutput = stdout.replace(/^\s+|\s+$/g, '')
+ return callback(null, pathOutput)
+ })
diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee
index dbdcb2f0d..a3d35a1de 100644
--- a/src/menu-manager.coffee
+++ b/src/menu-manager.coffee
@@ -149,9 +149,9 @@ class MenuManager
update: ->
return unless @initialized
- clearImmediate(@pendingUpdateOperation) if @pendingUpdateOperation?
+ clearTimeout(@pendingUpdateOperation) if @pendingUpdateOperation?
- @pendingUpdateOperation = setImmediate =>
+ @pendingUpdateOperation = setTimeout(=>
unsetKeystrokes = new Set
for binding in @keymapManager.getKeyBindings()
if binding.command is 'unset!'
@@ -168,6 +168,7 @@ class MenuManager
keystrokesByCommand[binding.command].unshift binding.keystrokes
@sendToBrowserProcess(@template, keystrokesByCommand)
+ , 1)
loadPlatformItems: ->
if platformMenu?
diff --git a/src/module-cache.coffee b/src/module-cache.coffee
deleted file mode 100644
index 5bc162ab1..000000000
--- a/src/module-cache.coffee
+++ /dev/null
@@ -1,285 +0,0 @@
-Module = require 'module'
-path = require 'path'
-semver = require 'semver'
-
-# Extend semver.Range to memoize matched versions for speed
-class Range extends semver.Range
- constructor: ->
- super
- @matchedVersions = new Set()
- @unmatchedVersions = new Set()
-
- test: (version) ->
- return true if @matchedVersions.has(version)
- return false if @unmatchedVersions.has(version)
-
- matches = super
- if matches
- @matchedVersions.add(version)
- else
- @unmatchedVersions.add(version)
- matches
-
-nativeModules = null
-
-cache =
- builtins: {}
- debug: false
- dependencies: {}
- extensions: {}
- folders: {}
- ranges: {}
- registered: false
- resourcePath: null
- resourcePathWithTrailingSlash: null
-
-# isAbsolute is inlined from fs-plus so that fs-plus itself can be required
-# from this cache.
-if process.platform is 'win32'
- isAbsolute = (pathToCheck) ->
- pathToCheck and (pathToCheck[1] is ':' or (pathToCheck[0] is '\\' and pathToCheck[1] is '\\'))
-else
- isAbsolute = (pathToCheck) ->
- pathToCheck and pathToCheck[0] is '/'
-
-isCorePath = (pathToCheck) ->
- pathToCheck.startsWith(cache.resourcePathWithTrailingSlash)
-
-loadDependencies = (modulePath, rootPath, rootMetadata, moduleCache) ->
- fs = require 'fs-plus'
-
- for childPath in fs.listSync(path.join(modulePath, 'node_modules'))
- continue if path.basename(childPath) is '.bin'
- continue if rootPath is modulePath and rootMetadata.packageDependencies?.hasOwnProperty(path.basename(childPath))
-
- childMetadataPath = path.join(childPath, 'package.json')
- continue unless fs.isFileSync(childMetadataPath)
-
- childMetadata = JSON.parse(fs.readFileSync(childMetadataPath))
- if childMetadata?.version
- try
- mainPath = require.resolve(childPath)
- catch error
- mainPath = null
-
- if mainPath
- moduleCache.dependencies.push
- name: childMetadata.name
- version: childMetadata.version
- path: path.relative(rootPath, mainPath)
-
- loadDependencies(childPath, rootPath, rootMetadata, moduleCache)
-
- return
-
-loadFolderCompatibility = (modulePath, rootPath, rootMetadata, moduleCache) ->
- fs = require 'fs-plus'
-
- metadataPath = path.join(modulePath, 'package.json')
- return unless fs.isFileSync(metadataPath)
-
- dependencies = JSON.parse(fs.readFileSync(metadataPath))?.dependencies ? {}
-
- for name, version of dependencies
- try
- new Range(version)
- catch error
- delete dependencies[name]
-
- onDirectory = (childPath) ->
- path.basename(childPath) isnt 'node_modules'
-
- extensions = ['.js', '.coffee', '.json', '.node']
- paths = {}
- onFile = (childPath) ->
- if path.extname(childPath) in extensions
- relativePath = path.relative(rootPath, path.dirname(childPath))
- paths[relativePath] = true
- fs.traverseTreeSync(modulePath, onFile, onDirectory)
-
- paths = Object.keys(paths)
- if paths.length > 0 and Object.keys(dependencies).length > 0
- moduleCache.folders.push({paths, dependencies})
-
- for childPath in fs.listSync(path.join(modulePath, 'node_modules'))
- continue if path.basename(childPath) is '.bin'
- continue if rootPath is modulePath and rootMetadata.packageDependencies?.hasOwnProperty(path.basename(childPath))
-
- loadFolderCompatibility(childPath, rootPath, rootMetadata, moduleCache)
-
- return
-
-loadExtensions = (modulePath, rootPath, rootMetadata, moduleCache) ->
- fs = require 'fs-plus'
- extensions = ['.js', '.coffee', '.json', '.node']
- nodeModulesPath = path.join(rootPath, 'node_modules')
-
- onFile = (filePath) ->
- filePath = path.relative(rootPath, filePath)
- segments = filePath.split(path.sep)
- return if 'test' in segments
- return if 'tests' in segments
- return if 'spec' in segments
- return if 'specs' in segments
- return if segments.length > 1 and not (segments[0] in ['exports', 'lib', 'node_modules', 'src', 'static', 'vendor'])
-
- extension = path.extname(filePath)
- if extension in extensions
- moduleCache.extensions[extension] ?= []
- moduleCache.extensions[extension].push(filePath)
-
- onDirectory = (childPath) ->
- # Don't include extensions from bundled packages
- # These are generated and stored in the package's own metadata cache
- if rootMetadata.name is 'atom'
- parentPath = path.dirname(childPath)
- if parentPath is nodeModulesPath
- packageName = path.basename(childPath)
- return false if rootMetadata.packageDependencies?.hasOwnProperty(packageName)
-
- true
-
- fs.traverseTreeSync(rootPath, onFile, onDirectory)
-
- return
-
-satisfies = (version, rawRange) ->
- unless parsedRange = cache.ranges[rawRange]
- parsedRange = new Range(rawRange)
- cache.ranges[rawRange] = parsedRange
- parsedRange.test(version)
-
-resolveFilePath = (relativePath, parentModule) ->
- return unless relativePath
- return unless parentModule?.filename
- return unless relativePath[0] is '.' or isAbsolute(relativePath)
-
- resolvedPath = path.resolve(path.dirname(parentModule.filename), relativePath)
- return unless isCorePath(resolvedPath)
-
- extension = path.extname(resolvedPath)
- if extension
- return resolvedPath if cache.extensions[extension]?.has(resolvedPath)
- else
- for extension, paths of cache.extensions
- resolvedPathWithExtension = "#{resolvedPath}#{extension}"
- return resolvedPathWithExtension if paths.has(resolvedPathWithExtension)
-
- return
-
-resolveModulePath = (relativePath, parentModule) ->
- return unless relativePath
- return unless parentModule?.filename
-
- nativeModules ?= process.binding('natives')
- return if nativeModules.hasOwnProperty(relativePath)
- return if relativePath[0] is '.'
- return if isAbsolute(relativePath)
-
- folderPath = path.dirname(parentModule.filename)
-
- range = cache.folders[folderPath]?[relativePath]
- unless range?
- if builtinPath = cache.builtins[relativePath]
- return builtinPath
- else
- return
-
- candidates = cache.dependencies[relativePath]
- return unless candidates?
-
- for version, resolvedPath of candidates
- if Module._cache.hasOwnProperty(resolvedPath) or isCorePath(resolvedPath)
- return resolvedPath if satisfies(version, range)
-
- return
-
-registerBuiltins = (devMode) ->
- if devMode or not cache.resourcePath.startsWith("#{process.resourcesPath}#{path.sep}")
- fs = require 'fs-plus'
- atomJsPath = path.join(cache.resourcePath, 'exports', 'atom.js')
- cache.builtins.atom = atomJsPath if fs.isFileSync(atomJsPath)
- cache.builtins.atom ?= path.join(cache.resourcePath, 'exports', 'atom.js')
-
- electronAsarRoot = path.join(process.resourcesPath, 'electron.asar')
-
- commonRoot = path.join(electronAsarRoot, 'common', 'api')
- commonBuiltins = ['callbacks-registry', 'clipboard', 'crash-reporter', 'shell']
- for builtin in commonBuiltins
- cache.builtins[builtin] = path.join(commonRoot, "#{builtin}.js")
-
- rendererRoot = path.join(electronAsarRoot, 'renderer', 'api')
- rendererBuiltins = ['ipc-renderer', 'remote', 'screen']
- for builtin in rendererBuiltins
- cache.builtins[builtin] = path.join(rendererRoot, "#{builtin}.js")
-
-exports.create = (modulePath) ->
- fs = require 'fs-plus'
-
- modulePath = fs.realpathSync(modulePath)
- metadataPath = path.join(modulePath, 'package.json')
- metadata = JSON.parse(fs.readFileSync(metadataPath))
-
- moduleCache =
- version: 1
- dependencies: []
- extensions: {}
- folders: []
-
- loadDependencies(modulePath, modulePath, metadata, moduleCache)
- loadFolderCompatibility(modulePath, modulePath, metadata, moduleCache)
- loadExtensions(modulePath, modulePath, metadata, moduleCache)
-
- metadata._atomModuleCache = moduleCache
- fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2))
-
- return
-
-exports.register = ({resourcePath, devMode}={}) ->
- return if cache.registered
-
- originalResolveFilename = Module._resolveFilename
- Module._resolveFilename = (relativePath, parentModule) ->
- resolvedPath = resolveModulePath(relativePath, parentModule)
- resolvedPath ?= resolveFilePath(relativePath, parentModule)
- resolvedPath ? originalResolveFilename(relativePath, parentModule)
-
- cache.registered = true
- cache.resourcePath = resourcePath
- cache.resourcePathWithTrailingSlash = "#{resourcePath}#{path.sep}"
- registerBuiltins(devMode)
-
- return
-
-exports.add = (directoryPath, metadata) ->
- # path.join isn't used in this function for speed since path.join calls
- # path.normalize and all the paths are already normalized here.
-
- unless metadata?
- try
- metadata = require("#{directoryPath}#{path.sep}package.json")
- catch error
- return
-
- cacheToAdd = metadata?._atomModuleCache
- return unless cacheToAdd?
-
- for dependency in cacheToAdd.dependencies ? []
- cache.dependencies[dependency.name] ?= {}
- cache.dependencies[dependency.name][dependency.version] ?= "#{directoryPath}#{path.sep}#{dependency.path}"
-
- for entry in cacheToAdd.folders ? []
- for folderPath in entry.paths
- if folderPath
- cache.folders["#{directoryPath}#{path.sep}#{folderPath}"] = entry.dependencies
- else
- cache.folders[directoryPath] = entry.dependencies
-
- for extension, paths of cacheToAdd.extensions
- cache.extensions[extension] ?= new Set()
- for filePath in paths
- cache.extensions[extension].add("#{directoryPath}#{path.sep}#{filePath}")
-
- return
-
-exports.cache = cache
diff --git a/src/module-cache.js b/src/module-cache.js
new file mode 100644
index 000000000..f83e54a28
--- /dev/null
+++ b/src/module-cache.js
@@ -0,0 +1,339 @@
+const Module = require('module')
+const path = require('path')
+const semver = require('semver')
+
+// Extend semver.Range to memoize matched versions for speed
+class Range extends semver.Range {
+ constructor () {
+ super(...arguments)
+ this.matchedVersions = new Set()
+ this.unmatchedVersions = new Set()
+ }
+
+ test (version) {
+ if (this.matchedVersions.has(version)) return true
+ if (this.unmatchedVersions.has(version)) return false
+
+ const matches = super.test(...arguments)
+ if (matches) {
+ this.matchedVersions.add(version)
+ } else {
+ this.unmatchedVersions.add(version)
+ }
+ return matches
+ }
+}
+
+let nativeModules = null
+
+const cache = {
+ builtins: {},
+ debug: false,
+ dependencies: {},
+ extensions: {},
+ folders: {},
+ ranges: {},
+ registered: false,
+ resourcePath: null,
+ resourcePathWithTrailingSlash: null
+}
+
+// isAbsolute is inlined from fs-plus so that fs-plus itself can be required
+// from this cache.
+let isAbsolute
+if (process.platform === 'win32') {
+ isAbsolute = pathToCheck => pathToCheck && ((pathToCheck[1] === ':') || ((pathToCheck[0] === '\\') && (pathToCheck[1] === '\\')))
+} else {
+ isAbsolute = pathToCheck => pathToCheck && (pathToCheck[0] === '/')
+}
+
+const isCorePath = pathToCheck => pathToCheck.startsWith(cache.resourcePathWithTrailingSlash)
+
+function loadDependencies (modulePath, rootPath, rootMetadata, moduleCache) {
+ const fs = require('fs-plus')
+
+ for (let childPath of fs.listSync(path.join(modulePath, 'node_modules'))) {
+ if (path.basename(childPath) === '.bin') continue
+ if (rootPath === modulePath && (rootMetadata.packageDependencies && rootMetadata.packageDependencies.hasOwnProperty(path.basename(childPath)))) {
+ continue
+ }
+
+ const childMetadataPath = path.join(childPath, 'package.json')
+ if (!fs.isFileSync(childMetadataPath)) continue
+
+ const childMetadata = JSON.parse(fs.readFileSync(childMetadataPath))
+ if (childMetadata && childMetadata.version) {
+ var mainPath
+ try {
+ mainPath = require.resolve(childPath)
+ } catch (error) {
+ mainPath = null
+ }
+
+ if (mainPath) {
+ moduleCache.dependencies.push({
+ name: childMetadata.name,
+ version: childMetadata.version,
+ path: path.relative(rootPath, mainPath)
+ })
+ }
+
+ loadDependencies(childPath, rootPath, rootMetadata, moduleCache)
+ }
+ }
+}
+
+function loadFolderCompatibility (modulePath, rootPath, rootMetadata, moduleCache) {
+ const fs = require('fs-plus')
+
+ const metadataPath = path.join(modulePath, 'package.json')
+ if (!fs.isFileSync(metadataPath)) return
+
+ const metadata = JSON.parse(fs.readFileSync(metadataPath))
+ const dependencies = metadata.dependencies || {}
+
+ for (let name in dependencies) {
+ if (!semver.validRange(dependencies[name])) {
+ delete dependencies[name]
+ }
+ }
+
+ const onDirectory = childPath => path.basename(childPath) !== 'node_modules'
+
+ const extensions = ['.js', '.coffee', '.json', '.node']
+ let paths = {}
+ function onFile (childPath) {
+ const needle = path.extname(childPath)
+ if (extensions.includes(needle)) {
+ const relativePath = path.relative(rootPath, path.dirname(childPath))
+ paths[relativePath] = true
+ }
+ }
+ fs.traverseTreeSync(modulePath, onFile, onDirectory)
+
+ paths = Object.keys(paths)
+ if (paths.length > 0 && Object.keys(dependencies).length > 0) {
+ moduleCache.folders.push({paths, dependencies})
+ }
+
+ for (let childPath of fs.listSync(path.join(modulePath, 'node_modules'))) {
+ if (path.basename(childPath) === '.bin') continue
+ if (rootPath === modulePath && (rootMetadata.packageDependencies && rootMetadata.packageDependencies.hasOwnProperty(path.basename(childPath)))) {
+ continue
+ }
+ loadFolderCompatibility(childPath, rootPath, rootMetadata, moduleCache)
+ }
+}
+
+function loadExtensions (modulePath, rootPath, rootMetadata, moduleCache) {
+ const fs = require('fs-plus')
+ const extensions = ['.js', '.coffee', '.json', '.node']
+ const nodeModulesPath = path.join(rootPath, 'node_modules')
+
+ function onFile (filePath) {
+ filePath = path.relative(rootPath, filePath)
+ const segments = filePath.split(path.sep)
+ if (segments.includes('test')) return
+ if (segments.includes('tests')) return
+ if (segments.includes('spec')) return
+ if (segments.includes('specs')) return
+ if (segments.length > 1 && !['exports', 'lib', 'node_modules', 'src', 'static', 'vendor'].includes(segments[0])) return
+
+ const extension = path.extname(filePath)
+ if (extensions.includes(extension)) {
+ if (moduleCache.extensions[extension] == null) { moduleCache.extensions[extension] = [] }
+ moduleCache.extensions[extension].push(filePath)
+ }
+ }
+
+ function onDirectory (childPath) {
+ // Don't include extensions from bundled packages
+ // These are generated and stored in the package's own metadata cache
+ if (rootMetadata.name === 'atom') {
+ const parentPath = path.dirname(childPath)
+ if (parentPath === nodeModulesPath) {
+ const packageName = path.basename(childPath)
+ if (rootMetadata.packageDependencies && rootMetadata.packageDependencies.hasOwnProperty(packageName)) return false
+ }
+ }
+
+ return true
+ }
+
+ fs.traverseTreeSync(rootPath, onFile, onDirectory)
+}
+
+function satisfies (version, rawRange) {
+ let parsedRange
+ if (!(parsedRange = cache.ranges[rawRange])) {
+ parsedRange = new Range(rawRange)
+ cache.ranges[rawRange] = parsedRange
+ }
+ return parsedRange.test(version)
+}
+
+function resolveFilePath (relativePath, parentModule) {
+ if (!relativePath) return
+ if (!(parentModule && parentModule.filename)) return
+ if (relativePath[0] !== '.' && !isAbsolute(relativePath)) return
+
+ const resolvedPath = path.resolve(path.dirname(parentModule.filename), relativePath)
+ if (!isCorePath(resolvedPath)) return
+
+ let extension = path.extname(resolvedPath)
+ if (extension) {
+ if (cache.extensions[extension] && cache.extensions[extension].has(resolvedPath)) return resolvedPath
+ } else {
+ for (extension in cache.extensions) {
+ const paths = cache.extensions[extension]
+ const resolvedPathWithExtension = `${resolvedPath}${extension}`
+ if (paths.has(resolvedPathWithExtension)) {
+ return resolvedPathWithExtension
+ }
+ }
+ }
+}
+
+function resolveModulePath (relativePath, parentModule) {
+ if (!relativePath) return
+ if (!(parentModule && parentModule.filename)) return
+
+ if (!nativeModules) nativeModules = process.binding('natives')
+ if (nativeModules.hasOwnProperty(relativePath)) return
+ if (relativePath[0] === '.') return
+ if (isAbsolute(relativePath)) return
+
+ const folderPath = path.dirname(parentModule.filename)
+
+ const range = cache.folders[folderPath] && cache.folders[folderPath][relativePath]
+ if (!range) {
+ const builtinPath = cache.builtins[relativePath]
+ if (builtinPath) {
+ return builtinPath
+ } else {
+ return
+ }
+ }
+
+ const candidates = cache.dependencies[relativePath]
+ if (candidates == null) return
+
+ for (let version in candidates) {
+ const resolvedPath = candidates[version]
+ if (Module._cache[resolvedPath] || isCorePath(resolvedPath)) {
+ if (satisfies(version, range)) return resolvedPath
+ }
+ }
+}
+
+function registerBuiltins (devMode) {
+ if (devMode || !cache.resourcePath.startsWith(`${process.resourcesPath}${path.sep}`)) {
+ const fs = require('fs-plus')
+ const atomJsPath = path.join(cache.resourcePath, 'exports', 'atom.js')
+ if (fs.isFileSync(atomJsPath)) { cache.builtins.atom = atomJsPath }
+ }
+ if (cache.builtins.atom == null) { cache.builtins.atom = path.join(cache.resourcePath, 'exports', 'atom.js') }
+
+ const electronAsarRoot = path.join(process.resourcesPath, 'electron.asar')
+
+ const commonRoot = path.join(electronAsarRoot, 'common', 'api')
+ const commonBuiltins = ['callbacks-registry', 'clipboard', 'crash-reporter', 'shell']
+ for (const builtin of commonBuiltins) {
+ cache.builtins[builtin] = path.join(commonRoot, `${builtin}.js`)
+ }
+
+ const rendererRoot = path.join(electronAsarRoot, 'renderer', 'api')
+ const rendererBuiltins = ['ipc-renderer', 'remote', 'screen']
+ for (const builtin of rendererBuiltins) {
+ cache.builtins[builtin] = path.join(rendererRoot, `${builtin}.js`)
+ }
+}
+
+exports.create = function (modulePath) {
+ const fs = require('fs-plus')
+
+ modulePath = fs.realpathSync(modulePath)
+ const metadataPath = path.join(modulePath, 'package.json')
+ const metadata = JSON.parse(fs.readFileSync(metadataPath))
+
+ const moduleCache = {
+ version: 1,
+ dependencies: [],
+ extensions: {},
+ folders: []
+ }
+
+ loadDependencies(modulePath, modulePath, metadata, moduleCache)
+ loadFolderCompatibility(modulePath, modulePath, metadata, moduleCache)
+ loadExtensions(modulePath, modulePath, metadata, moduleCache)
+
+ metadata._atomModuleCache = moduleCache
+ fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2))
+}
+
+exports.register = function ({resourcePath, devMode} = {}) {
+ if (cache.registered) return
+
+ const originalResolveFilename = Module._resolveFilename
+ Module._resolveFilename = function (relativePath, parentModule) {
+ let resolvedPath = resolveModulePath(relativePath, parentModule)
+ if (!resolvedPath) {
+ resolvedPath = resolveFilePath(relativePath, parentModule)
+ }
+ return resolvedPath || originalResolveFilename(relativePath, parentModule)
+ }
+
+ cache.registered = true
+ cache.resourcePath = resourcePath
+ cache.resourcePathWithTrailingSlash = `${resourcePath}${path.sep}`
+ registerBuiltins(devMode)
+}
+
+exports.add = function (directoryPath, metadata) {
+ // path.join isn't used in this function for speed since path.join calls
+ // path.normalize and all the paths are already normalized here.
+
+ if (metadata == null) {
+ try {
+ metadata = require(`${directoryPath}${path.sep}package.json`)
+ } catch (error) {
+ return
+ }
+ }
+
+ const cacheToAdd = metadata && metadata._atomModuleCache
+ if (!cacheToAdd) return
+
+ for (const dependency of cacheToAdd.dependencies || []) {
+ if (!cache.dependencies[dependency.name]) {
+ cache.dependencies[dependency.name] = {}
+ }
+ if (!cache.dependencies[dependency.name][dependency.version]) {
+ cache.dependencies[dependency.name][dependency.version] = `${directoryPath}${path.sep}${dependency.path}`
+ }
+ }
+
+ for (const entry of cacheToAdd.folders || []) {
+ for (const folderPath of entry.paths) {
+ if (folderPath) {
+ cache.folders[`${directoryPath}${path.sep}${folderPath}`] = entry.dependencies
+ } else {
+ cache.folders[directoryPath] = entry.dependencies
+ }
+ }
+ }
+
+ for (const extension in cacheToAdd.extensions) {
+ const paths = cacheToAdd.extensions[extension]
+ if (!cache.extensions[extension]) {
+ cache.extensions[extension] = new Set()
+ }
+ for (let filePath of paths) {
+ cache.extensions[extension].add(`${directoryPath}${path.sep}${filePath}`)
+ }
+ }
+}
+
+exports.cache = cache
+
+exports.Range = Range
diff --git a/src/package-manager.js b/src/package-manager.js
index 17a5f2214..8171ce19f 100644
--- a/src/package-manager.js
+++ b/src/package-manager.js
@@ -9,7 +9,7 @@ const CSON = require('season')
const ServiceHub = require('service-hub')
const Package = require('./package')
const ThemePackage = require('./theme-package')
-const {isDeprecatedPackage, getDeprecatedPackageMetadata} = require('./deprecated-packages')
+const ModuleCache = require('./module-cache')
const packageJSON = require('../package.json')
// Extended: Package manager for coordinating the lifecycle of Atom packages.
@@ -42,6 +42,8 @@ module.exports = class PackageManager {
this.triggeredActivationHooks = new Set()
this.packagesCache = packageJSON._atomPackages != null ? packageJSON._atomPackages : {}
this.packageDependencies = packageJSON.packageDependencies != null ? packageJSON.packageDependencies : {}
+ this.deprecatedPackages = packageJSON._deprecatedPackages || {}
+ this.deprecatedPackageRanges = {}
this.initialPackagesLoaded = false
this.initialPackagesActivated = false
this.preloadedPackages = {}
@@ -61,6 +63,7 @@ module.exports = class PackageManager {
if (params.configDirPath != null && !params.safeMode) {
if (this.devMode) {
this.packageDirPaths.push(path.join(params.configDirPath, 'dev', 'packages'))
+ this.packageDirPaths.push(path.join(this.resourcePath, 'packages'))
}
this.packageDirPaths.push(path.join(params.configDirPath, 'packages'))
}
@@ -219,11 +222,26 @@ module.exports = class PackageManager {
}
isDeprecatedPackage (name, version) {
- return isDeprecatedPackage(name, version)
+ const metadata = this.deprecatedPackages[name]
+ if (!metadata) return false
+ if (!metadata.version) return true
+
+ let range = this.deprecatedPackageRanges[metadata.version]
+ if (!range) {
+ try {
+ range = new ModuleCache.Range(metadata.version)
+ } catch (error) {
+ range = NullVersionRange
+ }
+ this.deprecatedPackageRanges[metadata.version] = range
+ }
+ return range.test(version)
}
getDeprecatedPackageMetadata (name) {
- return getDeprecatedPackageMetadata(name)
+ const metadata = this.deprecatedPackages[name]
+ if (metadata) Object.freeze(metadata)
+ return metadata
}
/*
@@ -870,3 +888,7 @@ module.exports = class PackageManager {
}
}
}
+
+const NullVersionRange = {
+ test () { return false }
+}
diff --git a/src/package.js b/src/package.js
index bbcb0061f..56764a86a 100644
--- a/src/package.js
+++ b/src/package.js
@@ -7,7 +7,6 @@ const dedent = require('dedent')
const CompileCache = require('./compile-cache')
const ModuleCache = require('./module-cache')
-const ScopedProperties = require('./scoped-properties')
const BufferedProcess = require('./buffered-process')
// Extended: Loads and activates a package's main module and resources such as
@@ -103,7 +102,7 @@ class Package {
this.activateKeymaps()
this.activateMenus()
for (let settings of this.settings) {
- settings.activate()
+ settings.activate(this.config)
}
this.settingsActivated = true
}
@@ -318,7 +317,7 @@ class Package {
if (!this.settingsActivated) {
for (let settings of this.settings) {
- settings.activate()
+ settings.activate(this.config)
}
this.settingsActivated = true
}
@@ -636,14 +635,14 @@ class Package {
this.settings = []
const loadSettingsFile = (settingsPath, callback) => {
- return ScopedProperties.load(settingsPath, this.config, (error, settings) => {
+ return SettingsFile.load(settingsPath, (error, settingsFile) => {
if (error) {
const detail = `${error.message} in ${settingsPath}`
const stack = `${error.stack}\n at ${settingsPath}:1:1`
this.notificationManager.addFatalError(`Failed to load the ${this.name} package settings`, {stack, detail, packageName: this.name, dismissable: true})
} else {
- this.settings.push(settings)
- if (this.settingsActivated) { settings.activate() }
+ this.settings.push(settingsFile)
+ if (this.settingsActivated) settingsFile.activate(this.config)
}
return callback()
})
@@ -652,10 +651,10 @@ class Package {
return new Promise(resolve => {
if (this.preloadedPackage && this.packageManager.packagesCache[this.name]) {
for (let settingsPath in this.packageManager.packagesCache[this.name].settings) {
- const scopedProperties = this.packageManager.packagesCache[this.name].settings[settingsPath]
- const settings = new ScopedProperties(`core:${settingsPath}`, scopedProperties || {}, this.config)
- this.settings.push(settings)
- if (this.settingsActivated) { settings.activate() }
+ const properties = this.packageManager.packagesCache[this.name].settings[settingsPath]
+ const settingsFile = new SettingsFile(`core:${settingsPath}`, properties || {})
+ this.settings.push(settingsFile)
+ if (this.settingsActivated) settingsFile.activate(this.config)
}
return resolve()
} else {
@@ -727,7 +726,7 @@ class Package {
grammar.deactivate()
}
for (let settings of this.settings) {
- settings.deactivate()
+ settings.deactivate(this.config)
}
if (this.stylesheetDisposables) this.stylesheetDisposables.dispose()
@@ -1105,3 +1104,32 @@ class Package {
})
}
}
+
+class SettingsFile {
+ static load (path, callback) {
+ CSON.readFile(path, (error, properties = {}) => {
+ if (error) {
+ callback(error)
+ } else {
+ callback(null, new SettingsFile(path, properties))
+ }
+ })
+ }
+
+ constructor (path, properties) {
+ this.path = path
+ this.properties = properties
+ }
+
+ activate (config) {
+ for (let selector in this.properties) {
+ config.set(null, this.properties[selector], {scopeSelector: selector, source: this.path})
+ }
+ }
+
+ deactivate (config) {
+ for (let selector in this.properties) {
+ config.unset(null, {scopeSelector: selector, source: this.path})
+ }
+ }
+}
diff --git a/src/pane-axis.coffee b/src/pane-axis.coffee
deleted file mode 100644
index c8fcc4108..000000000
--- a/src/pane-axis.coffee
+++ /dev/null
@@ -1,147 +0,0 @@
-{Emitter, CompositeDisposable} = require 'event-kit'
-{flatten} = require 'underscore-plus'
-Model = require './model'
-PaneAxisElement = require './pane-axis-element'
-
-module.exports =
-class PaneAxis extends Model
- parent: null
- container: null
- orientation: null
-
- @deserialize: (state, {deserializers, views}) ->
- state.children = state.children.map (childState) ->
- deserializers.deserialize(childState)
- new this(state, views)
-
- constructor: ({@orientation, children, flexScale}, @viewRegistry) ->
- @emitter = new Emitter
- @subscriptionsByChild = new WeakMap
- @subscriptions = new CompositeDisposable
- @children = []
- if children?
- @addChild(child) for child in children
- @flexScale = flexScale ? 1
-
- serialize: ->
- deserializer: 'PaneAxis'
- children: @children.map (child) -> child.serialize()
- orientation: @orientation
- flexScale: @flexScale
-
- getElement: ->
- @element ?= new PaneAxisElement().initialize(this, @viewRegistry)
-
- getFlexScale: -> @flexScale
-
- setFlexScale: (@flexScale) ->
- @emitter.emit 'did-change-flex-scale', @flexScale
- @flexScale
-
- getParent: -> @parent
-
- setParent: (@parent) -> @parent
-
- getContainer: -> @container
-
- setContainer: (container) ->
- if container and container isnt @container
- @container = container
- child.setContainer(container) for child in @children
-
- getOrientation: -> @orientation
-
- getChildren: -> @children.slice()
-
- getPanes: ->
- flatten(@children.map (child) -> child.getPanes())
-
- getItems: ->
- flatten(@children.map (child) -> child.getItems())
-
- onDidAddChild: (fn) ->
- @emitter.on 'did-add-child', fn
-
- onDidRemoveChild: (fn) ->
- @emitter.on 'did-remove-child', fn
-
- onDidReplaceChild: (fn) ->
- @emitter.on 'did-replace-child', fn
-
- onDidDestroy: (fn) ->
- @emitter.once 'did-destroy', fn
-
- onDidChangeFlexScale: (fn) ->
- @emitter.on 'did-change-flex-scale', fn
-
- observeFlexScale: (fn) ->
- fn(@flexScale)
- @onDidChangeFlexScale(fn)
-
- addChild: (child, index=@children.length) ->
- @children.splice(index, 0, child)
- child.setParent(this)
- child.setContainer(@container)
- @subscribeToChild(child)
- @emitter.emit 'did-add-child', {child, index}
-
- adjustFlexScale: ->
- # get current total flex scale of children
- total = 0
- total += child.getFlexScale() for child in @children
-
- needTotal = @children.length
- # set every child's flex scale by the ratio
- for child in @children
- child.setFlexScale(needTotal * child.getFlexScale() / total)
-
- removeChild: (child, replacing=false) ->
- index = @children.indexOf(child)
- throw new Error("Removing non-existent child") if index is -1
-
- @unsubscribeFromChild(child)
-
- @children.splice(index, 1)
- @adjustFlexScale()
- @emitter.emit 'did-remove-child', {child, index}
- @reparentLastChild() if not replacing and @children.length < 2
-
- replaceChild: (oldChild, newChild) ->
- @unsubscribeFromChild(oldChild)
- @subscribeToChild(newChild)
-
- newChild.setParent(this)
- newChild.setContainer(@container)
-
- index = @children.indexOf(oldChild)
- @children.splice(index, 1, newChild)
- @emitter.emit 'did-replace-child', {oldChild, newChild, index}
-
- insertChildBefore: (currentChild, newChild) ->
- index = @children.indexOf(currentChild)
- @addChild(newChild, index)
-
- insertChildAfter: (currentChild, newChild) ->
- index = @children.indexOf(currentChild)
- @addChild(newChild, index + 1)
-
- reparentLastChild: ->
- lastChild = @children[0]
- lastChild.setFlexScale(@flexScale)
- @parent.replaceChild(this, lastChild)
- @destroy()
-
- subscribeToChild: (child) ->
- subscription = child.onDidDestroy => @removeChild(child)
- @subscriptionsByChild.set(child, subscription)
- @subscriptions.add(subscription)
-
- unsubscribeFromChild: (child) ->
- subscription = @subscriptionsByChild.get(child)
- @subscriptions.remove(subscription)
- subscription.dispose()
-
- destroyed: ->
- @subscriptions.dispose()
- @emitter.emit 'did-destroy'
- @emitter.dispose()
diff --git a/src/pane-axis.js b/src/pane-axis.js
new file mode 100644
index 000000000..23c87f928
--- /dev/null
+++ b/src/pane-axis.js
@@ -0,0 +1,199 @@
+const {Emitter, CompositeDisposable} = require('event-kit')
+const {flatten} = require('underscore-plus')
+const Model = require('./model')
+const PaneAxisElement = require('./pane-axis-element')
+
+class PaneAxis extends Model {
+ static deserialize (state, {deserializers, views}) {
+ state.children = state.children.map(childState => deserializers.deserialize(childState))
+ return new PaneAxis(state, views)
+ }
+
+ constructor ({orientation, children, flexScale}, viewRegistry) {
+ super()
+ this.parent = null
+ this.container = null
+ this.orientation = orientation
+ this.viewRegistry = viewRegistry
+ this.emitter = new Emitter()
+ this.subscriptionsByChild = new WeakMap()
+ this.subscriptions = new CompositeDisposable()
+ this.flexScale = flexScale != null ? flexScale : 1
+ this.children = []
+ if (children) {
+ for (let child of children) {
+ this.addChild(child)
+ }
+ }
+ }
+
+ serialize () {
+ return {
+ deserializer: 'PaneAxis',
+ children: this.children.map(child => child.serialize()),
+ orientation: this.orientation,
+ flexScale: this.flexScale
+ }
+ }
+
+ getElement () {
+ if (!this.element) {
+ this.element = new PaneAxisElement().initialize(this, this.viewRegistry)
+ }
+ return this.element
+ }
+
+ getFlexScale () {
+ return this.flexScale
+ }
+
+ setFlexScale (flexScale) {
+ this.flexScale = flexScale
+ this.emitter.emit('did-change-flex-scale', this.flexScale)
+ return this.flexScale
+ }
+
+ getParent () {
+ return this.parent
+ }
+
+ setParent (parent) {
+ this.parent = parent
+ return this.parent
+ }
+
+ getContainer () {
+ return this.container
+ }
+
+ setContainer (container) {
+ if (container && (container !== this.container)) {
+ this.container = container
+ this.children.forEach(child => child.setContainer(container))
+ }
+ }
+
+ getOrientation () {
+ return this.orientation
+ }
+
+ getChildren () {
+ return this.children.slice()
+ }
+
+ getPanes () {
+ return flatten(this.children.map(child => child.getPanes()))
+ }
+
+ getItems () {
+ return flatten(this.children.map(child => child.getItems()))
+ }
+
+ onDidAddChild (fn) {
+ return this.emitter.on('did-add-child', fn)
+ }
+
+ onDidRemoveChild (fn) {
+ return this.emitter.on('did-remove-child', fn)
+ }
+
+ onDidReplaceChild (fn) {
+ return this.emitter.on('did-replace-child', fn)
+ }
+
+ onDidDestroy (fn) {
+ return this.emitter.once('did-destroy', fn)
+ }
+
+ onDidChangeFlexScale (fn) {
+ return this.emitter.on('did-change-flex-scale', fn)
+ }
+
+ observeFlexScale (fn) {
+ fn(this.flexScale)
+ return this.onDidChangeFlexScale(fn)
+ }
+
+ addChild (child, index = this.children.length) {
+ this.children.splice(index, 0, child)
+ child.setParent(this)
+ child.setContainer(this.container)
+ this.subscribeToChild(child)
+ return this.emitter.emit('did-add-child', {child, index})
+ }
+
+ adjustFlexScale () {
+ // get current total flex scale of children
+ let total = 0
+ for (var child of this.children) { total += child.getFlexScale() }
+
+ const needTotal = this.children.length
+ // set every child's flex scale by the ratio
+ for (child of this.children) {
+ child.setFlexScale((needTotal * child.getFlexScale()) / total)
+ }
+ }
+
+ removeChild (child, replacing = false) {
+ const index = this.children.indexOf(child)
+ if (index === -1) { throw new Error('Removing non-existent child') }
+
+ this.unsubscribeFromChild(child)
+
+ this.children.splice(index, 1)
+ this.adjustFlexScale()
+ this.emitter.emit('did-remove-child', {child, index})
+ if (!replacing && this.children.length < 2) {
+ this.reparentLastChild()
+ }
+ }
+
+ replaceChild (oldChild, newChild) {
+ this.unsubscribeFromChild(oldChild)
+ this.subscribeToChild(newChild)
+
+ newChild.setParent(this)
+ newChild.setContainer(this.container)
+
+ const index = this.children.indexOf(oldChild)
+ this.children.splice(index, 1, newChild)
+ this.emitter.emit('did-replace-child', {oldChild, newChild, index})
+ }
+
+ insertChildBefore (currentChild, newChild) {
+ const index = this.children.indexOf(currentChild)
+ return this.addChild(newChild, index)
+ }
+
+ insertChildAfter (currentChild, newChild) {
+ const index = this.children.indexOf(currentChild)
+ return this.addChild(newChild, index + 1)
+ }
+
+ reparentLastChild () {
+ const lastChild = this.children[0]
+ lastChild.setFlexScale(this.flexScale)
+ this.parent.replaceChild(this, lastChild)
+ this.destroy()
+ }
+
+ subscribeToChild (child) {
+ const subscription = child.onDidDestroy(() => this.removeChild(child))
+ this.subscriptionsByChild.set(child, subscription)
+ this.subscriptions.add(subscription)
+ }
+
+ unsubscribeFromChild (child) {
+ const subscription = this.subscriptionsByChild.get(child)
+ this.subscriptions.remove(subscription)
+ subscription.dispose()
+ }
+
+ destroyed () {
+ this.subscriptions.dispose()
+ this.emitter.emit('did-destroy')
+ this.emitter.dispose()
+ }
+}
+
+module.exports = PaneAxis
diff --git a/src/pane.js b/src/pane.js
index 162186572..fca1260c4 100644
--- a/src/pane.js
+++ b/src/pane.js
@@ -614,15 +614,15 @@ class Pane {
if (this.items.includes(item)) return
+ const itemSubscriptions = new CompositeDisposable()
+ this.subscriptionsPerItem.set(item, itemSubscriptions)
if (typeof item.onDidDestroy === 'function') {
- const itemSubscriptions = new CompositeDisposable()
itemSubscriptions.add(item.onDidDestroy(() => this.removeItem(item, false)))
- if (typeof item.onDidTerminatePendingState === 'function') {
- itemSubscriptions.add(item.onDidTerminatePendingState(() => {
- if (this.getPendingItem() === item) this.clearPendingItem()
- }))
- }
- this.subscriptionsPerItem.set(item, itemSubscriptions)
+ }
+ if (typeof item.onDidTerminatePendingState === 'function') {
+ itemSubscriptions.add(item.onDidTerminatePendingState(() => {
+ if (this.getPendingItem() === item) this.clearPendingItem()
+ }))
}
this.items.splice(index, 0, item)
diff --git a/src/project.js b/src/project.js
index 45a26fbb8..8ccf60c0b 100644
--- a/src/project.js
+++ b/src/project.js
@@ -199,7 +199,7 @@ class Project extends Model {
// const disposable = atom.project.onDidChangeFiles(events => {
// for (const event of events) {
// // "created", "modified", "deleted", or "renamed"
- // console.log(`Event action: ${event.type}`)
+ // console.log(`Event action: ${event.action}`)
//
// // absolute path to the filesystem entry that was touched
// console.log(`Event path: ${event.path}`)
@@ -234,6 +234,38 @@ class Project extends Model {
return this.emitter.on('did-change-files', callback)
}
+ // Public: Invoke the given callback with all current and future
+ // repositories in the project.
+ //
+ // * `callback` {Function} to be called with current and future
+ // repositories.
+ // * `repository` A {GitRepository} that is present at the time of
+ // subscription or that is added at some later time.
+ //
+ // Returns a {Disposable} on which `.dispose()` can be called to
+ // unsubscribe.
+ observeRepositories (callback) {
+ for (const repo of this.repositories) {
+ if (repo != null) {
+ callback(repo)
+ }
+ }
+
+ return this.onDidAddRepository(callback)
+ }
+
+ // Public: Invoke the given callback when a repository is added to the
+ // project.
+ //
+ // * `callback` {Function} to be called when a repository is added.
+ // * `repository` A {GitRepository}.
+ //
+ // Returns a {Disposable} on which `.dispose()` can be called to
+ // unsubscribe.
+ onDidAddRepository (callback) {
+ return this.emitter.on('did-add-repository', callback)
+ }
+
/*
Section: Accessing the git repository
*/
@@ -400,6 +432,9 @@ class Project extends Model {
if (repo) { break }
}
this.repositories.push(repo != null ? repo : null)
+ if (repo != null) {
+ this.emitter.emit('did-add-repository', repo)
+ }
if (options.emitEvent !== false) {
this.emitter.emit('did-change-paths', this.getPaths())
@@ -662,27 +697,32 @@ class Project extends Model {
// * `text` The {String} text to use as a buffer.
//
// Returns a {Promise} that resolves to the {TextBuffer}.
- buildBuffer (absoluteFilePath) {
+ async buildBuffer (absoluteFilePath) {
const params = {shouldDestroyOnFileDelete: this.shouldDestroyBufferOnFileDelete}
- let promise
+ let buffer
if (absoluteFilePath != null) {
if (this.loadPromisesByPath[absoluteFilePath] == null) {
this.loadPromisesByPath[absoluteFilePath] =
- TextBuffer.load(absoluteFilePath, params).catch(error => {
- delete this.loadPromisesByPath[absoluteFilePath]
- throw error
- })
+ TextBuffer.load(absoluteFilePath, params)
+ .then(result => {
+ delete this.loadPromisesByPath[absoluteFilePath]
+ return result
+ })
+ .catch(error => {
+ delete this.loadPromisesByPath[absoluteFilePath]
+ throw error
+ })
}
- promise = this.loadPromisesByPath[absoluteFilePath]
+ buffer = await this.loadPromisesByPath[absoluteFilePath]
} else {
- promise = Promise.resolve(new TextBuffer(params))
+ buffer = new TextBuffer(params)
}
- return promise.then(buffer => {
- delete this.loadPromisesByPath[absoluteFilePath]
- this.addBuffer(buffer)
- return buffer
- })
+
+ this.grammarRegistry.autoAssignLanguageMode(buffer)
+
+ this.addBuffer(buffer)
+ return buffer
}
addBuffer (buffer, options = {}) {
diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee
index a367e6188..badae227c 100644
--- a/src/register-default-commands.coffee
+++ b/src/register-default-commands.coffee
@@ -122,8 +122,6 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
commandRegistry.add(
'atom-text-editor',
stopEventPropagation({
- 'core:undo': -> @undo()
- 'core:redo': -> @redo()
'core:move-left': -> @moveLeft()
'core:move-right': -> @moveRight()
'core:select-left': -> @selectLeft()
@@ -166,15 +164,35 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
false
)
+ commandRegistry.add(
+ 'atom-text-editor:not([readonly])',
+ stopEventPropagation({
+ 'core:undo': -> @undo()
+ 'core:redo': -> @redo()
+ }),
+ false
+ )
+
commandRegistry.add(
'atom-text-editor',
+ stopEventPropagationAndGroupUndo(
+ config,
+ {
+ 'core:copy': -> @copySelectedText()
+ 'editor:copy-selection': -> @copyOnlySelectedText()
+ }
+ ),
+ false
+ )
+
+ commandRegistry.add(
+ 'atom-text-editor:not([readonly])',
stopEventPropagationAndGroupUndo(
config,
{
'core:backspace': -> @backspace()
'core:delete': -> @delete()
'core:cut': -> @cutSelectedText()
- 'core:copy': -> @copySelectedText()
'core:paste': -> @pasteText()
'editor:paste-without-reformatting': -> @pasteText({
normalizeLineEndings: false,
@@ -195,7 +213,6 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
'editor:transpose': -> @transpose()
'editor:upper-case': -> @upperCase()
'editor:lower-case': -> @lowerCase()
- 'editor:copy-selection': -> @copyOnlySelectedText()
}
),
false
@@ -266,7 +283,7 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
)
commandRegistry.add(
- 'atom-text-editor:not([mini])',
+ 'atom-text-editor:not([mini]):not([readonly])',
stopEventPropagationAndGroupUndo(
config,
{
diff --git a/src/safe-clipboard.coffee b/src/safe-clipboard.coffee
deleted file mode 100644
index 1f91803e2..000000000
--- a/src/safe-clipboard.coffee
+++ /dev/null
@@ -1,6 +0,0 @@
-# Using clipboard in renderer process is not safe on Linux.
-module.exports =
- if process.platform is 'linux' and process.type is 'renderer'
- require('electron').remote.clipboard
- else
- require('electron').clipboard
diff --git a/src/scope-descriptor.coffee b/src/scope-descriptor.coffee
deleted file mode 100644
index f1070f277..000000000
--- a/src/scope-descriptor.coffee
+++ /dev/null
@@ -1,63 +0,0 @@
-# Extended: Wraps an {Array} of `String`s. The Array describes a path from the
-# root of the syntax tree to a token including _all_ scope names for the entire
-# path.
-#
-# Methods that take a `ScopeDescriptor` will also accept an {Array} of {String}
-# scope names e.g. `['.source.js']`.
-#
-# You can use `ScopeDescriptor`s to get language-specific config settings via
-# {Config::get}.
-#
-# You should not need to create a `ScopeDescriptor` directly.
-#
-# * {TextEditor::getRootScopeDescriptor} to get the language's descriptor.
-# * {TextEditor::scopeDescriptorForBufferPosition} to get the descriptor at a
-# specific position in the buffer.
-# * {Cursor::getScopeDescriptor} to get a cursor's descriptor based on position.
-#
-# See the [scopes and scope descriptor guide](http://flight-manual.atom.io/behind-atom/sections/scoped-settings-scopes-and-scope-descriptors/)
-# for more information.
-module.exports =
-class ScopeDescriptor
- @fromObject: (scopes) ->
- if scopes instanceof ScopeDescriptor
- scopes
- else
- new ScopeDescriptor({scopes})
-
- ###
- Section: Construction and Destruction
- ###
-
- # Public: Create a {ScopeDescriptor} object.
- #
- # * `object` {Object}
- # * `scopes` {Array} of {String}s
- constructor: ({@scopes}) ->
-
- # Public: Returns an {Array} of {String}s
- getScopesArray: -> @scopes
-
- getScopeChain: ->
- # 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()
-
- isEqual: (other) ->
- if @scopes.length isnt other.scopes.length
- return false
- for scope, i in @scopes
- if scope isnt other.scopes[i]
- return false
- true
diff --git a/src/scope-descriptor.js b/src/scope-descriptor.js
new file mode 100644
index 000000000..63075e8a1
--- /dev/null
+++ b/src/scope-descriptor.js
@@ -0,0 +1,80 @@
+// Extended: Wraps an {Array} of `String`s. The Array describes a path from the
+// root of the syntax tree to a token including _all_ scope names for the entire
+// path.
+//
+// Methods that take a `ScopeDescriptor` will also accept an {Array} of {String}
+// scope names e.g. `['.source.js']`.
+//
+// You can use `ScopeDescriptor`s to get language-specific config settings via
+// {Config::get}.
+//
+// You should not need to create a `ScopeDescriptor` directly.
+//
+// * {TextEditor::getRootScopeDescriptor} to get the language's descriptor.
+// * {TextEditor::scopeDescriptorForBufferPosition} to get the descriptor at a
+// specific position in the buffer.
+// * {Cursor::getScopeDescriptor} to get a cursor's descriptor based on position.
+//
+// See the [scopes and scope descriptor guide](http://flight-manual.atom.io/behind-atom/sections/scoped-settings-scopes-and-scope-descriptors/)
+// for more information.
+module.exports =
+class ScopeDescriptor {
+ static fromObject (scopes) {
+ if (scopes instanceof ScopeDescriptor) {
+ return scopes
+ } else {
+ return new ScopeDescriptor({scopes})
+ }
+ }
+
+ /*
+ Section: Construction and Destruction
+ */
+
+ // Public: Create a {ScopeDescriptor} object.
+ //
+ // * `object` {Object}
+ // * `scopes` {Array} of {String}s
+ constructor ({scopes}) {
+ this.scopes = scopes
+ }
+
+ // Public: Returns an {Array} of {String}s
+ getScopesArray () {
+ return this.scopes
+ }
+
+ getScopeChain () {
+ // For backward compatibility, prefix TextMate-style scope names with
+ // leading dots (e.g. 'source.js' -> '.source.js').
+ if (this.scopes[0] != null && this.scopes[0].includes('.')) {
+ let result = ''
+ for (let i = 0; i < this.scopes.length; i++) {
+ const scope = this.scopes[i]
+ if (i > 0) { result += ' ' }
+ if (scope[0] !== '.') { result += '.' }
+ result += scope
+ }
+ return result
+ } else {
+ return this.scopes.join(' ')
+ }
+ }
+
+ toString () {
+ return this.getScopeChain()
+ }
+
+ isEqual (other) {
+ if (this.scopes.length !== other.scopes.length) {
+ return false
+ }
+ for (let i = 0; i < this.scopes.length; i++) {
+ const scope = this.scopes[i]
+ if (scope !== other.scopes[i]) {
+ return false
+ }
+ }
+ return true
+ }
+}
diff --git a/src/scoped-properties.coffee b/src/scoped-properties.coffee
deleted file mode 100644
index f8f8b4311..000000000
--- a/src/scoped-properties.coffee
+++ /dev/null
@@ -1,22 +0,0 @@
-CSON = require 'season'
-
-module.exports =
-class ScopedProperties
- @load: (scopedPropertiesPath, config, callback) ->
- CSON.readFile scopedPropertiesPath, (error, scopedProperties={}) ->
- if error?
- callback(error)
- else
- callback(null, new ScopedProperties(scopedPropertiesPath, scopedProperties, config))
-
- constructor: (@path, @scopedProperties, @config) ->
-
- activate: ->
- for selector, properties of @scopedProperties
- @config.set(null, properties, scopeSelector: selector, source: @path)
- return
-
- deactivate: ->
- for selector of @scopedProperties
- @config.unset(null, scopeSelector: selector, source: @path)
- return
diff --git a/src/selection.js b/src/selection.js
index 2c64fa126..70e5e73df 100644
--- a/src/selection.js
+++ b/src/selection.js
@@ -407,6 +407,25 @@ class Selection {
if (autoscroll) this.cursor.autoscroll()
}
+ // Private: Ensure that the {TextEditor} is not marked read-only before allowing a buffer modification to occur. if
+ // the editor is read-only, require an explicit opt-in option to proceed (`bypassReadOnly`) or throw an Error.
+ ensureWritable (methodName, opts) {
+ if (!opts.bypassReadOnly && this.editor.isReadOnly()) {
+ if (atom.inDevMode() || atom.inSpecMode()) {
+ const e = new Error('Attempt to mutate a read-only TextEditor through a Selection')
+ e.detail =
+ `Your package is attempting to call ${methodName} on a selection within an editor that has been marked ` +
+ ' read-only. Pass {bypassReadOnly: true} to modify it anyway, or test editors with .isReadOnly() before ' +
+ ' attempting modifications.'
+ throw e
+ }
+
+ return false
+ }
+
+ return true
+ }
+
/*
Section: Modifying the selected text
*/
@@ -427,8 +446,11 @@ class Selection {
// behavior is suppressed.
// level between the first lines and the trailing lines.
// * `normalizeLineEndings` (optional) {Boolean} (default: true)
- // * `undo` If `skip`, skips the undo stack for this operation.
+ // * `undo` *Deprecated* If `skip`, skips the undo stack for this operation. This property is deprecated. Call groupLastChanges() on the {TextBuffer} afterward instead.
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
insertText (text, options = {}) {
+ if (!this.ensureWritable('insertText', options)) return
+
let desiredIndentLevel, indentAdjustment
const oldBufferRange = this.getBufferRange()
const wasReversed = this.isReversed()
@@ -492,90 +514,134 @@ class Selection {
// Public: Removes the first character before the selection if the selection
// is empty otherwise it deletes the selection.
- backspace () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ backspace (options = {}) {
+ if (!this.ensureWritable('backspace', options)) return
if (this.isEmpty()) this.selectLeft()
- this.deleteSelectedText()
+ this.deleteSelectedText(options)
}
// Public: Removes the selection or, if nothing is selected, then all
// characters from the start of the selection back to the previous word
// boundary.
- deleteToPreviousWordBoundary () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ deleteToPreviousWordBoundary (options = {}) {
+ if (!this.ensureWritable('deleteToPreviousWordBoundary', options)) return
if (this.isEmpty()) this.selectToPreviousWordBoundary()
- this.deleteSelectedText()
+ this.deleteSelectedText(options)
}
// Public: Removes the selection or, if nothing is selected, then all
// characters from the start of the selection up to the next word
// boundary.
- deleteToNextWordBoundary () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ deleteToNextWordBoundary (options = {}) {
+ if (!this.ensureWritable('deleteToNextWordBoundary', options)) return
if (this.isEmpty()) this.selectToNextWordBoundary()
- this.deleteSelectedText()
+ this.deleteSelectedText(options)
}
// Public: Removes from the start of the selection to the beginning of the
// current word if the selection is empty otherwise it deletes the selection.
- deleteToBeginningOfWord () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ deleteToBeginningOfWord (options = {}) {
+ if (!this.ensureWritable('deleteToBeginningOfWord', options)) return
if (this.isEmpty()) this.selectToBeginningOfWord()
- this.deleteSelectedText()
+ this.deleteSelectedText(options)
}
// Public: Removes from the beginning of the line which the selection begins on
// all the way through to the end of the selection.
- deleteToBeginningOfLine () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ deleteToBeginningOfLine (options = {}) {
+ if (!this.ensureWritable('deleteToBeginningOfLine', options)) return
if (this.isEmpty() && this.cursor.isAtBeginningOfLine()) {
this.selectLeft()
} else {
this.selectToBeginningOfLine()
}
- this.deleteSelectedText()
+ this.deleteSelectedText(options)
}
// Public: Removes the selection or the next character after the start of the
// selection if the selection is empty.
- delete () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ delete (options = {}) {
+ if (!this.ensureWritable('delete', options)) return
if (this.isEmpty()) this.selectRight()
- this.deleteSelectedText()
+ this.deleteSelectedText(options)
}
// Public: If the selection is empty, removes all text from the cursor to the
// end of the line. If the cursor is already at the end of the line, it
// removes the following newline. If the selection isn't empty, only deletes
// the contents of the selection.
- deleteToEndOfLine () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ deleteToEndOfLine (options = {}) {
+ if (!this.ensureWritable('deleteToEndOfLine', options)) return
if (this.isEmpty()) {
if (this.cursor.isAtEndOfLine()) {
- this.delete()
+ this.delete(options)
return
}
this.selectToEndOfLine()
}
- this.deleteSelectedText()
+ this.deleteSelectedText(options)
}
// Public: Removes the selection or all characters from the start of the
// selection to the end of the current word if nothing is selected.
- deleteToEndOfWord () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ deleteToEndOfWord (options = {}) {
+ if (!this.ensureWritable('deleteToEndOfWord', options)) return
if (this.isEmpty()) this.selectToEndOfWord()
- this.deleteSelectedText()
+ this.deleteSelectedText(options)
}
// Public: Removes the selection or all characters from the start of the
// selection to the end of the current word if nothing is selected.
- deleteToBeginningOfSubword () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ deleteToBeginningOfSubword (options = {}) {
+ if (!this.ensureWritable('deleteToBeginningOfSubword', options)) return
if (this.isEmpty()) this.selectToPreviousSubwordBoundary()
- this.deleteSelectedText()
+ this.deleteSelectedText(options)
}
// Public: Removes the selection or all characters from the start of the
// selection to the end of the current word if nothing is selected.
- deleteToEndOfSubword () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ deleteToEndOfSubword (options = {}) {
+ if (!this.ensureWritable('deleteToEndOfSubword', options)) return
if (this.isEmpty()) this.selectToNextSubwordBoundary()
- this.deleteSelectedText()
+ this.deleteSelectedText(options)
}
// Public: Removes only the selected text.
- deleteSelectedText () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ deleteSelectedText (options = {}) {
+ if (!this.ensureWritable('deleteSelectedText', options)) return
const bufferRange = this.getBufferRange()
if (!bufferRange.isEmpty()) this.editor.buffer.delete(bufferRange)
if (this.cursor) this.cursor.setBufferPosition(bufferRange.start)
@@ -584,7 +650,11 @@ class Selection {
// Public: Removes the line at the beginning of the selection if the selection
// is empty unless the selection spans multiple lines in which case all lines
// are removed.
- deleteLine () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ deleteLine (options = {}) {
+ if (!this.ensureWritable('deleteLine', options)) return
const range = this.getBufferRange()
if (range.isEmpty()) {
const start = this.cursor.getScreenRow()
@@ -607,7 +677,11 @@ class Selection {
// be separated by a single space.
//
// If there selection spans more than one line, all the lines are joined together.
- joinLines () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ joinLines (options = {}) {
+ if (!this.ensureWritable('joinLines', options)) return
let joinMarker
const selectedRange = this.getBufferRange()
if (selectedRange.isEmpty()) {
@@ -629,7 +703,7 @@ class Selection {
})
if (trailingWhitespaceRange) {
this.setBufferRange(trailingWhitespaceRange)
- this.deleteSelectedText()
+ this.deleteSelectedText(options)
}
const currentRow = selectedRange.start.row
@@ -638,7 +712,7 @@ class Selection {
(nextRow <= this.editor.buffer.getLastRow()) &&
(this.editor.buffer.lineLengthForRow(nextRow) > 0) &&
(this.editor.buffer.lineLengthForRow(currentRow) > 0)
- if (insertSpace) this.insertText(' ')
+ if (insertSpace) this.insertText(' ', options)
this.cursor.moveToEndOfLine()
@@ -647,7 +721,7 @@ class Selection {
this.cursor.moveRight()
this.cursor.moveToFirstCharacterOfLine()
})
- this.deleteSelectedText()
+ this.deleteSelectedText(options)
if (insertSpace) this.cursor.moveLeft()
}
@@ -660,7 +734,11 @@ class Selection {
}
// Public: Removes one level of indent from the currently selected rows.
- outdentSelectedRows () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ outdentSelectedRows (options = {}) {
+ if (!this.ensureWritable('outdentSelectedRows', options)) return
const [start, end] = this.getBufferRowRange()
const {buffer} = this.editor
const leadingTabRegex = new RegExp(`^( {1,${this.editor.getTabLength()}}|\t)`)
@@ -674,7 +752,11 @@ class Selection {
// Public: Sets the indentation level of all selected rows to values suggested
// by the relevant grammars.
- autoIndentSelectedRows () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ autoIndentSelectedRows (options = {}) {
+ if (!this.ensureWritable('autoIndentSelectedRows', options)) return
const [start, end] = this.getBufferRowRange()
return this.editor.autoIndentBufferRows(start, end)
}
@@ -683,29 +765,45 @@ class Selection {
// of a comment.
//
// Removes the comment if they are currently wrapped in a comment.
- toggleLineComments () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ toggleLineComments (options = {}) {
+ if (!this.ensureWritable('toggleLineComments', options)) return
this.editor.toggleLineCommentsForBufferRows(...(this.getBufferRowRange() || []))
}
// Public: Cuts the selection until the end of the screen line.
- cutToEndOfLine (maintainClipboard) {
+ //
+ // * `maintainClipboard` {Boolean}
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ cutToEndOfLine (maintainClipboard, options = {}) {
+ if (!this.ensureWritable('cutToEndOfLine', options)) return
if (this.isEmpty()) this.selectToEndOfLine()
- return this.cut(maintainClipboard)
+ return this.cut(maintainClipboard, false, options.bypassReadOnly)
}
// Public: Cuts the selection until the end of the buffer line.
- cutToEndOfBufferLine (maintainClipboard) {
+ //
+ // * `maintainClipboard` {Boolean}
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ cutToEndOfBufferLine (maintainClipboard, options = {}) {
+ if (!this.ensureWritable('cutToEndOfBufferLine', options)) return
if (this.isEmpty()) this.selectToEndOfBufferLine()
- this.cut(maintainClipboard)
+ this.cut(maintainClipboard, false, options.bypassReadOnly)
}
// Public: Copies the selection to the clipboard and then deletes it.
//
// * `maintainClipboard` {Boolean} (default: false) See {::copy}
// * `fullLine` {Boolean} (default: false) See {::copy}
- cut (maintainClipboard = false, fullLine = false) {
+ // * `bypassReadOnly` {Boolean} (default: false) Must be `true` to modify text within a read-only editor.
+ cut (maintainClipboard = false, fullLine = false, bypassReadOnly = false) {
+ if (!this.ensureWritable('cut', {bypassReadOnly})) return
this.copy(maintainClipboard, fullLine)
- this.delete()
+ this.delete({bypassReadOnly})
}
// Public: Copies the current selection to the clipboard.
@@ -783,7 +881,9 @@ class Selection {
// * `options` (optional) {Object} with the keys:
// * `autoIndent` If `true`, the line is indented to an automatically-inferred
// level. Otherwise, {TextEditor::getTabText} is inserted.
- indent ({autoIndent} = {}) {
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ indent ({autoIndent, bypassReadOnly} = {}) {
+ if (!this.ensureWritable('indent', {bypassReadOnly})) return
const {row} = this.cursor.getBufferPosition()
if (this.isEmpty()) {
@@ -793,17 +893,21 @@ class Selection {
if (autoIndent && delta > 0) {
if (!this.editor.getSoftTabs()) delta = Math.max(delta, 1)
- this.insertText(this.editor.buildIndentString(delta))
+ this.insertText(this.editor.buildIndentString(delta), {bypassReadOnly})
} else {
- this.insertText(this.editor.buildIndentString(1, this.cursor.getBufferColumn()))
+ this.insertText(this.editor.buildIndentString(1, this.cursor.getBufferColumn()), {bypassReadOnly})
}
} else {
- this.indentSelectedRows()
+ this.indentSelectedRows({bypassReadOnly})
}
}
// Public: If the selection spans multiple rows, indent all of them.
- indentSelectedRows () {
+ //
+ // * `options` (optional) {Object} with the keys:
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
+ indentSelectedRows (options = {}) {
+ if (!this.ensureWritable('indentSelectedRows', options)) return
const [start, end] = this.getBufferRowRange()
for (let row = start; row <= end; row++) {
if (this.editor.buffer.lineLengthForRow(row) !== 0) {
@@ -933,14 +1037,15 @@ class Selection {
this.editor.cursorMoved(cursorMovedEvent)
}
- this.emitter.emit('did-change-range')
- this.editor.selectionRangeChanged({
+ const rangeChangedEvent = {
oldBufferRange: new Range(oldHeadBufferPosition, oldTailBufferPosition),
oldScreenRange: new Range(oldHeadScreenPosition, oldTailScreenPosition),
newBufferRange: this.getBufferRange(),
newScreenRange: this.getScreenRange(),
selection: this
- })
+ }
+ this.emitter.emit('did-change-range', rangeChangedEvent)
+ this.editor.selectionRangeChanged(rangeChangedEvent)
}
markerDidDestroy () {
diff --git a/src/selectors.js b/src/selectors.js
new file mode 100644
index 000000000..ce03b80b4
--- /dev/null
+++ b/src/selectors.js
@@ -0,0 +1,38 @@
+module.exports = {selectorMatchesAnyScope, matcherForSelector}
+
+const {isSubset} = require('underscore-plus')
+
+// Private: Parse a selector into parts.
+// If already parsed, returns the selector unmodified.
+//
+// * `selector` a {String|Array} specifying what to match
+// Returns selector parts, an {Array}.
+function parse (selector) {
+ return typeof selector === 'string'
+ ? selector.replace(/^\./, '').split('.')
+ : selector
+}
+
+const always = scope => true
+
+// Essential: Return a matcher function for a selector.
+//
+// * selector, a {String} selector
+// Returns {(scope: String) -> Boolean}, a matcher function returning
+// true iff the scope matches the selector.
+function matcherForSelector (selector) {
+ const parts = parse(selector)
+ if (typeof parts === 'function') return parts
+ return selector
+ ? scope => isSubset(parts, parse(scope))
+ : always
+}
+
+// Essential: Return true iff the selector matches any provided scope.
+//
+// * {String} selector
+// * {Array} scopes
+// Returns {Boolean} true if any scope matches the selector.
+function selectorMatchesAnyScope (selector, scopes) {
+ return !selector || scopes.some(matcherForSelector(selector))
+}
diff --git a/src/test.ejs b/src/test.ejs
new file mode 100644
index 000000000..7b93c31b3
--- /dev/null
+++ b/src/test.ejs
@@ -0,0 +1,9 @@
+
+
+<% if something() { %>
+
+ <%= html `ok how about this` %>
+
+<% } %>
+
+
diff --git a/src/text-editor-component.js b/src/text-editor-component.js
index 3e56dd821..ffb55c454 100644
--- a/src/text-editor-component.js
+++ b/src/text-editor-component.js
@@ -5,8 +5,8 @@ const {Point, Range} = require('text-buffer')
const LineTopIndex = require('line-top-index')
const TextEditor = require('./text-editor')
const {isPairedCharacter} = require('./text-utils')
-const clipboard = require('./safe-clipboard')
const electron = require('electron')
+const clipboard = electron.clipboard
const $ = etch.dom
let TextEditorElement
@@ -148,12 +148,13 @@ class TextEditorComponent {
this.lineNumbersToRender = {
maxDigits: 2,
bufferRows: [],
+ screenRows: [],
keys: [],
softWrappedFlags: [],
foldableFlags: []
}
this.decorationsToRender = {
- lineNumbers: null,
+ lineNumbers: new Map(),
lines: null,
highlights: [],
cursors: [],
@@ -458,15 +459,18 @@ class TextEditorComponent {
let clientContainerWidth = '100%'
if (this.hasInitialMeasurements) {
if (model.getAutoHeight()) {
- clientContainerHeight = this.getContentHeight()
- if (this.canScrollHorizontally()) clientContainerHeight += this.getHorizontalScrollbarHeight()
- clientContainerHeight += 'px'
+ clientContainerHeight =
+ this.getContentHeight() +
+ this.getHorizontalScrollbarHeight() +
+ 'px'
}
if (model.getAutoWidth()) {
style.width = 'min-content'
- clientContainerWidth = this.getGutterContainerWidth() + this.getContentWidth()
- if (this.canScrollVertically()) clientContainerWidth += this.getVerticalScrollbarWidth()
- clientContainerWidth += 'px'
+ clientContainerWidth =
+ this.getGutterContainerWidth() +
+ this.getContentWidth() +
+ this.getVerticalScrollbarWidth() +
+ 'px'
} else {
style.width = this.element.style.width
}
@@ -477,7 +481,7 @@ class TextEditorComponent {
attributes.mini = ''
}
- if (!this.isInputEnabled()) {
+ if (model.isReadOnly()) {
attributes.readonly = ''
}
@@ -751,20 +755,14 @@ class TextEditorComponent {
scrollLeft = this.getScrollLeft()
canScrollHorizontally = this.canScrollHorizontally()
canScrollVertically = this.canScrollVertically()
- horizontalScrollbarHeight =
- canScrollHorizontally
- ? this.getHorizontalScrollbarHeight()
- : 0
- verticalScrollbarWidth =
- canScrollVertically
- ? this.getVerticalScrollbarWidth()
- : 0
+ horizontalScrollbarHeight = this.getHorizontalScrollbarHeight()
+ verticalScrollbarWidth = this.getVerticalScrollbarWidth()
forceScrollbarVisible = this.remeasureScrollbars
} else {
forceScrollbarVisible = true
}
- const dummyScrollbarVnodes = [
+ return [
$(DummyScrollbarComponent, {
ref: 'verticalScrollbar',
orientation: 'vertical',
@@ -786,13 +784,10 @@ class TextEditorComponent {
scrollLeft,
verticalScrollbarWidth,
forceScrollbarVisible
- })
- ]
+ }),
- // If both scrollbars are visible, push a dummy element to force a "corner"
- // to render where the two scrollbars meet at the lower right
- if (verticalScrollbarWidth > 0 && horizontalScrollbarHeight > 0) {
- dummyScrollbarVnodes.push($.div(
+ // Force a "corner" to render where the two scrollbars meet at the lower right
+ $.div(
{
ref: 'scrollbarCorner',
className: 'scrollbar-corner',
@@ -805,10 +800,8 @@ class TextEditorComponent {
overflow: 'scroll'
}
}
- ))
- }
-
- return dummyScrollbarVnodes
+ )
+ ]
} else {
return null
}
@@ -856,10 +849,7 @@ class TextEditorComponent {
}
for (let i = 0; i < newClassList.length; i++) {
- const className = newClassList[i]
- if (!oldClassList || !oldClassList.includes(className)) {
- this.element.classList.add(className)
- }
+ this.element.classList.add(newClassList[i])
}
this.classList = newClassList
@@ -897,7 +887,7 @@ class TextEditorComponent {
queryLineNumbersToRender () {
const {model} = this.props
- if (!model.isLineNumberGutterVisible()) return
+ if (!model.anyLineNumberGutterVisible()) return
if (this.showLineNumbers !== model.doesShowLineNumbers()) {
this.remeasureGutterDimensions = true
this.showLineNumbers = model.doesShowLineNumbers()
@@ -953,7 +943,7 @@ class TextEditorComponent {
queryMaxLineNumberDigits () {
const {model} = this.props
- if (model.isLineNumberGutterVisible()) {
+ if (model.anyLineNumberGutterVisible()) {
const maxDigits = Math.max(2, model.getLineCount().toString().length)
if (maxDigits !== this.lineNumbersToRender.maxDigits) {
this.remeasureGutterDimensions = true
@@ -988,7 +978,7 @@ class TextEditorComponent {
}
queryDecorationsToRender () {
- this.decorationsToRender.lineNumbers = []
+ this.decorationsToRender.lineNumbers.clear()
this.decorationsToRender.lines = []
this.decorationsToRender.overlays.length = 0
this.decorationsToRender.customGutter.clear()
@@ -1051,7 +1041,17 @@ class TextEditorComponent {
}
addLineDecorationToRender (type, decoration, screenRange, reversed) {
- const decorationsToRender = (type === 'line') ? this.decorationsToRender.lines : this.decorationsToRender.lineNumbers
+ let decorationsToRender
+ if (type === 'line') {
+ decorationsToRender = this.decorationsToRender.lines
+ } else {
+ const gutterName = decoration.gutterName || 'line-number'
+ decorationsToRender = this.decorationsToRender.lineNumbers.get(gutterName)
+ if (!decorationsToRender) {
+ decorationsToRender = []
+ this.decorationsToRender.lineNumbers.set(gutterName, decorationsToRender)
+ }
+ }
let omitLastRow = false
if (screenRange.isEmpty()) {
@@ -2626,37 +2626,25 @@ class TextEditorComponent {
getScrollContainerHeight () {
if (this.props.model.getAutoHeight()) {
- return this.getScrollHeight()
+ return this.getScrollHeight() + this.getHorizontalScrollbarHeight()
} else {
return this.getClientContainerHeight()
}
}
getScrollContainerClientWidth () {
- if (this.canScrollVertically()) {
- return this.getScrollContainerWidth() - this.getVerticalScrollbarWidth()
- } else {
- return this.getScrollContainerWidth()
- }
+ return this.getScrollContainerWidth() - this.getVerticalScrollbarWidth()
}
getScrollContainerClientHeight () {
- if (this.canScrollHorizontally()) {
- return this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight()
- } else {
- return this.getScrollContainerHeight()
- }
+ return this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight()
}
canScrollVertically () {
const {model} = this.props
if (model.isMini()) return false
if (model.getAutoHeight()) return false
- if (this.getContentHeight() > this.getScrollContainerHeight()) return true
- return (
- this.getContentWidth() > this.getScrollContainerWidth() &&
- this.getContentHeight() > (this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight())
- )
+ return this.getContentHeight() > this.getScrollContainerClientHeight()
}
canScrollHorizontally () {
@@ -2664,11 +2652,7 @@ class TextEditorComponent {
if (model.isMini()) return false
if (model.getAutoWidth()) return false
if (model.isSoftWrapped()) return false
- if (this.getContentWidth() > this.getScrollContainerWidth()) return true
- return (
- this.getContentHeight() > this.getScrollContainerHeight() &&
- this.getContentWidth() > (this.getScrollContainerWidth() - this.getVerticalScrollbarWidth())
- )
+ return this.getContentWidth() > this.getScrollContainerClientWidth()
}
getScrollHeight () {
@@ -2965,11 +2949,11 @@ class TextEditorComponent {
}
setInputEnabled (inputEnabled) {
- this.props.model.update({readOnly: !inputEnabled})
+ this.props.model.update({keyboardInputEnabled: inputEnabled})
}
- isInputEnabled (inputEnabled) {
- return !this.props.model.isReadOnly()
+ isInputEnabled () {
+ return !this.props.model.isReadOnly() && this.props.model.isKeyboardInputEnabled()
}
getHiddenInput () {
@@ -3126,7 +3110,7 @@ class GutterContainerComponent {
},
$.div({style: innerStyle},
guttersToRender.map((gutter) => {
- if (gutter.name === 'line-number') {
+ if (gutter.type === 'line-number') {
return this.renderLineNumberGutter(gutter)
} else {
return $(CustomGutterComponent, {
@@ -3145,18 +3129,29 @@ class GutterContainerComponent {
renderLineNumberGutter (gutter) {
const {
- rootComponent, isLineNumberGutterVisible, showLineNumbers, hasInitialMeasurements, lineNumbersToRender,
+ rootComponent, showLineNumbers, hasInitialMeasurements, lineNumbersToRender,
renderedStartRow, renderedEndRow, rowsPerTile, decorationsToRender, didMeasureVisibleBlockDecoration,
scrollHeight, lineNumberGutterWidth, lineHeight
} = this.props
- if (!isLineNumberGutterVisible) return null
+ if (!gutter.isVisible()) {
+ return null
+ }
+
+ const oneTrueLineNumberGutter = gutter.name === 'line-number'
+ const ref = oneTrueLineNumberGutter ? 'lineNumberGutter' : undefined
+ const width = oneTrueLineNumberGutter ? lineNumberGutterWidth : undefined
if (hasInitialMeasurements) {
const {maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags} = lineNumbersToRender
return $(LineNumberGutterComponent, {
- ref: 'lineNumberGutter',
+ ref,
element: gutter.getElement(),
+ name: gutter.name,
+ className: gutter.className,
+ labelFn: gutter.labelFn,
+ onMouseDown: gutter.onMouseDown,
+ onMouseMove: gutter.onMouseMove,
rootComponent: rootComponent,
startRow: renderedStartRow,
endRow: renderedEndRow,
@@ -3167,18 +3162,22 @@ class GutterContainerComponent {
screenRows: screenRows,
softWrappedFlags: softWrappedFlags,
foldableFlags: foldableFlags,
- decorations: decorationsToRender.lineNumbers,
+ decorations: decorationsToRender.lineNumbers.get(gutter.name) || [],
blockDecorations: decorationsToRender.blocks,
didMeasureVisibleBlockDecoration: didMeasureVisibleBlockDecoration,
height: scrollHeight,
- width: lineNumberGutterWidth,
+ width,
lineHeight: lineHeight,
showLineNumbers
})
} else {
return $(LineNumberGutterComponent, {
- ref: 'lineNumberGutter',
+ ref,
element: gutter.getElement(),
+ name: gutter.name,
+ className: gutter.className,
+ onMouseDown: gutter.onMouseDown,
+ onMouseMove: gutter.onMouseMove,
maxDigits: lineNumbersToRender.maxDigits,
showLineNumbers
})
@@ -3206,7 +3205,8 @@ class LineNumberGutterComponent {
render () {
const {
rootComponent, showLineNumbers, height, width, startRow, endRow, rowsPerTile,
- maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags, decorations
+ maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags, decorations,
+ className
} = this.props
let children = null
@@ -3234,8 +3234,12 @@ class LineNumberGutterComponent {
let number = null
if (showLineNumbers) {
- number = softWrapped ? '•' : bufferRow + 1
- number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number
+ if (this.props.labelFn == null) {
+ number = softWrapped ? '•' : bufferRow + 1
+ number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number
+ } else {
+ number = this.props.labelFn({bufferRow, screenRow, foldable, softWrapped, maxDigits})
+ }
}
// We need to adjust the line number position to account for block
@@ -3262,6 +3266,7 @@ class LineNumberGutterComponent {
const tileTop = rootComponent.pixelPositionBeforeBlocksForRow(tileStartRow)
const tileBottom = rootComponent.pixelPositionBeforeBlocksForRow(tileEndRow)
const tileHeight = tileBottom - tileTop
+ const tileWidth = width != null && width > 0 ? width + 'px' : ''
children[i] = $.div({
key: rootComponent.idsByTileStartRow.get(tileStartRow),
@@ -3270,20 +3275,26 @@ class LineNumberGutterComponent {
position: 'absolute',
top: 0,
height: tileHeight + 'px',
- width: width + 'px',
+ width: tileWidth,
transform: `translateY(${tileTop}px)`
}
}, ...tileChildren)
}
}
+ let rootClassName = 'gutter line-numbers'
+ if (className) {
+ rootClassName += ' ' + className
+ }
+
return $.div(
{
- className: 'gutter line-numbers',
- attributes: {'gutter-name': 'line-number'},
+ className: rootClassName,
+ attributes: {'gutter-name': this.props.name},
style: {position: 'relative', height: ceilToPhysicalPixelBoundary(height) + 'px'},
on: {
- mousedown: this.didMouseDown
+ mousedown: this.didMouseDown,
+ mousemove: this.didMouseMove
}
},
$.div({key: 'placeholder', className: 'line-number dummy', style: {visibility: 'hidden'}},
@@ -3305,6 +3316,8 @@ class LineNumberGutterComponent {
if (oldProps.endRow !== newProps.endRow) return true
if (oldProps.rowsPerTile !== newProps.rowsPerTile) return true
if (oldProps.maxDigits !== newProps.maxDigits) return true
+ if (oldProps.labelFn !== newProps.labelFn) return true
+ if (oldProps.className !== newProps.className) return true
if (newProps.didMeasureVisibleBlockDecoration) return true
if (!arraysEqual(oldProps.keys, newProps.keys)) return true
if (!arraysEqual(oldProps.bufferRows, newProps.bufferRows)) return true
@@ -3351,7 +3364,27 @@ class LineNumberGutterComponent {
}
didMouseDown (event) {
- this.props.rootComponent.didMouseDownOnLineNumberGutter(event)
+ if (this.props.onMouseDown == null) {
+ this.props.rootComponent.didMouseDownOnLineNumberGutter(event)
+ } else {
+ const {bufferRow, screenRow} = event.target.dataset
+ this.props.onMouseDown({
+ bufferRow: parseInt(bufferRow, 10),
+ screenRow: parseInt(screenRow, 10),
+ domEvent: event
+ })
+ }
+ }
+
+ didMouseMove (event) {
+ if (this.props.onMouseMove != null) {
+ const {bufferRow, screenRow} = event.target.dataset
+ this.props.onMouseMove({
+ bufferRow: parseInt(bufferRow, 10),
+ screenRow: parseInt(screenRow, 10),
+ domEvent: event
+ })
+ }
}
}
@@ -3359,7 +3392,8 @@ class LineNumberComponent {
constructor (props) {
const {className, width, marginTop, bufferRow, screenRow, number, nodePool} = props
this.props = props
- const style = {width: width + 'px'}
+ const style = {}
+ if (width != null && width > 0) style.width = width + 'px'
if (marginTop != null && marginTop > 0) style.marginTop = marginTop + 'px'
this.element = nodePool.getElement('DIV', className, style)
this.element.dataset.bufferRow = bufferRow
@@ -3379,22 +3413,31 @@ class LineNumberComponent {
if (this.props.bufferRow !== bufferRow) this.element.dataset.bufferRow = bufferRow
if (this.props.screenRow !== screenRow) this.element.dataset.screenRow = screenRow
if (this.props.className !== className) this.element.className = className
- if (this.props.width !== width) this.element.style.width = width + 'px'
+ if (this.props.width !== width) {
+ if (width != null && width > 0) {
+ this.element.style.width = width + 'px'
+ } else {
+ this.element.style.width = ''
+ }
+ }
if (this.props.marginTop !== marginTop) {
- if (marginTop != null) {
+ if (marginTop != null && marginTop > 0) {
this.element.style.marginTop = marginTop + 'px'
} else {
this.element.style.marginTop = ''
}
}
+
if (this.props.number !== number) {
- if (number) {
- this.element.insertBefore(nodePool.getTextNode(number), this.element.firstChild)
- } else {
+ if (this.props.number != null) {
const numberNode = this.element.firstChild
numberNode.remove()
nodePool.release(numberNode)
}
+
+ if (number != null) {
+ this.element.insertBefore(nodePool.getTextNode(number), this.element.firstChild)
+ }
}
this.props = props
@@ -3420,9 +3463,13 @@ class CustomGutterComponent {
}
render () {
+ let className = 'gutter'
+ if (this.props.className) {
+ className += ' ' + this.props.className
+ }
return $.div(
{
- className: 'gutter',
+ className,
attributes: {'gutter-name': this.props.name},
style: {
display: this.props.visible ? '' : 'none'
@@ -3522,7 +3569,7 @@ class CursorsAndInputComponent {
const cursorStyle = {
height: cursorHeight,
- width: pixelWidth + 'px',
+ width: Math.min(pixelWidth, scrollWidth - pixelLeft) + 'px',
transform: `translate(${pixelLeft}px, ${pixelTop}px)`
}
if (extraCursorStyle) Object.assign(cursorStyle, extraCursorStyle)
diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js
index 132b24ffb..e9dbf2f5c 100644
--- a/src/text-editor-registry.js
+++ b/src/text-editor-registry.js
@@ -218,7 +218,7 @@ class TextEditorRegistry {
async updateAndMonitorEditorSettings (editor, oldLanguageMode) {
await this.initialPackageActivationPromise
this.updateEditorSettingsForLanguageMode(editor, oldLanguageMode)
- await this.subscribeToSettingsForEditorScope(editor)
+ this.subscribeToSettingsForEditorScope(editor)
}
updateEditorSettingsForLanguageMode (editor, oldLanguageMode) {
@@ -246,7 +246,9 @@ class TextEditorRegistry {
}
}
- async subscribeToSettingsForEditorScope (editor) {
+ subscribeToSettingsForEditorScope (editor) {
+ if (!this.editorsWithMaintainedConfig) return
+
const scopeDescriptor = editor.getRootScopeDescriptor()
const scopeChain = scopeDescriptor.getScopeChain()
diff --git a/src/text-editor.js b/src/text-editor.js
index 9bfa8ff3e..3616db28c 100644
--- a/src/text-editor.js
+++ b/src/text-editor.js
@@ -42,9 +42,10 @@ const DEFAULT_NON_WORD_CHARACTERS = "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…"
// then be called with all current editor instances and also when any editor is
// created in the future.
//
-// ```coffee
-// atom.workspace.observeTextEditors (editor) ->
+// ```js
+// atom.workspace.observeTextEditors(editor => {
// editor.insertText('Hello World')
+// })
// ```
//
// ## Buffer vs. Screen Coordinates
@@ -106,6 +107,13 @@ class TextEditor {
}
state.assert = atomEnvironment.assert.bind(atomEnvironment)
+
+ // Semantics of the readOnly flag have changed since its introduction.
+ // Only respect readOnly2, which has been set with the current readOnly semantics.
+ delete state.readOnly
+ state.readOnly = state.readOnly2
+ delete state.readOnly2
+
const editor = new TextEditor(state)
if (state.registered) {
const disposable = atomEnvironment.textEditors.add(editor)
@@ -129,6 +137,7 @@ class TextEditor {
this.decorationManager = params.decorationManager
this.selectionsMarkerLayer = params.selectionsMarkerLayer
this.mini = (params.mini != null) ? params.mini : false
+ this.keyboardInputEnabled = (params.keyboardInputEnabled != null) ? params.keyboardInputEnabled : true
this.readOnly = (params.readOnly != null) ? params.readOnly : false
this.placeholderText = params.placeholderText
this.showLineNumbers = params.showLineNumbers
@@ -224,7 +233,7 @@ class TextEditor {
this.defaultMarkerLayer = this.displayLayer.addMarkerLayer()
if (!this.selectionsMarkerLayer) {
- this.selectionsMarkerLayer = this.addMarkerLayer({maintainHistory: true, persistent: true})
+ this.selectionsMarkerLayer = this.addMarkerLayer({maintainHistory: true, persistent: true, role: 'selections'})
}
this.decorationManager = new DecorationManager(this)
@@ -249,6 +258,7 @@ class TextEditor {
this.gutterContainer = new GutterContainer(this)
this.lineNumberGutter = this.gutterContainer.addGutter({
name: 'line-number',
+ type: 'line-number',
priority: 0,
visible: params.lineNumberGutterVisible
})
@@ -415,6 +425,15 @@ class TextEditor {
}
break
+ case 'keyboardInputEnabled':
+ if (value !== this.keyboardInputEnabled) {
+ this.keyboardInputEnabled = value
+ if (this.component != null) {
+ this.component.scheduleUpdate()
+ }
+ }
+ break
+
case 'placeholderText':
if (value !== this.placeholderText) {
this.placeholderText = value
@@ -545,7 +564,8 @@ class TextEditor {
softWrapAtPreferredLineLength: this.softWrapAtPreferredLineLength,
preferredLineLength: this.preferredLineLength,
mini: this.mini,
- readOnly: this.readOnly,
+ readOnly2: this.readOnly, // readOnly encompassed both readOnly and keyboardInputEnabled
+ keyboardInputEnabled: this.keyboardInputEnabled,
editorWidthInChars: this.editorWidthInChars,
width: this.width,
maxScreenLineLength: this.maxScreenLineLength,
@@ -987,6 +1007,12 @@ class TextEditor {
isReadOnly () { return this.readOnly }
+ enableKeyboardInput (enabled) {
+ this.update({keyboardInputEnabled: enabled})
+ }
+
+ isKeyboardInputEnabled () { return this.keyboardInputEnabled }
+
onDidChangeMini (callback) {
return this.emitter.on('did-change-mini', callback)
}
@@ -995,6 +1021,10 @@ class TextEditor {
isLineNumberGutterVisible () { return this.lineNumberGutter.isVisible() }
+ anyLineNumberGutterVisible () {
+ return this.getGutters().some(gutter => gutter.type === 'line-number' && gutter.visible)
+ }
+
onDidChangeLineNumberGutterVisible (callback) {
return this.emitter.on('did-change-line-number-gutter-visible', callback)
}
@@ -1306,7 +1336,12 @@ class TextEditor {
// Essential: Replaces the entire contents of the buffer with the given {String}.
//
// * `text` A {String} to replace with
- setText (text) { return this.buffer.setText(text) }
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
+ setText (text, options = {}) {
+ if (!this.ensureWritable('setText', options)) return
+ return this.buffer.setText(text)
+ }
// Essential: Set the text in the given {Range} in buffer coordinates.
//
@@ -1314,10 +1349,12 @@ class TextEditor {
// * `text` A {String}
// * `options` (optional) {Object}
// * `normalizeLineEndings` (optional) {Boolean} (default: true)
- // * `undo` (optional) {String} 'skip' will skip the undo system
+ // * `undo` (optional) *Deprecated* {String} 'skip' will skip the undo system. This property is deprecated. Call groupLastChanges() on the {TextBuffer} afterward instead.
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
//
// Returns the {Range} of the newly-inserted text.
- setTextInBufferRange (range, text, options) {
+ setTextInBufferRange (range, text, options = {}) {
+ if (!this.ensureWritable('setTextInBufferRange', options)) return
return this.getBuffer().setTextInRange(range, text, options)
}
@@ -1326,9 +1363,9 @@ class TextEditor {
// * `text` A {String} representing the text to insert.
// * `options` (optional) See {Selection::insertText}.
//
- // Returns a {Range} when the text has been inserted
- // Returns a {Boolean} false when the text has not been inserted
+ // Returns a {Range} when the text has been inserted. Returns a {Boolean} `false` when the text has not been inserted.
insertText (text, options = {}) {
+ if (!this.ensureWritable('insertText', options)) return
if (!this.emitWillInsertTextEvent(text)) return false
let groupLastChanges = false
@@ -1352,20 +1389,31 @@ class TextEditor {
}
// Essential: For each selection, replace the selected text with a newline.
- insertNewline (options) {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ insertNewline (options = {}) {
return this.insertText('\n', options)
}
// Essential: For each selection, if the selection is empty, delete the character
// following the cursor. Otherwise delete the selected text.
- delete () {
- return this.mutateSelectedText(selection => selection.delete())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ delete (options = {}) {
+ if (!this.ensureWritable('delete', options)) return
+ return this.mutateSelectedText(selection => selection.delete(options))
}
// Essential: For each selection, if the selection is empty, delete the character
// preceding the cursor. Otherwise delete the selected text.
- backspace () {
- return this.mutateSelectedText(selection => selection.backspace())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ backspace (options = {}) {
+ if (!this.ensureWritable('backspace', options)) return
+ return this.mutateSelectedText(selection => selection.backspace(options))
}
// Extended: Mutate the text of all the selections in a single transaction.
@@ -1386,7 +1434,12 @@ class TextEditor {
// Move lines intersecting the most recent selection or multiple selections
// up by one row in screen coordinates.
- moveLineUp () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ moveLineUp (options = {}) {
+ if (!this.ensureWritable('moveLineUp', options)) return
+
const selections = this.getSelectedBufferRanges().sort((a, b) => a.compare(b))
if (selections[0].start.row === 0) return
@@ -1454,7 +1507,12 @@ class TextEditor {
// Move lines intersecting the most recent selection or multiple selections
// down by one row in screen coordinates.
- moveLineDown () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ moveLineDown (options = {}) {
+ if (!this.ensureWritable('moveLineDown', options)) return
+
const selections = this.getSelectedBufferRanges()
selections.sort((a, b) => b.compare(a))
@@ -1526,7 +1584,11 @@ class TextEditor {
}
// Move any active selections one column to the left.
- moveSelectionLeft () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ moveSelectionLeft (options = {}) {
+ if (!this.ensureWritable('moveSelectionLeft', options)) return
const selections = this.getSelectedBufferRanges()
const noSelectionAtStartOfLine = selections.every(selection => selection.start.column !== 0)
@@ -1550,7 +1612,11 @@ class TextEditor {
}
// Move any active selections one column to the right.
- moveSelectionRight () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ moveSelectionRight (options = {}) {
+ if (!this.ensureWritable('moveSelectionRight', options)) return
const selections = this.getSelectedBufferRanges()
const noSelectionAtEndOfLine = selections.every(selection => {
return selection.end.column !== this.buffer.lineLengthForRow(selection.end.row)
@@ -1575,7 +1641,12 @@ class TextEditor {
}
}
- duplicateLines () {
+ // Duplicate all lines containing active selections.
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ duplicateLines (options = {}) {
+ if (!this.ensureWritable('duplicateLines', options)) return
this.transact(() => {
const selections = this.getSelectionsOrderedByBufferPosition()
const previousSelectionRanges = []
@@ -1662,7 +1733,11 @@ class TextEditor {
//
// If the selection is empty, the characters preceding and following the cursor
// are swapped. Otherwise, the selected characters are reversed.
- transpose () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ transpose (options = {}) {
+ if (!this.ensureWritable('transpose', options)) return
this.mutateSelectedText(selection => {
if (selection.isEmpty()) {
selection.selectRight()
@@ -1680,23 +1755,35 @@ class TextEditor {
//
// For each selection, if the selection is empty, converts the containing word
// to upper case. Otherwise convert the selected text to upper case.
- upperCase () {
- this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toUpperCase())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ upperCase (options = {}) {
+ if (!this.ensureWritable('upperCase', options)) return
+ this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toUpperCase(options))
}
// Extended: Convert the selected text to lower case.
//
// For each selection, if the selection is empty, converts the containing word
// to upper case. Otherwise convert the selected text to upper case.
- lowerCase () {
- this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toLowerCase())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ lowerCase (options = {}) {
+ if (!this.ensureWritable('lowerCase', options)) return
+ this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toLowerCase(options))
}
// Extended: Toggle line comments for rows intersecting selections.
//
// If the current grammar doesn't support comments, does nothing.
- toggleLineCommentsInSelection () {
- this.mutateSelectedText(selection => selection.toggleLineComments())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ toggleLineCommentsInSelection (options = {}) {
+ if (!this.ensureWritable('toggleLineCommentsInSelection', options)) return
+ this.mutateSelectedText(selection => selection.toggleLineComments(options))
}
// Convert multiple lines to a single line.
@@ -1707,20 +1794,32 @@ class TextEditor {
//
// Joining a line means that multiple lines are converted to a single line with
// the contents of each of the original non-empty lines separated by a space.
- joinLines () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ joinLines (options = {}) {
+ if (!this.ensureWritable('joinLines', options)) return
this.mutateSelectedText(selection => selection.joinLines())
}
// Extended: For each cursor, insert a newline at beginning the following line.
- insertNewlineBelow () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ insertNewlineBelow (options = {}) {
+ if (!this.ensureWritable('insertNewlineBelow', options)) return
this.transact(() => {
this.moveToEndOfLine()
- this.insertNewline()
+ this.insertNewline(options)
})
}
// Extended: For each cursor, insert a newline at the end of the preceding line.
- insertNewlineAbove () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ insertNewlineAbove (options = {}) {
+ if (!this.ensureWritable('insertNewlineAbove', options)) return
this.transact(() => {
const bufferRow = this.getCursorBufferPosition().row
const indentLevel = this.indentationForBufferRow(bufferRow)
@@ -1728,7 +1827,7 @@ class TextEditor {
this.moveToBeginningOfLine()
this.moveLeft()
- this.insertNewline()
+ this.insertNewline(options)
if (this.shouldAutoIndent() && (this.indentationForBufferRow(bufferRow) < indentLevel)) {
this.setIndentationForBufferRow(bufferRow, indentLevel)
@@ -1744,62 +1843,117 @@ class TextEditor {
// Extended: For each selection, if the selection is empty, delete all characters
// of the containing word that precede the cursor. Otherwise delete the
// selected text.
- deleteToBeginningOfWord () {
- this.mutateSelectedText(selection => selection.deleteToBeginningOfWord())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ deleteToBeginningOfWord (options = {}) {
+ if (!this.ensureWritable('deleteToBeginningOfWord', options)) return
+ this.mutateSelectedText(selection => selection.deleteToBeginningOfWord(options))
}
// Extended: Similar to {::deleteToBeginningOfWord}, but deletes only back to the
// previous word boundary.
- deleteToPreviousWordBoundary () {
- this.mutateSelectedText(selection => selection.deleteToPreviousWordBoundary())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ deleteToPreviousWordBoundary (options = {}) {
+ if (!this.ensureWritable('deleteToPreviousWordBoundary', options)) return
+ this.mutateSelectedText(selection => selection.deleteToPreviousWordBoundary(options))
}
// Extended: Similar to {::deleteToEndOfWord}, but deletes only up to the
// next word boundary.
- deleteToNextWordBoundary () {
- this.mutateSelectedText(selection => selection.deleteToNextWordBoundary())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ deleteToNextWordBoundary (options = {}) {
+ if (!this.ensureWritable('deleteToNextWordBoundary', options)) return
+ this.mutateSelectedText(selection => selection.deleteToNextWordBoundary(options))
}
// Extended: For each selection, if the selection is empty, delete all characters
// of the containing subword following the cursor. Otherwise delete the selected
// text.
- deleteToBeginningOfSubword () {
- this.mutateSelectedText(selection => selection.deleteToBeginningOfSubword())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ deleteToBeginningOfSubword (options = {}) {
+ if (!this.ensureWritable('deleteToBeginningOfSubword', options)) return
+ this.mutateSelectedText(selection => selection.deleteToBeginningOfSubword(options))
}
// Extended: For each selection, if the selection is empty, delete all characters
// of the containing subword following the cursor. Otherwise delete the selected
// text.
- deleteToEndOfSubword () {
- this.mutateSelectedText(selection => selection.deleteToEndOfSubword())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ deleteToEndOfSubword (options = {}) {
+ if (!this.ensureWritable('deleteToEndOfSubword', options)) return
+ this.mutateSelectedText(selection => selection.deleteToEndOfSubword(options))
}
// Extended: For each selection, if the selection is empty, delete all characters
// of the containing line that precede the cursor. Otherwise delete the
// selected text.
- deleteToBeginningOfLine () {
- this.mutateSelectedText(selection => selection.deleteToBeginningOfLine())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ deleteToBeginningOfLine (options = {}) {
+ if (!this.ensureWritable('deleteToBeginningOfLine', options)) return
+ this.mutateSelectedText(selection => selection.deleteToBeginningOfLine(options))
}
// Extended: For each selection, if the selection is not empty, deletes the
// selection; otherwise, deletes all characters of the containing line
// following the cursor. If the cursor is already at the end of the line,
// deletes the following newline.
- deleteToEndOfLine () {
- this.mutateSelectedText(selection => selection.deleteToEndOfLine())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ deleteToEndOfLine (options = {}) {
+ if (!this.ensureWritable('deleteToEndOfLine', options)) return
+ this.mutateSelectedText(selection => selection.deleteToEndOfLine(options))
}
// Extended: For each selection, if the selection is empty, delete all characters
// of the containing word following the cursor. Otherwise delete the selected
// text.
- deleteToEndOfWord () {
- this.mutateSelectedText(selection => selection.deleteToEndOfWord())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ deleteToEndOfWord (options = {}) {
+ if (!this.ensureWritable('deleteToEndOfWord', options)) return
+ this.mutateSelectedText(selection => selection.deleteToEndOfWord(options))
}
// Extended: Delete all lines intersecting selections.
- deleteLine () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ deleteLine (options = {}) {
+ if (!this.ensureWritable('deleteLine', options)) return
this.mergeSelectionsOnSameRows()
- this.mutateSelectedText(selection => selection.deleteLine())
+ this.mutateSelectedText(selection => selection.deleteLine(options))
+ }
+
+ // Private: Ensure that this editor is not marked read-only before allowing a buffer modification to occur. If
+ // the editor is read-only, require an explicit opt-in option to proceed (`bypassReadOnly`) or throw an Error.
+ ensureWritable (methodName, opts) {
+ if (!opts.bypassReadOnly && this.isReadOnly()) {
+ if (atom.inDevMode() || atom.inSpecMode()) {
+ const e = new Error('Attempt to mutate a read-only TextEditor')
+ e.detail =
+ `Your package is attempting to call ${methodName} on an editor that has been marked read-only. ` +
+ 'Pass {bypassReadOnly: true} to modify it anyway, or test editors with .isReadOnly() before attempting ' +
+ 'modifications.'
+ throw e
+ }
+
+ return false
+ }
+
+ return true
}
/*
@@ -1807,14 +1961,22 @@ class TextEditor {
*/
// Essential: Undo the last change.
- undo () {
- this.avoidMergingSelections(() => this.buffer.undo())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ undo (options = {}) {
+ if (!this.ensureWritable('undo', options)) return
+ this.avoidMergingSelections(() => this.buffer.undo({selectionsMarkerLayer: this.selectionsMarkerLayer}))
this.getLastSelection().autoscroll()
}
// Essential: Redo the last change.
- redo () {
- this.avoidMergingSelections(() => this.buffer.redo())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
+ redo (options = {}) {
+ if (!this.ensureWritable('redo', options)) return
+ this.avoidMergingSelections(() => this.buffer.redo({selectionsMarkerLayer: this.selectionsMarkerLayer}))
this.getLastSelection().autoscroll()
}
@@ -1831,7 +1993,13 @@ class TextEditor {
// still 'groupable', the two transactions are merged with respect to undo and redo.
// * `fn` A {Function} to call inside the transaction.
transact (groupingInterval, fn) {
- return this.buffer.transact(groupingInterval, fn)
+ const options = {selectionsMarkerLayer: this.selectionsMarkerLayer}
+ if (typeof groupingInterval === 'function') {
+ fn = groupingInterval
+ } else {
+ options.groupingInterval = groupingInterval
+ }
+ return this.buffer.transact(options, fn)
}
// Extended: Abort an open transaction, undoing any operations performed so far
@@ -1842,7 +2010,9 @@ class TextEditor {
// with {::revertToCheckpoint} and {::groupChangesSinceCheckpoint}.
//
// Returns a checkpoint value.
- createCheckpoint () { return this.buffer.createCheckpoint() }
+ createCheckpoint () {
+ return this.buffer.createCheckpoint({selectionsMarkerLayer: this.selectionsMarkerLayer})
+ }
// Extended: Revert the buffer to the state it was in when the given
// checkpoint was created.
@@ -1866,7 +2036,9 @@ class TextEditor {
// * `checkpoint` The checkpoint from which to group changes.
//
// Returns a {Boolean} indicating whether the operation succeeded.
- groupChangesSinceCheckpoint (checkpoint) { return this.buffer.groupChangesSinceCheckpoint(checkpoint) }
+ groupChangesSinceCheckpoint (checkpoint) {
+ return this.buffer.groupChangesSinceCheckpoint(checkpoint, {selectionsMarkerLayer: this.selectionsMarkerLayer})
+ }
/*
Section: TextEditor Coordinates
@@ -1957,11 +2129,11 @@ class TextEditor {
//
// ## Examples
//
- // ```coffee
- // editor.clipBufferPosition([-1, -1]) # -> `[0, 0]`
+ // ```js
+ // editor.clipBufferPosition([-1, -1]) // -> `[0, 0]`
//
- // # When the line at buffer row 2 is 10 characters long
- // editor.clipBufferPosition([2, Infinity]) # -> `[2, 10]`
+ // // When the line at buffer row 2 is 10 characters long
+ // editor.clipBufferPosition([2, Infinity]) // -> `[2, 10]`
// ```
//
// * `bufferPosition` The {Point} representing the position to clip.
@@ -1986,11 +2158,11 @@ class TextEditor {
//
// ## Examples
//
- // ```coffee
- // editor.clipScreenPosition([-1, -1]) # -> `[0, 0]`
+ // ```js
+ // editor.clipScreenPosition([-1, -1]) // -> `[0, 0]`
//
- // # When the line at screen row 2 is 10 characters long
- // editor.clipScreenPosition([2, Infinity]) # -> `[2, 10]`
+ // // When the line at screen row 2 is 10 characters long
+ // editor.clipScreenPosition([2, Infinity]) // -> `[2, 10]`
// ```
//
// * `screenPosition` The {Point} representing the position to clip.
@@ -3548,13 +3720,21 @@ class TextEditor {
}
// Extended: Indent rows intersecting selections by one level.
- indentSelectedRows () {
- return this.mutateSelectedText(selection => selection.indentSelectedRows())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
+ indentSelectedRows (options = {}) {
+ if (!this.ensureWritable('indentSelectedRows', options)) return
+ return this.mutateSelectedText(selection => selection.indentSelectedRows(options))
}
// Extended: Outdent rows intersecting selections by one level.
- outdentSelectedRows () {
- return this.mutateSelectedText(selection => selection.outdentSelectedRows())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
+ outdentSelectedRows (options = {}) {
+ if (!this.ensureWritable('outdentSelectedRows', options)) return
+ return this.mutateSelectedText(selection => selection.outdentSelectedRows(options))
}
// Extended: Get the indentation level of the given line of text.
@@ -3585,13 +3765,21 @@ class TextEditor {
// Extended: Indent rows intersecting selections based on the grammar's suggested
// indent level.
- autoIndentSelectedRows () {
- return this.mutateSelectedText(selection => selection.autoIndentSelectedRows())
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
+ autoIndentSelectedRows (options = {}) {
+ if (!this.ensureWritable('autoIndentSelectedRows', options)) return
+ return this.mutateSelectedText(selection => selection.autoIndentSelectedRows(options))
}
// Indent all lines intersecting selections. See {Selection::indent} for more
// information.
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
indent (options = {}) {
+ if (!this.ensureWritable('indent', options)) return
if (options.autoIndent == null) options.autoIndent = this.shouldAutoIndent()
this.mutateSelectedText(selection => selection.indent(options))
}
@@ -3729,14 +3917,18 @@ class TextEditor {
}
// Essential: For each selection, cut the selected text.
- cutSelectedText () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
+ cutSelectedText (options = {}) {
+ if (!this.ensureWritable('cutSelectedText', options)) return
let maintainClipboard = false
this.mutateSelectedText(selection => {
if (selection.isEmpty()) {
selection.selectLine()
- selection.cut(maintainClipboard, true)
+ selection.cut(maintainClipboard, true, options.bypassReadOnly)
} else {
- selection.cut(maintainClipboard, false)
+ selection.cut(maintainClipboard, false, options.bypassReadOnly)
}
maintainClipboard = true
})
@@ -3750,7 +3942,8 @@ class TextEditor {
// corresponding clipboard selection text.
//
// * `options` (optional) See {Selection::insertText}.
- pasteText (options) {
+ pasteText (options = {}) {
+ if (!this.ensureWritable('parseText', options)) return
options = Object.assign({}, options)
let {text: clipboardText, metadata} = this.constructor.clipboard.readWithMetadata()
if (!this.emitWillInsertTextEvent(clipboardText)) return false
@@ -3791,10 +3984,14 @@ class TextEditor {
// Essential: For each selection, if the selection is empty, cut all characters
// of the containing screen line following the cursor. Otherwise cut the selected
// text.
- cutToEndOfLine () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
+ cutToEndOfLine (options = {}) {
+ if (!this.ensureWritable('cutToEndOfLine', options)) return
let maintainClipboard = false
this.mutateSelectedText(selection => {
- selection.cutToEndOfLine(maintainClipboard)
+ selection.cutToEndOfLine(maintainClipboard, options)
maintainClipboard = true
})
}
@@ -3802,10 +3999,14 @@ class TextEditor {
// Essential: For each selection, if the selection is empty, cut all characters
// of the containing buffer line following the cursor. Otherwise cut the
// selected text.
- cutToEndOfBufferLine () {
+ //
+ // * `options` (optional) {Object}
+ // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
+ cutToEndOfBufferLine (options = {}) {
+ if (!this.ensureWritable('cutToEndOfBufferLine', options)) return
let maintainClipboard = false
this.mutateSelectedText(selection => {
- selection.cutToEndOfBufferLine(maintainClipboard)
+ selection.cutToEndOfBufferLine(maintainClipboard, options)
maintainClipboard = true
})
}
@@ -3897,7 +4098,7 @@ class TextEditor {
// Extended: Unfold all existing folds.
unfoldAll () {
const result = this.displayLayer.destroyAllFolds()
- this.scrollToCursorPosition()
+ if (result.length > 0) this.scrollToCursorPosition()
return result
}
@@ -4015,6 +4216,29 @@ class TextEditor {
// window. (default: -100)
// * `visible` (optional) {Boolean} specifying whether the gutter is visible
// initially after being created. (default: true)
+ // * `type` (optional) {String} specifying the type of gutter to create. `'decorated'`
+ // gutters are useful as a destination for decorations created with {Gutter::decorateMarker}.
+ // `'line-number'` gutters.
+ // * `class` (optional) {String} added to the CSS classnames of the gutter's root DOM element.
+ // * `labelFn` (optional) {Function} called by a `'line-number'` gutter to generate the label for each line number
+ // element. Should return a {String} that will be used to label the corresponding line.
+ // * `lineData` an {Object} containing information about each line to label.
+ // * `bufferRow` {Number} indicating the zero-indexed buffer index of this line.
+ // * `screenRow` {Number} indicating the zero-indexed screen index.
+ // * `foldable` {Boolean} that is `true` if a fold may be created here.
+ // * `softWrapped` {Boolean} if this screen row is the soft-wrapped continuation of the same buffer row.
+ // * `maxDigits` {Number} the maximum number of digits necessary to represent any known screen row.
+ // * `onMouseDown` (optional) {Function} to be called when a mousedown event is received by a line-number
+ // element within this `type: 'line-number'` {Gutter}. If unspecified, the default behavior is to select the
+ // clicked buffer row.
+ // * `lineData` an {Object} containing information about the line that's being clicked.
+ // * `bufferRow` {Number} of the originating line element
+ // * `screenRow` {Number}
+ // * `onMouseMove` (optional) {Function} to be called when a mousemove event occurs on a line-number element within
+ // within this `type: 'line-number'` {Gutter}.
+ // * `lineData` an {Object} containing information about the line that's being clicked.
+ // * `bufferRow` {Number} of the originating line element
+ // * `screenRow` {Number}
//
// Returns the newly-created {Gutter}.
addGutter (options) {
@@ -4619,7 +4843,7 @@ class TextEditor {
let endRow = bufferRow
const rowCount = this.getLineCount()
- while (endRow < rowCount) {
+ while (endRow + 1 < rowCount) {
if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(endRow + 1))) break
if (languageMode.isRowCommented(endRow + 1) !== isCommented) break
endRow++
diff --git a/src/text-mate-language-mode.js b/src/text-mate-language-mode.js
index 152636ab7..471af9af2 100644
--- a/src/text-mate-language-mode.js
+++ b/src/text-mate-language-mode.js
@@ -7,6 +7,7 @@ const ScopeDescriptor = require('./scope-descriptor')
const NullGrammar = require('./null-grammar')
const {OnigRegExp} = require('oniguruma')
const {toFirstMateScopeId, fromFirstMateScopeId} = require('./first-mate-helpers')
+const {selectorMatchesAnyScope} = require('./selectors')
const NON_WHITESPACE_REGEX = /\S/
@@ -235,15 +236,18 @@ class TextMateLanguageMode {
return this.buffer.getTextInRange([[0, 0], [10, 0]])
}
- hasTokenForSelector (selector) {
+ updateForInjection (grammar) {
+ if (!grammar.injectionSelector) return
for (const tokenizedLine of this.tokenizedLines) {
if (tokenizedLine) {
for (let token of tokenizedLine.tokens) {
- if (selector.matches(token.scopes)) return true
+ if (grammar.injectionSelector.matches(token.scopes)) {
+ this.retokenizeLines()
+ return
+ }
}
}
}
- return false
}
retokenizeLines () {
@@ -723,14 +727,6 @@ class TextMateLanguageMode {
TextMateLanguageMode.prototype.chunkSize = 50
-function selectorMatchesAnyScope (selector, scopes) {
- const targetClasses = selector.replace(/^\./, '').split('.')
- return scopes.some((scope) => {
- const scopeClasses = scope.split('.')
- return _.isSubset(targetClasses, scopeClasses)
- })
-}
-
class TextMateHighlightIterator {
constructor (languageMode) {
this.languageMode = languageMode
diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js
index 34f96775b..07b4f196a 100644
--- a/src/tooltip-manager.js
+++ b/src/tooltip-manager.js
@@ -153,9 +153,11 @@ class TooltipManager {
}
window.addEventListener('resize', hideTooltip)
+ window.addEventListener('keydown', hideTooltip)
const disposable = new Disposable(() => {
window.removeEventListener('resize', hideTooltip)
+ window.removeEventListener('keydown', hideTooltip)
hideTooltip()
tooltip.destroy()
diff --git a/src/tree-sitter-grammar.js b/src/tree-sitter-grammar.js
index d00344fb1..fc572221a 100644
--- a/src/tree-sitter-grammar.js
+++ b/src/tree-sitter-grammar.js
@@ -6,12 +6,17 @@ 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.scopeName = params.scopeName
+
+ // TODO - Remove the `RegExp` spelling and only support `Regex`, once all of the existing
+ // Tree-sitter grammars are updated to spell it `Regex`.
+ this.contentRegex = buildRegex(params.contentRegex || params.contentRegExp)
+ this.injectionRegex = buildRegex(params.injectionRegex || params.injectionRegExp)
+ this.firstLineRegex = buildRegex(params.firstLineRegex)
this.folds = params.folds || []
+ this.folds.forEach(normalizeFoldSpecification)
this.commentStrings = {
commentStartString: params.comments && params.comments.start,
@@ -20,14 +25,22 @@ class TreeSitterGrammar {
const scopeSelectors = {}
for (const key in params.scopes || {}) {
- scopeSelectors[key] = params.scopes[key]
- .split('.')
- .map(s => `syntax--${s}`)
- .join(' ')
+ const classes = toSyntaxClasses(params.scopes[key])
+ const selectors = key.split(/,\s+/)
+ for (let selector of selectors) {
+ selector = selector.trim()
+ if (!selector) continue
+ if (scopeSelectors[selector]) {
+ scopeSelectors[selector] = [].concat(scopeSelectors[selector], classes)
+ } else {
+ scopeSelectors[selector] = classes
+ }
+ }
}
this.scopeMap = new SyntaxScopeMap(scopeSelectors)
- this.fileTypes = params.fileTypes
+ this.fileTypes = params.fileTypes || []
+ this.injectionPoints = params.injectionPoints || []
// TODO - When we upgrade to a new enough version of node, use `require.resolve`
// with the new `paths` option instead of this private API.
@@ -39,11 +52,16 @@ class TreeSitterGrammar {
this.languageModule = require(languageModulePath)
this.scopesById = new Map()
+ this.conciseScopesById = new Map()
this.idsByScope = {}
this.nextScopeId = 256 + 1
this.registration = null
}
+ inspect () {
+ return `TreeSitterGrammar {scopeName: ${this.scopeName}}`
+ }
+
idForScope (scope) {
let id = this.idsByScope[scope]
if (!id) {
@@ -58,8 +76,15 @@ class TreeSitterGrammar {
return this.scopesById.get(id)
}
- get scopeName () {
- return this.id
+ scopeNameForScopeId (id) {
+ let result = this.conciseScopesById.get(id)
+ if (!result) {
+ result = this.scopesById.get(id)
+ .slice('syntax--'.length)
+ .replace(/ syntax--/g, '.')
+ this.conciseScopesById.set(id, result)
+ }
+ return result
}
activate () {
@@ -70,3 +95,56 @@ class TreeSitterGrammar {
if (this.registration) this.registration.dispose()
}
}
+
+const toSyntaxClasses = scopes =>
+ typeof scopes === 'string'
+ ? scopes
+ .split('.')
+ .map(s => `syntax--${s}`)
+ .join(' ')
+ : Array.isArray(scopes)
+ ? scopes.map(toSyntaxClasses)
+ : scopes.match
+ ? {match: new RegExp(scopes.match), scopes: toSyntaxClasses(scopes.scopes)}
+ : Object.assign({}, scopes, {scopes: toSyntaxClasses(scopes.scopes)})
+
+const NODE_NAME_REGEX = /[\w_]+/
+
+function matcherForSpec (spec) {
+ if (typeof spec === 'string') {
+ if (spec[0] === '"' && spec[spec.length - 1] === '"') {
+ return {
+ type: spec.substr(1, spec.length - 2),
+ named: false
+ }
+ }
+
+ if (!NODE_NAME_REGEX.test(spec)) {
+ return {type: spec, named: false}
+ }
+
+ return {type: spec, named: true}
+ }
+ return spec
+}
+
+function normalizeFoldSpecification (spec) {
+ if (spec.type) {
+ if (Array.isArray(spec.type)) {
+ spec.matchers = spec.type.map(matcherForSpec)
+ } else {
+ spec.matchers = [matcherForSpec(spec.type)]
+ }
+ }
+
+ if (spec.start) normalizeFoldSpecification(spec.start)
+ if (spec.end) normalizeFoldSpecification(spec.end)
+}
+
+function buildRegex (value) {
+ // Allow multiple alternatives to be specified via an array, for
+ // readability of the grammar file
+ if (Array.isArray(value)) value = value.map(_ => `(${_})`).join('|')
+ if (typeof value === 'string') return new RegExp(value)
+ return null
+}
diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js
index 0d2fab8cf..05aeaaa2b 100644
--- a/src/tree-sitter-language-mode.js
+++ b/src/tree-sitter-language-mode.js
@@ -1,49 +1,117 @@
-const {Document} = require('tree-sitter')
-const {Point, Range} = require('text-buffer')
-const {Emitter, Disposable} = require('event-kit')
+const Parser = require('tree-sitter')
+const {Point, Range, spliceArray} = require('text-buffer')
+const {Patch} = require('superstring')
+const {Emitter} = require('event-kit')
const ScopeDescriptor = require('./scope-descriptor')
const TokenizedLine = require('./tokenized-line')
const TextMateLanguageMode = require('./text-mate-language-mode')
+const {matcherForSelector} = require('./selectors')
let nextId = 0
+const MAX_RANGE = new Range(Point.ZERO, Point.INFINITY).freeze()
+const PARSER_POOL = []
+const WORD_REGEX = /\w/
-module.exports =
class TreeSitterLanguageMode {
- constructor ({buffer, grammar, config}) {
+ static _patchSyntaxNode () {
+ if (!Parser.SyntaxNode.prototype.hasOwnProperty('range')) {
+ Object.defineProperty(Parser.SyntaxNode.prototype, 'range', {
+ get () {
+ return rangeForNode(this)
+ }
+ })
+ }
+ }
+
+ constructor ({buffer, grammar, config, grammars, syncOperationLimit}) {
+ TreeSitterLanguageMode._patchSyntaxNode()
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.grammarRegistry = grammars
+ this.parser = new Parser()
+ this.rootLanguageLayer = new LanguageLayer(this, grammar)
+ this.injectionsMarkerLayer = buffer.addMarkerLayer()
+
+ if (syncOperationLimit != null) {
+ this.syncOperationLimit = syncOperationLimit
+ }
+
+ this.rootScopeDescriptor = new ScopeDescriptor({scopes: [this.grammar.scopeName]})
this.emitter = new Emitter()
this.isFoldableCache = []
+ this.hasQueuedParse = false
+
+ this.grammarForLanguageString = this.grammarForLanguageString.bind(this)
+ this.emitRangeUpdate = this.emitRangeUpdate.bind(this)
+
+ this.subscription = this.buffer.onDidChangeText(({changes}) => {
+ for (let i = 0, {length} = changes; i < length; i++) {
+ const {oldRange, newRange} = changes[i]
+ spliceArray(
+ this.isFoldableCache,
+ newRange.start.row,
+ oldRange.end.row - oldRange.start.row,
+ {length: newRange.end.row - newRange.start.row}
+ )
+ }
+
+ this.rootLanguageLayer.update(null)
+ })
+
+ this.rootLanguageLayer.update(null).then(() =>
+ this.emitter.emit('did-tokenize')
+ )
// 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
+ destroy () {
+ this.injectionsMarkerLayer.destroy()
+ this.subscription.dispose()
+ this.rootLanguageLayer = null
+ this.parser = null
}
- 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()
+ getLanguageId () {
+ return this.grammar.scopeName
+ }
+
+ bufferDidChange (change) {
+ this.rootLanguageLayer.handleTextChange(change)
+ for (const marker of this.injectionsMarkerLayer.getMarkers()) {
+ marker.languageLayer.handleTextChange(change)
+ }
+ }
+
+ parse (language, oldTree, ranges) {
+ const parser = PARSER_POOL.pop() || new Parser()
+ parser.setLanguage(language)
+ const result = parser.parseTextBuffer(this.buffer.buffer, oldTree, {
+ syncOperationLimit: this.syncOperationLimit,
+ includedRanges: ranges
})
+
+ if (result.then) {
+ return result.then(tree => {
+ PARSER_POOL.push(parser)
+ return tree
+ })
+ } else {
+ PARSER_POOL.push(parser)
+ return result
+ }
+ }
+
+ get tree () {
+ return this.rootLanguageLayer.tree
+ }
+
+ updateForInjection (grammar) {
+ this.rootLanguageLayer.updateInjections(grammar)
}
/*
@@ -51,17 +119,16 @@ class TreeSitterLanguageMode {
*/
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)
+ if (!this.rootLanguageLayer) return new NullHighlightIterator()
+ const layerIterators = [
+ this.rootLanguageLayer.buildHighlightIterator(),
+ ...this.injectionsMarkerLayer.getMarkers().map(m => m.languageLayer.buildHighlightIterator())
+ ]
+ return new HighlightIterator(this, layerIterators)
+ }
+
+ onDidTokenize (callback) {
+ return this.emitter.on('did-tokenize', callback)
}
onDidChangeHighlighting (callback) {
@@ -80,7 +147,15 @@ class TreeSitterLanguageMode {
return this.grammar.commentStrings
}
- isRowCommented () {
+ isRowCommented (row) {
+ const firstNonWhitespaceRange = this.buffer.findInRangeSync(
+ /\S/,
+ new Range(new Point(row, 0), new Point(row, Infinity))
+ )
+ if (firstNonWhitespaceRange) {
+ const firstNode = this.getSyntaxNodeContainingRange(firstNonWhitespaceRange)
+ if (firstNode) return firstNode.type.includes('comment')
+ }
return false
}
@@ -137,13 +212,17 @@ class TreeSitterLanguageMode {
return this.getFoldableRangesAtIndentLevel(null)
}
+ /**
+ * TODO: Make this method generate folds for nested languages (currently,
+ * folds are only generated for the root language layer).
+ */
getFoldableRangesAtIndentLevel (goalLevel) {
let result = []
- let stack = [{node: this.document.rootNode, level: 0}]
+ let stack = [{node: this.tree.rootNode, level: 0}]
while (stack.length > 0) {
const {node, level} = stack.pop()
- const range = this.getFoldableRangeForNode(node)
+ const range = this.getFoldableRangeForNode(node, this.grammar)
if (range) {
if (goalLevel == null || level === goalLevel) {
let updatedExistingRange = false
@@ -183,54 +262,71 @@ class TreeSitterLanguageMode {
}
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
+ if (!this.tree) return null
+
+ let smallestRange
+ this._forEachTreeWithRange(new Range(point, point), (tree, grammar) => {
+ let node = tree.rootNode.descendantForPosition(this.buffer.clipPosition(point))
+ while (node) {
+ if (existenceOnly && node.startPosition.row < point.row) return
+ if (node.endPosition.row > point.row) {
+ const range = this.getFoldableRangeForNode(node, grammar)
+ if (range && rangeIsSmaller(range, smallestRange)) {
+ smallestRange = range
+ return
+ }
+ }
+ node = node.parent
}
- node = node.parent
+ })
+
+ return existenceOnly
+ ? smallestRange && smallestRange.start.row === point.row
+ : smallestRange
+ }
+
+ _forEachTreeWithRange (range, callback) {
+ if (this.rootLanguageLayer.tree) {
+ callback(this.rootLanguageLayer.tree, this.rootLanguageLayer.grammar)
+ }
+
+ const injectionMarkers = this.injectionsMarkerLayer.findMarkers({
+ intersectsRange: range
+ })
+
+ for (const injectionMarker of injectionMarkers) {
+ const {tree, grammar} = injectionMarker.languageLayer
+ if (tree) callback(tree, grammar)
}
}
- getFoldableRangeForNode (node, existenceOnly) {
- const {children, type: nodeType} = node
+ getFoldableRangeForNode (node, grammar, existenceOnly) {
+ const {children} = node
const childCount = children.length
- let childTypes
- for (var i = 0, {length} = this.grammar.folds; i < length; i++) {
- const foldEntry = this.grammar.folds[i]
+ for (var i = 0, {length} = grammar.folds; i < length; i++) {
+ const foldSpec = grammar.folds[i]
- if (foldEntry.type) {
- if (typeof foldEntry.type === 'string') {
- if (foldEntry.type !== nodeType) continue
- } else {
- if (!foldEntry.type.includes(nodeType)) continue
- }
- }
+ if (foldSpec.matchers && !hasMatchingFoldSpec(foldSpec.matchers, node)) continue
let foldStart
- const startEntry = foldEntry.start
+ const startEntry = foldSpec.start
if (startEntry) {
+ let foldStartNode
if (startEntry.index != null) {
- const child = children[startEntry.index]
- if (!child || (startEntry.type && startEntry.type !== child.type)) continue
- foldStart = child.endPosition
+ foldStartNode = children[startEntry.index]
+ if (!foldStartNode || startEntry.matchers && !hasMatchingFoldSpec(startEntry.matchers, foldStartNode)) continue
} 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
+ foldStartNode = children.find(child => hasMatchingFoldSpec(startEntry.matchers, child))
+ if (!foldStartNode) continue
}
+ foldStart = new Point(foldStartNode.endPosition.row, Infinity)
} else {
foldStart = new Point(node.startPosition.row, Infinity)
}
let foldEnd
- const endEntry = foldEntry.end
+ const endEntry = foldSpec.end
if (endEntry) {
let foldEndNode
if (endEntry.index != null) {
@@ -238,18 +334,17 @@ class TreeSitterLanguageMode {
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]
+ foldEndNode = children.find(child => hasMatchingFoldSpec(endEntry.matchers, child))
+ if (!foldEndNode) continue
}
- if (foldEndNode.endIndex - foldEndNode.startIndex > 1 && foldEndNode.startPosition.row > foldStart.row) {
- foldEnd = new Point(foldEndNode.startPosition.row - 1, Infinity)
- } else {
- foldEnd = foldEndNode.startPosition
+ if (foldEndNode.startPosition.row <= foldStart.row) continue
+
+ foldEnd = foldEndNode.startPosition
+ if (this.buffer.findInRangeSync(
+ WORD_REGEX, new Range(foldEnd, new Point(foldEnd.row, Infinity))
+ )) {
+ foldEnd = new Point(foldEnd.row - 1, Infinity)
}
} else {
const {endPosition} = node
@@ -267,29 +362,52 @@ class TreeSitterLanguageMode {
}
/*
- Syntax Tree APIs
+ Section - Syntax Tree APIs
*/
- getRangeForSyntaxNodeContainingRange (range) {
+ getSyntaxNodeContainingRange (range, where = _ => true) {
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)
+ const searchEndIndex = Math.max(0, endIndex - 1)
+
+ let smallestNode
+ this._forEachTreeWithRange(range, tree => {
+ let node = tree.rootNode.descendantForIndex(startIndex, searchEndIndex)
+ while (node) {
+ if (nodeContainsIndices(node, startIndex, endIndex) && where(node)) {
+ if (nodeIsSmaller(node, smallestNode)) smallestNode = node
+ break
+ }
+ node = node.parent
+ }
+ })
+
+ return smallestNode
}
- bufferRangeForScopeAtPosition (position) {
- return this.getRangeForSyntaxNodeContainingRange(new Range(position, position))
+ getRangeForSyntaxNodeContainingRange (range, where) {
+ const node = this.getSyntaxNodeContainingRange(range, where)
+ return node && node.range
+ }
+
+ getSyntaxNodeAtPosition (position, where) {
+ return this.getSyntaxNodeContainingRange(new Range(position, position), where)
+ }
+
+ bufferRangeForScopeAtPosition (selector, position) {
+ if (typeof selector === 'string') {
+ const match = matcherForSelector(selector)
+ selector = ({type}) => match(type)
+ }
+ if (selector === null) selector = undefined
+ const node = this.getSyntaxNodeAtPosition(position, selector)
+ return node && node.range
}
/*
Section - Backward compatibility shims
*/
- onDidTokenize (callback) { return new Disposable(() => {}) }
-
tokenizedLineForRow (row) {
return new TokenizedLine({
openScopes: [],
@@ -303,52 +421,346 @@ class TreeSitterLanguageMode {
}
scopeDescriptorForPosition (point) {
- point = Point.fromObject(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
+ const iterator = this.buildHighlightIterator()
+ const scopes = []
+ for (const scope of iterator.seek(point)) {
+ scopes.push(this.grammar.scopeNameForScopeId(scope, false))
}
- result.push(this.grammar.id)
- return new ScopeDescriptor({scopes: result.reverse()})
- }
-
- hasTokenForSelector (scopeSelector) {
- return false
+ for (const scope of iterator.getOpenScopeIds()) {
+ scopes.push(this.grammar.scopeNameForScopeId(scope, false))
+ }
+ if (scopes.length === 0 || scopes[0] !== this.grammar.scopeName) {
+ scopes.unshift(this.grammar.scopeName)
+ }
+ return new ScopeDescriptor({scopes})
}
getGrammar () {
return this.grammar
}
+
+ /*
+ Section - Private
+ */
+
+ grammarForLanguageString (languageString) {
+ return this.grammarRegistry.treeSitterGrammarForLanguageString(languageString)
+ }
+
+ emitRangeUpdate (range) {
+ 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)
+ }
}
-class TreeSitterHighlightIterator {
- constructor (layer, document) {
- this.layer = layer
+class LanguageLayer {
+ constructor (languageMode, grammar, contentChildTypes) {
+ this.languageMode = languageMode
+ this.grammar = grammar
+ this.tree = null
+ this.currentParsePromise = null
+ this.patchSinceCurrentParseStarted = null
+ this.contentChildTypes = contentChildTypes
+ }
- // 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
+ buildHighlightIterator () {
+ if (this.tree) {
+ return new LayerHighlightIterator(this, this.tree.walk())
+ } else {
+ return new NullHighlightIterator()
+ }
+ }
+
+ handleTextChange ({oldRange, newRange, oldText, newText}) {
+ if (this.tree) {
+ this.tree.edit(this._treeEditForBufferChange(
+ oldRange.start, oldRange.end, newRange.end, oldText, newText
+ ))
+
+ if (this.editedRange) {
+ if (newRange.start.isLessThan(this.editedRange.start)) {
+ this.editedRange.start = newRange.start
+ }
+ if (oldRange.end.isLessThan(this.editedRange.end)) {
+ this.editedRange.end = newRange.end.traverse(this.editedRange.end.traversalFrom(oldRange.end))
+ } else {
+ this.editedRange.end = newRange.end
+ }
+ } else {
+ this.editedRange = newRange.copy()
+ }
+ }
+
+ if (this.patchSinceCurrentParseStarted) {
+ this.patchSinceCurrentParseStarted.splice(
+ oldRange.start,
+ oldRange.end,
+ newRange.end,
+ oldText,
+ newText
+ )
+ }
+ }
+
+ destroy () {
+ for (const marker of this.languageMode.injectionsMarkerLayer.getMarkers()) {
+ if (marker.parentLanguageLayer === this) {
+ marker.languageLayer.destroy()
+ marker.destroy()
+ }
+ }
+ }
+
+ async update (nodeRangeSet) {
+ if (!this.currentParsePromise) {
+ do {
+ const params = {async: false}
+ this.currentParsePromise = this._performUpdate(nodeRangeSet, params)
+ if (!params.async) break
+ await this.currentParsePromise
+ } while (this.tree && this.tree.rootNode.hasChanges())
+ this.currentParsePromise = null
+ }
+ }
+
+ updateInjections (grammar) {
+ if (grammar.injectionRegex) {
+ if (!this.currentParsePromise) this.currentParsePromise = Promise.resolve()
+ this.currentParsePromise = this.currentParsePromise.then(async () => {
+ await this._populateInjections(MAX_RANGE, null)
+ this.currentParsePromise = null
+ })
+ }
+ }
+
+ async _performUpdate (nodeRangeSet, params) {
+ let includedRanges = null
+ if (nodeRangeSet) {
+ includedRanges = nodeRangeSet.getRanges()
+ if (includedRanges.length === 0) {
+ this.tree = null
+ return
+ }
+ }
+
+ let affectedRange = this.editedRange
+ this.editedRange = null
+
+ this.patchSinceCurrentParseStarted = new Patch()
+ let tree = this.languageMode.parse(
+ this.grammar.languageModule,
+ this.tree,
+ includedRanges
+ )
+ if (tree.then) {
+ params.async = true
+ tree = await tree
+ }
+ tree.buffer = this.languageMode.buffer
+
+ const changes = this.patchSinceCurrentParseStarted.getChanges()
+ this.patchSinceCurrentParseStarted = null
+ for (let i = changes.length - 1; i >= 0; i--) {
+ const {oldStart, oldEnd, newEnd, oldText, newText} = changes[i]
+ tree.edit(this._treeEditForBufferChange(
+ oldStart, oldEnd, newEnd, oldText, newText
+ ))
+ }
+
+ if (this.tree) {
+ const rangesWithSyntaxChanges = this.tree.getChangedRanges(tree)
+ this.tree = tree
+
+ if (!affectedRange) return
+ if (rangesWithSyntaxChanges.length > 0) {
+ for (const range of rangesWithSyntaxChanges) {
+ this.languageMode.emitRangeUpdate(rangeForNode(range))
+ }
+
+ affectedRange = affectedRange.union(new Range(
+ rangesWithSyntaxChanges[0].startPosition,
+ last(rangesWithSyntaxChanges).endPosition
+ ))
+ } else {
+ this.languageMode.emitRangeUpdate(affectedRange)
+ }
+ } else {
+ this.tree = tree
+ this.languageMode.emitRangeUpdate(rangeForNode(tree.rootNode))
+ if (includedRanges) {
+ affectedRange = new Range(includedRanges[0].startPosition, last(includedRanges).endPosition)
+ } else {
+ affectedRange = MAX_RANGE
+ }
+ }
+
+ const injectionPromise = this._populateInjections(affectedRange, nodeRangeSet)
+ if (injectionPromise) {
+ params.async = true
+ return injectionPromise
+ }
+ }
+
+ _populateInjections (range, nodeRangeSet) {
+ const {injectionsMarkerLayer, grammarForLanguageString} = this.languageMode
+
+ const existingInjectionMarkers = injectionsMarkerLayer
+ .findMarkers({intersectsRange: range})
+ .filter(marker => marker.parentLanguageLayer === this)
+
+ if (existingInjectionMarkers.length > 0) {
+ range = range.union(new Range(
+ existingInjectionMarkers[0].getRange().start,
+ last(existingInjectionMarkers).getRange().end
+ ))
+ }
+
+ const markersToUpdate = new Map()
+ for (const injectionPoint of this.grammar.injectionPoints) {
+ const nodes = this.tree.rootNode.descendantsOfType(
+ injectionPoint.type,
+ range.start,
+ range.end
+ )
+
+ for (const node of nodes) {
+ const languageName = injectionPoint.language(node)
+ if (!languageName) continue
+
+ const grammar = grammarForLanguageString(languageName)
+ if (!grammar) continue
+
+ const contentNodes = injectionPoint.content(node)
+ if (!contentNodes) continue
+
+ const injectionNodes = [].concat(contentNodes)
+ if (!injectionNodes.length) continue
+
+ const injectionRange = rangeForNode(node)
+ let marker = existingInjectionMarkers.find(m =>
+ m.getRange().isEqual(injectionRange) &&
+ m.languageLayer.grammar === grammar
+ )
+ if (!marker) {
+ marker = injectionsMarkerLayer.markRange(injectionRange)
+ marker.languageLayer = new LanguageLayer(this.languageMode, grammar, injectionPoint.contentChildTypes)
+ marker.parentLanguageLayer = this
+ }
+
+ markersToUpdate.set(marker, new NodeRangeSet(nodeRangeSet, injectionNodes))
+ }
+ }
+
+ for (const marker of existingInjectionMarkers) {
+ if (!markersToUpdate.has(marker)) {
+ marker.languageLayer.destroy()
+ this.languageMode.emitRangeUpdate(marker.getRange())
+ marker.destroy()
+ }
+ }
+
+ if (markersToUpdate.size > 0) {
+ this.lastUpdateWasAsync = true
+ const promises = []
+ for (const [marker, nodeRangeSet] of markersToUpdate) {
+ promises.push(marker.languageLayer.update(nodeRangeSet))
+ }
+ return Promise.all(promises)
+ }
+ }
+
+ _treeEditForBufferChange (start, oldEnd, newEnd, oldText, newText) {
+ const startIndex = this.languageMode.buffer.characterIndexForPosition(start)
+ return {
+ startIndex,
+ oldEndIndex: startIndex + oldText.length,
+ newEndIndex: startIndex + newText.length,
+ startPosition: start,
+ oldEndPosition: oldEnd,
+ newEndPosition: newEnd
+ }
+ }
+}
+
+class HighlightIterator {
+ constructor (languageMode, iterators) {
+ this.languageMode = languageMode
+ this.iterators = iterators.sort((a, b) => b.getIndex() - a.getIndex())
+ }
+
+ seek (targetPosition) {
+ const containingTags = []
+ const containingTagStartIndices = []
+ const targetIndex = this.languageMode.buffer.characterIndexForPosition(targetPosition)
+ for (let i = this.iterators.length - 1; i >= 0; i--) {
+ this.iterators[i].seek(targetIndex, containingTags, containingTagStartIndices)
+ }
+ this.iterators.sort((a, b) => b.getIndex() - a.getIndex())
+ return containingTags
+ }
+
+ moveToSuccessor () {
+ const lastIndex = this.iterators.length - 1
+ const leader = this.iterators[lastIndex]
+ leader.moveToSuccessor()
+ const leaderCharIndex = leader.getIndex()
+ let i = lastIndex
+ while (i > 0 && this.iterators[i - 1].getIndex() < leaderCharIndex) i--
+ if (i < lastIndex) this.iterators.splice(i, 0, this.iterators.pop())
+ }
+
+ getPosition () {
+ return last(this.iterators).getPosition()
+ }
+
+ getCloseScopeIds () {
+ return last(this.iterators).getCloseScopeIds()
+ }
+
+ getOpenScopeIds () {
+ return last(this.iterators).getOpenScopeIds()
+ }
+
+ logState () {
+ const iterator = last(this.iterators)
+ if (iterator.treeCursor) {
+ console.log(
+ iterator.getPosition(),
+ iterator.treeCursor.nodeType,
+ new Range(
+ iterator.languageLayer.tree.rootNode.startPosition,
+ iterator.languageLayer.tree.rootNode.endPosition
+ ).toString()
+ )
+ console.log('close', iterator.closeTags.map(id => this.shortClassNameForScopeId(id)))
+ console.log('open', iterator.openTags.map(id => this.shortClassNameForScopeId(id)))
+ }
+ }
+
+ shortClassNameForScopeId (id) {
+ return this.languageMode.classNameForScopeId(id).replace(/syntax--/g, '')
+ }
+}
+
+class LayerHighlightIterator {
+ constructor (languageLayer, treeCursor) {
+ this.languageLayer = languageLayer
+
+ // The iterator is always positioned at either the start or the end of some node
+ // in the syntax tree.
+ this.atEnd = false
+ this.treeCursor = treeCursor
// 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 = []
+ this.containingNodeEndIndices = []
// 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*
@@ -357,42 +769,57 @@ class TreeSitterHighlightIterator {
this.openTags = []
}
- seek (targetPosition) {
- const containingTags = []
+ seek (targetIndex, containingTags, containingTagStartIndices) {
+ while (this.treeCursor.gotoParent()) {}
+ this.done = false
+ this.atEnd = true
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)
+ this.containingNodeEndIndices.length = 0
- var node = this.layer.document.rootNode
- var childIndex = -1
- var nodeContainsTarget = true
+ const containingTagEndIndices = []
+
+ if (targetIndex >= this.treeCursor.endIndex) {
+ this.done = true
+ return
+ }
+
+ let childIndex = -1
for (;;) {
- this.currentNode = node
- this.currentChildIndex = childIndex
- if (!nodeContainsTarget) break
- this.containingNodeTypes.push(node.type)
+ this.containingNodeTypes.push(this.treeCursor.nodeType)
this.containingNodeChildIndices.push(childIndex)
+ this.containingNodeEndIndices.push(this.treeCursor.endIndex)
- const scopeName = this.currentScopeName()
- if (scopeName) {
- const id = this.layer.grammar.idForScope(scopeName)
- if (this.currentIndex === node.startIndex) {
- this.openTags.push(id)
+ const scopeId = this._currentScopeId()
+ if (scopeId) {
+ if (this.treeCursor.startIndex < targetIndex) {
+ insertContainingTag(
+ scopeId, this.treeCursor.startIndex,
+ containingTags, containingTagStartIndices
+ )
+ containingTagEndIndices.push(this.treeCursor.endIndex)
} else {
- containingTags.push(id)
+ this.atEnd = false
+ this.openTags.push(scopeId)
+ this._moveDown()
+ break
}
}
- node = node.firstChildForIndex(this.currentIndex)
- if (node) {
- if (node.startIndex > this.currentIndex) nodeContainsTarget = false
- childIndex = node.childIndex
- } else {
- break
+ childIndex = this.treeCursor.gotoFirstChildForIndex(targetIndex)
+ if (childIndex === null) break
+ if (this.treeCursor.startIndex >= targetIndex) this.atEnd = false
+ }
+
+ if (this.atEnd) {
+ const currentIndex = this.treeCursor.endIndex
+ for (let i = 0, {length} = containingTags; i < length; i++) {
+ if (containingTagEndIndices[i] === currentIndex) {
+ this.closeTags.push(containingTags[i])
+ }
}
}
@@ -403,48 +830,45 @@ class TreeSitterHighlightIterator {
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 && nextSibling.endIndex > this.currentIndex) {
- 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
- }
+ while (!this.done && !this.closeTags.length && !this.openTags.length) {
+ if (this.atEnd) {
+ if (this._moveRight()) {
+ const scopeId = this._currentScopeId()
+ if (scopeId) this.openTags.push(scopeId)
+ this.atEnd = false
+ this._moveDown()
+ } else if (this._moveUp(true)) {
+ this.atEnd = true
+ } else {
+ this.done = true
}
- } else {
- this.currentNode = this.currentNode.nextSibling
+ } else if (!this._moveDown()) {
+ const scopeId = this._currentScopeId()
+ if (scopeId) this.closeTags.push(scopeId)
+ this.atEnd = true
+ this._moveUp(false)
}
- } while (this.closeTags.length === 0 && this.openTags.length === 0 && this.currentNode)
-
- return true
+ }
}
getPosition () {
- return this.currentPosition
+ if (this.done) {
+ return Point.INFINITY
+ } else if (this.atEnd) {
+ return this.treeCursor.endPosition
+ } else {
+ return this.treeCursor.startPosition
+ }
+ }
+
+ getIndex () {
+ if (this.done) {
+ return Infinity
+ } else if (this.atEnd) {
+ return this.treeCursor.endIndex
+ } else {
+ return this.treeCursor.startIndex
+ }
}
getCloseScopeIds () {
@@ -456,67 +880,219 @@ class TreeSitterHighlightIterator {
}
// Private methods
+ _moveUp (atLastChild) {
+ let result = false
+ const {endIndex} = this.treeCursor
+ let depth = this.containingNodeEndIndices.length
- descendLeft () {
- let child
- while ((child = this.currentNode.firstChild) && this.currentIndex === child.startIndex) {
- this.currentNode = child
- this.currentChildIndex = 0
- this.pushOpenTag()
+ // The iterator should not move up until it has visited all of the children of this node.
+ while (depth > 1 && (atLastChild || this.containingNodeEndIndices[depth - 2] === endIndex)) {
+ atLastChild = false
+ result = true
+ this.treeCursor.gotoParent()
+ this.containingNodeTypes.pop()
+ this.containingNodeChildIndices.pop()
+ this.containingNodeEndIndices.pop()
+ --depth
+ const scopeId = this._currentScopeId()
+ if (scopeId) this.closeTags.push(scopeId)
+ }
+ return result
+ }
+
+ _moveDown () {
+ let result = false
+ const {startIndex} = this.treeCursor
+
+ // Once the iterator has found a scope boundary, it needs to stay at the same
+ // position, so it should not move down if the first child node starts later than the
+ // current node.
+ while (this.treeCursor.gotoFirstChild()) {
+ if ((this.closeTags.length || this.openTags.length) &&
+ this.treeCursor.startIndex > startIndex) {
+ this.treeCursor.gotoParent()
+ break
+ }
+
+ result = true
+ this.containingNodeTypes.push(this.treeCursor.nodeType)
+ this.containingNodeChildIndices.push(0)
+ this.containingNodeEndIndices.push(this.treeCursor.endIndex)
+
+ const scopeId = this._currentScopeId()
+ if (scopeId) this.openTags.push(scopeId)
+ }
+
+ return result
+ }
+
+ _moveRight () {
+ if (this.treeCursor.gotoNextSibling()) {
+ const depth = this.containingNodeTypes.length
+ this.containingNodeTypes[depth - 1] = this.treeCursor.nodeType
+ this.containingNodeChildIndices[depth - 1]++
+ this.containingNodeEndIndices[depth - 1] = this.treeCursor.endIndex
+ return true
}
}
- currentScopeName () {
- return this.layer.grammar.scopeMap.get(
+ _currentScopeId () {
+ const rules = this.languageLayer.grammar.scopeMap.get(
this.containingNodeTypes,
this.containingNodeChildIndices,
- this.currentNode.isNamed
+ this.treeCursor.nodeIsNamed
)
- }
-
- 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))
+ const scopes = applyLeafRules(rules, this.treeCursor)
+ if (scopes) {
+ return this.languageLayer.languageMode.grammar.idForScope(scopes)
+ }
}
}
-class TreeSitterTextBufferInput {
- constructor (buffer) {
- this.buffer = buffer
- this.position = {row: 0, column: 0}
- this.isBetweenCRLF = false
- }
-
- seek (offset, position) {
- this.position = position
- this.isBetweenCRLF = this.position.column > this.buffer.lineLengthForRow(this.position.row)
- }
-
- read () {
- const endPosition = this.buffer.clipPosition(new Point(this.position.row + 1000, 0))
- let text = this.buffer.getTextInRange([this.position, endPosition])
- if (this.isBetweenCRLF) {
- text = text.slice(1)
- this.isBetweenCRLF = false
+const applyLeafRules = (rules, cursor) => {
+ if (!rules || typeof rules === 'string') return rules
+ if (Array.isArray(rules)) {
+ for (let i = 0, {length} = rules; i !== length; ++i) {
+ const result = applyLeafRules(rules[i], cursor)
+ if (result) return result
}
- this.position = endPosition
- return text
+ return undefined
}
+ if (typeof rules === 'object') {
+ if (rules.exact) {
+ return cursor.nodeText === rules.exact
+ ? applyLeafRules(rules.scopes, cursor)
+ : undefined
+ }
+ if (rules.match) {
+ return rules.match.test(cursor.nodeText)
+ ? applyLeafRules(rules.scopes, cursor)
+ : undefined
+ }
+ }
+}
+
+class NullHighlightIterator {
+ seek () { return [] }
+ moveToSuccessor () {}
+ getIndex () { return Infinity }
+ getPosition () { return Point.INFINITY }
+ getOpenScopeIds () { return [] }
+ getCloseScopeIds () { return [] }
+}
+
+class NodeRangeSet {
+ constructor (previous, nodes) {
+ this.previous = previous
+ this.nodes = nodes
+ }
+
+ getRanges () {
+ const previousRanges = this.previous && this.previous.getRanges()
+ const result = []
+
+ for (const node of this.nodes) {
+ let position = node.startPosition
+ let index = node.startIndex
+
+ for (const child of node.children) {
+ const nextPosition = child.startPosition
+ const nextIndex = child.startIndex
+ if (nextIndex > index) {
+ this._pushRange(previousRanges, result, {
+ startIndex: index,
+ endIndex: nextIndex,
+ startPosition: position,
+ endPosition: nextPosition
+ })
+ }
+ position = child.endPosition
+ index = child.endIndex
+ }
+
+ if (node.endIndex > index) {
+ this._pushRange(previousRanges, result, {
+ startIndex: index,
+ endIndex: node.endIndex,
+ startPosition: position,
+ endPosition: node.endPosition
+ })
+ }
+ }
+
+ return result
+ }
+
+ _pushRange (previousRanges, newRanges, newRange) {
+ if (!previousRanges) {
+ newRanges.push(newRange)
+ return
+ }
+
+ for (const previousRange of previousRanges) {
+ if (previousRange.endIndex <= newRange.startIndex) continue
+ if (previousRange.startIndex >= newRange.endIndex) break
+ newRanges.push({
+ startIndex: Math.max(previousRange.startIndex, newRange.startIndex),
+ endIndex: Math.min(previousRange.endIndex, newRange.endIndex),
+ startPosition: Point.max(previousRange.startPosition, newRange.startPosition),
+ endPosition: Point.min(previousRange.endPosition, newRange.endPosition)
+ })
+ }
+ }
+}
+
+function insertContainingTag (tag, index, tags, indices) {
+ const i = indices.findIndex(existingIndex => existingIndex > index)
+ if (i === -1) {
+ tags.push(tag)
+ indices.push(index)
+ } else {
+ tags.splice(i, 0, tag)
+ indices.splice(i, 0, index)
+ }
+}
+
+// Return true iff `mouse` is smaller than `house`. Only correct if
+// mouse and house overlap.
+//
+// * `mouse` {Range}
+// * `house` {Range}
+function rangeIsSmaller (mouse, house) {
+ if (!house) return true
+ const mvec = vecFromRange(mouse)
+ const hvec = vecFromRange(house)
+ return Point.min(mvec, hvec) === mvec
+}
+
+function vecFromRange ({start, end}) {
+ return end.translate(start.negate())
+}
+
+function rangeForNode (node) {
+ return new Range(node.startPosition, node.endPosition)
+}
+
+function nodeContainsIndices (node, start, end) {
+ if (node.startIndex < start) return node.endIndex >= end
+ if (node.startIndex === start) return node.endIndex > end
+ return false
+}
+
+function nodeIsSmaller (left, right) {
+ if (!left) return false
+ if (!right) return true
+ return left.endIndex - left.startIndex < right.endIndex - right.startIndex
}
function last (array) {
return array[array.length - 1]
}
+function hasMatchingFoldSpec (specs, node) {
+ return specs.some(({type, named}) => type === node.type && named === node.isNamed)
+}
+
// TODO: Remove this once TreeSitterLanguageMode implements its own auto-indent system.
[
'_suggestedIndentForLineWithScopeAtBufferRow',
@@ -524,7 +1100,13 @@ function last (array) {
'increaseIndentRegexForScopeDescriptor',
'decreaseIndentRegexForScopeDescriptor',
'decreaseNextIndentRegexForScopeDescriptor',
- 'regexForPattern'
+ 'regexForPattern',
+ 'getNonWordCharacters'
].forEach(methodName => {
- module.exports.prototype[methodName] = TextMateLanguageMode.prototype[methodName]
+ TreeSitterLanguageMode.prototype[methodName] = TextMateLanguageMode.prototype[methodName]
})
+
+TreeSitterLanguageMode.LanguageLayer = LanguageLayer
+TreeSitterLanguageMode.prototype.syncOperationLimit = 1000
+
+module.exports = TreeSitterLanguageMode
diff --git a/src/update-process-env.js b/src/update-process-env.js
index 6dab00a7d..20d937d96 100644
--- a/src/update-process-env.js
+++ b/src/update-process-env.js
@@ -18,7 +18,7 @@ async function updateProcessEnv (launchEnv) {
if (launchEnv) {
if (shouldGetEnvFromShell(launchEnv)) {
envToAssign = await getEnvFromShell(launchEnv)
- } else if (launchEnv.PWD) {
+ } else if (launchEnv.PWD || launchEnv.PROMPT || launchEnv.PSModulePath) {
envToAssign = launchEnv
}
}
diff --git a/src/workspace-element.js b/src/workspace-element.js
index 5531aafdf..f94dbd6e9 100644
--- a/src/workspace-element.js
+++ b/src/workspace-element.js
@@ -310,7 +310,7 @@ class WorkspaceElement extends HTMLElement {
}
}
- runPackageSpecs () {
+ runPackageSpecs (options = {}) {
const activePaneItem = this.model.getActivePaneItem()
const activePath = activePaneItem && typeof activePaneItem.getPath === 'function' ? activePaneItem.getPath() : null
let projectPath
@@ -326,7 +326,7 @@ class WorkspaceElement extends HTMLElement {
specPath = testPath
}
- ipcRenderer.send('run-package-specs', specPath)
+ ipcRenderer.send('run-package-specs', specPath, options)
}
}
diff --git a/src/workspace.js b/src/workspace.js
index de51651ec..a3f85ddeb 100644
--- a/src/workspace.js
+++ b/src/workspace.js
@@ -225,6 +225,8 @@ module.exports = class Workspace extends Model {
modal: new PanelContainer({viewRegistry: this.viewRegistry, location: 'modal'})
}
+ this.incoming = new Map()
+
this.subscribeToEvents()
}
@@ -495,14 +497,22 @@ module.exports = class Workspace extends Model {
if (item instanceof TextEditor) {
const subscriptions = new CompositeDisposable(
this.textEditorRegistry.add(item),
- this.textEditorRegistry.maintainConfig(item),
- item.observeGrammar(this.handleGrammarUsed.bind(this))
+ this.textEditorRegistry.maintainConfig(item)
)
if (!this.project.findBufferForId(item.buffer.id)) {
this.project.addBuffer(item.buffer)
}
item.onDidDestroy(() => { subscriptions.dispose() })
this.emitter.emit('did-add-text-editor', {textEditor: item, pane, index})
+ // It's important to call handleGrammarUsed after emitting the did-add event:
+ // if we activate a package between adding the editor to the registry and emitting
+ // the package may receive the editor twice from `observeTextEditors`.
+ // (Note that the item can be destroyed by an `observeTextEditors` handler.)
+ if (!item.isDestroyed()) {
+ subscriptions.add(
+ item.observeGrammar(this.handleGrammarUsed.bind(this))
+ )
+ }
}
})
}
@@ -921,133 +931,150 @@ module.exports = class Workspace extends Model {
if (typeof item.getURI === 'function') uri = item.getURI()
}
- if (!atom.config.get('core.allowPendingPaneItems')) {
- options.pending = false
- }
-
- // Avoid adding URLs as recent documents to work-around this Spotlight crash:
- // https://github.com/atom/atom/issues/10071
- if (uri && (!url.parse(uri).protocol || process.platform === 'win32')) {
- this.applicationDelegate.addRecentDocument(uri)
- }
-
- let pane, itemExistsInWorkspace
-
- // Try to find an existing item in the workspace.
- if (item || uri) {
- if (options.pane) {
- pane = options.pane
- } else if (options.searchAllPanes) {
- pane = item ? this.paneForItem(item) : this.paneForURI(uri)
+ let resolveItem = () => {}
+ if (uri) {
+ const incomingItem = this.incoming.get(uri)
+ if (!incomingItem) {
+ this.incoming.set(uri, new Promise(resolve => { resolveItem = resolve }))
} else {
- // If an item with the given URI is already in the workspace, assume
- // that item's pane container is the preferred location for that URI.
- let container
- if (uri) container = this.paneContainerForURI(uri)
- if (!container) container = this.getActivePaneContainer()
+ await incomingItem
+ }
+ }
- // The `split` option affects where we search for the item.
- pane = container.getActivePane()
- switch (options.split) {
- case 'left':
- pane = pane.findLeftmostSibling()
- break
- case 'right':
- pane = pane.findRightmostSibling()
- break
- case 'up':
- pane = pane.findTopmostSibling()
- break
- case 'down':
- pane = pane.findBottommostSibling()
- break
- }
+ try {
+ if (!atom.config.get('core.allowPendingPaneItems')) {
+ options.pending = false
}
- if (pane) {
- if (item) {
- itemExistsInWorkspace = pane.getItems().includes(item)
+ // Avoid adding URLs as recent documents to work-around this Spotlight crash:
+ // https://github.com/atom/atom/issues/10071
+ if (uri && (!url.parse(uri).protocol || process.platform === 'win32')) {
+ this.applicationDelegate.addRecentDocument(uri)
+ }
+
+ let pane, itemExistsInWorkspace
+
+ // Try to find an existing item in the workspace.
+ if (item || uri) {
+ if (options.pane) {
+ pane = options.pane
+ } else if (options.searchAllPanes) {
+ pane = item ? this.paneForItem(item) : this.paneForURI(uri)
} else {
- item = pane.itemForURI(uri)
- itemExistsInWorkspace = item != null
+ // If an item with the given URI is already in the workspace, assume
+ // that item's pane container is the preferred location for that URI.
+ let container
+ if (uri) container = this.paneContainerForURI(uri)
+ if (!container) container = this.getActivePaneContainer()
+
+ // The `split` option affects where we search for the item.
+ pane = container.getActivePane()
+ switch (options.split) {
+ case 'left':
+ pane = pane.findLeftmostSibling()
+ break
+ case 'right':
+ pane = pane.findRightmostSibling()
+ break
+ case 'up':
+ pane = pane.findTopmostSibling()
+ break
+ case 'down':
+ pane = pane.findBottommostSibling()
+ break
+ }
+ }
+
+ if (pane) {
+ if (item) {
+ itemExistsInWorkspace = pane.getItems().includes(item)
+ } else {
+ item = pane.itemForURI(uri)
+ itemExistsInWorkspace = item != null
+ }
}
}
- }
- // If we already have an item at this stage, we won't need to do an async
- // lookup of the URI, so we yield the event loop to ensure this method
- // is consistently asynchronous.
- if (item) await Promise.resolve()
+ // If we already have an item at this stage, we won't need to do an async
+ // lookup of the URI, so we yield the event loop to ensure this method
+ // is consistently asynchronous.
+ if (item) await Promise.resolve()
- if (!itemExistsInWorkspace) {
- item = item || await this.createItemForURI(uri, options)
- if (!item) return
+ if (!itemExistsInWorkspace) {
+ item = item || await this.createItemForURI(uri, options)
+ if (!item) return
- if (options.pane) {
- pane = options.pane
+ if (options.pane) {
+ pane = options.pane
+ } else {
+ let location = options.location
+ if (!location && !options.split && uri && this.enablePersistence) {
+ location = await this.itemLocationStore.load(uri)
+ }
+ if (!location && typeof item.getDefaultLocation === 'function') {
+ location = item.getDefaultLocation()
+ }
+
+ const allowedLocations = typeof item.getAllowedLocations === 'function' ? item.getAllowedLocations() : ALL_LOCATIONS
+ location = allowedLocations.includes(location) ? location : allowedLocations[0]
+
+ const container = this.paneContainers[location] || this.getCenter()
+ pane = container.getActivePane()
+ switch (options.split) {
+ case 'left':
+ pane = pane.findLeftmostSibling()
+ break
+ case 'right':
+ pane = pane.findOrCreateRightmostSibling()
+ break
+ case 'up':
+ pane = pane.findTopmostSibling()
+ break
+ case 'down':
+ pane = pane.findOrCreateBottommostSibling()
+ break
+ }
+ }
+ }
+
+ if (!options.pending && (pane.getPendingItem() === item)) {
+ pane.clearPendingItem()
+ }
+
+ this.itemOpened(item)
+
+ if (options.activateItem === false) {
+ pane.addItem(item, {pending: options.pending})
} else {
- let location = options.location
- if (!location && !options.split && uri && this.enablePersistence) {
- location = await this.itemLocationStore.load(uri)
- }
- if (!location && typeof item.getDefaultLocation === 'function') {
- location = item.getDefaultLocation()
- }
+ pane.activateItem(item, {pending: options.pending})
+ }
- const allowedLocations = typeof item.getAllowedLocations === 'function' ? item.getAllowedLocations() : ALL_LOCATIONS
- location = allowedLocations.includes(location) ? location : allowedLocations[0]
+ if (options.activatePane !== false) {
+ pane.activate()
+ }
- const container = this.paneContainers[location] || this.getCenter()
- pane = container.getActivePane()
- switch (options.split) {
- case 'left':
- pane = pane.findLeftmostSibling()
- break
- case 'right':
- pane = pane.findOrCreateRightmostSibling()
- break
- case 'up':
- pane = pane.findTopmostSibling()
- break
- case 'down':
- pane = pane.findOrCreateBottommostSibling()
- break
+ let initialColumn = 0
+ let initialLine = 0
+ if (!Number.isNaN(options.initialLine)) {
+ initialLine = options.initialLine
+ }
+ if (!Number.isNaN(options.initialColumn)) {
+ initialColumn = options.initialColumn
+ }
+ if (initialLine >= 0 || initialColumn >= 0) {
+ if (typeof item.setCursorBufferPosition === 'function') {
+ item.setCursorBufferPosition([initialLine, initialColumn])
}
}
- }
- if (!options.pending && (pane.getPendingItem() === item)) {
- pane.clearPendingItem()
- }
-
- this.itemOpened(item)
-
- if (options.activateItem === false) {
- pane.addItem(item, {pending: options.pending})
- } else {
- pane.activateItem(item, {pending: options.pending})
- }
-
- if (options.activatePane !== false) {
- pane.activate()
- }
-
- let initialColumn = 0
- let initialLine = 0
- if (!Number.isNaN(options.initialLine)) {
- initialLine = options.initialLine
- }
- if (!Number.isNaN(options.initialColumn)) {
- initialColumn = options.initialColumn
- }
- if (initialLine >= 0 || initialColumn >= 0) {
- if (typeof item.setCursorBufferPosition === 'function') {
- item.setCursorBufferPosition([initialLine, initialColumn])
+ const index = pane.getActiveItemIndex()
+ this.emitter.emit('did-open', {uri, pane, item, index})
+ if (uri) {
+ this.incoming.delete(uri)
}
+ } finally {
+ resolveItem()
}
-
- const index = pane.getActiveItemIndex()
- this.emitter.emit('did-open', {uri, pane, item, index})
return item
}
@@ -1217,42 +1244,32 @@ module.exports = class Workspace extends Model {
const fileSize = fs.getSizeSync(filePath)
- 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.',
- detail: 'Do you still want to load this file?',
- buttons: ['Proceed', 'Cancel']
- }, response => {
- if (response === 1) {
- rejectConfirmFileOpenPromise()
- } else {
- resolveConfirmFileOpenPromise()
- }
+ await new Promise((resolve, reject) => {
+ this.applicationDelegate.confirm({
+ message: 'Atom will be unresponsive during the loading of very large files.',
+ detail: 'Do you still want to load this file?',
+ buttons: ['Proceed', 'Cancel']
+ }, response => {
+ if (response === 1) {
+ const error = new Error()
+ error.code = 'CANCELLED'
+ reject(error)
+ } else {
+ resolve()
+ }
+ })
})
- } else {
- resolveConfirmFileOpenPromise()
}
- 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
- }
+ const buffer = await this.project.bufferForPath(filePath, options)
+ return this.textEditorRegistry.build(Object.assign({buffer, autoHeight: false}, options))
}
handleGrammarUsed (grammar) {
if (grammar == null) { return }
- return this.packageManager.triggerActivationHook(`${grammar.packageName}:grammar-used`)
+ this.packageManager.triggerActivationHook(`${grammar.scopeName}:root-scope-used`)
+ this.packageManager.triggerActivationHook(`${grammar.packageName}:grammar-used`)
}
// Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`.
diff --git a/static/cursors.less b/static/cursors.less
index 843dab2c6..5cbfadef6 100644
--- a/static/cursors.less
+++ b/static/cursors.less
@@ -8,7 +8,7 @@
@ibeam-2x: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAAz0lEQVRIx2NgYGBY/R8I/vx5eelX3n82IJ9FxGf6tksvf/8FiTMQAcAGQMDvSwu09abffY8QYSAScNk45G198eX//yev73/4///701eh//kZSARckrNBRvz//+8+6ZohwCzjGNjdgQxkAg7B9WADeBjIBqtJCbhRA0YNoIkBSNmaPEMoNmA0FkYNoFKhapJ6FGyAH3nauaSmPfwI0v/3OukVi0CIZ+F25KrtYcx/CTIy0e+rC7R1Z4KMICVTQQ14feVXIbR695u14+Ir4gwAAD49E54wc1kWAAAAAElFTkSuQmCC');
.cursor-white() {
- cursor: -webkit-image-set(@ibeam-1x 1dppx, @ibeam-2x 2dppx) 5 8, text;
+ cursor: -webkit-image-set(@ibeam-1x 1x, @ibeam-2x 2x) 5 8, text;
}
// Editors
diff --git a/stylelint.config.js b/stylelint.config.js
index 49caab46d..136c754fa 100644
--- a/stylelint.config.js
+++ b/stylelint.config.js
@@ -19,5 +19,6 @@ module.exports = {
"rule-empty-line-before": null, // TODO: enable?
"at-rule-empty-line-before": null, // TODO: enable?
"font-family-no-duplicate-names": null, // TODO: enable?
+ "unit-no-unknown": [true, {"ignoreUnits": [ "x" ]}], // Needed for -webkit-image-set 1x/2x units
}
}
diff --git a/vendor/jasmine.js b/vendor/jasmine.js
index 1b80d662f..ac443e1c8 100644
--- a/vendor/jasmine.js
+++ b/vendor/jasmine.js
@@ -2317,6 +2317,14 @@ jasmine.Spec.prototype.waitsFor = function(latchFunction, optional_timeoutMessag
}
}
+ if (optional_timeoutMessage_ == null) {
+ const objectToCaptureStack = {}
+ Error.captureStackTrace(objectToCaptureStack, waitsFor)
+ const stack = objectToCaptureStack.stack
+ const line = stack.split('\n')[1]
+ optional_timeoutMessage_ = `condition ${line}`
+ }
+
var waitsForFunc = new jasmine.WaitsForBlock(this.env, optional_timeout_, latchFunction_, optional_timeoutMessage_, this);
this.addToQueue(waitsForFunc);
return this;