diff --git a/benchmarks/benchmark-runner.js b/benchmarks/benchmark-runner.js
index 56a37cfd4..7c45b442c 100644
--- a/benchmarks/benchmark-runner.js
+++ b/benchmarks/benchmark-runner.js
@@ -1,11 +1,9 @@
-/** @babel */
+const Chart = require('chart.js')
+const glob = require('glob')
+const fs = require('fs-plus')
+const path = require('path')
-import Chart from 'chart.js'
-import glob from 'glob'
-import fs from 'fs-plus'
-import path from 'path'
-
-export default async function ({test, benchmarkPaths}) {
+module.exports = async ({test, benchmarkPaths}) => {
document.body.style.backgroundColor = '#ffffff'
document.body.style.overflow = 'auto'
diff --git a/benchmarks/text-editor-large-file-construction.bench.js b/benchmarks/text-editor-large-file-construction.bench.js
index ec037e9e4..ff564e5ca 100644
--- a/benchmarks/text-editor-large-file-construction.bench.js
+++ b/benchmarks/text-editor-large-file-construction.bench.js
@@ -1,6 +1,4 @@
-/** @babel */
-
-import {TextEditor, TextBuffer} from 'atom'
+const {TextEditor, TextBuffer} = require('atom')
const MIN_SIZE_IN_KB = 0 * 1024
const MAX_SIZE_IN_KB = 10 * 1024
@@ -8,7 +6,7 @@ const SIZE_STEP_IN_KB = 1024
const LINE_TEXT = 'Lorem ipsum dolor sit amet\n'
const TEXT = LINE_TEXT.repeat(Math.ceil(MAX_SIZE_IN_KB * 1024 / LINE_TEXT.length))
-export default async function ({test}) {
+module.exports = async ({test}) => {
const data = []
document.body.appendChild(atom.workspace.getElement())
@@ -27,6 +25,7 @@ export default async function ({test}) {
let t0 = window.performance.now()
const buffer = new TextBuffer({text})
const editor = new TextEditor({buffer, autoHeight: false, largeFileMode: true})
+ atom.grammars.autoAssignLanguageMode(buffer)
atom.workspace.getActivePane().activateItem(editor)
let t1 = window.performance.now()
diff --git a/benchmarks/text-editor-long-lines.bench.js b/benchmarks/text-editor-long-lines.bench.js
index ac90e4a71..92a9b9b9e 100644
--- a/benchmarks/text-editor-long-lines.bench.js
+++ b/benchmarks/text-editor-long-lines.bench.js
@@ -1,8 +1,6 @@
-/** @babel */
-
-import path from 'path'
-import fs from 'fs'
-import {TextEditor, TextBuffer} from 'atom'
+const path = require('path')
+const fs = require('fs')
+const {TextEditor, TextBuffer} = require('atom')
const SIZES_IN_KB = [
512,
@@ -12,7 +10,7 @@ const SIZES_IN_KB = [
const REPEATED_TEXT = fs.readFileSync(path.join(__dirname, '..', 'spec', 'fixtures', 'sample.js'), 'utf8').replace(/\n/g, '')
const TEXT = REPEATED_TEXT.repeat(Math.ceil(SIZES_IN_KB[SIZES_IN_KB.length - 1] * 1024 / REPEATED_TEXT.length))
-export default async function ({test}) {
+module.exports = async ({test}) => {
const data = []
const workspaceElement = atom.workspace.getElement()
@@ -34,7 +32,7 @@ export default async function ({test}) {
let t0 = window.performance.now()
const buffer = new TextBuffer({text})
const editor = new TextEditor({buffer, autoHeight: false, largeFileMode: true})
- editor.setGrammar(atom.grammars.grammarForScopeName('source.js'))
+ atom.grammars.assignLanguageMode(buffer, 'source.js')
atom.workspace.getActivePane().activateItem(editor)
let t1 = window.performance.now()
diff --git a/package.json b/package.json
index 3c53ff0a6..b1c0fa7af 100644
--- a/package.json
+++ b/package.json
@@ -70,7 +70,7 @@
"service-hub": "^0.7.4",
"sinon": "1.17.4",
"temp": "^0.8.3",
- "text-buffer": "13.8.6",
+ "text-buffer": "13.9.1",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"winreg": "^1.2.1",
diff --git a/spec/atom-environment-spec.js b/spec/atom-environment-spec.js
index 84b415eab..e3b7b83e7 100644
--- a/spec/atom-environment-spec.js
+++ b/spec/atom-environment-spec.js
@@ -301,8 +301,9 @@ describe('AtomEnvironment', () => {
})
it('serializes the text editor registry', async () => {
+ await atom.packages.activatePackage('language-text')
const editor = await atom.workspace.open('sample.js')
- atom.textEditors.setGrammarOverride(editor, 'text.plain')
+ expect(atom.grammars.assignLanguageMode(editor, 'text.plain')).toBe(true)
const atom2 = new AtomEnvironment({
applicationDelegate: atom.applicationDelegate,
@@ -318,7 +319,9 @@ describe('AtomEnvironment', () => {
atom2.initialize({document, window})
await atom2.deserialize(atom.serialize())
- expect(atom2.textEditors.getGrammarOverride(editor)).toBe('text.plain')
+ await atom2.packages.activatePackage('language-text')
+ const editor2 = atom2.workspace.getActiveTextEditor()
+ expect(editor2.getBuffer().getLanguageMode().getLanguageId()).toBe('text.plain')
atom2.destroy()
})
diff --git a/spec/git-repository-spec.js b/spec/git-repository-spec.js
index e03a9788a..61c80ee48 100644
--- a/spec/git-repository-spec.js
+++ b/spec/git-repository-spec.js
@@ -366,6 +366,7 @@ describe('GitRepository', () => {
notificationManager: atom.notifications,
packageManager: atom.packages,
confirm: atom.confirm,
+ grammarRegistry: atom.grammars,
applicationDelegate: atom.applicationDelegate
})
await project2.deserialize(atom.project.serialize({isUnloading: false}))
diff --git a/spec/grammar-registry-spec.js b/spec/grammar-registry-spec.js
new file mode 100644
index 000000000..c51ea03b9
--- /dev/null
+++ b/spec/grammar-registry-spec.js
@@ -0,0 +1,393 @@
+const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
+
+const path = require('path')
+const fs = require('fs-plus')
+const temp = require('temp').track()
+const TextBuffer = require('text-buffer')
+const GrammarRegistry = require('../src/grammar-registry')
+
+describe('GrammarRegistry', () => {
+ let grammarRegistry
+
+ beforeEach(() => {
+ grammarRegistry = new GrammarRegistry({config: atom.config})
+ })
+
+ describe('.assignLanguageMode(buffer, languageName)', () => {
+ it('assigns to the buffer a language mode with the given language name', async () => {
+ grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
+ grammarRegistry.loadGrammarSync(require.resolve('language-css/grammars/css.cson'))
+
+ const buffer = new TextBuffer()
+ expect(grammarRegistry.assignLanguageMode(buffer, 'source.js')).toBe(true)
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js')
+
+ // Returns true if we found the grammar, even if it didn't change
+ expect(grammarRegistry.assignLanguageMode(buffer, 'source.js')).toBe(true)
+
+ // Language names are not case-sensitive
+ expect(grammarRegistry.assignLanguageMode(buffer, 'source.css')).toBe(true)
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css')
+
+ // Returns false if no language is found
+ expect(grammarRegistry.assignLanguageMode(buffer, 'blub')).toBe(false)
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css')
+ })
+
+ describe('when no languageName is passed', () => {
+ it('makes the buffer use the null grammar', () => {
+ grammarRegistry.loadGrammarSync(require.resolve('language-css/grammars/css.cson'))
+
+ const buffer = new TextBuffer()
+ expect(grammarRegistry.assignLanguageMode(buffer, 'source.css')).toBe(true)
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css')
+
+ expect(grammarRegistry.assignLanguageMode(buffer, null)).toBe(true)
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('text.plain.null-grammar')
+ })
+ })
+ })
+
+ describe('.autoAssignLanguageMode(buffer)', () => {
+ it('assigns to the buffer a language mode based on the best available grammar', () => {
+ grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
+ grammarRegistry.loadGrammarSync(require.resolve('language-css/grammars/css.cson'))
+
+ const buffer = new TextBuffer()
+ buffer.setPath('foo.js')
+ expect(grammarRegistry.assignLanguageMode(buffer, 'source.css')).toBe(true)
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css')
+
+ grammarRegistry.autoAssignLanguageMode(buffer)
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js')
+ })
+ })
+
+ describe('.maintainLanguageMode(buffer)', () => {
+ it('assigns a grammar to the buffer based on its path', async () => {
+ const buffer = new TextBuffer()
+
+ grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
+ grammarRegistry.loadGrammarSync(require.resolve('language-c/grammars/c.cson'))
+
+ buffer.setPath('test.js')
+ grammarRegistry.maintainLanguageMode(buffer)
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js')
+
+ buffer.setPath('test.c')
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('source.c')
+ })
+
+ it('updates the buffer\'s grammar when a more appropriate grammar is added for its path', async () => {
+ const buffer = new TextBuffer()
+ expect(buffer.getLanguageMode().getLanguageId()).toBe(null)
+
+ buffer.setPath('test.js')
+ grammarRegistry.maintainLanguageMode(buffer)
+
+ grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js')
+ })
+
+ it('can be overridden by calling .assignLanguageMode', () => {
+ const buffer = new TextBuffer()
+
+ buffer.setPath('test.js')
+ grammarRegistry.maintainLanguageMode(buffer)
+
+ grammarRegistry.loadGrammarSync(require.resolve('language-css/grammars/css.cson'))
+ expect(grammarRegistry.assignLanguageMode(buffer, 'source.css')).toBe(true)
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css')
+
+ grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css')
+ })
+
+ it('returns a disposable that can be used to stop the registry from updating the buffer', async () => {
+ const buffer = new TextBuffer()
+ grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
+
+ const previousSubscriptionCount = buffer.emitter.getTotalListenerCount()
+ const disposable = grammarRegistry.maintainLanguageMode(buffer)
+ expect(buffer.emitter.getTotalListenerCount()).toBeGreaterThan(previousSubscriptionCount)
+ expect(retainedBufferCount(grammarRegistry)).toBe(1)
+
+ buffer.setPath('test.js')
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js')
+
+ buffer.setPath('test.txt')
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('text.plain.null-grammar')
+
+ disposable.dispose()
+ expect(buffer.emitter.getTotalListenerCount()).toBe(previousSubscriptionCount)
+ expect(retainedBufferCount(grammarRegistry)).toBe(0)
+
+ buffer.setPath('test.js')
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('text.plain.null-grammar')
+ expect(retainedBufferCount(grammarRegistry)).toBe(0)
+ })
+
+ it('doesn\'t do anything when called a second time with the same buffer', async () => {
+ const buffer = new TextBuffer()
+ grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
+ const disposable1 = grammarRegistry.maintainLanguageMode(buffer)
+ const disposable2 = grammarRegistry.maintainLanguageMode(buffer)
+
+ buffer.setPath('test.js')
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js')
+
+ disposable2.dispose()
+ buffer.setPath('test.txt')
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('text.plain.null-grammar')
+
+ disposable1.dispose()
+ buffer.setPath('test.js')
+ expect(buffer.getLanguageMode().getLanguageId()).toBe('text.plain.null-grammar')
+ })
+
+ it('does not retain the buffer after the buffer is destroyed', () => {
+ const buffer = new TextBuffer()
+ grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
+
+ const disposable = grammarRegistry.maintainLanguageMode(buffer)
+ expect(retainedBufferCount(grammarRegistry)).toBe(1)
+ expect(subscriptionCount(grammarRegistry)).toBe(2)
+
+ buffer.destroy()
+ expect(retainedBufferCount(grammarRegistry)).toBe(0)
+ expect(subscriptionCount(grammarRegistry)).toBe(0)
+ expect(buffer.emitter.getTotalListenerCount()).toBe(0)
+
+ disposable.dispose()
+ expect(retainedBufferCount(grammarRegistry)).toBe(0)
+ expect(subscriptionCount(grammarRegistry)).toBe(0)
+ })
+
+ it('does not retain the buffer when the grammar registry is destroyed', () => {
+ const buffer = new TextBuffer()
+ grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
+
+ const disposable = grammarRegistry.maintainLanguageMode(buffer)
+ expect(retainedBufferCount(grammarRegistry)).toBe(1)
+ expect(subscriptionCount(grammarRegistry)).toBe(2)
+
+ grammarRegistry.clear()
+
+ expect(retainedBufferCount(grammarRegistry)).toBe(0)
+ expect(subscriptionCount(grammarRegistry)).toBe(0)
+ expect(buffer.emitter.getTotalListenerCount()).toBe(0)
+ })
+ })
+
+ describe('.selectGrammar(filePath)', () => {
+ it('always returns a grammar', () => {
+ const registry = new GrammarRegistry({config: atom.config})
+ expect(registry.selectGrammar().scopeName).toBe('text.plain.null-grammar')
+ })
+
+ it('selects the text.plain grammar over the null grammar', async () => {
+ await atom.packages.activatePackage('language-text')
+ expect(atom.grammars.selectGrammar('test.txt').scopeName).toBe('text.plain')
+ })
+
+ it('selects a grammar based on the file path case insensitively', async () => {
+ await atom.packages.activatePackage('language-coffee-script')
+ expect(atom.grammars.selectGrammar('/tmp/source.coffee').scopeName).toBe('source.coffee')
+ expect(atom.grammars.selectGrammar('/tmp/source.COFFEE').scopeName).toBe('source.coffee')
+ })
+
+ describe('on Windows', () => {
+ let originalPlatform
+
+ beforeEach(() => {
+ originalPlatform = process.platform
+ Object.defineProperty(process, 'platform', {value: 'win32'})
+ })
+
+ afterEach(() => {
+ Object.defineProperty(process, 'platform', {value: originalPlatform})
+ })
+
+ it('normalizes back slashes to forward slashes when matching the fileTypes', async () => {
+ await atom.packages.activatePackage('language-git')
+ expect(atom.grammars.selectGrammar('something\\.git\\config').scopeName).toBe('source.git-config')
+ })
+ })
+
+ it("can use the filePath to load the correct grammar based on the grammar's filetype", async () => {
+ await atom.packages.activatePackage('language-git')
+ await atom.packages.activatePackage('language-javascript')
+ await atom.packages.activatePackage('language-ruby')
+
+ expect(atom.grammars.selectGrammar('file.js').name).toBe('JavaScript') // based on extension (.js)
+ expect(atom.grammars.selectGrammar(path.join(temp.dir, '.git', 'config')).name).toBe('Git Config') // based on end of the path (.git/config)
+ expect(atom.grammars.selectGrammar('Rakefile').name).toBe('Ruby') // based on the file's basename (Rakefile)
+ expect(atom.grammars.selectGrammar('curb').name).toBe('Null Grammar')
+ expect(atom.grammars.selectGrammar('/hu.git/config').name).toBe('Null Grammar')
+ })
+
+ it("uses the filePath's shebang line if the grammar cannot be determined by the extension or basename", async () => {
+ await atom.packages.activatePackage('language-javascript')
+ await atom.packages.activatePackage('language-ruby')
+
+ const filePath = require.resolve('./fixtures/shebang')
+ expect(atom.grammars.selectGrammar(filePath).name).toBe('Ruby')
+ })
+
+ it('uses the number of newlines in the first line regex to determine the number of lines to test against', async () => {
+ await atom.packages.activatePackage('language-property-list')
+ await atom.packages.activatePackage('language-coffee-script')
+
+ let fileContent = 'first-line\n'
+ expect(atom.grammars.selectGrammar('dummy.coffee', fileContent).name).toBe('CoffeeScript')
+
+ fileContent = ''
+ expect(atom.grammars.selectGrammar('grammar.tmLanguage', fileContent).name).toBe('Null Grammar')
+
+ fileContent += '\n'
+ expect(atom.grammars.selectGrammar('grammar.tmLanguage', fileContent).name).toBe('Property List (XML)')
+ })
+
+ it("doesn't read the file when the file contents are specified", async () => {
+ await atom.packages.activatePackage('language-ruby')
+
+ const filePath = require.resolve('./fixtures/shebang')
+ const filePathContents = fs.readFileSync(filePath, 'utf8')
+ spyOn(fs, 'read').andCallThrough()
+ expect(atom.grammars.selectGrammar(filePath, filePathContents).name).toBe('Ruby')
+ expect(fs.read).not.toHaveBeenCalled()
+ })
+
+ describe('when multiple grammars have matching fileTypes', () => {
+ it('selects the grammar with the longest fileType match', () => {
+ const grammarPath1 = temp.path({suffix: '.json'})
+ fs.writeFileSync(grammarPath1, JSON.stringify({
+ name: 'test1',
+ scopeName: 'source1',
+ fileTypes: ['test']
+ }))
+ const grammar1 = atom.grammars.loadGrammarSync(grammarPath1)
+ expect(atom.grammars.selectGrammar('more.test', '')).toBe(grammar1)
+ fs.removeSync(grammarPath1)
+
+ const grammarPath2 = temp.path({suffix: '.json'})
+ fs.writeFileSync(grammarPath2, JSON.stringify({
+ name: 'test2',
+ scopeName: 'source2',
+ fileTypes: ['test', 'more.test']
+ }))
+ const grammar2 = atom.grammars.loadGrammarSync(grammarPath2)
+ expect(atom.grammars.selectGrammar('more.test', '')).toBe(grammar2)
+ return fs.removeSync(grammarPath2)
+ })
+ })
+
+ it('favors non-bundled packages when breaking scoring ties', async () => {
+ await atom.packages.activatePackage('language-ruby')
+ await atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'packages', 'package-with-rb-filetype'))
+
+ atom.grammars.grammarForScopeName('source.ruby').bundledPackage = true
+ atom.grammars.grammarForScopeName('test.rb').bundledPackage = false
+
+ expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env ruby').scopeName).toBe('source.ruby')
+ expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env testruby').scopeName).toBe('test.rb')
+ expect(atom.grammars.selectGrammar('test.rb').scopeName).toBe('test.rb')
+ })
+
+ describe('when there is no file path', () => {
+ it('does not throw an exception (regression)', () => {
+ expect(() => atom.grammars.selectGrammar(null, '#!/usr/bin/ruby')).not.toThrow()
+ expect(() => atom.grammars.selectGrammar(null, '')).not.toThrow()
+ expect(() => atom.grammars.selectGrammar(null, null)).not.toThrow()
+ })
+ })
+
+ describe('when the user has custom grammar file types', () => {
+ it('considers the custom file types as well as those defined in the grammar', async () => {
+ await atom.packages.activatePackage('language-ruby')
+ atom.config.set('core.customFileTypes', {'source.ruby': ['Cheffile']})
+ expect(atom.grammars.selectGrammar('build/Cheffile', 'cookbook "postgres"').scopeName).toBe('source.ruby')
+ })
+
+ it('favors user-defined file types over built-in ones of equal length', async () => {
+ await atom.packages.activatePackage('language-ruby')
+ await atom.packages.activatePackage('language-coffee-script')
+
+ atom.config.set('core.customFileTypes', {
+ 'source.coffee': ['Rakefile'],
+ 'source.ruby': ['Cakefile']
+ })
+ expect(atom.grammars.selectGrammar('Rakefile', '').scopeName).toBe('source.coffee')
+ expect(atom.grammars.selectGrammar('Cakefile', '').scopeName).toBe('source.ruby')
+ })
+
+ it('favors user-defined file types over grammars with matching first-line-regexps', async () => {
+ await atom.packages.activatePackage('language-ruby')
+ await atom.packages.activatePackage('language-javascript')
+
+ atom.config.set('core.customFileTypes', {'source.ruby': ['bootstrap']})
+ expect(atom.grammars.selectGrammar('bootstrap', '#!/usr/bin/env node').scopeName).toBe('source.ruby')
+ })
+ })
+
+ it('favors a grammar with a matching file type over one with m matching first line pattern', async () => {
+ await atom.packages.activatePackage('language-ruby')
+ await atom.packages.activatePackage('language-javascript')
+ expect(atom.grammars.selectGrammar('foo.rb', '#!/usr/bin/env node').scopeName).toBe('source.ruby')
+ })
+ })
+
+ describe('.removeGrammar(grammar)', () => {
+ it("removes the grammar, so it won't be returned by selectGrammar", async () => {
+ await atom.packages.activatePackage('language-javascript')
+ const grammar = atom.grammars.selectGrammar('foo.js')
+ atom.grammars.removeGrammar(grammar)
+ expect(atom.grammars.selectGrammar('foo.js').name).not.toBe(grammar.name)
+ })
+ })
+
+ describe('serialization', () => {
+ it('persists editors\' grammar overrides', async () => {
+ const buffer1 = new TextBuffer()
+ const buffer2 = new TextBuffer()
+
+ grammarRegistry.loadGrammarSync(require.resolve('language-c/grammars/c.cson'))
+ grammarRegistry.loadGrammarSync(require.resolve('language-html/grammars/html.cson'))
+ grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
+
+ grammarRegistry.maintainLanguageMode(buffer1)
+ grammarRegistry.maintainLanguageMode(buffer2)
+ grammarRegistry.assignLanguageMode(buffer1, 'source.c')
+ grammarRegistry.assignLanguageMode(buffer2, 'source.js')
+
+ const buffer1Copy = await TextBuffer.deserialize(buffer1.serialize())
+ const buffer2Copy = await TextBuffer.deserialize(buffer2.serialize())
+
+ const grammarRegistryCopy = new GrammarRegistry({config: atom.config})
+ grammarRegistryCopy.deserialize(JSON.parse(JSON.stringify(grammarRegistry.serialize())))
+
+ grammarRegistryCopy.loadGrammarSync(require.resolve('language-c/grammars/c.cson'))
+ grammarRegistryCopy.loadGrammarSync(require.resolve('language-html/grammars/html.cson'))
+
+ expect(buffer1Copy.getLanguageMode().getLanguageId()).toBe(null)
+ expect(buffer2Copy.getLanguageMode().getLanguageId()).toBe(null)
+
+ grammarRegistryCopy.maintainLanguageMode(buffer1Copy)
+ grammarRegistryCopy.maintainLanguageMode(buffer2Copy)
+ expect(buffer1Copy.getLanguageMode().getLanguageId()).toBe('source.c')
+ expect(buffer2Copy.getLanguageMode().getLanguageId()).toBe(null)
+
+ grammarRegistryCopy.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
+ expect(buffer1Copy.getLanguageMode().getLanguageId()).toBe('source.c')
+ expect(buffer2Copy.getLanguageMode().getLanguageId()).toBe('source.js')
+ })
+ })
+})
+
+function retainedBufferCount (grammarRegistry) {
+ return grammarRegistry.grammarScoresByBuffer.size
+}
+
+function subscriptionCount (grammarRegistry) {
+ return grammarRegistry.subscriptions.disposables.size
+}
diff --git a/spec/grammars-spec.coffee b/spec/grammars-spec.coffee
deleted file mode 100644
index db716528d..000000000
--- a/spec/grammars-spec.coffee
+++ /dev/null
@@ -1,182 +0,0 @@
-path = require 'path'
-fs = require 'fs-plus'
-temp = require('temp').track()
-GrammarRegistry = require '../src/grammar-registry'
-Grim = require 'grim'
-
-describe "the `grammars` global", ->
- beforeEach ->
- waitsForPromise ->
- atom.packages.activatePackage('language-text')
-
- waitsForPromise ->
- atom.packages.activatePackage('language-javascript')
-
- waitsForPromise ->
- atom.packages.activatePackage('language-coffee-script')
-
- waitsForPromise ->
- atom.packages.activatePackage('language-ruby')
-
- waitsForPromise ->
- atom.packages.activatePackage('language-git')
-
- afterEach ->
- waitsForPromise ->
- atom.packages.deactivatePackages()
- runs ->
- atom.packages.unloadPackages()
- try
- temp.cleanupSync()
-
- describe ".selectGrammar(filePath)", ->
- it "always returns a grammar", ->
- registry = new GrammarRegistry(config: atom.config)
- expect(registry.selectGrammar().scopeName).toBe 'text.plain.null-grammar'
-
- it "selects the text.plain grammar over the null grammar", ->
- expect(atom.grammars.selectGrammar('test.txt').scopeName).toBe 'text.plain'
-
- it "selects a grammar based on the file path case insensitively", ->
- expect(atom.grammars.selectGrammar('/tmp/source.coffee').scopeName).toBe 'source.coffee'
- expect(atom.grammars.selectGrammar('/tmp/source.COFFEE').scopeName).toBe 'source.coffee'
-
- describe "on Windows", ->
- originalPlatform = null
-
- beforeEach ->
- originalPlatform = process.platform
- Object.defineProperty process, 'platform', value: 'win32'
-
- afterEach ->
- Object.defineProperty process, 'platform', value: originalPlatform
-
- it "normalizes back slashes to forward slashes when matching the fileTypes", ->
- expect(atom.grammars.selectGrammar('something\\.git\\config').scopeName).toBe 'source.git-config'
-
- it "can use the filePath to load the correct grammar based on the grammar's filetype", ->
- waitsForPromise ->
- atom.packages.activatePackage('language-git')
-
- runs ->
- expect(atom.grammars.selectGrammar("file.js").name).toBe "JavaScript" # based on extension (.js)
- expect(atom.grammars.selectGrammar(path.join(temp.dir, '.git', 'config')).name).toBe "Git Config" # based on end of the path (.git/config)
- expect(atom.grammars.selectGrammar("Rakefile").name).toBe "Ruby" # based on the file's basename (Rakefile)
- expect(atom.grammars.selectGrammar("curb").name).toBe "Null Grammar"
- expect(atom.grammars.selectGrammar("/hu.git/config").name).toBe "Null Grammar"
-
- it "uses the filePath's shebang line if the grammar cannot be determined by the extension or basename", ->
- filePath = require.resolve("./fixtures/shebang")
- expect(atom.grammars.selectGrammar(filePath).name).toBe "Ruby"
-
- it "uses the number of newlines in the first line regex to determine the number of lines to test against", ->
- waitsForPromise ->
- atom.packages.activatePackage('language-property-list')
-
- runs ->
- fileContent = "first-line\n"
- expect(atom.grammars.selectGrammar("dummy.coffee", fileContent).name).toBe "CoffeeScript"
-
- fileContent = ''
- expect(atom.grammars.selectGrammar("grammar.tmLanguage", fileContent).name).toBe "Null Grammar"
-
- fileContent += '\n'
- expect(atom.grammars.selectGrammar("grammar.tmLanguage", fileContent).name).toBe "Property List (XML)"
-
- it "doesn't read the file when the file contents are specified", ->
- filePath = require.resolve("./fixtures/shebang")
- filePathContents = fs.readFileSync(filePath, 'utf8')
- spyOn(fs, 'read').andCallThrough()
- expect(atom.grammars.selectGrammar(filePath, filePathContents).name).toBe "Ruby"
- expect(fs.read).not.toHaveBeenCalled()
-
- describe "when multiple grammars have matching fileTypes", ->
- it "selects the grammar with the longest fileType match", ->
- grammarPath1 = temp.path(suffix: '.json')
- fs.writeFileSync grammarPath1, JSON.stringify(
- name: 'test1'
- scopeName: 'source1'
- fileTypes: ['test']
- )
- grammar1 = atom.grammars.loadGrammarSync(grammarPath1)
- expect(atom.grammars.selectGrammar('more.test', '')).toBe grammar1
- fs.removeSync(grammarPath1)
-
- grammarPath2 = temp.path(suffix: '.json')
- fs.writeFileSync grammarPath2, JSON.stringify(
- name: 'test2'
- scopeName: 'source2'
- fileTypes: ['test', 'more.test']
- )
- grammar2 = atom.grammars.loadGrammarSync(grammarPath2)
- expect(atom.grammars.selectGrammar('more.test', '')).toBe grammar2
- fs.removeSync(grammarPath2)
-
- it "favors non-bundled packages when breaking scoring ties", ->
- waitsForPromise ->
- atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'packages', 'package-with-rb-filetype'))
-
- runs ->
- atom.grammars.grammarForScopeName('source.ruby').bundledPackage = true
- atom.grammars.grammarForScopeName('test.rb').bundledPackage = false
-
- expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env ruby').scopeName).toBe 'source.ruby'
- expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env testruby').scopeName).toBe 'test.rb'
- expect(atom.grammars.selectGrammar('test.rb').scopeName).toBe 'test.rb'
-
- describe "when there is no file path", ->
- it "does not throw an exception (regression)", ->
- expect(-> atom.grammars.selectGrammar(null, '#!/usr/bin/ruby')).not.toThrow()
- expect(-> atom.grammars.selectGrammar(null, '')).not.toThrow()
- expect(-> atom.grammars.selectGrammar(null, null)).not.toThrow()
-
- describe "when the user has custom grammar file types", ->
- it "considers the custom file types as well as those defined in the grammar", ->
- atom.config.set('core.customFileTypes', 'source.ruby': ['Cheffile'])
- expect(atom.grammars.selectGrammar('build/Cheffile', 'cookbook "postgres"').scopeName).toBe 'source.ruby'
-
- it "favors user-defined file types over built-in ones of equal length", ->
- atom.config.set('core.customFileTypes',
- 'source.coffee': ['Rakefile'],
- 'source.ruby': ['Cakefile']
- )
- expect(atom.grammars.selectGrammar('Rakefile', '').scopeName).toBe 'source.coffee'
- expect(atom.grammars.selectGrammar('Cakefile', '').scopeName).toBe 'source.ruby'
-
- it "favors user-defined file types over grammars with matching first-line-regexps", ->
- atom.config.set('core.customFileTypes', 'source.ruby': ['bootstrap'])
- expect(atom.grammars.selectGrammar('bootstrap', '#!/usr/bin/env node').scopeName).toBe 'source.ruby'
-
- describe "when there is a grammar with a first line pattern, the file type of the file is known, but from a different grammar", ->
- it "favors file type over the matching pattern", ->
- expect(atom.grammars.selectGrammar('foo.rb', '#!/usr/bin/env node').scopeName).toBe 'source.ruby'
-
- describe ".removeGrammar(grammar)", ->
- it "removes the grammar, so it won't be returned by selectGrammar", ->
- grammar = atom.grammars.selectGrammar('foo.js')
- atom.grammars.removeGrammar(grammar)
- expect(atom.grammars.selectGrammar('foo.js').name).not.toBe grammar.name
-
- describe "grammar overrides", ->
- it "logs deprecations and uses the TextEditorRegistry", ->
- editor = null
-
- waitsForPromise ->
- atom.workspace.open('sample.js').then (e) -> editor = e
-
- runs ->
- spyOn(Grim, 'deprecate')
-
- atom.grammars.setGrammarOverrideForPath(editor.getPath(), 'source.ruby')
- expect(Grim.deprecate.callCount).toBe 1
- expect(editor.getGrammar().name).toBe 'Ruby'
-
- expect(atom.grammars.grammarOverrideForPath(editor.getPath())).toBe('source.ruby')
- expect(Grim.deprecate.callCount).toBe 2
-
- atom.grammars.clearGrammarOverrideForPath(editor.getPath(), 'source.ruby')
- expect(Grim.deprecate.callCount).toBe 3
- expect(editor.getGrammar().name).toBe 'JavaScript'
-
- expect(atom.grammars.grammarOverrideForPath(editor.getPath())).toBe(undefined)
- expect(Grim.deprecate.callCount).toBe 4
diff --git a/spec/project-spec.js b/spec/project-spec.js
index 0f003b26b..bd6bb1fa6 100644
--- a/spec/project-spec.js
+++ b/spec/project-spec.js
@@ -35,7 +35,12 @@ describe('Project', () => {
})
it("does not deserialize paths to directories that don't exist", () => {
- deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
+ deserializedProject = new Project({
+ notificationManager: atom.notifications,
+ packageManager: atom.packages,
+ confirm: atom.confirm,
+ grammarRegistry: atom.grammars
+ })
const state = atom.project.serialize()
state.paths.push('/directory/that/does/not/exist')
@@ -55,7 +60,12 @@ describe('Project', () => {
const childPath = path.join(temp.mkdirSync('atom-spec-project'), 'child')
fs.mkdirSync(childPath)
- deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
+ deserializedProject = new Project({
+ notificationManager: atom.notifications,
+ packageManager: atom.packages,
+ confirm: atom.confirm,
+ grammarRegistry: atom.grammars
+ })
atom.project.setPaths([childPath])
const state = atom.project.serialize()
@@ -80,7 +90,12 @@ describe('Project', () => {
runs(() => {
expect(atom.project.getBuffers().length).toBe(1)
- deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
+ deserializedProject = new Project({
+ notificationManager: atom.notifications,
+ packageManager: atom.packages,
+ confirm: atom.confirm,
+ grammarRegistry: atom.grammars
+ })
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
@@ -93,7 +108,12 @@ describe('Project', () => {
runs(() => {
expect(atom.project.getBuffers().length).toBe(1)
- deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
+ deserializedProject = new Project({
+ notificationManager: atom.notifications,
+ packageManager: atom.packages,
+ confirm: atom.confirm,
+ grammarRegistry: atom.grammars
+ })
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
@@ -113,7 +133,12 @@ describe('Project', () => {
runs(() => {
expect(atom.project.getBuffers().length).toBe(1)
fs.mkdirSync(pathToOpen)
- deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
+ deserializedProject = new Project({
+ notificationManager: atom.notifications,
+ packageManager: atom.packages,
+ confirm: atom.confirm,
+ grammarRegistry: atom.grammars
+ })
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
@@ -131,7 +156,12 @@ describe('Project', () => {
runs(() => {
expect(atom.project.getBuffers().length).toBe(1)
fs.chmodSync(pathToOpen, '000')
- deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
+ deserializedProject = new Project({
+ notificationManager: atom.notifications,
+ packageManager: atom.packages,
+ confirm: atom.confirm,
+ grammarRegistry: atom.grammars
+ })
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
@@ -148,7 +178,12 @@ describe('Project', () => {
runs(() => {
expect(atom.project.getBuffers().length).toBe(1)
fs.unlinkSync(pathToOpen)
- deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
+ deserializedProject = new Project({
+ notificationManager: atom.notifications,
+ packageManager: atom.packages,
+ confirm: atom.confirm,
+ grammarRegistry: atom.grammars
+ })
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
@@ -165,7 +200,12 @@ describe('Project', () => {
atom.workspace.getActiveTextEditor().setText('unsaved\n')
expect(atom.project.getBuffers().length).toBe(1)
- deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
+ deserializedProject = new Project({
+ notificationManager: atom.notifications,
+ packageManager: atom.packages,
+ confirm: atom.confirm,
+ grammarRegistry: atom.grammars
+ })
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
@@ -189,7 +229,12 @@ describe('Project', () => {
layerA = bufferA.addMarkerLayer({persistent: true})
markerA = layerA.markPosition([0, 3])
bufferA.append('!')
- notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
+ notQuittingProject = new Project({
+ notificationManager: atom.notifications,
+ packageManager: atom.packages,
+ confirm: atom.confirm,
+ grammarRegistry: atom.grammars
+ })
})
waitsForPromise(() => notQuittingProject.deserialize(atom.project.serialize({isUnloading: false})))
@@ -197,7 +242,12 @@ describe('Project', () => {
runs(() => {
expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).toBeUndefined()
expect(notQuittingProject.getBuffers()[0].undo()).toBe(false)
- quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
+ quittingProject = new Project({
+ notificationManager: atom.notifications,
+ packageManager: atom.packages,
+ confirm: atom.confirm,
+ grammarRegistry: atom.grammars
+ })
})
waitsForPromise(() => quittingProject.deserialize(atom.project.serialize({isUnloading: true})))
@@ -209,7 +259,7 @@ describe('Project', () => {
})
})
- describe('when an editor is saved and the project has no path', () =>
+ describe('when an editor is saved and the project has no path', () => {
it("sets the project's path to the saved file's parent directory", () => {
const tempFile = temp.openSync().path
atom.project.setPaths([])
@@ -222,7 +272,7 @@ describe('Project', () => {
runs(() => expect(atom.project.getPaths()[0]).toBe(path.dirname(tempFile)))
})
- )
+ })
describe('before and after saving a buffer', () => {
let buffer
@@ -422,7 +472,7 @@ describe('Project', () => {
atom.project.onDidAddBuffer(newBufferHandler)
})
- describe("when given an absolute path that isn't currently open", () =>
+ describe("when given an absolute path that isn't currently open", () => {
it("returns a new edit session for the given path and emits 'buffer-created'", () => {
let editor = null
waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
@@ -432,9 +482,9 @@ describe('Project', () => {
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
})
})
- )
+ })
- describe("when given a relative path that isn't currently opened", () =>
+ describe("when given a relative path that isn't currently opened", () => {
it("returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", () => {
let editor = null
waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
@@ -444,9 +494,9 @@ describe('Project', () => {
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
})
})
- )
+ })
- describe('when passed the path to a buffer that is currently opened', () =>
+ describe('when passed the path to a buffer that is currently opened', () => {
it('returns a new edit session containing currently opened buffer', () => {
let editor = null
@@ -465,9 +515,9 @@ describe('Project', () => {
})
)
})
- )
+ })
- describe('when not passed a path', () =>
+ describe('when not passed a path', () => {
it("returns a new edit session and emits 'buffer-created'", () => {
let editor = null
waitsForPromise(() => atom.workspace.open().then(o => { editor = o }))
@@ -477,7 +527,7 @@ describe('Project', () => {
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
})
})
- )
+ })
})
describe('.bufferForPath(path)', () => {
@@ -537,7 +587,7 @@ describe('Project', () => {
})
describe('.repositoryForDirectory(directory)', () => {
- it('resolves to null when the directory does not have a repository', () =>
+ it('resolves to null when the directory does not have a repository', () => {
waitsForPromise(() => {
const directory = new Directory('/tmp')
return atom.project.repositoryForDirectory(directory).then((result) => {
@@ -546,9 +596,9 @@ describe('Project', () => {
expect(atom.project.repositoryPromisesByPath.size).toBe(0)
})
})
- )
+ })
- it('resolves to a GitRepository and is cached when the given directory is a Git repo', () =>
+ it('resolves to a GitRepository and is cached when the given directory is a Git repo', () => {
waitsForPromise(() => {
const directory = new Directory(path.join(__dirname, '..'))
const promise = atom.project.repositoryForDirectory(directory)
@@ -561,7 +611,7 @@ describe('Project', () => {
expect(atom.project.repositoryForDirectory(directory)).toBe(promise)
})
})
- )
+ })
it('creates a new repository if a previous one with the same directory had been destroyed', () => {
let repository = null
@@ -582,14 +632,14 @@ describe('Project', () => {
})
describe('.setPaths(paths, options)', () => {
- describe('when path is a file', () =>
+ describe('when path is a file', () => {
it("sets its path to the file's parent directory and updates the root directory", () => {
const filePath = require.resolve('./fixtures/dir/a')
atom.project.setPaths([filePath])
expect(atom.project.getPaths()[0]).toEqual(path.dirname(filePath))
expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(filePath))
})
- )
+ })
describe('when path is a directory', () => {
it('assigns the directories and repositories', () => {
@@ -636,13 +686,13 @@ describe('Project', () => {
})
})
- describe('when no paths are given', () =>
+ describe('when no paths are given', () => {
it('clears its path', () => {
atom.project.setPaths([])
expect(atom.project.getPaths()).toEqual([])
expect(atom.project.getDirectories()).toEqual([])
})
- )
+ })
it('normalizes the path to remove consecutive slashes, ., and .. segments', () => {
atom.project.setPaths([`${require.resolve('./fixtures/dir/a')}${path.sep}b${path.sep}${path.sep}..`])
@@ -693,9 +743,9 @@ describe('Project', () => {
expect(atom.project.getPaths()).toEqual(previousPaths)
})
- it('optionally throws on non-existent directories', () =>
+ it('optionally throws on non-existent directories', () => {
expect(() => atom.project.addPath('/this-definitely/does-not-exist', {mustExist: true})).toThrow()
- )
+ })
})
describe('.removePath(path)', () => {
@@ -813,7 +863,7 @@ describe('Project', () => {
})
})
- describe('.onDidAddBuffer()', () =>
+ describe('.onDidAddBuffer()', () => {
it('invokes the callback with added text buffers', () => {
const buffers = []
const added = []
@@ -838,9 +888,9 @@ describe('Project', () => {
expect(added).toEqual([buffers[1]])
})
})
-)
+ })
- describe('.observeBuffers()', () =>
+ describe('.observeBuffers()', () => {
it('invokes the observer with current and future text buffers', () => {
const buffers = []
const observed = []
@@ -872,7 +922,7 @@ describe('Project', () => {
expect(observed).toEqual(buffers)
})
})
- )
+ })
describe('.relativize(path)', () => {
it('returns the path, relative to whichever root directory it is inside of', () => {
@@ -906,21 +956,21 @@ describe('Project', () => {
expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')])
})
- describe("when the given path isn't inside of any of the project's path", () =>
+ describe("when the given path isn't inside of any of the project's path", () => {
it('returns null for the root path, and the given path unchanged', () => {
const randomPath = path.join('some', 'random', 'path')
expect(atom.project.relativizePath(randomPath)).toEqual([null, randomPath])
})
- )
+ })
- describe('when the given path is a URL', () =>
+ describe('when the given path is a URL', () => {
it('returns null for the root path, and the given path unchanged', () => {
const url = 'http://the-path'
expect(atom.project.relativizePath(url)).toEqual([null, url])
})
- )
+ })
- describe('when the given path is inside more than one root folder', () =>
+ describe('when the given path is inside more than one root folder', () => {
it('uses the root folder that is closest to the given path', () => {
atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir'))
@@ -933,10 +983,10 @@ describe('Project', () => {
path.join('somewhere', 'something.txt')
])
})
- )
+ })
})
- describe('.contains(path)', () =>
+ describe('.contains(path)', () => {
it('returns whether or not the given path is in one of the root directories', () => {
const rootPath = atom.project.getPaths()[0]
const childPath = path.join(rootPath, 'some', 'child', 'directory')
@@ -945,11 +995,11 @@ describe('Project', () => {
const randomPath = path.join('some', 'random', 'path')
expect(atom.project.contains(randomPath)).toBe(false)
})
- )
+ })
- describe('.resolvePath(uri)', () =>
+ describe('.resolvePath(uri)', () => {
it('normalizes disk drive letter in passed path on #win32', () => {
expect(atom.project.resolvePath('d:\\file.txt')).toEqual('D:\\file.txt')
})
- )
+ })
})
diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee
index 7efad7dd4..f39c9a42f 100644
--- a/spec/spec-helper.coffee
+++ b/spec/spec-helper.coffee
@@ -7,6 +7,7 @@ fs = require 'fs-plus'
Grim = require 'grim'
pathwatcher = require 'pathwatcher'
FindParentDir = require 'find-parent-dir'
+{CompositeDisposable} = require 'event-kit'
TextEditor = require '../src/text-editor'
TextEditorElement = require '../src/text-editor-element'
@@ -102,6 +103,18 @@ beforeEach ->
TokenizedBuffer.prototype.chunkSize = Infinity
spyOn(TokenizedBuffer.prototype, "tokenizeInBackground").andCallFake -> @tokenizeNextChunk()
+ # Without this spy, TextEditor.onDidTokenize callbacks would not be called
+ # after the buffer's language mode changed, because by the time the editor
+ # called its new language mode's onDidTokenize method, the language mode
+ # would already be fully tokenized.
+ spyOn(TextEditor.prototype, "onDidTokenize").andCallFake (callback) ->
+ new CompositeDisposable(
+ @emitter.on("did-tokenize", callback),
+ @onDidChangeGrammar =>
+ if @buffer.getLanguageMode().tokenizeInBackground.originalValue
+ callback()
+ )
+
clipboardContent = 'initial clipboard content'
spyOn(clipboard, 'writeText').andCallFake (text) -> clipboardContent = text
spyOn(clipboard, 'readText').andCallFake -> clipboardContent
diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js
index c18e0d84a..47ba4ca63 100644
--- a/spec/text-editor-component-spec.js
+++ b/spec/text-editor-component-spec.js
@@ -25,6 +25,8 @@ document.registerElement('text-editor-component-test-element', {
})
})
+const editors = []
+
describe('TextEditorComponent', () => {
beforeEach(() => {
jasmine.useRealClock()
@@ -35,6 +37,13 @@ describe('TextEditorComponent', () => {
jasmine.attachToDOM(scrollbarStyle)
})
+ afterEach(() => {
+ for (const editor of editors) {
+ editor.destroy()
+ }
+ editors.length = 0
+ })
+
describe('rendering', () => {
it('renders lines and line numbers for the visible region', async () => {
const {component, element, editor} = buildComponent({rowsPerTile: 3, autoHeight: false})
@@ -786,7 +795,7 @@ describe('TextEditorComponent', () => {
const {editor, element, component} = buildComponent()
expect(element.dataset.grammar).toBe('text plain null-grammar')
- editor.setGrammar(atom.grammars.grammarForScopeName('source.js'))
+ atom.grammars.assignLanguageMode(editor.getBuffer(), 'source.js')
await component.getNextUpdatePromise()
expect(element.dataset.grammar).toBe('source js')
})
@@ -4482,9 +4491,11 @@ function buildEditor (params = {}) {
for (const paramName of ['mini', 'autoHeight', 'autoWidth', 'lineNumberGutterVisible', 'showLineNumbers', 'placeholderText', 'softWrapped', 'scrollSensitivity']) {
if (params[paramName] != null) editorParams[paramName] = params[paramName]
}
+ atom.grammars.autoAssignLanguageMode(buffer)
const editor = new TextEditor(editorParams)
editor.testAutoscrollRequests = []
editor.onDidRequestAutoscroll((request) => { editor.testAutoscrollRequests.push(request) })
+ editors.push(editor)
return editor
}
diff --git a/spec/text-editor-element-spec.js b/spec/text-editor-element-spec.js
index 7cdd374a1..7c05aced4 100644
--- a/spec/text-editor-element-spec.js
+++ b/spec/text-editor-element-spec.js
@@ -77,13 +77,11 @@ describe('TextEditorElement', () => {
})
describe('when the model is assigned', () =>
- it("adds the 'mini' attribute if .isMini() returns true on the model", function (done) {
+ it("adds the 'mini' attribute if .isMini() returns true on the model", async () => {
const element = buildTextEditorElement()
element.getModel().update({mini: true})
- atom.views.getNextUpdatePromise().then(() => {
- expect(element.hasAttribute('mini')).toBe(true)
- done()
- })
+ await atom.views.getNextUpdatePromise()
+ expect(element.hasAttribute('mini')).toBe(true)
})
)
@@ -268,12 +266,11 @@ describe('TextEditorElement', () => {
})
)
- describe('::setUpdatedSynchronously', () =>
+ describe('::setUpdatedSynchronously', () => {
it('controls whether the text editor is updated synchronously', () => {
spyOn(window, 'requestAnimationFrame').andCallFake(fn => fn())
const element = buildTextEditorElement()
- jasmine.attachToDOM(element)
expect(element.isUpdatedSynchronously()).toBe(false)
@@ -288,7 +285,7 @@ describe('TextEditorElement', () => {
expect(window.requestAnimationFrame).not.toHaveBeenCalled()
expect(element.textContent).toContain('goodbye')
})
- )
+ })
describe('::getDefaultCharacterWidth', () => {
it('returns 0 before the element is attached', () => {
diff --git a/spec/text-editor-registry-spec.js b/spec/text-editor-registry-spec.js
index 017ef1f1b..63dd4a0e1 100644
--- a/spec/text-editor-registry-spec.js
+++ b/spec/text-editor-registry-spec.js
@@ -1,10 +1,8 @@
-/** @babel */
-
-import TextEditorRegistry from '../src/text-editor-registry'
-import TextEditor from '../src/text-editor'
-import TextBuffer from 'text-buffer'
-import {it, fit, ffit, fffit} from './async-spec-helpers'
-import dedent from 'dedent'
+const TextEditorRegistry = require('../src/text-editor-registry')
+const TextEditor = require('../src/text-editor')
+const TextBuffer = require('text-buffer')
+const {it, fit, ffit, fffit} = require('./async-spec-helpers')
+const dedent = require('dedent')
describe('TextEditorRegistry', function () {
let registry, editor, initialPackageActivation
@@ -20,6 +18,7 @@ describe('TextEditorRegistry', function () {
})
editor = new TextEditor({autoHeight: false})
+ expect(atom.grammars.assignLanguageMode(editor, 'text.plain.null-grammar')).toBe(true)
})
afterEach(function () {
@@ -71,128 +70,17 @@ describe('TextEditorRegistry', function () {
atom.config.set('editor.tabLength', 8, {scope: '.source.js'})
const editor = registry.build({buffer: new TextBuffer({filePath: 'test.js'})})
- expect(editor.getGrammar().name).toBe("JavaScript")
expect(editor.getTabLength()).toBe(8)
})
})
- describe('.maintainGrammar', function () {
- it('assigns a grammar to the editor based on its path', async function () {
- await atom.packages.activatePackage('language-javascript')
- await atom.packages.activatePackage('language-c')
-
- editor.getBuffer().setPath('test.js')
- registry.maintainGrammar(editor)
-
- expect(editor.getGrammar().name).toBe('JavaScript')
-
- editor.getBuffer().setPath('test.c')
- expect(editor.getGrammar().name).toBe('C')
- })
-
- it('updates the editor\'s grammar when a more appropriate grammar is added for its path', async function () {
- expect(editor.getGrammar().name).toBe('Null Grammar')
-
- editor.getBuffer().setPath('test.js')
- registry.maintainGrammar(editor)
- await atom.packages.activatePackage('language-javascript')
- expect(editor.getGrammar().name).toBe('JavaScript')
- })
-
- it('returns a disposable that can be used to stop the registry from updating the editor', async function () {
- await atom.packages.activatePackage('language-javascript')
-
- const previousSubscriptionCount = getSubscriptionCount(editor)
- const disposable = registry.maintainGrammar(editor)
- expect(getSubscriptionCount(editor)).toBeGreaterThan(previousSubscriptionCount)
- expect(registry.editorsWithMaintainedGrammar.size).toBe(1)
-
- editor.getBuffer().setPath('test.js')
- expect(editor.getGrammar().name).toBe('JavaScript')
-
- editor.getBuffer().setPath('test.txt')
- expect(editor.getGrammar().name).toBe('Null Grammar')
-
- disposable.dispose()
- expect(getSubscriptionCount(editor)).toBe(previousSubscriptionCount)
- expect(registry.editorsWithMaintainedGrammar.size).toBe(0)
-
- editor.getBuffer().setPath('test.js')
- expect(editor.getGrammar().name).toBe('Null Grammar')
- expect(retainedEditorCount(registry)).toBe(0)
- })
-
- describe('when called twice with a given editor', function () {
- it('does nothing the second time', async function () {
- await atom.packages.activatePackage('language-javascript')
- const disposable1 = registry.maintainGrammar(editor)
- const disposable2 = registry.maintainGrammar(editor)
-
- editor.getBuffer().setPath('test.js')
- expect(editor.getGrammar().name).toBe('JavaScript')
-
- disposable2.dispose()
- editor.getBuffer().setPath('test.txt')
- expect(editor.getGrammar().name).toBe('Null Grammar')
-
- disposable1.dispose()
- editor.getBuffer().setPath('test.js')
- expect(editor.getGrammar().name).toBe('Null Grammar')
- })
- })
- })
-
- describe('.setGrammarOverride', function () {
- it('sets the editor\'s grammar and does not update it based on other criteria', async function () {
- await atom.packages.activatePackage('language-c')
- await atom.packages.activatePackage('language-javascript')
-
- registry.maintainGrammar(editor)
- editor.getBuffer().setPath('file-1.js')
- expect(editor.getGrammar().name).toBe('JavaScript')
-
- registry.setGrammarOverride(editor, 'source.c')
- expect(editor.getGrammar().name).toBe('C')
-
- editor.getBuffer().setPath('file-3.rb')
- await atom.packages.activatePackage('language-ruby')
- expect(editor.getGrammar().name).toBe('C')
-
- editor.getBuffer().setPath('file-1.js')
- expect(editor.getGrammar().name).toBe('C')
- })
- })
-
- describe('.clearGrammarOverride', function () {
- it('resumes setting the grammar based on its path and content', async function () {
- await atom.packages.activatePackage('language-c')
- await atom.packages.activatePackage('language-javascript')
-
- registry.maintainGrammar(editor)
- editor.getBuffer().setPath('file-1.js')
- expect(editor.getGrammar().name).toBe('JavaScript')
-
- registry.setGrammarOverride(editor, 'source.c')
- expect(registry.getGrammarOverride(editor)).toBe('source.c')
- expect(editor.getGrammar().name).toBe('C')
-
- registry.clearGrammarOverride(editor)
- expect(editor.getGrammar().name).toBe('JavaScript')
-
- editor.getBuffer().setPath('file-3.rb')
- await atom.packages.activatePackage('language-ruby')
- expect(editor.getGrammar().name).toBe('Ruby')
- expect(registry.getGrammarOverride(editor)).toBe(undefined)
- })
- })
-
describe('.maintainConfig(editor)', function () {
it('does not update the editor when config settings change for unrelated scope selectors', async function () {
await atom.packages.activatePackage('language-javascript')
const editor2 = new TextEditor()
- editor2.setGrammar(atom.grammars.selectGrammar('test.js'))
+ atom.grammars.assignLanguageMode(editor2, 'source.js')
registry.maintainConfig(editor)
registry.maintainConfig(editor2)
@@ -254,14 +142,14 @@ describe('TextEditorRegistry', function () {
atom.config.set('core.fileEncoding', 'utf16le', {scopeSelector: '.source.js'})
expect(editor.getEncoding()).toBe('utf8')
- editor.setGrammar(atom.grammars.grammarForScopeName('source.js'))
+ atom.grammars.assignLanguageMode(editor, 'source.js')
await initialPackageActivation
expect(editor.getEncoding()).toBe('utf16le')
atom.config.set('core.fileEncoding', 'utf16be', {scopeSelector: '.source.js'})
expect(editor.getEncoding()).toBe('utf16be')
- editor.setGrammar(atom.grammars.selectGrammar('test.txt'))
+ atom.grammars.assignLanguageMode(editor, 'text.plain.null-grammar')
await initialPackageActivation
expect(editor.getEncoding()).toBe('utf8')
})
@@ -331,7 +219,7 @@ 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 atom.packages.activatePackage('language-javascript')
- editor.setGrammar(atom.grammars.selectGrammar('test.js'))
+ atom.grammars.assignLanguageMode(editor, 'source.js')
atom.config.set('editor.tabType', 'auto')
registry.maintainConfig(editor)
@@ -624,19 +512,6 @@ describe('TextEditorRegistry', function () {
expect(editor.getUndoGroupingInterval()).toBe(300)
})
- it('sets the non-word characters based on the config', async function () {
- editor.update({nonWordCharacters: '()'})
- expect(editor.getNonWordCharacters()).toBe('()')
-
- atom.config.set('editor.nonWordCharacters', '(){}')
- registry.maintainConfig(editor)
- await initialPackageActivation
- expect(editor.getNonWordCharacters()).toBe('(){}')
-
- atom.config.set('editor.nonWordCharacters', '(){}[]')
- expect(editor.getNonWordCharacters()).toBe('(){}[]')
- })
-
it('sets the scroll sensitivity based on the config', async function () {
editor.update({scrollSensitivity: 50})
expect(editor.getScrollSensitivity()).toBe(50)
@@ -650,21 +525,6 @@ describe('TextEditorRegistry', function () {
expect(editor.getScrollSensitivity()).toBe(70)
})
- it('gives the editor a scoped-settings delegate based on the config', async function () {
- atom.config.set('editor.nonWordCharacters', '()')
- atom.config.set('editor.nonWordCharacters', '(){}', {scopeSelector: '.a.b .c.d'})
- atom.config.set('editor.nonWordCharacters', '(){}[]', {scopeSelector: '.e.f *'})
-
- registry.maintainConfig(editor)
- await initialPackageActivation
-
- let delegate = editor.getScopedSettingsDelegate()
-
- expect(delegate.getNonWordCharacters(['a.b', 'c.d'])).toBe('(){}')
- expect(delegate.getNonWordCharacters(['e.f', 'g.h'])).toBe('(){}[]')
- expect(delegate.getNonWordCharacters(['i.j'])).toBe('()')
- })
-
describe('when called twice with a given editor', function () {
it('does nothing the second time', async function () {
editor.update({scrollSensitivity: 50})
@@ -686,46 +546,6 @@ describe('TextEditorRegistry', function () {
})
})
})
-
- describe('serialization', function () {
- it('persists editors\' grammar overrides', async function () {
- const editor2 = new TextEditor()
-
- await atom.packages.activatePackage('language-c')
- await atom.packages.activatePackage('language-html')
- await atom.packages.activatePackage('language-javascript')
-
- registry.maintainGrammar(editor)
- registry.maintainGrammar(editor2)
- registry.setGrammarOverride(editor, 'source.c')
- registry.setGrammarOverride(editor2, 'source.js')
-
- await atom.packages.deactivatePackage('language-javascript')
-
- const editorCopy = TextEditor.deserialize(editor.serialize(), atom)
- const editor2Copy = TextEditor.deserialize(editor2.serialize(), atom)
-
- const registryCopy = new TextEditorRegistry({
- assert: atom.assert,
- config: atom.config,
- grammarRegistry: atom.grammars,
- packageManager: {deferredActivationHooks: null}
- })
- registryCopy.deserialize(JSON.parse(JSON.stringify(registry.serialize())))
-
- expect(editorCopy.getGrammar().name).toBe('Null Grammar')
- expect(editor2Copy.getGrammar().name).toBe('Null Grammar')
-
- registryCopy.maintainGrammar(editorCopy)
- registryCopy.maintainGrammar(editor2Copy)
- expect(editorCopy.getGrammar().name).toBe('C')
- expect(editor2Copy.getGrammar().name).toBe('Null Grammar')
-
- await atom.packages.activatePackage('language-javascript')
- expect(editorCopy.getGrammar().name).toBe('C')
- expect(editor2Copy.getGrammar().name).toBe('JavaScript')
- })
- })
})
function getSubscriptionCount (editor) {
diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js
index 198cf1c43..67cf368cc 100644
--- a/spec/text-editor-spec.js
+++ b/spec/text-editor-spec.js
@@ -7,6 +7,7 @@ const dedent = require('dedent')
const clipboard = require('../src/safe-clipboard')
const TextEditor = require('../src/text-editor')
const TextBuffer = require('text-buffer')
+const TokenizedBuffer = require('../src/tokenized-buffer')
describe('TextEditor', () => {
let buffer, editor, lineLengths
@@ -85,22 +86,6 @@ describe('TextEditor', () => {
})
})
- describe('when the editor is constructed with the largeFileMode option set to true', () => {
- it("loads the editor but doesn't tokenize", async () => {
- editor = await atom.workspace.openTextFile('sample.js', {largeFileMode: true})
- buffer = editor.getBuffer()
- expect(editor.lineTextForScreenRow(0)).toBe(buffer.lineForRow(0))
- expect(editor.tokensForScreenRow(0).length).toBe(1)
- expect(editor.tokensForScreenRow(1).length).toBe(2) // soft tab
- expect(editor.lineTextForScreenRow(12)).toBe(buffer.lineForRow(12))
- expect(editor.getCursorScreenPosition()).toEqual([0, 0])
-
- editor.insertText('hey"')
- expect(editor.tokensForScreenRow(0).length).toBe(1)
- expect(editor.tokensForScreenRow(1).length).toBe(2)
- })
- })
-
describe('.copy()', () => {
it('returns a different editor with the same initial state', () => {
expect(editor.getAutoHeight()).toBeFalsy()
@@ -1356,7 +1341,7 @@ describe('TextEditor', () => {
})
it('will limit paragraph range to comments', () => {
- editor.setGrammar(atom.grammars.grammarForScopeName('source.js'))
+ atom.grammars.assignLanguageMode(editor.getBuffer(), 'source.js')
editor.setText(dedent`
var quicksort = function () {
/* Single line comment block */
@@ -2081,14 +2066,13 @@ describe('TextEditor', () => {
expect(scopeDescriptors[0].getScopesArray()).toEqual(['source.js'])
expect(scopeDescriptors[1].getScopesArray()).toEqual(['source.js', 'string.quoted.single.js'])
- editor.setScopedSettingsDelegate({
- getNonWordCharacters (scopes) {
- const result = '/\()"\':,.;<>~!@#$%^&*|+=[]{}`?'
- if (scopes.some(scope => scope.startsWith('string'))) {
- return result
- } else {
- return result + '-'
- }
+ spyOn(editor.getBuffer().getLanguageMode(), 'getNonWordCharacters').andCallFake(function (position) {
+ const result = '/\()"\':,.;<>~!@#$%^&*|+=[]{}`?'
+ const scopes = this.scopeDescriptorForPosition(position).getScopesArray()
+ if (scopes.some(scope => scope.startsWith('string'))) {
+ return result
+ } else {
+ return result + '-'
}
})
@@ -3694,7 +3678,7 @@ describe('TextEditor', () => {
describe('when a newline is appended with a trailing closing tag behind the cursor (e.g. by pressing enter in the middel of a line)', () => {
it('indents the new line to the correct level when editor.autoIndent is true and using a curly-bracket language', () => {
editor.update({autoIndent: true})
- editor.setGrammar(atom.grammars.selectGrammar('file.js'))
+ atom.grammars.assignLanguageMode(editor, 'source.js')
editor.setText('var test = () => {\n return true;};')
editor.setCursorBufferPosition([1, 14])
editor.insertNewline()
@@ -3703,7 +3687,7 @@ describe('TextEditor', () => {
})
it('indents the new line to the current level when editor.autoIndent is true and no increaseIndentPattern is specified', () => {
- editor.setGrammar(atom.grammars.selectGrammar('file'))
+ atom.grammars.assignLanguageMode(editor, null)
editor.update({autoIndent: true})
editor.setText(' if true')
editor.setCursorBufferPosition([0, 8])
@@ -3716,7 +3700,7 @@ describe('TextEditor', () => {
it('indents the new line to the correct level when editor.autoIndent is true and using an off-side rule language', async () => {
await atom.packages.activatePackage('language-coffee-script')
editor.update({autoIndent: true})
- editor.setGrammar(atom.grammars.selectGrammar('file.coffee'))
+ atom.grammars.assignLanguageMode(editor, 'source.coffee')
editor.setText('if true\n return trueelse\n return false')
editor.setCursorBufferPosition([1, 13])
editor.insertNewline()
@@ -3730,7 +3714,7 @@ describe('TextEditor', () => {
it('indents the new line to the correct level when editor.autoIndent is true', async () => {
await atom.packages.activatePackage('language-go')
editor.update({autoIndent: true})
- editor.setGrammar(atom.grammars.selectGrammar('file.go'))
+ atom.grammars.assignLanguageMode(editor, 'source.go')
editor.setText('fmt.Printf("some%s",\n "thing")')
editor.setCursorBufferPosition([1, 10])
editor.insertNewline()
@@ -5622,21 +5606,30 @@ describe('TextEditor', () => {
})
})
- describe('when a better-matched grammar is added to syntax', () => {
- it('switches to the better-matched grammar and re-tokenizes the buffer', async () => {
- editor.destroy()
+ describe('when the buffer\'s language mode changes', () => {
+ it('notifies onDidTokenize observers when retokenization is finished', async () => {
+ // Exercise the full `tokenizeInBackground` code path, which bails out early if
+ // `.setVisible` has not been called with `true`.
+ jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground')
+ jasmine.attachToDOM(editor.getElement())
- const jsGrammar = atom.grammars.selectGrammar('a.js')
- atom.grammars.removeGrammar(jsGrammar)
+ const events = []
+ editor.onDidTokenize(event => events.push(event))
- editor = await atom.workspace.open('sample.js', {autoIndent: false})
+ await atom.packages.activatePackage('language-c')
+ expect(atom.grammars.assignLanguageMode(editor.getBuffer(), 'source.c')).toBe(true)
+ advanceClock(1)
+ expect(events.length).toBe(1)
+ })
- expect(editor.getGrammar()).toBe(atom.grammars.nullGrammar)
- expect(editor.tokensForScreenRow(0).length).toBe(1)
+ it('notifies onDidChangeGrammar observers', async () => {
+ const events = []
+ editor.onDidChangeGrammar(grammar => events.push(grammar))
- atom.grammars.addGrammar(jsGrammar)
- expect(editor.getGrammar()).toBe(jsGrammar)
- expect(editor.tokensForScreenRow(0).length).toBeGreaterThan(1)
+ await atom.packages.activatePackage('language-c')
+ expect(atom.grammars.assignLanguageMode(editor.getBuffer(), 'source.c')).toBe(true)
+ expect(events.length).toBe(1)
+ expect(events[0].name).toBe('C')
})
})
@@ -6630,17 +6623,6 @@ describe('TextEditor', () => {
})
})
- describe('when the editor is constructed with the grammar option set', () => {
- beforeEach(async () => {
- await atom.packages.activatePackage('language-coffee-script')
- })
-
- it('sets the grammar', () => {
- editor = new TextEditor({grammar: atom.grammars.grammarForScopeName('source.coffee')})
- expect(editor.getGrammar().name).toBe('CoffeeScript')
- })
- })
-
describe('softWrapAtPreferredLineLength', () => {
it('soft wraps the editor at the preferred line length unless the editor is narrower or the editor is mini', () => {
editor.update({
@@ -6701,6 +6683,7 @@ describe('TextEditor', () => {
beforeEach(async () => {
editor = await atom.workspace.open('sample.js')
jasmine.unspy(editor, 'shouldPromptToSave')
+ spyOn(atom.stateStore, 'isConnected').andReturn(true)
})
it('returns true when buffer has unsaved changes', () => {
@@ -6828,7 +6811,7 @@ describe('TextEditor', () => {
})
it('does nothing for empty lines and null grammar', () => {
- editor.setGrammar(atom.grammars.grammarForScopeName('text.plain.null-grammar'))
+ atom.grammars.assignLanguageMode(editor, null)
editor.setCursorBufferPosition([10, 0])
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(10)).toBe('')
diff --git a/spec/token-iterator-spec.js b/spec/token-iterator-spec.js
index f6d43395c..19e8431f3 100644
--- a/spec/token-iterator-spec.js
+++ b/spec/token-iterator-spec.js
@@ -26,12 +26,12 @@ x\
`})
const tokenizedBuffer = new TokenizedBuffer({
buffer,
+ grammar,
config: atom.config,
grammarRegistry: atom.grammars,
packageManager: atom.packages,
assert: atom.assert
})
- tokenizedBuffer.setGrammar(grammar)
const tokenIterator = tokenizedBuffer.tokenizedLines[1].getTokenIterator()
tokenIterator.next()
diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js
index b1574673a..54601ba2d 100644
--- a/spec/tokenized-buffer-spec.js
+++ b/spec/tokenized-buffer-spec.js
@@ -8,9 +8,10 @@ const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-help
const {ScopedSettingsDelegate} = require('../src/text-editor-registry')
describe('TokenizedBuffer', () => {
- let tokenizedBuffer, buffer
+ let tokenizedBuffer, buffer, config
beforeEach(async () => {
+ config = atom.config
// enable async tokenization
TokenizedBuffer.prototype.chunkSize = 5
jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground')
@@ -22,39 +23,38 @@ describe('TokenizedBuffer', () => {
tokenizedBuffer && tokenizedBuffer.destroy()
})
- function startTokenizing (tokenizedBuffer) {
- tokenizedBuffer.setVisible(true)
- }
-
function fullyTokenize (tokenizedBuffer) {
- tokenizedBuffer.setVisible(true)
+ tokenizedBuffer.startTokenizing()
while (tokenizedBuffer.firstInvalidRow() != null) {
advanceClock()
}
}
- describe('serialization', () => {
- describe('when the underlying buffer has a path', () => {
- beforeEach(async () => {
- buffer = atom.project.bufferForPathSync('sample.js')
- await atom.packages.activatePackage('language-coffee-script')
+ describe('when the editor is constructed with the largeFileMode option set to true', () => {
+ it("loads the editor but doesn't tokenize", async () => {
+ const line = 'a b c d\n'
+ buffer = new TextBuffer(line.repeat(256 * 1024))
+ expect(buffer.getText().length).toBe(2 * 1024 * 1024)
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer,
+ grammar: atom.grammars.grammarForScopeName('source.js'),
+ tabLength: 2
})
+ buffer.setLanguageMode(tokenizedBuffer)
- it('deserializes it searching among the buffers in the current project', () => {
- const tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2})
- const tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom)
- expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer)
- })
- })
+ expect(tokenizedBuffer.isRowCommented(0)).toBeFalsy()
- describe('when the underlying buffer has no path', () => {
- beforeEach(() => buffer = atom.project.bufferForPathSync(null))
+ // It treats the entire line as one big token
+ let iterator = tokenizedBuffer.buildHighlightIterator()
+ iterator.seek({row: 0, column: 0})
+ iterator.moveToSuccessor()
+ expect(iterator.getPosition()).toEqual({row: 0, column: 7})
- it('deserializes it searching among the buffers in the current project', () => {
- const tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2})
- const tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom)
- expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer)
- })
+ buffer.insert([0, 0], 'hey"')
+ iterator = tokenizedBuffer.buildHighlightIterator()
+ iterator.seek({row: 0, column: 0})
+ iterator.moveToSuccessor()
+ expect(iterator.getPosition()).toEqual({row: 0, column: 11})
})
})
@@ -62,8 +62,8 @@ describe('TokenizedBuffer', () => {
describe('when the buffer is destroyed', () => {
beforeEach(() => {
buffer = atom.project.bufferForPathSync('sample.js')
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
- startTokenizing(tokenizedBuffer)
+ tokenizedBuffer = new TokenizedBuffer({buffer, config, config, grammar: atom.grammars.grammarForScopeName('source.js')})
+ tokenizedBuffer.startTokenizing()
})
it('stops tokenization', () => {
@@ -77,8 +77,9 @@ describe('TokenizedBuffer', () => {
describe('when the buffer contains soft-tabs', () => {
beforeEach(() => {
buffer = atom.project.bufferForPathSync('sample.js')
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
- startTokenizing(tokenizedBuffer)
+ tokenizedBuffer = new TokenizedBuffer({buffer, config, grammar: atom.grammars.grammarForScopeName('source.js')})
+ buffer.setLanguageMode(tokenizedBuffer)
+ tokenizedBuffer.startTokenizing()
})
afterEach(() => {
@@ -271,7 +272,7 @@ describe('TokenizedBuffer', () => {
})
})
- describe('when there is an insertion that is larger than the chunk size', () =>
+ describe('when there is an insertion that is larger than the chunk size', () => {
it('tokenizes the initial chunk synchronously, then tokenizes the remaining lines in the background', () => {
const commentBlock = _.multiplyString('// a comment\n', tokenizedBuffer.chunkSize + 2)
buffer.insert([0, 0], commentBlock)
@@ -283,22 +284,6 @@ describe('TokenizedBuffer', () => {
expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy()
expect(tokenizedBuffer.tokenizedLines[6].ruleStack != null).toBeTruthy()
})
- )
-
- it('does not break out soft tabs across a scope boundary', async () => {
- await atom.packages.activatePackage('language-gfm')
-
- tokenizedBuffer.setTabLength(4)
- tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('.md'))
- buffer.setText(' 0) length += tag
- }
-
- expect(length).toBe(4)
})
})
})
@@ -308,8 +293,8 @@ describe('TokenizedBuffer', () => {
atom.packages.activatePackage('language-coffee-script')
buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee')
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2})
- startTokenizing(tokenizedBuffer)
+ tokenizedBuffer = new TokenizedBuffer({buffer, config, grammar: atom.grammars.grammarForScopeName('source.coffee')})
+ tokenizedBuffer.startTokenizing()
})
afterEach(() => {
@@ -328,7 +313,7 @@ describe('TokenizedBuffer', () => {
const tokenizedHandler = jasmine.createSpy('tokenized handler')
editor.tokenizedBuffer.onDidTokenize(tokenizedHandler)
- fullyTokenize(editor.tokenizedBuffer)
+ fullyTokenize(editor.getBuffer().getLanguageMode())
expect(tokenizedHandler.callCount).toBe(1)
})
@@ -346,16 +331,16 @@ describe('TokenizedBuffer', () => {
describe('when the grammar is updated because a grammar it includes is activated', async () => {
it('re-emits the `tokenized` event', async () => {
- const editor = await atom.workspace.open('coffee.coffee')
+ let tokenizationCount = 0
- const tokenizedHandler = jasmine.createSpy('tokenized handler')
- editor.tokenizedBuffer.onDidTokenize(tokenizedHandler)
- fullyTokenize(editor.tokenizedBuffer)
- tokenizedHandler.reset()
+ const editor = await atom.workspace.open('coffee.coffee')
+ editor.onDidTokenize(() => { tokenizationCount++ })
+ fullyTokenize(editor.getBuffer().getLanguageMode())
+ tokenizationCount = 0
await atom.packages.activatePackage('language-coffee-script')
- fullyTokenize(editor.tokenizedBuffer)
- expect(tokenizedHandler.callCount).toBe(1)
+ fullyTokenize(editor.getBuffer().getLanguageMode())
+ expect(tokenizationCount).toBe(1)
})
it('retokenizes the buffer', async () => {
@@ -365,7 +350,7 @@ describe('TokenizedBuffer', () => {
buffer = atom.project.bufferForPathSync()
buffer.setText("
<%= User.find(2).full_name %>
")
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2})
+ tokenizedBuffer = new TokenizedBuffer({buffer, config, grammar: atom.grammars.selectGrammar('test.erb')})
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({
value: "",
@@ -386,7 +371,7 @@ describe('TokenizedBuffer', () => {
spyOn(NullGrammar, 'tokenizeLine').andCallThrough()
buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar')
buffer.setText('a\nb\nc')
- tokenizedBuffer = new TokenizedBuffer({buffer, tabLength: 2})
+ tokenizedBuffer = new TokenizedBuffer({buffer, config})
const tokenizeCallback = jasmine.createSpy('onDidTokenize')
tokenizedBuffer.onDidTokenize(tokenizeCallback)
@@ -414,7 +399,7 @@ describe('TokenizedBuffer', () => {
it('returns the correct token (regression)', () => {
buffer = atom.project.bufferForPathSync('sample.js')
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
+ tokenizedBuffer = new TokenizedBuffer({buffer, config, grammar: atom.grammars.grammarForScopeName('source.js')})
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenForPosition([1, 0]).scopes).toEqual(['source.js'])
expect(tokenizedBuffer.tokenForPosition([1, 1]).scopes).toEqual(['source.js'])
@@ -425,7 +410,7 @@ describe('TokenizedBuffer', () => {
describe('.bufferRangeForScopeAtPosition(selector, position)', () => {
beforeEach(() => {
buffer = atom.project.bufferForPathSync('sample.js')
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
+ tokenizedBuffer = new TokenizedBuffer({buffer, config, grammar: atom.grammars.grammarForScopeName('source.js')})
fullyTokenize(tokenizedBuffer)
})
@@ -451,12 +436,12 @@ describe('TokenizedBuffer', () => {
it("returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", () => {
buffer = atom.project.bufferForPathSync('sample.js')
const grammar = atom.grammars.grammarForScopeName('source.js')
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
+ tokenizedBuffer = new TokenizedBuffer({buffer, config, grammar})
const line0 = buffer.lineForRow(0)
const jsScopeStartId = grammar.startIdForScope(grammar.scopeName)
const jsScopeEndId = grammar.endIdForScope(grammar.scopeName)
- startTokenizing(tokenizedBuffer)
+ tokenizedBuffer.startTokenizing()
expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([jsScopeStartId, line0.length, jsScopeEndId])
@@ -464,23 +449,12 @@ describe('TokenizedBuffer', () => {
expect(tokenizedBuffer.tokenizedLines[0]).not.toBeUndefined()
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).not.toEqual([jsScopeStartId, line0.length, jsScopeEndId])
-
- const nullScopeStartId = NullGrammar.startIdForScope(NullGrammar.scopeName)
- const nullScopeEndId = NullGrammar.endIdForScope(NullGrammar.scopeName)
- tokenizedBuffer.setGrammar(NullGrammar)
- startTokenizing(tokenizedBuffer)
- expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
- expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
- expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId])
- advanceClock(1)
- expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
- expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId])
})
it('returns undefined if the requested row is outside the buffer range', () => {
buffer = atom.project.bufferForPathSync('sample.js')
const grammar = atom.grammars.grammarForScopeName('source.js')
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
+ tokenizedBuffer = new TokenizedBuffer({buffer, config, grammar})
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLineForRow(999)).toBeUndefined()
})
@@ -490,10 +464,10 @@ describe('TokenizedBuffer', () => {
describe('iterator', () => {
it('iterates over the syntactic scope boundaries', () => {
buffer = new TextBuffer({text: 'var foo = 1 /*\nhello*/var bar = 2\n'})
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
+ tokenizedBuffer = new TokenizedBuffer({buffer, config, grammar: atom.grammars.grammarForScopeName('source.js')})
fullyTokenize(tokenizedBuffer)
- const iterator = tokenizedBuffer.buildIterator()
+ const iterator = tokenizedBuffer.buildHighlightIterator()
iterator.seek(Point(0, 0))
const expectedBoundaries = [
@@ -555,10 +529,10 @@ describe('TokenizedBuffer', () => {
await atom.packages.activatePackage('language-coffee-script')
buffer = new TextBuffer({text: '# hello\n# world'})
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2})
+ tokenizedBuffer = new TokenizedBuffer({buffer, config, grammar: atom.grammars.grammarForScopeName('source.coffee')})
fullyTokenize(tokenizedBuffer)
- const iterator = tokenizedBuffer.buildIterator()
+ const iterator = tokenizedBuffer.buildHighlightIterator()
iterator.seek(Point(0, 0))
iterator.moveToSuccessor()
iterator.moveToSuccessor()
@@ -585,10 +559,10 @@ describe('TokenizedBuffer', () => {
})
buffer = new TextBuffer({text: 'start x\nend x\nx'})
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
+ tokenizedBuffer = new TokenizedBuffer({buffer, config, grammar})
fullyTokenize(tokenizedBuffer)
- const iterator = tokenizedBuffer.buildIterator()
+ const iterator = tokenizedBuffer.buildHighlightIterator()
iterator.seek(Point(1, 0))
expect(iterator.getPosition()).toEqual([1, 0])
@@ -648,7 +622,8 @@ describe('TokenizedBuffer', () => {
buffer = atom.project.bufferForPathSync('sample.js')
buffer.insert([10, 0], ' // multi-line\n // comment\n // block\n')
buffer.insert([0, 0], '// multi-line\n// comment\n// block\n')
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
+ tokenizedBuffer = new TokenizedBuffer({buffer, config, grammar: atom.grammars.grammarForScopeName('source.js')})
+ buffer.setLanguageMode(tokenizedBuffer)
fullyTokenize(tokenizedBuffer)
})
@@ -735,7 +710,7 @@ describe('TokenizedBuffer', () => {
}
`)
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({buffer, config})
expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(0, 2))).toBe(dedent `
if (a) {⋯
@@ -797,7 +772,7 @@ describe('TokenizedBuffer', () => {
}
`)
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({buffer, config})
expect(tokenizedBuffer.getFoldableRanges(2).map(r => r.toString())).toEqual([
...tokenizedBuffer.getFoldableRangesAtIndentLevel(0, 2),
@@ -827,7 +802,7 @@ describe('TokenizedBuffer', () => {
}
`)
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({buffer, config})
expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, 5), 2)).toBeNull()
@@ -872,10 +847,10 @@ describe('TokenizedBuffer', () => {
buffer = editor.buffer
tokenizedBuffer = editor.tokenizedBuffer
- expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [20, Infinity]])
- expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [17, Infinity]])
- expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [17, Infinity]])
- expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(19, Infinity))).toEqual([[19, Infinity], [20, Infinity]])
+ expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity), 2)).toEqual([[0, Infinity], [20, Infinity]])
+ expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity), 2)).toEqual([[1, Infinity], [17, Infinity]])
+ expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity), 2)).toEqual([[1, Infinity], [17, Infinity]])
+ expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(19, Infinity), 2)).toEqual([[19, Infinity], [20, Infinity]])
})
it('works for javascript', async () => {
@@ -884,10 +859,10 @@ describe('TokenizedBuffer', () => {
buffer = editor.buffer
tokenizedBuffer = editor.tokenizedBuffer
- expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [12, Infinity]])
- expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [9, Infinity]])
- expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [9, Infinity]])
- expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(4, Infinity))).toEqual([[4, Infinity], [7, Infinity]])
+ expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity), 2)).toEqual([[0, Infinity], [12, Infinity]])
+ expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity), 2)).toEqual([[1, Infinity], [9, Infinity]])
+ expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity), 2)).toEqual([[1, Infinity], [9, Infinity]])
+ expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(4, Infinity), 2)).toEqual([[4, Infinity], [7, Infinity]])
})
})
diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js
index 1bde0e6fe..83a4429b6 100644
--- a/spec/workspace-spec.js
+++ b/spec/workspace-spec.js
@@ -43,7 +43,8 @@ describe('Workspace', () => {
notificationManager: atom.notifications,
packageManager: atom.packages,
confirm: atom.confirm.bind(atom),
- applicationDelegate: atom.applicationDelegate
+ applicationDelegate: atom.applicationDelegate,
+ grammarRegistry: atom.grammars
})
return atom.project.deserialize(projectState).then(() => {
workspace = atom.workspace = new Workspace({
@@ -656,17 +657,6 @@ describe('Workspace', () => {
})
})
- describe('when the file is over 2MB', () => {
- it('opens the editor with largeFileMode: true', () => {
- spyOn(fs, 'getSizeSync').andReturn(2 * 1048577) // 2MB
-
- let editor = null
- waitsForPromise(() => workspace.open('sample.js').then(e => { editor = e }))
-
- runs(() => expect(editor.largeFileMode).toBe(true))
- })
- })
-
describe('when the file is over user-defined limit', () => {
const shouldPromptForFileOfSize = (size, shouldPrompt) => {
spyOn(fs, 'getSizeSync').andReturn(size * 1048577)
@@ -689,7 +679,6 @@ describe('Workspace', () => {
runs(() => {
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
- expect(editor.largeFileMode).toBe(true)
})
} else {
runs(() => expect(editor).not.toBeUndefined())
@@ -1237,29 +1226,22 @@ describe('Workspace', () => {
})
describe('the grammar-used hook', () => {
- it('fires when opening a file or changing the grammar of an open file', () => {
- let editor = null
- let javascriptGrammarUsed = false
- let coffeescriptGrammarUsed = false
+ 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 })
atom.packages.triggerDeferredActivationHooks()
+ atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', resolveJavascriptGrammarUsed)
+ atom.packages.onDidTriggerActivationHook('language-coffee-script:grammar-used', resolveCoffeeScriptGrammarUsed)
- runs(() => {
- atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', () => { javascriptGrammarUsed = true })
- atom.packages.onDidTriggerActivationHook('language-coffee-script:grammar-used', () => { coffeescriptGrammarUsed = true })
- })
+ const editor = await atom.workspace.open('sample.js', {autoIndent: false})
+ await atom.packages.activatePackage('language-javascript')
+ await javascriptGrammarUsed
- waitsForPromise(() => atom.workspace.open('sample.js', {autoIndent: false}).then(o => { editor = o }))
-
- waitsForPromise(() => atom.packages.activatePackage('language-javascript'))
-
- waitsFor(() => javascriptGrammarUsed)
-
- waitsForPromise(() => atom.packages.activatePackage('language-coffee-script'))
-
- runs(() => editor.setGrammar(atom.grammars.selectGrammar('.coffee')))
-
- waitsFor(() => coffeescriptGrammarUsed)
+ await atom.packages.activatePackage('language-coffee-script')
+ atom.grammars.assignLanguageMode(editor, 'source.coffee')
+ await coffeescriptGrammarUsed
})
})
@@ -1521,34 +1503,27 @@ describe('Workspace', () => {
})
describe('when an editor is destroyed', () => {
- it('removes the editor', () => {
- let editor = null
-
- waitsForPromise(() => workspace.open('a').then(e => { editor = e }))
-
- runs(() => {
- expect(workspace.getTextEditors()).toHaveLength(1)
- editor.destroy()
- expect(workspace.getTextEditors()).toHaveLength(0)
- })
+ it('removes the editor', async () => {
+ const editor = await workspace.open('a')
+ expect(workspace.getTextEditors()).toHaveLength(1)
+ editor.destroy()
+ expect(workspace.getTextEditors()).toHaveLength(0)
})
})
describe('when an editor is copied because its pane is split', () => {
- it('sets up the new editor to be configured by the text editor registry', () => {
- waitsForPromise(() => atom.packages.activatePackage('language-javascript'))
+ it('sets up the new editor to be configured by the text editor registry', async () => {
+ await atom.packages.activatePackage('language-javascript')
- waitsForPromise(() =>
- workspace.open('a').then(editor => {
- atom.textEditors.setGrammarOverride(editor, 'source.js')
- expect(editor.getGrammar().name).toBe('JavaScript')
+ const editor = await workspace.open('a')
- workspace.getActivePane().splitRight({copyActiveItem: true})
- const newEditor = workspace.getActiveTextEditor()
- expect(newEditor).not.toBe(editor)
- expect(newEditor.getGrammar().name).toBe('JavaScript')
- })
- )
+ atom.grammars.assignLanguageMode(editor, 'source.js')
+ expect(editor.getGrammar().name).toBe('JavaScript')
+
+ workspace.getActivePane().splitRight({copyActiveItem: true})
+ const newEditor = workspace.getActiveTextEditor()
+ expect(newEditor).not.toBe(editor)
+ expect(newEditor.getGrammar().name).toBe('JavaScript')
})
})
@@ -2789,7 +2764,7 @@ i = /test/; #FIXME\
})
describe('grammar activation', () => {
- it('notifies the workspace of which grammar is used', () => {
+ it('notifies the workspace of which grammar is used', async () => {
atom.packages.triggerDeferredActivationHooks()
const javascriptGrammarUsed = jasmine.createSpy('js grammar used')
@@ -2800,24 +2775,22 @@ i = /test/; #FIXME\
atom.packages.onDidTriggerActivationHook('language-ruby:grammar-used', rubyGrammarUsed)
atom.packages.onDidTriggerActivationHook('language-c:grammar-used', cGrammarUsed)
- waitsForPromise(() => atom.packages.activatePackage('language-ruby'))
- waitsForPromise(() => atom.packages.activatePackage('language-javascript'))
- waitsForPromise(() => atom.packages.activatePackage('language-c'))
- waitsForPromise(() => atom.workspace.open('sample-with-comments.js'))
+ await atom.packages.activatePackage('language-ruby')
+ await atom.packages.activatePackage('language-javascript')
+ await atom.packages.activatePackage('language-c')
+ await atom.workspace.open('sample-with-comments.js')
- runs(() => {
- // Hooks are triggered when opening new editors
- expect(javascriptGrammarUsed).toHaveBeenCalled()
+ // Hooks are triggered when opening new editors
+ expect(javascriptGrammarUsed).toHaveBeenCalled()
- // Hooks are triggered when changing existing editors grammars
- atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.c'))
- expect(cGrammarUsed).toHaveBeenCalled()
+ // Hooks are triggered when changing existing editors grammars
+ atom.grammars.assignLanguageMode(atom.workspace.getActiveTextEditor(), 'source.c')
+ expect(cGrammarUsed).toHaveBeenCalled()
- // Hooks are triggered when editors are added in other ways.
- atom.workspace.getActivePane().splitRight({copyActiveItem: true})
- atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.ruby'))
- expect(rubyGrammarUsed).toHaveBeenCalled()
- })
+ // Hooks are triggered when editors are added in other ways.
+ atom.workspace.getActivePane().splitRight({copyActiveItem: true})
+ atom.grammars.assignLanguageMode(atom.workspace.getActiveTextEditor(), 'source.ruby')
+ expect(rubyGrammarUsed).toHaveBeenCalled()
})
})
diff --git a/src/atom-environment.js b/src/atom-environment.js
index 663bb6c00..1671ea7c7 100644
--- a/src/atom-environment.js
+++ b/src/atom-environment.js
@@ -71,7 +71,6 @@ class AtomEnvironment {
this.deserializers = new DeserializerManager(this)
this.deserializeTimings = {}
this.views = new ViewRegistry(this)
- TextEditor.setScheduler(this.views)
this.notifications = new NotificationManager()
this.stateStore = new StateStore('AtomEnvironments', 1)
@@ -112,7 +111,13 @@ class AtomEnvironment {
this.packages.setContextMenuManager(this.contextMenu)
this.packages.setThemeManager(this.themes)
- this.project = new Project({notificationManager: this.notifications, packageManager: this.packages, config: this.config, applicationDelegate: this.applicationDelegate})
+ this.project = new Project({
+ notificationManager: this.notifications,
+ packageManager: this.packages,
+ grammarRegistry: this.grammars,
+ config: this.config,
+ applicationDelegate: this.applicationDelegate
+ })
this.commandInstaller = new CommandInstaller(this.applicationDelegate)
this.protocolHandlerInstaller = new ProtocolHandlerInstaller()
@@ -815,10 +820,9 @@ class AtomEnvironment {
project: this.project.serialize(options),
workspace: this.workspace.serialize(),
packageStates: this.packages.serialize(),
- grammars: {grammarOverridesByPath: this.grammars.grammarOverridesByPath},
+ grammars: this.grammars.serialize(),
fullScreen: this.isFullScreen(),
- windowDimensions: this.windowDimensions,
- textEditors: this.textEditors.serialize()
+ windowDimensions: this.windowDimensions
}
}
@@ -1112,11 +1116,6 @@ class AtomEnvironment {
async deserialize (state) {
if (!state) return Promise.resolve()
- const grammarOverridesByPath = state.grammars && state.grammars.grammarOverridesByPath
- if (grammarOverridesByPath) {
- this.grammars.grammarOverridesByPath = grammarOverridesByPath
- }
-
this.setFullScreen(state.fullScreen)
const missingProjectPaths = []
@@ -1141,7 +1140,7 @@ class AtomEnvironment {
this.deserializeTimings.project = Date.now() - startTime
- if (state.textEditors) this.textEditors.deserialize(state.textEditors)
+ if (state.grammars) this.grammars.deserialize(state.grammars)
startTime = Date.now()
if (state.workspace) this.workspace.deserialize(state.workspace, this.deserializers)
diff --git a/src/cursor.js b/src/cursor.js
index 181eeb971..41e47bb75 100644
--- a/src/cursor.js
+++ b/src/cursor.js
@@ -705,7 +705,7 @@ class Cursor extends Model {
*/
getNonWordCharacters () {
- return this.editor.getNonWordCharacters(this.getScopeDescriptor().getScopesArray())
+ return this.editor.getNonWordCharacters(this.getBufferPosition())
}
changePosition (options, fn) {
diff --git a/src/grammar-registry.js b/src/grammar-registry.js
index f2994acf1..87a3701f9 100644
--- a/src/grammar-registry.js
+++ b/src/grammar-registry.js
@@ -1,28 +1,153 @@
const _ = require('underscore-plus')
+const Grim = require('grim')
const FirstMate = require('first-mate')
+const {Disposable, CompositeDisposable} = require('event-kit')
+const TokenizedBuffer = require('./tokenized-buffer')
const Token = require('./token')
const fs = require('fs-plus')
-const Grim = require('grim')
+const {Point, Range} = require('text-buffer')
-const PathSplitRegex = new RegExp('[/.]')
+const GRAMMAR_SELECTION_RANGE = Range(Point.ZERO, Point(10, 0)).freeze()
+const PATH_SPLIT_REGEX = new RegExp('[/.]')
-// Extended: Syntax class holding the grammars used for tokenizing.
+// Extended: This class holds the grammars used for tokenizing.
//
// An instance of this class is always available as the `atom.grammars` global.
-//
-// The Syntax class also contains properties for things such as the
-// language-specific comment regexes. See {::getProperty} for more details.
module.exports =
class GrammarRegistry extends FirstMate.GrammarRegistry {
constructor ({config} = {}) {
super({maxTokensPerLine: 100, maxLineLength: 1000})
this.config = config
+ this.subscriptions = new CompositeDisposable()
+ }
+
+ clear () {
+ super.clear()
+ if (this.subscriptions) this.subscriptions.dispose()
+ this.subscriptions = new CompositeDisposable()
+ this.languageOverridesByBufferId = new Map()
+ this.grammarScoresByBuffer = new Map()
+
+ const grammarAddedOrUpdated = this.grammarAddedOrUpdated.bind(this)
+ this.onDidAddGrammar(grammarAddedOrUpdated)
+ this.onDidUpdateGrammar(grammarAddedOrUpdated)
+ }
+
+ serialize () {
+ const languageOverridesByBufferId = {}
+ this.languageOverridesByBufferId.forEach((languageId, bufferId) => {
+ languageOverridesByBufferId[bufferId] = languageId
+ })
+ return {languageOverridesByBufferId}
+ }
+
+ deserialize (params) {
+ for (const bufferId in params.languageOverridesByBufferId || {}) {
+ this.languageOverridesByBufferId.set(
+ bufferId,
+ params.languageOverridesByBufferId[bufferId]
+ )
+ }
}
createToken (value, scopes) {
return new Token({value, scopes})
}
+ // Extended: set a {TextBuffer}'s language mode based on its path and content,
+ // and continue to update its language mode as grammars are added or updated, or
+ // the buffer's file path changes.
+ //
+ // * `buffer` The {TextBuffer} whose language mode will be maintained.
+ //
+ // Returns a {Disposable} that can be used to stop updating the buffer's
+ // language mode.
+ maintainLanguageMode (buffer) {
+ this.grammarScoresByBuffer.set(buffer, null)
+
+ const languageOverride = this.languageOverridesByBufferId.get(buffer.id)
+ if (languageOverride) {
+ this.assignLanguageMode(buffer, languageOverride)
+ } else {
+ this.autoAssignLanguageMode(buffer)
+ }
+
+ const pathChangeSubscription = buffer.onDidChangePath(() => {
+ this.grammarScoresByBuffer.delete(buffer)
+ if (!this.languageOverridesByBufferId.has(buffer.id)) {
+ this.autoAssignLanguageMode(buffer)
+ }
+ })
+
+ const destroySubscription = buffer.onDidDestroy(() => {
+ this.grammarScoresByBuffer.delete(buffer)
+ this.languageOverridesByBufferId.delete(buffer.id)
+ this.subscriptions.remove(destroySubscription)
+ this.subscriptions.remove(pathChangeSubscription)
+ })
+
+ this.subscriptions.add(pathChangeSubscription, destroySubscription)
+
+ return new Disposable(() => {
+ destroySubscription.dispose()
+ pathChangeSubscription.dispose()
+ this.subscriptions.remove(pathChangeSubscription)
+ this.subscriptions.remove(destroySubscription)
+ this.grammarScoresByBuffer.delete(buffer)
+ this.languageOverridesByBufferId.delete(buffer.id)
+ })
+ }
+
+ // Extended: Force a {TextBuffer} to use a different grammar than the
+ // one that would otherwise be selected for it.
+ //
+ // * `buffer` The {TextBuffer} whose gramamr will be set.
+ // * `languageId` The {String} id of the desired language.
+ //
+ // Returns a {Boolean} that indicates whether the language was successfully
+ // found.
+ assignLanguageMode (buffer, languageId) {
+ if (buffer.getBuffer) buffer = buffer.getBuffer()
+
+ let grammar = null
+ if (languageId != null) {
+ grammar = this.grammarForScopeName(languageId)
+ if (!grammar) return false
+ this.languageOverridesByBufferId.set(buffer.id, languageId)
+ } else {
+ this.languageOverridesByBufferId.set(buffer.id, null)
+ grammar = this.nullGrammar
+ }
+
+ this.grammarScoresByBuffer.set(buffer, null)
+ if (grammar.scopeName !== buffer.getLanguageMode().getLanguageId()) {
+ buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
+ }
+
+ return true
+ }
+
+ // Extended: Remove any language mode override that has been set for the
+ // given {TextBuffer}. This will assign to the buffer the best language
+ // mode available.
+ //
+ // * `buffer` The {TextBuffer}.
+ autoAssignLanguageMode (buffer) {
+ const result = this.selectGrammarWithScore(
+ buffer.getPath(),
+ buffer.getTextInRange(GRAMMAR_SELECTION_RANGE)
+ )
+ this.languageOverridesByBufferId.delete(buffer.id)
+ this.grammarScoresByBuffer.set(buffer, result.score)
+ if (result.grammar.scopeName !== buffer.getLanguageMode().getLanguageId()) {
+ buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(result.grammar, buffer))
+ }
+ }
+
+ languageModeForGrammarAndBuffer (grammar, buffer) {
+ return new TokenizedBuffer({grammar, buffer, config: this.config})
+ }
+
// Extended: Select a grammar for the given file path and file contents.
//
// This picks the best match by checking the file path and contents against
@@ -70,7 +195,7 @@ class GrammarRegistry extends FirstMate.GrammarRegistry {
if (!filePath) { return -1 }
if (process.platform === 'win32') { filePath = filePath.replace(/\\/g, '/') }
- const pathComponents = filePath.toLowerCase().split(PathSplitRegex)
+ const pathComponents = filePath.toLowerCase().split(PATH_SPLIT_REGEX)
let pathScore = -1
let customFileTypes
@@ -85,7 +210,7 @@ class GrammarRegistry extends FirstMate.GrammarRegistry {
for (let i = 0; i < fileTypes.length; i++) {
const fileType = fileTypes[i]
- const fileTypeComponents = fileType.toLowerCase().split(PathSplitRegex)
+ const fileTypeComponents = fileType.toLowerCase().split(PATH_SPLIT_REGEX)
const pathSuffix = pathComponents.slice(-fileTypeComponents.length)
if (_.isEqual(pathSuffix, fileTypeComponents)) {
pathScore = Math.max(pathScore, fileType.length)
@@ -126,46 +251,65 @@ class GrammarRegistry extends FirstMate.GrammarRegistry {
//
// Returns a {String} such as `"source.js"`.
grammarOverrideForPath (filePath) {
- Grim.deprecate('Use atom.textEditors.getGrammarOverride(editor) instead')
-
- const editor = getEditorForPath(filePath)
- if (editor) {
- return atom.textEditors.getGrammarOverride(editor)
- }
+ Grim.deprecate('Use buffer.getLanguageMode().getLanguageId() instead')
+ const buffer = atom.project.findBufferForPath(filePath)
+ if (buffer) return this.languageOverridesByBufferId.get(buffer.id)
}
// Deprecated: Set the grammar override for the given file path.
//
// * `filePath` A non-empty {String} file path.
- // * `scopeName` A {String} such as `"source.js"`.
+ // * `languageId` A {String} such as `"source.js"`.
//
// Returns undefined.
- setGrammarOverrideForPath (filePath, scopeName) {
- Grim.deprecate('Use atom.textEditors.setGrammarOverride(editor, scopeName) instead')
-
- const editor = getEditorForPath(filePath)
- if (editor) {
- atom.textEditors.setGrammarOverride(editor, scopeName)
+ setGrammarOverrideForPath (filePath, languageId) {
+ Grim.deprecate('Use atom.grammars.assignLanguageMode(buffer, languageId) instead')
+ const buffer = atom.project.findBufferForPath(filePath)
+ if (buffer) {
+ const grammar = this.grammarForScopeName(languageId)
+ if (grammar) this.languageOverridesByBufferId.set(buffer.id, grammar.name)
}
}
- // Deprecated: Remove the grammar override for the given file path.
+ // Remove the grammar override for the given file path.
//
// * `filePath` A {String} file path.
//
// Returns undefined.
clearGrammarOverrideForPath (filePath) {
- Grim.deprecate('Use atom.textEditors.clearGrammarOverride(editor) instead')
+ Grim.deprecate('Use atom.grammars.autoAssignLanguageMode(buffer) instead')
+ const buffer = atom.project.findBufferForPath(filePath)
+ if (buffer) this.languageOverridesByBufferId.delete(buffer.id)
+ }
- const editor = getEditorForPath(filePath)
- if (editor) {
- atom.textEditors.clearGrammarOverride(editor)
- }
- }
-}
-
-function getEditorForPath (filePath) {
- if (filePath != null) {
- return atom.workspace.getTextEditors().find(editor => editor.getPath() === filePath)
+ grammarAddedOrUpdated (grammar) {
+ 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.scopeName === buffer.getLanguageMode().getLanguageId() ||
+ grammar.scopeName === languageOverride)) {
+ buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
+ } else if (!languageOverride) {
+ const score = this.getGrammarScore(
+ grammar,
+ buffer.getPath(),
+ buffer.getTextInRange(GRAMMAR_SELECTION_RANGE)
+ )
+
+ const currentScore = this.grammarScoresByBuffer.get(buffer)
+ if (currentScore == null || score > currentScore) {
+ buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
+ this.grammarScoresByBuffer.set(buffer, score)
+ }
+ }
+ })
}
}
diff --git a/src/initialize-application-window.coffee b/src/initialize-application-window.coffee
index 0c4c0a391..f8f670cf5 100644
--- a/src/initialize-application-window.coffee
+++ b/src/initialize-application-window.coffee
@@ -67,6 +67,7 @@ global.atom = new AtomEnvironment({
enablePersistence: true
})
+TextEditor.setScheduler(global.atom.views)
global.atom.preloadPackages()
# Like sands through the hourglass, so are the days of our lives.
diff --git a/src/initialize-test-window.coffee b/src/initialize-test-window.coffee
index 5ad10670a..c6aaada0e 100644
--- a/src/initialize-test-window.coffee
+++ b/src/initialize-test-window.coffee
@@ -82,6 +82,7 @@ module.exports = ({blobStore}) ->
params.onlyLoadBaseStyleSheets = true unless params.hasOwnProperty("onlyLoadBaseStyleSheets")
atomEnvironment = new AtomEnvironment(params)
atomEnvironment.initialize(params)
+ TextEditor.setScheduler(atomEnvironment.views)
atomEnvironment
promise = testRunner({
diff --git a/src/project.js b/src/project.js
index 92a11ec7a..8de92b97e 100644
--- a/src/project.js
+++ b/src/project.js
@@ -2,7 +2,7 @@ const path = require('path')
const _ = require('underscore-plus')
const fs = require('fs-plus')
-const {Emitter, Disposable} = require('event-kit')
+const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
const TextBuffer = require('text-buffer')
const {watchPath} = require('./path-watcher')
@@ -19,10 +19,12 @@ class Project extends Model {
Section: Construction and Destruction
*/
- constructor ({notificationManager, packageManager, config, applicationDelegate}) {
+ constructor ({notificationManager, packageManager, config, applicationDelegate, grammarRegistry}) {
super()
this.notificationManager = notificationManager
this.applicationDelegate = applicationDelegate
+ this.grammarRegistry = grammarRegistry
+
this.emitter = new Emitter()
this.buffers = []
this.rootDirectories = []
@@ -35,6 +37,7 @@ class Project extends Model {
this.watcherPromisesByPath = {}
this.retiredBufferIDs = new Set()
this.retiredBufferPaths = new Set()
+ this.subscriptions = new CompositeDisposable()
this.consumeServices(packageManager)
}
@@ -54,6 +57,9 @@ class Project extends Model {
this.emitter.dispose()
this.emitter = new Emitter()
+ this.subscriptions.dispose()
+ this.subscriptions = new CompositeDisposable()
+
for (let buffer of this.buffers) {
if (buffer != null) buffer.destroy()
}
@@ -104,6 +110,7 @@ class Project extends Model {
return Promise.all(bufferPromises).then(buffers => {
this.buffers = buffers.filter(Boolean)
for (let buffer of this.buffers) {
+ this.grammarRegistry.maintainLanguageMode(buffer)
this.subscribeToBuffer(buffer)
}
this.setPaths(state.paths || [], {mustExist: true, exact: true})
@@ -654,11 +661,8 @@ class Project extends Model {
}
addBuffer (buffer, options = {}) {
- return this.addBufferAtIndex(buffer, this.buffers.length, options)
- }
-
- addBufferAtIndex (buffer, index, options = {}) {
- this.buffers.splice(index, 0, buffer)
+ this.buffers.push(buffer)
+ this.subscriptions.add(this.grammarRegistry.maintainLanguageMode(buffer))
this.subscribeToBuffer(buffer)
this.emitter.emit('did-add-buffer', buffer)
return buffer
diff --git a/src/selection.js b/src/selection.js
index a54ba68b8..99c1ea95e 100644
--- a/src/selection.js
+++ b/src/selection.js
@@ -448,9 +448,19 @@ class Selection {
if (options.autoIndent && textIsAutoIndentable && !NonWhitespaceRegExp.test(precedingText) && (remainingLines.length > 0)) {
autoIndentFirstLine = true
const firstLine = precedingText + firstInsertedLine
- desiredIndentLevel = this.editor.tokenizedBuffer.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine)
- indentAdjustment = desiredIndentLevel - this.editor.indentLevelForLine(firstLine)
- this.adjustIndent(remainingLines, indentAdjustment)
+ const languageMode = this.editor.buffer.getLanguageMode()
+ desiredIndentLevel = (
+ languageMode.suggestedIndentForLineAtBufferRow &&
+ languageMode.suggestedIndentForLineAtBufferRow(
+ oldBufferRange.start.row,
+ firstLine,
+ this.editor.getTabLength()
+ )
+ )
+ if (desiredIndentLevel != null) {
+ indentAdjustment = desiredIndentLevel - this.editor.indentLevelForLine(firstLine)
+ this.adjustIndent(remainingLines, indentAdjustment)
+ }
}
text = firstInsertedLine
diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js
index d891a5868..650d945fb 100644
--- a/src/text-editor-registry.js
+++ b/src/text-editor-registry.js
@@ -1,9 +1,6 @@
-/** @babel */
-
-import {Emitter, Disposable, CompositeDisposable} from 'event-kit'
-import {Point, Range} from 'text-buffer'
-import TextEditor from './text-editor'
-import ScopeDescriptor from './scope-descriptor'
+const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
+const TextEditor = require('./text-editor')
+const ScopeDescriptor = require('./scope-descriptor')
const EDITOR_PARAMS_BY_SETTING_KEY = [
['core.fileEncoding', 'encoding'],
@@ -23,12 +20,9 @@ const EDITOR_PARAMS_BY_SETTING_KEY = [
['editor.autoIndentOnPaste', 'autoIndentOnPaste'],
['editor.scrollPastEnd', 'scrollPastEnd'],
['editor.undoGroupingInterval', 'undoGroupingInterval'],
- ['editor.nonWordCharacters', 'nonWordCharacters'],
['editor.scrollSensitivity', 'scrollSensitivity']
]
-const GRAMMAR_SELECTION_RANGE = Range(Point.ZERO, Point(10, 0)).freeze()
-
// Experimental: This global registry tracks registered `TextEditors`.
//
// If you want to add functionality to a wider set of text editors than just
@@ -40,13 +34,11 @@ const GRAMMAR_SELECTION_RANGE = Range(Point.ZERO, Point(10, 0)).freeze()
// them for observation via `atom.textEditors.add`. **Important:** When you're
// done using your editor, be sure to call `dispose` on the returned disposable
// to avoid leaking editors.
-export default class TextEditorRegistry {
- constructor ({config, grammarRegistry, assert, packageManager}) {
+module.exports =
+class TextEditorRegistry {
+ constructor ({config, assert, packageManager}) {
this.assert = assert
this.config = config
- this.grammarRegistry = grammarRegistry
- this.scopedSettingsDelegate = new ScopedSettingsDelegate(config)
- this.grammarAddedOrUpdated = this.grammarAddedOrUpdated.bind(this)
this.clear()
this.initialPackageActivationPromise = new Promise((resolve) => {
@@ -83,10 +75,6 @@ export default class TextEditorRegistry {
this.editorsWithMaintainedGrammar = new Set()
this.editorGrammarOverrides = {}
this.editorGrammarScores = new WeakMap()
- this.subscriptions.add(
- this.grammarRegistry.onDidAddGrammar(this.grammarAddedOrUpdated),
- this.grammarRegistry.onDidUpdateGrammar(this.grammarAddedOrUpdated)
- )
}
destroy () {
@@ -114,10 +102,10 @@ export default class TextEditorRegistry {
let scope = null
if (params.buffer) {
- const filePath = params.buffer.getPath()
- const headContent = params.buffer.getTextInRange(GRAMMAR_SELECTION_RANGE)
- params.grammar = this.grammarRegistry.selectGrammar(filePath, headContent)
- scope = new ScopeDescriptor({scopes: [params.grammar.scopeName]})
+ const {grammar} = params.buffer.getLanguageMode()
+ if (grammar) {
+ scope = new ScopeDescriptor({scopes: [grammar.scopeName]})
+ }
}
Object.assign(params, this.textEditorParamsForScope(scope))
@@ -159,8 +147,6 @@ export default class TextEditorRegistry {
}
this.editorsWithMaintainedConfig.add(editor)
- editor.setScopedSettingsDelegate(this.scopedSettingsDelegate)
-
this.subscribeToSettingsForEditorScope(editor)
const grammarChangeSubscription = editor.onDidChangeGrammar(() => {
this.subscribeToSettingsForEditorScope(editor)
@@ -182,7 +168,6 @@ export default class TextEditorRegistry {
return new Disposable(() => {
this.editorsWithMaintainedConfig.delete(editor)
- editor.setScopedSettingsDelegate(null)
tokenizeSubscription.dispose()
grammarChangeSubscription.dispose()
this.subscriptions.remove(grammarChangeSubscription)
@@ -190,134 +175,43 @@ export default class TextEditorRegistry {
})
}
- // Set a {TextEditor}'s grammar based on its path and content, and continue
- // to update its grammar as grammars are added or updated, or the editor's
- // file path changes.
+ // Deprecated: set a {TextEditor}'s grammar based on its path and content,
+ // and continue to update its grammar as grammars are added or updated, or
+ // the editor's file path changes.
//
// * `editor` The editor whose grammar will be maintained.
//
// Returns a {Disposable} that can be used to stop updating the editor's
// grammar.
maintainGrammar (editor) {
- if (this.editorsWithMaintainedGrammar.has(editor)) {
- return new Disposable(noop)
- }
-
- this.editorsWithMaintainedGrammar.add(editor)
-
- const buffer = editor.getBuffer()
- for (let existingEditor of this.editorsWithMaintainedGrammar) {
- if (existingEditor.getBuffer() === buffer) {
- const existingOverride = this.editorGrammarOverrides[existingEditor.id]
- if (existingOverride) {
- this.editorGrammarOverrides[editor.id] = existingOverride
- }
- break
- }
- }
-
- this.selectGrammarForEditor(editor)
-
- const pathChangeSubscription = editor.onDidChangePath(() => {
- this.editorGrammarScores.delete(editor)
- this.selectGrammarForEditor(editor)
- })
-
- this.subscriptions.add(pathChangeSubscription)
-
- return new Disposable(() => {
- delete this.editorGrammarOverrides[editor.id]
- this.editorsWithMaintainedGrammar.delete(editor)
- this.subscriptions.remove(pathChangeSubscription)
- pathChangeSubscription.dispose()
- })
+ atom.grammars.maintainGrammar(editor.getBuffer())
}
- // Force a {TextEditor} to use a different grammar than the one that would
- // otherwise be selected for it.
+ // Deprecated: Force a {TextEditor} to use a different grammar than the
+ // one that would otherwise be selected for it.
//
// * `editor` The editor whose gramamr will be set.
- // * `scopeName` The {String} root scope name for the desired {Grammar}.
- setGrammarOverride (editor, scopeName) {
- this.editorGrammarOverrides[editor.id] = scopeName
- this.editorGrammarScores.delete(editor)
- editor.setGrammar(this.grammarRegistry.grammarForScopeName(scopeName))
+ // * `languageId` The {String} language ID for the desired {Grammar}.
+ setGrammarOverride (editor, languageId) {
+ atom.grammars.assignLanguageMode(editor.getBuffer(), languageId)
}
- // Retrieve the grammar scope name that has been set as a grammar override
- // for the given {TextEditor}.
+ // Deprecated: Retrieve the grammar scope name that has been set as a
+ // grammar override for the given {TextEditor}.
//
// * `editor` The editor.
//
// Returns a {String} scope name, or `null` if no override has been set
// for the given editor.
getGrammarOverride (editor) {
- return this.editorGrammarOverrides[editor.id]
+ return editor.getBuffer().getLanguageMode().grammar.scopeName
}
- // Remove any grammar override that has been set for the given {TextEditor}.
+ // Deprecated: Remove any grammar override that has been set for the given {TextEditor}.
//
// * `editor` The editor.
clearGrammarOverride (editor) {
- delete this.editorGrammarOverrides[editor.id]
- this.selectGrammarForEditor(editor)
- }
-
- // Private
-
- grammarAddedOrUpdated (grammar) {
- this.editorsWithMaintainedGrammar.forEach((editor) => {
- if (grammar.injectionSelector) {
- if (editor.tokenizedBuffer.hasTokenForSelector(grammar.injectionSelector)) {
- editor.tokenizedBuffer.retokenizeLines()
- }
- return
- }
-
- const grammarOverride = this.editorGrammarOverrides[editor.id]
- if (grammarOverride) {
- if (grammar.scopeName === grammarOverride) {
- editor.setGrammar(grammar)
- }
- } else {
- const score = this.grammarRegistry.getGrammarScore(
- grammar,
- editor.getPath(),
- editor.getTextInBufferRange(GRAMMAR_SELECTION_RANGE)
- )
-
- let currentScore = this.editorGrammarScores.get(editor)
- if (currentScore == null || score > currentScore) {
- editor.setGrammar(grammar)
- this.editorGrammarScores.set(editor, score)
- }
- }
- })
- }
-
- selectGrammarForEditor (editor) {
- const grammarOverride = this.editorGrammarOverrides[editor.id]
-
- if (grammarOverride) {
- const grammar = this.grammarRegistry.grammarForScopeName(grammarOverride)
- editor.setGrammar(grammar)
- return
- }
-
- const {grammar, score} = this.grammarRegistry.selectGrammarWithScore(
- editor.getPath(),
- editor.getTextInBufferRange(GRAMMAR_SELECTION_RANGE)
- )
-
- if (!grammar) {
- throw new Error(`No grammar found for path: ${editor.getPath()}`)
- }
-
- const currentScore = this.editorGrammarScores.get(editor)
- if (currentScore == null || score > currentScore) {
- editor.setGrammar(grammar)
- this.editorGrammarScores.set(editor, score)
- }
+ atom.grammars.autoAssignLanguageMode(editor.getBuffer())
}
async subscribeToSettingsForEditorScope (editor) {
@@ -390,44 +284,3 @@ function shouldEditorUseSoftTabs (editor, tabType, softTabs) {
}
function noop () {}
-
-class ScopedSettingsDelegate {
- constructor (config) {
- this.config = config
- }
-
- getNonWordCharacters (scope) {
- return this.config.get('editor.nonWordCharacters', {scope: scope})
- }
-
- getIncreaseIndentPattern (scope) {
- return this.config.get('editor.increaseIndentPattern', {scope: scope})
- }
-
- getDecreaseIndentPattern (scope) {
- return this.config.get('editor.decreaseIndentPattern', {scope: scope})
- }
-
- getDecreaseNextIndentPattern (scope) {
- return this.config.get('editor.decreaseNextIndentPattern', {scope: scope})
- }
-
- getFoldEndPattern (scope) {
- return this.config.get('editor.foldEndPattern', {scope: scope})
- }
-
- getCommentStrings (scope) {
- const commentStartEntries = this.config.getAll('editor.commentStart', {scope})
- const commentEndEntries = this.config.getAll('editor.commentEnd', {scope})
- const commentStartEntry = commentStartEntries[0]
- const commentEndEntry = commentEndEntries.find((entry) => {
- return entry.scopeSelector === commentStartEntry.scopeSelector
- })
- return {
- commentStartString: commentStartEntry && commentStartEntry.value,
- commentEndString: commentEndEntry && commentEndEntry.value
- }
- }
-}
-
-TextEditorRegistry.ScopedSettingsDelegate = ScopedSettingsDelegate
diff --git a/src/text-editor.js b/src/text-editor.js
index a0b9d19a0..c2b616ec2 100644
--- a/src/text-editor.js
+++ b/src/text-editor.js
@@ -7,9 +7,10 @@ const {CompositeDisposable, Disposable, Emitter} = require('event-kit')
const TextBuffer = require('text-buffer')
const {Point, Range} = TextBuffer
const DecorationManager = require('./decoration-manager')
-const TokenizedBuffer = require('./tokenized-buffer')
const Cursor = require('./cursor')
const Selection = require('./selection')
+const NullGrammar = require('./null-grammar')
+const TokenizedBuffer = require('./tokenized-buffer')
const TextMateScopeSelector = require('first-mate').ScopeSelector
const GutterContainer = require('./gutter-container')
@@ -22,6 +23,8 @@ const NON_WHITESPACE_REGEXP = /\S/
const ZERO_WIDTH_NBSP = '\ufeff'
let nextId = 0
+const DEFAULT_NON_WORD_CHARACTERS = "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…"
+
// Essential: This class represents all essential editing state for a single
// {TextBuffer}, including cursor and selection positions, folds, and soft wraps.
// If you're manipulating the state of an editor, use this class.
@@ -86,12 +89,13 @@ class TextEditor {
static deserialize (state, atomEnvironment) {
if (state.version !== SERIALIZATION_VERSION) return null
- try {
- const tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment)
- if (!tokenizedBuffer) return null
+ let bufferId = state.tokenizedBuffer
+ ? state.tokenizedBuffer.bufferId
+ : state.bufferId
- state.tokenizedBuffer = tokenizedBuffer
- state.tabLength = state.tokenizedBuffer.getTabLength()
+ try {
+ state.buffer = atomEnvironment.project.bufferForIdSync(bufferId)
+ if (!state.buffer) return null
} catch (error) {
if (error.syscall === 'read') {
return // Error reading the file, don't deserialize an editor for it
@@ -100,7 +104,6 @@ class TextEditor {
}
}
- state.buffer = state.tokenizedBuffer.buffer
state.assert = atomEnvironment.assert.bind(atomEnvironment)
const editor = new TextEditor(state)
if (state.registered) {
@@ -123,7 +126,6 @@ class TextEditor {
this.mini = (params.mini != null) ? params.mini : false
this.placeholderText = params.placeholderText
this.showLineNumbers = params.showLineNumbers
- this.largeFileMode = params.largeFileMode
this.assert = params.assert || (condition => condition)
this.showInvisibles = (params.showInvisibles != null) ? params.showInvisibles : true
this.autoHeight = params.autoHeight
@@ -142,7 +144,6 @@ class TextEditor {
this.autoIndent = (params.autoIndent != null) ? params.autoIndent : true
this.autoIndentOnPaste = (params.autoIndentOnPaste != null) ? params.autoIndentOnPaste : true
this.undoGroupingInterval = (params.undoGroupingInterval != null) ? params.undoGroupingInterval : 300
- this.nonWordCharacters = (params.nonWordCharacters != null) ? params.nonWordCharacters : "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…"
this.softWrapped = (params.softWrapped != null) ? params.softWrapped : false
this.softWrapAtPreferredLineLength = (params.softWrapAtPreferredLineLength != null) ? params.softWrapAtPreferredLineLength : false
this.preferredLineLength = (params.preferredLineLength != null) ? params.preferredLineLength : 80
@@ -171,17 +172,20 @@ class TextEditor {
this.selections = []
this.hasTerminatedPendingState = false
- this.buffer = params.buffer || new TextBuffer({
- shouldDestroyOnFileDelete () { return atom.config.get('core.closeDeletedFileTabs') }
- })
+ if (params.buffer) {
+ this.buffer = params.buffer
+ } else {
+ this.buffer = new TextBuffer({
+ shouldDestroyOnFileDelete () { return atom.config.get('core.closeDeletedFileTabs') }
+ })
+ this.buffer.setLanguageMode(new TokenizedBuffer({buffer: this.buffer, config: atom.config}))
+ }
- this.tokenizedBuffer = params.tokenizedBuffer || new TokenizedBuffer({
- grammar: params.grammar,
- tabLength,
- buffer: this.buffer,
- largeFileMode: this.largeFileMode,
- assert: this.assert
+ const languageMode = this.buffer.getLanguageMode()
+ this.languageModeSubscription = languageMode.onDidTokenize && languageMode.onDidTokenize(() => {
+ this.emitter.emit('did-tokenize')
})
+ if (this.languageModeSubscription) this.disposables.add(this.languageModeSubscription)
if (params.displayLayer) {
this.displayLayer = params.displayLayer
@@ -217,8 +221,6 @@ class TextEditor {
this.selectionsMarkerLayer = this.addMarkerLayer({maintainHistory: true, persistent: true})
}
- this.displayLayer.setTextDecorationLayer(this.tokenizedBuffer)
-
this.decorationManager = new DecorationManager(this)
this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'cursor'})
if (!this.isMini()) this.decorateCursorLine()
@@ -271,9 +273,8 @@ class TextEditor {
return this
}
- get languageMode () {
- return this.tokenizedBuffer
- }
+ get languageMode () { return this.buffer.getLanguageMode() }
+ get tokenizedBuffer () { return this.buffer.getLanguageMode() }
get rowsPerPage () {
return this.getRowsPerPage()
@@ -319,10 +320,6 @@ class TextEditor {
this.undoGroupingInterval = value
break
- case 'nonWordCharacters':
- this.nonWordCharacters = value
- break
-
case 'scrollSensitivity':
this.scrollSensitivity = value
break
@@ -344,8 +341,7 @@ class TextEditor {
break
case 'tabLength':
- if (value > 0 && value !== this.tokenizedBuffer.getTabLength()) {
- this.tokenizedBuffer.setTabLength(value)
+ if (value > 0 && value !== this.displayLayer.tabLength) {
displayLayerParams.tabLength = value
}
break
@@ -513,26 +509,22 @@ class TextEditor {
}
serialize () {
- const tokenizedBufferState = this.tokenizedBuffer.serialize()
-
return {
deserializer: 'TextEditor',
version: SERIALIZATION_VERSION,
- // TODO: Remove this forward-compatible fallback once 1.8 reaches stable.
- displayBuffer: {tokenizedBuffer: tokenizedBufferState},
-
- tokenizedBuffer: tokenizedBufferState,
displayLayerId: this.displayLayer.id,
selectionsMarkerLayerId: this.selectionsMarkerLayer.id,
initialScrollTopRow: this.getScrollTopRow(),
initialScrollLeftColumn: this.getScrollLeftColumn(),
+ tabLength: this.displayLayer.tabLength,
atomicSoftTabs: this.displayLayer.atomicSoftTabs,
softWrapHangingIndentLength: this.displayLayer.softWrapHangingIndent,
id: this.id,
+ bufferId: this.buffer.id,
softTabs: this.softTabs,
softWrapped: this.softWrapped,
softWrapAtPreferredLineLength: this.softWrapAtPreferredLineLength,
@@ -540,7 +532,6 @@ class TextEditor {
mini: this.mini,
editorWidthInChars: this.editorWidthInChars,
width: this.width,
- largeFileMode: this.largeFileMode,
maxScreenLineLength: this.maxScreenLineLength,
registered: this.registered,
invisibles: this.invisibles,
@@ -553,6 +544,7 @@ class TextEditor {
subscribeToBuffer () {
this.buffer.retain()
+ this.disposables.add(this.buffer.onDidChangeLanguageMode(this.handleLanguageModeChange.bind(this)))
this.disposables.add(this.buffer.onDidChangePath(() => {
this.emitter.emit('did-change-title', this.getTitle())
this.emitter.emit('did-change-path', this.getPath())
@@ -576,7 +568,6 @@ class TextEditor {
}
subscribeToDisplayLayer () {
- this.disposables.add(this.tokenizedBuffer.onDidChangeGrammar(this.handleGrammarChange.bind(this)))
this.disposables.add(this.displayLayer.onDidChange(changes => {
this.mergeIntersectingSelections()
if (this.component) this.component.didChangeDisplayLayer(changes)
@@ -596,7 +587,6 @@ class TextEditor {
this.alive = false
this.disposables.dispose()
this.displayLayer.destroy()
- this.tokenizedBuffer.destroy()
for (let selection of this.selections.slice()) {
selection.destroy()
}
@@ -731,7 +721,9 @@ class TextEditor {
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeGrammar (callback) {
- return this.emitter.on('did-change-grammar', callback)
+ return this.buffer.onDidChangeLanguageMode(() => {
+ callback(this.buffer.getLanguageMode().grammar)
+ })
}
// Extended: Calls your `callback` when the result of {::isModified} changes.
@@ -947,7 +939,7 @@ class TextEditor {
selectionsMarkerLayer,
softTabs,
suppressCursorCreation: true,
- tabLength: this.tokenizedBuffer.getTabLength(),
+ tabLength: this.getTabLength(),
initialScrollTopRow: this.getScrollTopRow(),
initialScrollLeftColumn: this.getScrollLeftColumn(),
assert: this.assert,
@@ -960,7 +952,12 @@ class TextEditor {
}
// Controls visibility based on the given {Boolean}.
- setVisible (visible) { this.tokenizedBuffer.setVisible(visible) }
+ setVisible (visible) {
+ if (visible) {
+ const languageMode = this.buffer.getLanguageMode()
+ if (languageMode.startTokenizing) languageMode.startTokenizing()
+ }
+ }
setMini (mini) {
this.update({mini})
@@ -3353,7 +3350,7 @@ class TextEditor {
// Essential: Get the on-screen length of tab characters.
//
// Returns a {Number}.
- getTabLength () { return this.tokenizedBuffer.getTabLength() }
+ getTabLength () { return this.displayLayer.tabLength }
// Essential: Set the on-screen length of tab characters. Setting this to a
// {Number} This will override the `editor.tabLength` setting.
@@ -3384,9 +3381,10 @@ class TextEditor {
// Returns a {Boolean} or undefined if no non-comment lines had leading
// whitespace.
usesSoftTabs () {
+ const languageMode = this.buffer.getLanguageMode()
+ const hasIsRowCommented = languageMode.isRowCommented
for (let bufferRow = 0, end = Math.min(1000, this.buffer.getLastRow()); bufferRow <= end; bufferRow++) {
- const tokenizedLine = this.tokenizedBuffer.tokenizedLines[bufferRow]
- if (tokenizedLine && tokenizedLine.isComment()) continue
+ if (hasIsRowCommented && languageMode.isRowCommented(bufferRow)) continue
const line = this.buffer.lineForRow(bufferRow)
if (line[0] === ' ') return true
if (line[0] === '\t') return false
@@ -3509,7 +3507,19 @@ class TextEditor {
//
// Returns a {Number}.
indentLevelForLine (line) {
- return this.tokenizedBuffer.indentLevelForLine(line)
+ const tabLength = this.getTabLength()
+ let indentLength = 0
+ for (let i = 0, {length} = line; i < length; i++) {
+ const char = line[i]
+ if (char === '\t') {
+ indentLength += tabLength - (indentLength % tabLength)
+ } else if (char === ' ') {
+ indentLength++
+ } else {
+ break
+ }
+ }
+ return indentLength / tabLength
}
// Extended: Indent rows intersecting selections based on the grammar's suggested
@@ -3542,27 +3552,24 @@ class TextEditor {
// Essential: Get the current {Grammar} of this editor.
getGrammar () {
- return this.tokenizedBuffer.grammar
+ const languageMode = this.buffer.getLanguageMode()
+ return languageMode.getGrammar && languageMode.getGrammar() || NullGrammar
}
- // Essential: Set the current {Grammar} of this editor.
+ // Deprecated: Set the current {Grammar} of this editor.
//
// Assigning a grammar will cause the editor to re-tokenize based on the new
// grammar.
//
// * `grammar` {Grammar}
setGrammar (grammar) {
- return this.tokenizedBuffer.setGrammar(grammar)
- }
-
- // Reload the grammar based on the file name.
- reloadGrammar () {
- return this.tokenizedBuffer.reloadGrammar()
+ const buffer = this.getBuffer()
+ buffer.setLanguageMode(atom.grammars.languageModeForGrammarAndBuffer(grammar, buffer))
}
// Experimental: Get a notification when async tokenization is completed.
onDidTokenize (callback) {
- return this.tokenizedBuffer.onDidTokenize(callback)
+ return this.emitter.on('did-tokenize', callback)
}
/*
@@ -3573,7 +3580,7 @@ class TextEditor {
// e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with
// {Config::get} to get language specific config values.
getRootScopeDescriptor () {
- return this.tokenizedBuffer.rootScopeDescriptor
+ return this.buffer.getLanguageMode().rootScopeDescriptor
}
// Essential: Get the syntactic scopeDescriptor for the given position in buffer
@@ -3587,7 +3594,7 @@ class TextEditor {
//
// Returns a {ScopeDescriptor}.
scopeDescriptorForBufferPosition (bufferPosition) {
- return this.tokenizedBuffer.scopeDescriptorForPosition(bufferPosition)
+ return this.buffer.getLanguageMode().scopeDescriptorForPosition(bufferPosition)
}
// Extended: Get the range in buffer coordinates of all tokens surrounding the
@@ -3604,7 +3611,7 @@ class TextEditor {
}
bufferRangeForScopeAtPosition (scopeSelector, position) {
- return this.tokenizedBuffer.bufferRangeForScopeAtPosition(scopeSelector, position)
+ return this.buffer.getLanguageMode().bufferRangeForScopeAtPosition(scopeSelector, position)
}
// Extended: Determine if the given row is entirely a comment
@@ -3622,7 +3629,7 @@ class TextEditor {
}
tokenForBufferPosition (bufferPosition) {
- return this.tokenizedBuffer.tokenForPosition(bufferPosition)
+ return this.buffer.getLanguageMode().tokenForPosition(bufferPosition)
}
/*
@@ -3749,7 +3756,11 @@ class TextEditor {
// level.
foldCurrentRow () {
const {row} = this.getCursorBufferPosition()
- const range = this.tokenizedBuffer.getFoldableRangeContainingPoint(Point(row, Infinity))
+ const languageMode = this.buffer.getLanguageMode()
+ const range = (
+ languageMode.getFoldableRangeContainingPoint &&
+ languageMode.getFoldableRangeContainingPoint(Point(row, Infinity), this.getTabLength())
+ )
if (range) return this.displayLayer.foldBufferRange(range)
}
@@ -3768,8 +3779,12 @@ class TextEditor {
// * `bufferRow` A {Number}.
foldBufferRow (bufferRow) {
let position = Point(bufferRow, Infinity)
+ const languageMode = this.buffer.getLanguageMode()
while (true) {
- const foldableRange = this.tokenizedBuffer.getFoldableRangeContainingPoint(position, this.getTabLength())
+ const foldableRange = (
+ languageMode.getFoldableRangeContainingPoint &&
+ languageMode.getFoldableRangeContainingPoint(position, this.getTabLength())
+ )
if (foldableRange) {
const existingFolds = this.displayLayer.foldsIntersectingBufferRange(Range(foldableRange.start, foldableRange.start))
if (existingFolds.length === 0) {
@@ -3803,8 +3818,13 @@ class TextEditor {
// Extended: Fold all foldable lines.
foldAll () {
+ const languageMode = this.buffer.getLanguageMode()
+ const foldableRanges = (
+ languageMode.getFoldableRanges &&
+ languageMode.getFoldableRanges(this.getTabLength())
+ )
this.displayLayer.destroyAllFolds()
- for (let range of this.tokenizedBuffer.getFoldableRanges(this.getTabLength())) {
+ for (let range of foldableRanges || []) {
this.displayLayer.foldBufferRange(range)
}
}
@@ -3820,8 +3840,13 @@ class TextEditor {
//
// * `level` A {Number}.
foldAllAtIndentLevel (level) {
+ const languageMode = this.buffer.getLanguageMode()
+ const foldableRanges = (
+ languageMode.getFoldableRangesAtIndentLevel &&
+ languageMode.getFoldableRangesAtIndentLevel(level, this.getTabLength())
+ )
this.displayLayer.destroyAllFolds()
- for (let range of this.tokenizedBuffer.getFoldableRangesAtIndentLevel(level, this.getTabLength())) {
+ for (let range of foldableRanges || []) {
this.displayLayer.foldBufferRange(range)
}
}
@@ -3834,7 +3859,8 @@ class TextEditor {
//
// Returns a {Boolean}.
isFoldableAtBufferRow (bufferRow) {
- return this.tokenizedBuffer.isFoldableAtRow(bufferRow)
+ const languageMode = this.buffer.getLanguageMode()
+ return languageMode.isFoldableAtRow && languageMode.isFoldableAtRow(bufferRow)
}
// Extended: Determine whether the given row in screen coordinates is foldable.
@@ -4039,18 +4065,6 @@ class TextEditor {
Section: Config
*/
- // Experimental: Supply an object that will provide the editor with settings
- // for specific syntactic scopes. See the `ScopedSettingsDelegate` in
- // `text-editor-registry.js` for an example implementation.
- setScopedSettingsDelegate (scopedSettingsDelegate) {
- this.scopedSettingsDelegate = scopedSettingsDelegate
- this.tokenizedBuffer.scopedSettingsDelegate = this.scopedSettingsDelegate
- }
-
- // Experimental: Retrieve the {Object} that provides the editor with settings
- // for specific syntactic scopes.
- getScopedSettingsDelegate () { return this.scopedSettingsDelegate }
-
// Experimental: Is auto-indentation enabled for this editor?
//
// Returns a {Boolean}.
@@ -4098,21 +4112,34 @@ class TextEditor {
// for the purpose of word-based cursor movements.
//
// Returns a {String} containing the non-word characters.
- getNonWordCharacters (scopes) {
- if (this.scopedSettingsDelegate && this.scopedSettingsDelegate.getNonWordCharacters) {
- return this.scopedSettingsDelegate.getNonWordCharacters(scopes) || this.nonWordCharacters
- } else {
- return this.nonWordCharacters
- }
+ getNonWordCharacters (position) {
+ const languageMode = this.buffer.getLanguageMode()
+ return (
+ languageMode.getNonWordCharacters &&
+ languageMode.getNonWordCharacters(position || Point(0, 0))
+ ) || DEFAULT_NON_WORD_CHARACTERS
}
/*
Section: Event Handlers
*/
- handleGrammarChange () {
+ handleLanguageModeChange () {
this.unfoldAll()
- return this.emitter.emit('did-change-grammar', this.getGrammar())
+ if (this.languageModeSubscription) {
+ this.languageModeSubscription.dispose()
+ this.disposables.remove(this.languageModeSubscription)
+ }
+ const languageMode = this.buffer.getLanguageMode()
+
+ if (this.component && this.component.visible && languageMode.startTokenizing) {
+ languageMode.startTokenizing()
+ }
+ this.languageModeSubscription = languageMode.onDidTokenize && languageMode.onDidTokenize(() => {
+ this.emitter.emit('did-tokenize')
+ })
+ if (this.languageModeSubscription) this.disposables.add(this.languageModeSubscription)
+ this.emitter.emit('did-change-grammar', languageMode.grammar)
}
/*
@@ -4382,7 +4409,11 @@ class TextEditor {
*/
suggestedIndentForBufferRow (bufferRow, options) {
- return this.tokenizedBuffer.suggestedIndentForBufferRow(bufferRow, options)
+ const languageMode = this.buffer.getLanguageMode()
+ return (
+ languageMode.suggestedIndentForBufferRow &&
+ languageMode.suggestedIndentForBufferRow(bufferRow, this.getTabLength(), options)
+ )
}
// Given a buffer row, indent it.
@@ -4407,17 +4438,21 @@ class TextEditor {
}
autoDecreaseIndentForBufferRow (bufferRow) {
- const indentLevel = this.tokenizedBuffer.suggestedIndentForEditedBufferRow(bufferRow)
+ const languageMode = this.buffer.getLanguageMode()
+ const indentLevel = (
+ languageMode.suggestedIndentForEditedBufferRow &&
+ languageMode.suggestedIndentForEditedBufferRow(bufferRow, this.getTabLength())
+ )
if (indentLevel != null) this.setIndentationForBufferRow(bufferRow, indentLevel)
}
toggleLineCommentForBufferRow (row) { this.toggleLineCommentsForBufferRows(row, row) }
toggleLineCommentsForBufferRows (start, end) {
- let {
- commentStartString,
- commentEndString
- } = this.tokenizedBuffer.commentStringsForPosition(Point(start, 0))
+ const languageMode = this.buffer.getLanguageMode()
+ let {commentStartString, commentEndString} =
+ languageMode.commentStringsForPosition &&
+ languageMode.commentStringsForPosition(Point(start, 0)) || {}
if (!commentStartString) return
commentStartString = commentStartString.trim()
@@ -4508,12 +4543,13 @@ class TextEditor {
rowRangeForParagraphAtBufferRow (bufferRow) {
if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(bufferRow))) return
- const isCommented = this.tokenizedBuffer.isRowCommented(bufferRow)
+ const languageMode = this.buffer.getLanguageMode()
+ const isCommented = languageMode.isRowCommented(bufferRow)
let startRow = bufferRow
while (startRow > 0) {
if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(startRow - 1))) break
- if (this.tokenizedBuffer.isRowCommented(startRow - 1) !== isCommented) break
+ if (languageMode.isRowCommented(startRow - 1) !== isCommented) break
startRow--
}
@@ -4521,7 +4557,7 @@ class TextEditor {
const rowCount = this.getLineCount()
while (endRow < rowCount) {
if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(endRow + 1))) break
- if (this.tokenizedBuffer.isRowCommented(endRow + 1) !== isCommented) break
+ if (languageMode.isRowCommented(endRow + 1) !== isCommented) break
endRow++
}
diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js
index 2a9446256..b29977616 100644
--- a/src/tokenized-buffer.js
+++ b/src/tokenized-buffer.js
@@ -16,15 +16,6 @@ const prefixedScopes = new Map()
module.exports =
class TokenizedBuffer {
- static deserialize (state, atomEnvironment) {
- const buffer = atomEnvironment.project.bufferForIdSync(state.bufferId)
- if (!buffer) return null
-
- state.buffer = buffer
- state.assert = atomEnvironment.assert
- return new TokenizedBuffer(state)
- }
-
constructor (params) {
this.emitter = new Emitter()
this.disposables = new CompositeDisposable()
@@ -32,16 +23,19 @@ class TokenizedBuffer {
this.regexesByPattern = {}
this.alive = true
- this.visible = false
+ this.tokenizationStarted = false
this.id = params.id != null ? params.id : nextId++
this.buffer = params.buffer
- this.tabLength = params.tabLength
this.largeFileMode = params.largeFileMode
- this.assert = params.assert
- this.scopedSettingsDelegate = params.scopedSettingsDelegate
+ this.config = params.config
+ this.largeFileMode = params.largeFileMode != null
+ ? params.largeFileMode
+ : this.buffer.buffer.getLength() >= 2 * 1024 * 1024
- this.setGrammar(params.grammar || NullGrammar)
- this.disposables.add(this.buffer.registerTextDecorationLayer(this))
+ this.grammar = params.grammar || NullGrammar
+ this.rootScopeDescriptor = new ScopeDescriptor({scopes: [this.grammar.scopeName]})
+ this.disposables.add(this.grammar.onDidUpdate(() => this.retokenizeLines()))
+ this.retokenizeLines()
}
destroy () {
@@ -59,6 +53,19 @@ class TokenizedBuffer {
return !this.alive
}
+ getGrammar () {
+ return this.grammar
+ }
+
+ getLanguageId () {
+ return this.grammar.scopeName
+ }
+
+ getNonWordCharacters (position) {
+ const scope = this.scopeDescriptorForPosition(position)
+ return this.config.get('editor.nonWordCharacters', {scope})
+ }
+
/*
Section - auto-indent
*/
@@ -68,10 +75,14 @@ class TokenizedBuffer {
// * bufferRow - A {Number} indicating the buffer row
//
// Returns a {Number}.
- suggestedIndentForBufferRow (bufferRow, options) {
- const line = this.buffer.lineForRow(bufferRow)
- const tokenizedLine = this.tokenizedLineForRow(bufferRow)
- return this._suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
+ suggestedIndentForBufferRow (bufferRow, tabLength, options) {
+ return this._suggestedIndentForTokenizedLineAtBufferRow(
+ bufferRow,
+ this.buffer.lineForRow(bufferRow),
+ this.tokenizedLineForRow(bufferRow),
+ tabLength,
+ options
+ )
}
// Get the suggested indentation level for a given line of text, if it were inserted at the given
@@ -80,9 +91,13 @@ class TokenizedBuffer {
// * bufferRow - A {Number} indicating the buffer row
//
// Returns a {Number}.
- suggestedIndentForLineAtBufferRow (bufferRow, line, options) {
- const tokenizedLine = this.buildTokenizedLineForRowWithText(bufferRow, line)
- return this._suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
+ suggestedIndentForLineAtBufferRow (bufferRow, line, tabLength) {
+ return this._suggestedIndentForTokenizedLineAtBufferRow(
+ bufferRow,
+ line,
+ this.buildTokenizedLineForRowWithText(bufferRow, line),
+ tabLength
+ )
}
// Get the suggested indentation level for a line in the buffer on which the user is currently
@@ -93,9 +108,9 @@ class TokenizedBuffer {
// * bufferRow - The row {Number}
//
// Returns a {Number}.
- suggestedIndentForEditedBufferRow (bufferRow) {
+ suggestedIndentForEditedBufferRow (bufferRow, tabLength) {
const line = this.buffer.lineForRow(bufferRow)
- const currentIndentLevel = this.indentLevelForLine(line)
+ const currentIndentLevel = this.indentLevelForLine(line, tabLength)
if (currentIndentLevel === 0) return
const scopeDescriptor = this.scopeDescriptorForPosition([bufferRow, 0])
@@ -108,7 +123,7 @@ class TokenizedBuffer {
if (precedingRow == null) return
const precedingLine = this.buffer.lineForRow(precedingRow)
- let desiredIndentLevel = this.indentLevelForLine(precedingLine)
+ let desiredIndentLevel = this.indentLevelForLine(precedingLine, tabLength)
const increaseIndentRegex = this.increaseIndentRegexForScopeDescriptor(scopeDescriptor)
if (increaseIndentRegex) {
@@ -125,7 +140,7 @@ class TokenizedBuffer {
return desiredIndentLevel
}
- _suggestedIndentForTokenizedLineAtBufferRow (bufferRow, line, tokenizedLine, options) {
+ _suggestedIndentForTokenizedLineAtBufferRow (bufferRow, line, tokenizedLine, tabLength, options) {
const iterator = tokenizedLine.getTokenIterator()
iterator.next()
const scopeDescriptor = new ScopeDescriptor({scopes: iterator.getScopes()})
@@ -144,7 +159,7 @@ class TokenizedBuffer {
}
const precedingLine = this.buffer.lineForRow(precedingRow)
- let desiredIndentLevel = this.indentLevelForLine(precedingLine)
+ let desiredIndentLevel = this.indentLevelForLine(precedingLine, tabLength)
if (!increaseIndentRegex) return desiredIndentLevel
if (!this.isRowCommented(precedingRow)) {
@@ -164,15 +179,24 @@ class TokenizedBuffer {
*/
commentStringsForPosition (position) {
- if (this.scopedSettingsDelegate) {
- const scope = this.scopeDescriptorForPosition(position)
- return this.scopedSettingsDelegate.getCommentStrings(scope)
- } else {
- return {}
+ const scope = this.scopeDescriptorForPosition(position)
+ const commentStartEntries = this.config.getAll('editor.commentStart', {scope})
+ const commentEndEntries = this.config.getAll('editor.commentEnd', {scope})
+ const commentStartEntry = commentStartEntries[0]
+ const commentEndEntry = commentEndEntries.find((entry) => {
+ return entry.scopeSelector === commentStartEntry.scopeSelector
+ })
+ return {
+ commentStartString: commentStartEntry && commentStartEntry.value,
+ commentEndString: commentEndEntry && commentEndEntry.value
}
}
- buildIterator () {
+ /*
+ Section - Syntax Highlighting
+ */
+
+ buildHighlightIterator () {
return new TokenizedBufferIterator(this)
}
@@ -196,47 +220,14 @@ class TokenizedBuffer {
return []
}
- onDidInvalidateRange (fn) {
- return this.emitter.on('did-invalidate-range', fn)
- }
-
- serialize () {
- return {
- deserializer: 'TokenizedBuffer',
- bufferPath: this.buffer.getPath(),
- bufferId: this.buffer.getId(),
- tabLength: this.tabLength,
- largeFileMode: this.largeFileMode
- }
- }
-
- observeGrammar (callback) {
- callback(this.grammar)
- return this.onDidChangeGrammar(callback)
- }
-
- onDidChangeGrammar (callback) {
- return this.emitter.on('did-change-grammar', callback)
+ onDidChangeHighlighting (fn) {
+ return this.emitter.on('did-change-highlighting', fn)
}
onDidTokenize (callback) {
return this.emitter.on('did-tokenize', callback)
}
- setGrammar (grammar) {
- if (!grammar || grammar === this.grammar) return
-
- this.grammar = grammar
- this.rootScopeDescriptor = new ScopeDescriptor({scopes: [this.grammar.scopeName]})
-
- if (this.grammarUpdateDisposable) this.grammarUpdateDisposable.dispose()
- this.grammarUpdateDisposable = this.grammar.onDidUpdate(() => this.retokenizeLines())
- this.disposables.add(this.grammarUpdateDisposable)
-
- this.retokenizeLines()
- this.emitter.emit('did-change-grammar', grammar)
- }
-
getGrammarSelectionContent () {
return this.buffer.getTextInRange([[0, 0], [10, 0]])
}
@@ -264,21 +255,15 @@ class TokenizedBuffer {
}
}
- setVisible (visible) {
- this.visible = visible
- if (this.visible && this.grammar.name !== 'Null Grammar' && !this.largeFileMode) {
+ startTokenizing () {
+ this.tokenizationStarted = true
+ if (this.grammar.name !== 'Null Grammar' && !this.largeFileMode) {
this.tokenizeInBackground()
}
}
- getTabLength () { return this.tabLength }
-
- setTabLength (tabLength) {
- this.tabLength = tabLength
- }
-
tokenizeInBackground () {
- if (!this.visible || this.pendingChunk || !this.alive) return
+ if (!this.tokenizationStarted || this.pendingChunk || !this.alive) return
this.pendingChunk = true
_.defer(() => {
@@ -316,7 +301,7 @@ class TokenizedBuffer {
this.validateRow(endRow)
if (!filledRegion) this.invalidateRow(endRow + 1)
- this.emitter.emit('did-invalidate-range', Range(Point(startRow, 0), Point(endRow + 1, 0)))
+ this.emitter.emit('did-change-highlighting', Range(Point(startRow, 0), Point(endRow + 1, 0)))
}
if (this.firstInvalidRow() != null) {
@@ -486,18 +471,6 @@ class TokenizedBuffer {
while (true) {
if (scopes.pop() === matchingStartTag) break
if (scopes.length === 0) {
- this.assert(false, 'Encountered an unmatched scope end tag.', error => {
- error.metadata = {
- grammarScopeName: this.grammar.scopeName,
- unmatchedEndTag: this.grammar.scopeForId(tag)
- }
- const path = require('path')
- error.privateMetadataDescription = `The contents of \`${path.basename(this.buffer.getPath())}\``
- error.privateMetadata = {
- filePath: this.buffer.getPath(),
- fileContents: this.buffer.getText()
- }
- })
break
}
}
@@ -507,7 +480,7 @@ class TokenizedBuffer {
return scopes
}
- indentLevelForLine (line, tabLength = this.tabLength) {
+ indentLevelForLine (line, tabLength) {
let indentLength = 0
for (let i = 0, {length} = line; i < length; i++) {
const char = line[i]
@@ -712,28 +685,20 @@ class TokenizedBuffer {
return foldEndRow
}
- increaseIndentRegexForScopeDescriptor (scopeDescriptor) {
- if (this.scopedSettingsDelegate) {
- return this.regexForPattern(this.scopedSettingsDelegate.getIncreaseIndentPattern(scopeDescriptor))
- }
+ increaseIndentRegexForScopeDescriptor (scope) {
+ return this.regexForPattern(this.config.get('editor.increaseIndentPattern', {scope}))
}
- decreaseIndentRegexForScopeDescriptor (scopeDescriptor) {
- if (this.scopedSettingsDelegate) {
- return this.regexForPattern(this.scopedSettingsDelegate.getDecreaseIndentPattern(scopeDescriptor))
- }
+ decreaseIndentRegexForScopeDescriptor (scope) {
+ return this.regexForPattern(this.config.get('editor.decreaseIndentPattern', {scope}))
}
- decreaseNextIndentRegexForScopeDescriptor (scopeDescriptor) {
- if (this.scopedSettingsDelegate) {
- return this.regexForPattern(this.scopedSettingsDelegate.getDecreaseNextIndentPattern(scopeDescriptor))
- }
+ decreaseNextIndentRegexForScopeDescriptor (scope) {
+ return this.regexForPattern(this.config.get('editor.decreaseNextIndentPattern', {scope}))
}
- foldEndRegexForScopeDescriptor (scopes) {
- if (this.scopedSettingsDelegate) {
- return this.regexForPattern(this.scopedSettingsDelegate.getFoldEndPattern(scopes))
- }
+ foldEndRegexForScopeDescriptor (scope) {
+ return this.regexForPattern(this.config.get('editor.foldEndPattern', {scope}))
}
regexForPattern (pattern) {
diff --git a/src/workspace.js b/src/workspace.js
index defb43df0..9f2ad397b 100644
--- a/src/workspace.js
+++ b/src/workspace.js
@@ -494,7 +494,6 @@ module.exports = class Workspace extends Model {
if (item instanceof TextEditor) {
const subscriptions = new CompositeDisposable(
this.textEditorRegistry.add(item),
- this.textEditorRegistry.maintainGrammar(item),
this.textEditorRegistry.maintainConfig(item),
item.observeGrammar(this.handleGrammarUsed.bind(this))
)
@@ -1212,9 +1211,7 @@ module.exports = class Workspace extends Model {
}
const fileSize = fs.getSizeSync(filePath)
-
- const largeFileMode = fileSize >= (2 * 1048576) // 2MB
- if (fileSize >= (this.config.get('core.warnOnLargeFileLimit') * 1048576)) { // 20MB by default
+ if (fileSize >= (this.config.get('core.warnOnLargeFileLimit') * 1048576)) {
const choice = this.applicationDelegate.confirm({
message: 'Atom will be unresponsive during the loading of very large files.',
detailedMessage: 'Do you still want to load this file?',
@@ -1229,7 +1226,7 @@ module.exports = class Workspace extends Model {
return this.project.bufferForPath(filePath, options)
.then(buffer => {
- return this.textEditorRegistry.build(Object.assign({buffer, largeFileMode, autoHeight: false}, options))
+ return this.textEditorRegistry.build(Object.assign({buffer, autoHeight: false}, options))
})
}
@@ -1250,11 +1247,8 @@ module.exports = class Workspace extends Model {
// Returns a {TextEditor}.
buildTextEditor (params) {
const editor = this.textEditorRegistry.build(params)
- const subscriptions = new CompositeDisposable(
- this.textEditorRegistry.maintainGrammar(editor),
- this.textEditorRegistry.maintainConfig(editor)
- )
- editor.onDidDestroy(() => { subscriptions.dispose() })
+ const subscription = this.textEditorRegistry.maintainConfig(editor)
+ editor.onDidDestroy(() => subscription.dispose())
return editor
}