diff --git a/package.json b/package.json
index 2c8efb2d1..0ed8ce98e 100644
--- a/package.json
+++ b/package.json
@@ -157,9 +157,9 @@
"symbols-view": "https://www.atom.io/api/packages/symbols-view/versions/0.118.2/tarball",
"tabs": "https://www.atom.io/api/packages/tabs/versions/0.109.2/tarball",
"temp": "^0.8.3",
- "text-buffer": "13.14.4",
+ "text-buffer": "13.14.5",
"timecop": "https://www.atom.io/api/packages/timecop/versions/0.36.2/tarball",
- "tree-sitter": "0.12.12",
+ "tree-sitter": "0.12.20",
"tree-view": "https://www.atom.io/api/packages/tree-view/versions/0.222.0/tarball",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.8",
@@ -203,7 +203,7 @@
"exception-reporting": "0.43.1",
"find-and-replace": "0.215.11",
"fuzzy-finder": "1.8.2",
- "github": "0.17.3",
+ "github": "0.18.2",
"git-diff": "1.3.9",
"go-to-line": "0.33.0",
"grammar-selector": "0.50.1",
@@ -238,10 +238,10 @@
"language-gfm": "0.90.5",
"language-git": "0.19.1",
"language-go": "0.45.4",
- "language-html": "0.50.1",
+ "language-html": "0.50.3",
"language-hyperlink": "0.16.3",
"language-java": "0.30.0",
- "language-javascript": "0.128.8",
+ "language-javascript": "0.128.11",
"language-json": "0.19.2",
"language-less": "0.34.2",
"language-make": "0.22.3",
diff --git a/spec/grammar-registry-spec.js b/spec/grammar-registry-spec.js
index abb3b189a..bcd57f3a2 100644
--- a/spec/grammar-registry-spec.js
+++ b/spec/grammar-registry-spec.js
@@ -525,6 +525,34 @@ describe('GrammarRegistry', () => {
})
})
+ describe('.addInjectionPoint(languageId, {type, language, content})', () => {
+ const injectionPoint = {
+ type: 'some_node_type',
+ language() { return 'some_language_name' },
+ content(node) { return node }
+ }
+
+ beforeEach(() => {
+ atom.config.set('core.useTreeSitterParsers', true)
+ })
+
+ it('adds an injection point to the grammar with the given id', async () => {
+ await atom.packages.activatePackage('language-javascript')
+ atom.grammars.addInjectionPoint('javascript', injectionPoint)
+ const grammar = atom.grammars.grammarForId('javascript')
+ expect(grammar.injectionPoints).toContain(injectionPoint)
+ })
+
+ describe('when called before a grammar with the given id is loaded', () => {
+ it('adds the injection point once the grammar is loaded', async () => {
+ atom.grammars.addInjectionPoint('javascript', injectionPoint)
+ await atom.packages.activatePackage('language-javascript')
+ const grammar = atom.grammars.grammarForId('javascript')
+ expect(grammar.injectionPoints).toContain(injectionPoint)
+ })
+ })
+ })
+
describe('serialization', () => {
it('persists editors\' grammar overrides', async () => {
const buffer1 = new TextBuffer()
diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js
index 014d8122e..c23849d30 100644
--- a/spec/tree-sitter-language-mode-spec.js
+++ b/spec/tree-sitter-language-mode-spec.js
@@ -11,6 +11,7 @@ const cGrammarPath = require.resolve('language-c/grammars/tree-sitter-c.cson')
const pythonGrammarPath = require.resolve('language-python/grammars/tree-sitter-python.cson')
const jsGrammarPath = require.resolve('language-javascript/grammars/tree-sitter-javascript.cson')
const htmlGrammarPath = require.resolve('language-html/grammars/tree-sitter-html.cson')
+const ejsGrammarPath = require.resolve('language-html/grammars/tree-sitter-ejs.cson')
describe('TreeSitterLanguageMode', () => {
let editor, buffer
@@ -18,6 +19,7 @@ describe('TreeSitterLanguageMode', () => {
beforeEach(async () => {
editor = await atom.workspace.open('')
buffer = editor.getBuffer()
+ editor.displayLayer.reset({foldCharacter: '…'})
})
describe('highlighting', () => {
@@ -32,10 +34,11 @@ describe('TreeSitterLanguageMode', () => {
}
})
+ buffer.setText('aa.bbb = cc(d.eee());')
+
const languageMode = new TreeSitterLanguageMode({buffer, grammar})
buffer.setLanguageMode(languageMode)
- buffer.setText('aa.bbb = cc(d.eee());')
- await languageMode.reparsePromise
+ await nextHighlightingUpdate(languageMode)
expectTokensToEqual(editor, [[
{text: 'aa.', scopes: ['source']},
@@ -61,10 +64,11 @@ describe('TreeSitterLanguageMode', () => {
}
})
+ buffer.setText('a = bb.ccc();')
+
const languageMode = new TreeSitterLanguageMode({buffer, grammar})
buffer.setLanguageMode(languageMode)
- buffer.setText('a = bb.ccc();')
- await languageMode.reparsePromise
+ await nextHighlightingUpdate(languageMode)
expectTokensToEqual(editor, [[
{text: 'a', scopes: ['source', 'variable']},
@@ -87,17 +91,18 @@ describe('TreeSitterLanguageMode', () => {
}
})
+ buffer.setText('a\n .b();')
+
const languageMode = new TreeSitterLanguageMode({buffer, grammar})
buffer.setLanguageMode(languageMode)
- buffer.setText('a\n .b();')
- await languageMode.reparsePromise
+ await nextHighlightingUpdate(languageMode)
expectTokensToEqual(editor, [
[
{text: 'a', scopes: ['variable']},
],
[
- {text: ' ', scopes: ['whitespace']},
+ {text: ' ', scopes: ['leading-whitespace']},
{text: '.', scopes: []},
{text: 'b', scopes: ['function']},
{text: '();', scopes: []}
@@ -114,12 +119,12 @@ describe('TreeSitterLanguageMode', () => {
}
})
+ buffer.setText('int main() {\n int a\n int b;\n}');
+
const languageMode = new TreeSitterLanguageMode({buffer, grammar})
buffer.setLanguageMode(languageMode)
- buffer.setText('int main() {\n int a\n int b;\n}');
- await languageMode.reparsePromise
+ await nextHighlightingUpdate(languageMode)
- editor.screenLineForScreenRow(0)
expect(
languageMode.tree.rootNode.descendantForPosition(Point(1, 2), Point(1, 6)).toString()
).toBe('(declaration (primitive_type) (identifier) (MISSING))')
@@ -132,13 +137,13 @@ describe('TreeSitterLanguageMode', () => {
{text: '() {', scopes: []}
],
[
- {text: ' ', scopes: ['whitespace']},
+ {text: ' ', scopes: ['leading-whitespace']},
{text: 'int', scopes: ['type']},
{text: ' ', scopes: []},
{text: 'a', scopes: ['variable']}
],
[
- {text: ' ', scopes: ['whitespace']},
+ {text: ' ', scopes: ['leading-whitespace']},
{text: 'int', scopes: ['type']},
{text: ' ', scopes: []},
{text: 'b', scopes: ['variable']},
@@ -159,10 +164,11 @@ describe('TreeSitterLanguageMode', () => {
}
})
+ buffer.setText('a(\nb,\nc\n')
+
const languageMode = new TreeSitterLanguageMode({buffer, grammar})
buffer.setLanguageMode(languageMode)
- buffer.setText('a(\nb,\nc\n')
- await languageMode.reparsePromise
+ await nextHighlightingUpdate(languageMode)
// missing closing paren
expectTokensToEqual(editor, [
@@ -173,7 +179,7 @@ describe('TreeSitterLanguageMode', () => {
])
buffer.append(')')
- await languageMode.reparsePromise
+ await nextHighlightingUpdate(languageMode)
expectTokensToEqual(editor, [
[
{text: 'a', scopes: ['function']},
@@ -195,15 +201,16 @@ describe('TreeSitterLanguageMode', () => {
}
})
- const languageMode = new TreeSitterLanguageMode({buffer, grammar})
- buffer.setLanguageMode(languageMode)
buffer.setText([
'// abc',
'',
'a("b").c'
].join('\r\n'))
- await languageMode.reparsePromise
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+
+ await nextHighlightingUpdate(languageMode)
expectTokensToEqual(editor, [
[{text: '// abc', scopes: ['comment']}],
[{text: '', scopes: []}],
@@ -216,12 +223,12 @@ describe('TreeSitterLanguageMode', () => {
])
buffer.insert([2, 0], ' ')
- await languageMode.reparsePromise
+ await nextHighlightingUpdate(languageMode)
expectTokensToEqual(editor, [
[{text: '// abc', scopes: ['comment']}],
[{text: '', scopes: []}],
[
- {text: ' ', scopes: ['whitespace']},
+ {text: ' ', scopes: ['leading-whitespace']},
{text: 'a(', scopes: []},
{text: '"b"', scopes: ['string']},
{text: ').', scopes: []},
@@ -230,6 +237,82 @@ describe('TreeSitterLanguageMode', () => {
])
})
+ it('handles multi-line nodes with children on different lines (regression)', async () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ parser: 'tree-sitter-javascript',
+ scopes: {
+ 'template_string': 'string',
+ '"${"': 'interpolation',
+ '"}"': 'interpolation'
+ }
+ });
+
+ buffer.setText('`\na${1}\nb${2}\n`;')
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+ await nextHighlightingUpdate(languageMode)
+
+ expectTokensToEqual(editor, [
+ [
+ {text: '`', scopes: ['string']}
+ ], [
+ {text: 'a', scopes: ['string']},
+ {text: '${', scopes: ['string', 'interpolation']},
+ {text: '1', scopes: ['string']},
+ {text: '}', scopes: ['string', 'interpolation']}
+ ], [
+ {text: 'b', scopes: ['string']},
+ {text: '${', scopes: ['string', 'interpolation']},
+ {text: '2', scopes: ['string']},
+ {text: '}', scopes: ['string', 'interpolation']}
+ ],
+ [
+ {text: '`', scopes: ['string']},
+ {text: ';', scopes: []}
+ ]
+ ])
+ })
+
+ it('handles folds inside of highlighted tokens', async () => {
+ const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ parser: 'tree-sitter-javascript',
+ scopes: {
+ 'comment': 'comment',
+ 'call_expression > identifier': 'function',
+ }
+ })
+
+ buffer.setText(dedent `
+ /*
+ * Hello
+ */
+
+ hello();
+ `)
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+ await nextHighlightingUpdate(languageMode)
+
+ editor.foldBufferRange([[0, 2], [2, 0]])
+
+ expectTokensToEqual(editor, [
+ [
+ {text: '/*', scopes: ['comment']},
+ {text: '…', scopes: ['fold-marker']},
+ {text: ' */', scopes: ['comment']}
+ ],
+ [
+ {text: '', scopes: []}
+ ],
+ [
+ {text: 'hello', scopes: ['function']},
+ {text: '();', scopes: []},
+ ]
+ ])
+ })
+
describe('when the buffer changes during a parse', () => {
it('immediately parses again when the current parse completes', async () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
@@ -240,11 +323,14 @@ describe('TreeSitterLanguageMode', () => {
'new_expression > call_expression > identifier': 'constructor'
}
})
- const languageMode = new TreeSitterLanguageMode({buffer, grammar})
- buffer.setLanguageMode(languageMode)
buffer.setText('abc;');
- await languageMode.reparsePromise
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+ await nextHighlightingUpdate(languageMode)
+ await new Promise(process.nextTick)
+
expectTokensToEqual(editor, [
[
{text: 'abc', scopes: ['variable']},
@@ -269,8 +355,7 @@ describe('TreeSitterLanguageMode', () => {
],
])
- await languageMode.reparsePromise
- expect(languageMode.reparsePromise).not.toBeNull()
+ await nextHighlightingUpdate(languageMode)
expectTokensToEqual(editor, [
[
{text: 'new ', scopes: []},
@@ -279,8 +364,7 @@ describe('TreeSitterLanguageMode', () => {
],
])
- await languageMode.reparsePromise
- expect(languageMode.reparsePromise).toBeNull()
+ await nextHighlightingUpdate(languageMode)
expectTokensToEqual(editor, [
[
{text: 'new ', scopes: []},
@@ -290,13 +374,265 @@ describe('TreeSitterLanguageMode', () => {
])
})
})
+
+ describe('injectionPoints and injectionPatterns', () => {
+ let jsGrammar, htmlGrammar
+
+ beforeEach(() => {
+ jsGrammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ id: 'javascript',
+ parser: 'tree-sitter-javascript',
+ scopes: {
+ 'property_identifier': 'property',
+ 'call_expression > identifier': 'function',
+ 'template_string': 'string',
+ 'template_substitution > "${"': 'interpolation',
+ 'template_substitution > "}"': 'interpolation'
+ },
+ injectionRegExp: 'javascript',
+ injectionPoints: [HTML_TEMPLATE_LITERAL_INJECTION_POINT]
+ })
+
+ htmlGrammar = new TreeSitterGrammar(atom.grammars, htmlGrammarPath, {
+ id: 'html',
+ parser: 'tree-sitter-html',
+ scopes: {
+ fragment: 'html',
+ tag_name: 'tag',
+ attribute_name: 'attr'
+ },
+ injectionRegExp: 'html',
+ injectionPoints: [SCRIPT_TAG_INJECTION_POINT]
+ })
+ })
+
+ it('highlights code inside of injection points', async () => {
+ atom.grammars.addGrammar(jsGrammar)
+ atom.grammars.addGrammar(htmlGrammar)
+ buffer.setText('node.innerHTML = html `\na ${b}
\n`;')
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar: jsGrammar, grammars: atom.grammars})
+ buffer.setLanguageMode(languageMode)
+ await nextHighlightingUpdate(languageMode)
+ await nextHighlightingUpdate(languageMode)
+
+ expectTokensToEqual(editor, [
+ [
+ {text: 'node.', scopes: []},
+ {text: 'innerHTML', scopes: ['property']},
+ {text: ' = ', scopes: []},
+ {text: 'html', scopes: ['function']},
+ {text: ' ', scopes: []},
+ {text: '`', scopes: ['string']},
+ {text: '', scopes: ['string', 'html']}
+ ], [
+ {text: 'a ', scopes: ['string', 'html']},
+ {text: '${', scopes: ['string', 'html', 'interpolation']},
+ {text: 'b', scopes: ['string', 'html']},
+ {text: '}', scopes: ['string', 'html', 'interpolation']},
+ {text: '<', scopes: ['string', 'html']},
+ {text: 'img', scopes: ['string', 'html', 'tag']},
+ {text: ' ', scopes: ['string', 'html']},
+ {text: 'src', scopes: ['string', 'html', 'attr']},
+ {text: '="d">', scopes: ['string', 'html']}
+ ], [
+ {text: '`', scopes: ['string']},
+ {text: ';', scopes: []},
+ ],
+ ])
+
+ const range = buffer.findSync('html')
+ buffer.setTextInRange(range, 'xml')
+ await nextHighlightingUpdate(languageMode)
+ await nextHighlightingUpdate(languageMode)
+
+ expectTokensToEqual(editor, [
+ [
+ {text: 'node.', scopes: []},
+ {text: 'innerHTML', scopes: ['property']},
+ {text: ' = ', scopes: []},
+ {text: 'xml', scopes: ['function']},
+ {text: ' ', scopes: []},
+ {text: '`', scopes: ['string']}
+ ], [
+ {text: 'a ', scopes: ['string']},
+ {text: '${', scopes: ['string', 'interpolation']},
+ {text: 'b', scopes: ['string']},
+ {text: '}', scopes: ['string', 'interpolation']},
+ {text: '
', scopes: ['string']},
+ ], [
+ {text: '`', scopes: ['string']},
+ {text: ';', scopes: []},
+ ],
+ ])
+ })
+
+ it('highlights the content after injections', async () => {
+ atom.grammars.addGrammar(jsGrammar)
+ atom.grammars.addGrammar(htmlGrammar)
+ buffer.setText('\n
\n
')
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar: htmlGrammar, grammars: atom.grammars})
+ buffer.setLanguageMode(languageMode)
+ await nextHighlightingUpdate(languageMode)
+ await nextHighlightingUpdate(languageMode)
+
+ expectTokensToEqual(editor, [
+ [
+ {text: '<', scopes: ['html']},
+ {text: 'script', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']},
+ ],
+ [
+ {text: 'hello', scopes: ['html', 'function']},
+ {text: '();', scopes: ['html']},
+ ],
+ [
+ {text: '', scopes: ['html']},
+ {text: 'script', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']},
+ ],
+ [
+ {text: '<', scopes: ['html']},
+ {text: 'div', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']},
+ ],
+ [
+ {text: '', scopes: ['html']},
+ {text: 'div', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']},
+ ]
+ ])
+ })
+
+ it('updates buffers highlighting when a grammar with injectionRegExp is added', async () => {
+ atom.grammars.addGrammar(jsGrammar)
+
+ buffer.setText('node.innerHTML = html `\na ${b}
\n`;')
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar: jsGrammar, grammars: atom.grammars})
+ buffer.setLanguageMode(languageMode)
+
+ await nextHighlightingUpdate(languageMode)
+ expectTokensToEqual(editor, [
+ [
+ {text: 'node.', scopes: []},
+ {text: 'innerHTML', scopes: ['property']},
+ {text: ' = ', scopes: []},
+ {text: 'html', scopes: ['function']},
+ {text: ' ', scopes: []},
+ {text: '`', scopes: ['string']}
+ ], [
+ {text: 'a ', scopes: ['string']},
+ {text: '${', scopes: ['string', 'interpolation']},
+ {text: 'b', scopes: ['string']},
+ {text: '}', scopes: ['string', 'interpolation']},
+ {text: '
', scopes: ['string']},
+ ], [
+ {text: '`', scopes: ['string']},
+ {text: ';', scopes: []},
+ ],
+ ])
+
+ atom.grammars.addGrammar(htmlGrammar)
+ await nextHighlightingUpdate(languageMode)
+ expectTokensToEqual(editor, [
+ [
+ {text: 'node.', scopes: []},
+ {text: 'innerHTML', scopes: ['property']},
+ {text: ' = ', scopes: []},
+ {text: 'html', scopes: ['function']},
+ {text: ' ', scopes: []},
+ {text: '`', scopes: ['string']},
+ {text: '', scopes: ['string', 'html']}
+ ], [
+ {text: 'a ', scopes: ['string', 'html']},
+ {text: '${', scopes: ['string', 'html', 'interpolation']},
+ {text: 'b', scopes: ['string', 'html']},
+ {text: '}', scopes: ['string', 'html', 'interpolation']},
+ {text: '<', scopes: ['string', 'html']},
+ {text: 'img', scopes: ['string', 'html', 'tag']},
+ {text: ' ', scopes: ['string', 'html']},
+ {text: 'src', scopes: ['string', 'html', 'attr']},
+ {text: '="d">', scopes: ['string', 'html']}
+ ], [
+ {text: '`', scopes: ['string']},
+ {text: ';', scopes: []},
+ ],
+ ])
+ })
+
+ it('handles injections that intersect', async () => {
+ const ejsGrammar = new TreeSitterGrammar(atom.grammars, ejsGrammarPath, {
+ id: 'ejs',
+ parser: 'tree-sitter-embedded-template',
+ scopes: {
+ '"<%="': 'directive',
+ '"%>"': 'directive',
+ },
+ injectionPoints: [
+ {
+ type: 'template',
+ language (node) { return 'javascript' },
+ content (node) { return node.descendantsOfType('code') }
+ },
+ {
+ type: 'template',
+ language (node) { return 'html' },
+ content (node) { return node.descendantsOfType('content') }
+ }
+ ]
+ })
+
+ atom.grammars.addGrammar(jsGrammar)
+ atom.grammars.addGrammar(htmlGrammar)
+
+ buffer.setText('\n\n')
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar: ejsGrammar, grammars: atom.grammars})
+ buffer.setLanguageMode(languageMode)
+
+ // 4 parses: EJS, HTML, template JS, script tag JS
+ await nextHighlightingUpdate(languageMode)
+ await nextHighlightingUpdate(languageMode)
+ await nextHighlightingUpdate(languageMode)
+ await nextHighlightingUpdate(languageMode)
+
+ expectTokensToEqual(editor, [
+ [
+ {text: '<', scopes: ['html']},
+ {text: 'body', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']}
+ ],
+ [
+ {text: '<', scopes: ['html']},
+ {text: 'script', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']}
+ ],
+ [
+ {text: 'b', scopes: ['html', 'function']},
+ {text: '(', scopes: ['html']},
+ {text: '<%=', scopes: ['html', 'directive']},
+ {text: ' c.', scopes: ['html']},
+ {text: 'd', scopes: ['html', 'property']},
+ {text: ' ', scopes: ['html']},
+ {text: '%>', scopes: ['html', 'directive']},
+ {text: ')', scopes: ['html']},
+ ],
+ [
+ {text: '', scopes: ['html']},
+ {text: 'script', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']}
+ ],
+ [
+ {text: '', scopes: ['html']},
+ {text: 'body', scopes: ['html', 'tag']},
+ {text: '>', scopes: ['html']}
+ ],
+ ])
+ })
+ })
})
describe('folding', () => {
- beforeEach(() => {
- editor.displayLayer.reset({foldCharacter: '…'})
- })
-
it('can fold nodes that start and end with specified tokens', async () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
@@ -312,8 +648,6 @@ describe('TreeSitterLanguageMode', () => {
]
})
- const languageMode = new TreeSitterLanguageMode({buffer, grammar})
- buffer.setLanguageMode(languageMode)
buffer.setText(dedent `
module.exports =
class A {
@@ -324,9 +658,10 @@ describe('TreeSitterLanguageMode', () => {
}
}
`)
- await languageMode.reparsePromise
- editor.screenLineForScreenRow(0)
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+ await nextHighlightingUpdate(languageMode)
expect(editor.isFoldableAtBufferRow(0)).toBe(false)
expect(editor.isFoldableAtBufferRow(1)).toBe(true)
@@ -375,8 +710,6 @@ describe('TreeSitterLanguageMode', () => {
]
})
- const languageMode = new TreeSitterLanguageMode({buffer, grammar})
- buffer.setLanguageMode(languageMode)
buffer.setText(dedent `
const element1 = {
world
`)
- await languageMode.reparsePromise
- editor.screenLineForScreenRow(0)
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+ await nextHighlightingUpdate(languageMode)
expect(editor.isFoldableAtBufferRow(0)).toBe(true)
expect(editor.isFoldableAtBufferRow(1)).toBe(false)
@@ -427,8 +761,6 @@ describe('TreeSitterLanguageMode', () => {
]
})
- const languageMode = new TreeSitterLanguageMode({buffer, grammar})
- buffer.setLanguageMode(languageMode)
buffer.setText(dedent `
/**
* Important
@@ -437,9 +769,10 @@ describe('TreeSitterLanguageMode', () => {
Also important
*/
`)
- await languageMode.reparsePromise
- editor.screenLineForScreenRow(0)
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+ await nextHighlightingUpdate(languageMode)
expect(editor.isFoldableAtBufferRow(0)).toBe(true)
expect(editor.isFoldableAtBufferRow(1)).toBe(false)
@@ -488,8 +821,6 @@ describe('TreeSitterLanguageMode', () => {
]
})
- const languageMode = new TreeSitterLanguageMode({buffer, grammar})
- buffer.setLanguageMode(languageMode)
buffer.setText(dedent `
#ifndef FOO_H_
#define FOO_H_
@@ -513,9 +844,10 @@ describe('TreeSitterLanguageMode', () => {
#endif
`)
- await languageMode.reparsePromise
- editor.screenLineForScreenRow(0)
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+ await nextHighlightingUpdate(languageMode)
editor.foldBufferRow(3)
expect(getDisplayText(editor)).toBe(dedent `
@@ -588,8 +920,6 @@ describe('TreeSitterLanguageMode', () => {
]
})
- const languageMode = new TreeSitterLanguageMode({buffer, grammar})
- buffer.setLanguageMode(languageMode)
buffer.setText(dedent `
@@ -597,7 +927,9 @@ describe('TreeSitterLanguageMode', () => {
`)
- await languageMode.reparsePromise
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar})
+ buffer.setLanguageMode(languageMode)
+ await nextHighlightingUpdate(languageMode)
// Void elements have only one child
expect(editor.isFoldableAtBufferRow(1)).toBe(false)
@@ -611,7 +943,7 @@ describe('TreeSitterLanguageMode', () => {
})
describe('when folding a node that ends with a line break', () => {
- it('ends the fold at the end of the previous line', () => {
+ it('ends the fold at the end of the previous line', async () => {
const grammar = new TreeSitterGrammar(atom.grammars, pythonGrammarPath, {
parser: 'tree-sitter-python',
folds: [
@@ -631,9 +963,9 @@ describe('TreeSitterLanguageMode', () => {
print 'c'
print 'd'
`)
- buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
- editor.screenLineForScreenRow(0)
+ buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
+ await nextHighlightingUpdate(buffer.getLanguageMode())
editor.foldBufferRow(0)
expect(getDisplayText(editor)).toBe(dedent `
@@ -645,19 +977,95 @@ describe('TreeSitterLanguageMode', () => {
`)
})
})
+
+ it('folds code in injected languages', async () => {
+ const htmlGrammar = new TreeSitterGrammar(atom.grammars, htmlGrammarPath, {
+ id: 'html',
+ parser: 'tree-sitter-html',
+ scopes: {},
+ folds: [{
+ type: ['element', 'raw_element'],
+ start: {index: 0},
+ end: {index: -1}
+ }],
+ injectionRegExp: 'html'
+ })
+
+ const jsGrammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ id: 'javascript',
+ parser: 'tree-sitter-javascript',
+ scopes: {},
+ folds: [{
+ type: ['template_string'],
+ start: {index: 0},
+ end: {index: -1},
+ },
+ {
+ start: {index: 0, type: '('},
+ end: {index: -1, type: ')'}
+ }],
+ injectionRegExp: 'javascript',
+ injectionPoints: [HTML_TEMPLATE_LITERAL_INJECTION_POINT]
+ })
+
+ atom.grammars.addGrammar(htmlGrammar)
+
+ buffer.setText(
+ `a = html \`
+
+ c\${def(
+ 1,
+ 2,
+ 3,
+ )}e\${f}g
+
+ \`
+ `
+ )
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar: jsGrammar, grammars: atom.grammars})
+ buffer.setLanguageMode(languageMode)
+
+ await nextHighlightingUpdate(languageMode)
+ await nextHighlightingUpdate(languageMode)
+
+ editor.foldBufferRow(2)
+ expect(getDisplayText(editor)).toBe(
+ `a = html \`
+
+ c\${def(…)}e\${f}g
+
+ \`
+ `
+ )
+
+ editor.foldBufferRow(1)
+ expect(getDisplayText(editor)).toBe(
+ `a = html \`
+ …
+
+ \`
+ `
+ )
+
+ editor.foldBufferRow(0)
+ expect(getDisplayText(editor)).toBe(
+ `a = html \`…\`
+ `
+ )
+ })
})
describe('.scopeDescriptorForPosition', () => {
- it('returns a scope descriptor representing the given position in the syntax tree', () => {
+ it('returns a scope descriptor representing the given position in the syntax tree', async () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
id: 'javascript',
parser: 'tree-sitter-javascript'
})
buffer.setText('foo({bar: baz});')
- buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
- editor.screenLineForScreenRow(0)
+ buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
+ await nextHighlightingUpdate(buffer.getLanguageMode())
expect(editor.scopeDescriptorForBufferPosition([0, 6]).getScopesArray()).toEqual([
'javascript',
'program',
@@ -669,10 +1077,65 @@ describe('TreeSitterLanguageMode', () => {
'property_identifier'
])
})
+
+ it('includes nodes in injected syntax trees', async () => {
+ const jsGrammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ id: 'javascript',
+ parser: 'tree-sitter-javascript',
+ scopes: {},
+ injectionRegExp: 'javascript',
+ injectionPoints: [HTML_TEMPLATE_LITERAL_INJECTION_POINT]
+ })
+
+ const htmlGrammar = new TreeSitterGrammar(atom.grammars, htmlGrammarPath, {
+ id: 'html',
+ parser: 'tree-sitter-html',
+ scopes: {},
+ injectionRegExp: 'html',
+ injectionPoints: [SCRIPT_TAG_INJECTION_POINT]
+ })
+
+ atom.grammars.addGrammar(jsGrammar)
+ atom.grammars.addGrammar(htmlGrammar)
+
+ buffer.setText(`
+
+
+
+ `)
+
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar: htmlGrammar, grammars: atom.grammars})
+ buffer.setLanguageMode(languageMode)
+ await nextHighlightingUpdate(languageMode)
+ await nextHighlightingUpdate(languageMode)
+ await nextHighlightingUpdate(languageMode)
+
+ const position = buffer.findSync('name').start
+ expect(languageMode.scopeDescriptorForPosition(position).getScopesArray()).toEqual([
+ 'html',
+ 'fragment',
+ 'element',
+ 'raw_element',
+ 'raw_text',
+ 'program',
+ 'expression_statement',
+ 'call_expression',
+ 'template_string',
+ 'fragment',
+ 'element',
+ 'template_substitution',
+ 'member_expression',
+ 'property_identifier'
+ ])
+ })
})
describe('TextEditor.selectLargerSyntaxNode and .selectSmallerSyntaxNode', () => {
- it('expands and contract the selection based on the syntax tree', () => {
+ it('expands and contracts the selection based on the syntax tree', async () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
scopes: {'program': 'source'}
@@ -684,9 +1147,9 @@ describe('TreeSitterLanguageMode', () => {
g()
}
`)
- buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
- editor.screenLineForScreenRow(0)
+ buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
+ await nextHighlightingUpdate(buffer.getLanguageMode())
editor.setCursorBufferPosition([1, 3])
editor.selectLargerSyntaxNode()
@@ -711,9 +1174,72 @@ describe('TreeSitterLanguageMode', () => {
editor.selectSmallerSyntaxNode()
expect(editor.getSelectedBufferRange()).toEqual([[1, 3], [1, 3]])
})
+
+ it('handles injected languages', async () => {
+ const jsGrammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
+ id: 'javascript',
+ parser: 'tree-sitter-javascript',
+ scopes: {
+ 'property_identifier': 'property',
+ 'call_expression > identifier': 'function',
+ 'template_string': 'string',
+ 'template_substitution > "${"': 'interpolation',
+ 'template_substitution > "}"': 'interpolation'
+ },
+ injectionRegExp: 'javascript',
+ injectionPoints: [HTML_TEMPLATE_LITERAL_INJECTION_POINT]
+ })
+
+ const htmlGrammar = new TreeSitterGrammar(atom.grammars, htmlGrammarPath, {
+ id: 'html',
+ parser: 'tree-sitter-html',
+ scopes: {
+ fragment: 'html',
+ tag_name: 'tag',
+ attribute_name: 'attr'
+ },
+ injectionRegExp: 'html'
+ })
+
+ atom.grammars.addGrammar(htmlGrammar)
+
+ buffer.setText('a = html ` c${def()}e${f}g `')
+ const languageMode = new TreeSitterLanguageMode({buffer, grammar: jsGrammar, grammars: atom.grammars})
+ buffer.setLanguageMode(languageMode)
+
+ await nextHighlightingUpdate(languageMode)
+ await nextHighlightingUpdate(languageMode)
+
+ editor.setCursorBufferPosition({row: 0, column: buffer.getText().indexOf('ef()')})
+ editor.selectLargerSyntaxNode()
+ expect(editor.getSelectedText()).toBe('def')
+ editor.selectLargerSyntaxNode()
+ expect(editor.getSelectedText()).toBe('def()')
+ editor.selectLargerSyntaxNode()
+ expect(editor.getSelectedText()).toBe('${def()}')
+ editor.selectLargerSyntaxNode()
+ expect(editor.getSelectedText()).toBe('c${def()}e${f}g')
+ editor.selectLargerSyntaxNode()
+ expect(editor.getSelectedText()).toBe('c${def()}e${f}g')
+ editor.selectLargerSyntaxNode()
+ expect(editor.getSelectedText()).toBe(' c${def()}e${f}g ')
+ editor.selectLargerSyntaxNode()
+ expect(editor.getSelectedText()).toBe('` c${def()}e${f}g `')
+ editor.selectLargerSyntaxNode()
+ expect(editor.getSelectedText()).toBe('html ` c${def()}e${f}g `')
+ })
})
})
+function nextHighlightingUpdate (languageMode) {
+ return new Promise(resolve => {
+ const subscription = languageMode.onDidChangeHighlighting(() => {
+ subscription.dispose()
+ resolve()
+ })
+ })
+}
+
function getDisplayText (editor) {
return editor.displayLayer.getText()
}
@@ -740,7 +1266,7 @@ function expectTokensToEqual (editor, expectedTokenLines) {
text,
scopes: scopes.map(scope => scope
.split(' ')
- .map(className => className.slice('syntax--'.length))
+ .map(className => className.replace('syntax--', ''))
.join(' '))
}))
}
@@ -760,3 +1286,21 @@ function expectTokensToEqual (editor, expectedTokenLines) {
// due to subsequent edits can be tested.
editor.displayLayer.getScreenLines(0, Infinity)
}
+
+const HTML_TEMPLATE_LITERAL_INJECTION_POINT = {
+ type: 'call_expression',
+ language (node) {
+ if (node.lastChild.type === 'template_string' && node.firstChild.type === 'identifier') {
+ return node.firstChild.text
+ }
+ },
+ content (node) {
+ return node.lastChild
+ }
+}
+
+const SCRIPT_TAG_INJECTION_POINT = {
+ type: 'raw_element',
+ language () { return 'javascript' },
+ content (node) { return node.child(1) }
+}
diff --git a/src/grammar-registry.js b/src/grammar-registry.js
index a2a5917da..101a38007 100644
--- a/src/grammar-registry.js
+++ b/src/grammar-registry.js
@@ -169,7 +169,7 @@ class GrammarRegistry {
languageModeForGrammarAndBuffer (grammar, buffer) {
if (grammar instanceof TreeSitterGrammar) {
- return new TreeSitterLanguageMode({grammar, buffer, config: this.config})
+ return new TreeSitterLanguageMode({grammar, buffer, config: this.config, grammars: this})
} else {
return new TextMateLanguageMode({grammar, buffer, config: this.config})
}
@@ -291,8 +291,9 @@ class GrammarRegistry {
forEachGrammar (callback) {
this.textmateRegistry.grammars.forEach(callback)
- for (let grammarId in this.treeSitterGrammarsById) {
- callback(this.treeSitterGrammarsById[grammarId])
+ for (const grammarId in this.treeSitterGrammarsById) {
+ const grammar = this.treeSitterGrammarsById[grammarId]
+ if (grammar.id) callback(grammar)
}
}
@@ -347,26 +348,23 @@ class GrammarRegistry {
this.grammarScoresByBuffer.forEach((score, buffer) => {
const languageMode = buffer.getLanguageMode()
- if (grammar.injectionSelector) {
- if (languageMode.hasTokenForSelector(grammar.injectionSelector)) {
- languageMode.retokenizeLines()
- }
- return
- }
-
const languageOverride = this.languageOverridesByBufferId.get(buffer.id)
if ((grammar.id === buffer.getLanguageMode().getLanguageId() ||
grammar.id === languageOverride)) {
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
+ return
} else if (!languageOverride) {
const score = this.getGrammarScore(grammar, buffer.getPath(), getGrammarSelectionContent(buffer))
const currentScore = this.grammarScoresByBuffer.get(buffer)
if (currentScore == null || score > currentScore) {
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
this.grammarScoresByBuffer.set(buffer, score)
+ return
}
}
+
+ languageMode.updateForInjection(grammar)
})
}
@@ -391,6 +389,32 @@ class GrammarRegistry {
return this.textmateRegistry.onDidUpdateGrammar(callback)
}
+ // Experimental: Specify a type of syntax node that may embed other languages.
+ //
+ // * `grammarId` The {String} id of the parent language
+ // * `injectionPoint` An {Object} with the following keys:
+ // * `type` The {String} type of syntax node that may embed other languages
+ // * `language` A {Function} that is called with syntax nodes of the specified `type` and
+ // returns a {String} that will be tested against other grammars' `injectionRegExp` in
+ // order to determine what language should be embedded.
+ // * `content` A {Function} that is called with syntax nodes of the specified `type` and
+ // returns another syntax node or array of syntax nodes that contain the embedded source code.
+ addInjectionPoint (grammarId, injectionPoint) {
+ const grammar = this.treeSitterGrammarsById[grammarId]
+ if (grammar) {
+ grammar.injectionPoints.push(injectionPoint)
+ } else {
+ this.treeSitterGrammarsById[grammarId] = {
+ injectionPoints: [injectionPoint]
+ }
+ }
+ return new Disposable(() => {
+ const grammar = this.treeSitterGrammarsById[grammarId]
+ const index = grammar.injectionPoints.indexOf(injectionPoint)
+ if (index !== -1) grammar.injectionPoints.splice(index, 1)
+ })
+ }
+
get nullGrammar () {
return this.textmateRegistry.nullGrammar
}
@@ -409,12 +433,14 @@ class GrammarRegistry {
addGrammar (grammar) {
if (grammar instanceof TreeSitterGrammar) {
+ const existingParams = this.treeSitterGrammarsById[grammar.id] || {}
this.treeSitterGrammarsById[grammar.id] = grammar
if (grammar.legacyScopeName) {
this.config.setLegacyScopeAliasForNewScope(grammar.id, grammar.legacyScopeName)
this.textMateScopeNamesByTreeSitterLanguageId.set(grammar.id, grammar.legacyScopeName)
this.treeSitterLanguageIdsByTextMateScopeName.set(grammar.legacyScopeName, grammar.id)
}
+ if (existingParams.injectionPoints) grammar.injectionPoints.push(...existingParams.injectionPoints)
this.grammarAddedOrUpdated(grammar)
return new Disposable(() => this.removeGrammar(grammar))
} else {
@@ -515,6 +541,15 @@ class GrammarRegistry {
return this.textmateRegistry.scopeForId(id)
}
+ treeSitterGrammarForLanguageString (languageString) {
+ for (const id in this.treeSitterGrammarsById) {
+ const grammar = this.treeSitterGrammarsById[id]
+ if (grammar.injectionRegExp && grammar.injectionRegExp.test(languageString)) {
+ return grammar
+ }
+ }
+ }
+
normalizeLanguageId (languageId) {
if (this.config.get('core.useTreeSitterParsers')) {
return this.treeSitterLanguageIdsByTextMateScopeName.get(languageId) || languageId
diff --git a/src/initialize-application-window.coffee b/src/initialize-application-window.coffee
index f8f670cf5..e3e24eb87 100644
--- a/src/initialize-application-window.coffee
+++ b/src/initialize-application-window.coffee
@@ -36,6 +36,8 @@ if global.isGeneratingSnapshot
require('image-view')
require('incompatible-packages')
require('keybinding-resolver')
+ require('language-html')
+ require('language-javascript')
require('line-ending-selector')
require('link')
require('markdown-preview')
diff --git a/src/project.js b/src/project.js
index 8c98224d0..4e51efcf8 100644
--- a/src/project.js
+++ b/src/project.js
@@ -685,9 +685,6 @@ class Project extends Model {
}
this.grammarRegistry.autoAssignLanguageMode(buffer)
- if (buffer.languageMode.initialize) {
- await buffer.languageMode.initialize()
- }
this.addBuffer(buffer)
return buffer
diff --git a/src/test.ejs b/src/test.ejs
new file mode 100644
index 000000000..7b93c31b3
--- /dev/null
+++ b/src/test.ejs
@@ -0,0 +1,9 @@
+
+
+<% if something() { %>
+
+ <%= html `ok how about this` %>
+
+<% } %>
+
+
diff --git a/src/text-mate-language-mode.js b/src/text-mate-language-mode.js
index 152636ab7..9abe55ecb 100644
--- a/src/text-mate-language-mode.js
+++ b/src/text-mate-language-mode.js
@@ -235,15 +235,18 @@ class TextMateLanguageMode {
return this.buffer.getTextInRange([[0, 0], [10, 0]])
}
- hasTokenForSelector (selector) {
+ updateForInjection (grammar) {
+ if (!grammar.injectionSelector) return
for (const tokenizedLine of this.tokenizedLines) {
if (tokenizedLine) {
for (let token of tokenizedLine.tokens) {
- if (selector.matches(token.scopes)) return true
+ if (grammar.injectionSelector.matches(token.scopes)) {
+ this.retokenizeLines()
+ return
+ }
}
}
}
- return false
}
retokenizeLines () {
diff --git a/src/tree-sitter-grammar.js b/src/tree-sitter-grammar.js
index d00344fb1..acea24213 100644
--- a/src/tree-sitter-grammar.js
+++ b/src/tree-sitter-grammar.js
@@ -10,6 +10,7 @@ class TreeSitterGrammar {
this.name = params.name
this.legacyScopeName = params.legacyScopeName
if (params.contentRegExp) this.contentRegExp = new RegExp(params.contentRegExp)
+ if (params.injectionRegExp) this.injectionRegExp = new RegExp(params.injectionRegExp)
this.folds = params.folds || []
@@ -28,6 +29,7 @@ class TreeSitterGrammar {
this.scopeMap = new SyntaxScopeMap(scopeSelectors)
this.fileTypes = params.fileTypes
+ this.injectionPoints = params.injectionPoints || []
// TODO - When we upgrade to a new enough version of node, use `require.resolve`
// with the new `paths` option instead of this private API.
diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js
index 5d8e743ed..7d0377f28 100644
--- a/src/tree-sitter-language-mode.js
+++ b/src/tree-sitter-language-mode.js
@@ -1,119 +1,115 @@
const Parser = require('tree-sitter')
const {Point, Range} = require('text-buffer')
+const {Patch} = require('superstring')
const {Emitter, Disposable} = require('event-kit')
const ScopeDescriptor = require('./scope-descriptor')
const TokenizedLine = require('./tokenized-line')
const TextMateLanguageMode = require('./text-mate-language-mode')
let nextId = 0
+const MAX_RANGE = new Range(Point.ZERO, Point.INFINITY).freeze()
+const PARSER_POOL = []
-module.exports =
class TreeSitterLanguageMode {
- constructor ({buffer, grammar, config}) {
+ static _patchSyntaxNode () {
+ if (!Parser.SyntaxNode.prototype.hasOwnProperty('text')) {
+ Object.defineProperty(Parser.SyntaxNode.prototype, 'text', {
+ get () {
+ return this.tree.buffer.getTextInRange(new Range(this.startPosition, this.endPosition))
+ }
+ })
+ }
+ }
+
+ constructor ({buffer, grammar, config, grammars}) {
+ TreeSitterLanguageMode._patchSyntaxNode()
this.id = nextId++
this.buffer = buffer
this.grammar = grammar
this.config = config
+ this.grammarRegistry = grammars
this.parser = new Parser()
- this.parser.setLanguage(grammar.languageModule)
- this.tree = null
+ this.rootLanguageLayer = new LanguageLayer(this, grammar)
+ this.injectionsMarkerLayer = buffer.addMarkerLayer()
+
this.rootScopeDescriptor = new ScopeDescriptor({scopes: [this.grammar.id]})
this.emitter = new Emitter()
this.isFoldableCache = []
this.hasQueuedParse = false
- this.changeListsSinceCurrentParse = []
- this.subscription = this.buffer.onDidChangeText(async ({changes}) => {
- if (this.reparsePromise) {
- this.changeListsSinceCurrentParse.push(changes)
- } else {
- this.reparsePromise = this.reparse()
+
+ this.grammarForLanguageString = this.grammarForLanguageString.bind(this)
+ this.emitRangeUpdate = this.emitRangeUpdate.bind(this)
+
+ this.subscription = this.buffer.onDidChangeText(({changes}) => {
+ for (let i = changes.length - 1; i >= 0; i--) {
+ const {oldRange, newRange} = changes[i]
+ const startRow = oldRange.start.row
+ const oldEndRow = oldRange.end.row
+ const newEndRow = newRange.end.row
+ this.isFoldableCache.splice(
+ startRow,
+ oldEndRow - startRow,
+ ...new Array(newEndRow - startRow)
+ )
}
+
+ this.rootLanguageLayer.update(null)
})
+ this.rootLanguageLayer.update(null)
+
// TODO: Remove this once TreeSitterLanguageMode implements its own auto-indentation system. This
// is temporarily needed in order to delegate to the TextMateLanguageMode's auto-indent system.
this.regexesByPattern = {}
}
destroy () {
+ this.injectionsMarkerLayer.destroy()
this.subscription.dispose()
- this.tree = null
+ this.rootLanguageLayer = null
this.parser = null
}
- async initialize () {
- this.tree = await this.parser.parseTextBuffer(this.buffer.buffer)
- }
-
- ensureParseTree () {
- if (!this.tree) {
- this.tree = this.parser.parseTextBufferSync(this.buffer.buffer)
- }
- }
-
getLanguageId () {
return this.grammar.id
}
bufferDidChange (change) {
- this.ensureParseTree()
- const {oldRange, newRange} = change
- const startRow = oldRange.start.row
- const oldEndRow = oldRange.end.row
- const newEndRow = newRange.end.row
- this.isFoldableCache.splice(startRow, oldEndRow - startRow, ...new Array(newEndRow - startRow))
- this.tree.edit(this.treeEditForBufferChange(change))
+ this.rootLanguageLayer.handleTextChange(change)
+ for (const marker of this.injectionsMarkerLayer.getMarkers()) {
+ marker.languageLayer.handleTextChange(change)
+ }
+ }
+
+ async parse (language, oldTree, ranges) {
+ const parser = PARSER_POOL.pop() || new Parser()
+ parser.setLanguage(language)
+ const newTree = await parser.parseTextBuffer(this.buffer.buffer, oldTree, {
+ syncOperationLimit: 1000,
+ includedRanges: ranges
+ })
+ PARSER_POOL.push(parser)
+ return newTree
+ }
+
+ get tree () {
+ return this.rootLanguageLayer.tree
+ }
+
+ updateForInjection (grammar) {
+ this.rootLanguageLayer.updateInjections(grammar)
}
/*
Section - Highlighting
*/
- treeEditForBufferChange ({oldRange, newRange, oldText, newText}) {
- const startIndex = this.buffer.characterIndexForPosition(oldRange.start)
- return {
- startIndex,
- oldEndIndex: startIndex + oldText.length,
- newEndIndex: startIndex + newText.length,
- startPosition: oldRange.start,
- oldEndPosition: oldRange.end,
- newEndPosition: newRange.end
- }
- }
-
- async reparse () {
- const tree = await this.parser.parseTextBuffer(this.buffer.buffer, this.tree, {
- syncOperationLimit: 1000
- })
- const invalidatedRanges = this.tree.getChangedRanges(tree)
-
- for (let i = 0, n = invalidatedRanges.length; i < n; i++) {
- const range = invalidatedRanges[i]
- const startRow = range.start.row
- const endRow = range.end.row
- for (let row = startRow; row < endRow; row++) {
- this.isFoldableCache[row] = undefined
- }
- this.emitter.emit('did-change-highlighting', range)
- }
-
- this.tree = tree
- if (this.changeListsSinceCurrentParse.length > 0) {
- for (const changeList of this.changeListsSinceCurrentParse) {
- for (let i = changeList.length - 1; i >= 0; i--) {
- this.tree.edit(this.treeEditForBufferChange(changeList[i]))
- }
- }
- this.changeListsSinceCurrentParse.length = 0
- this.reparsePromise = this.reparse()
- } else {
- this.reparsePromise = null
- }
- }
-
buildHighlightIterator () {
- this.ensureParseTree()
- return new TreeSitterHighlightIterator(this, this.tree.walk())
+ const layerIterators = [
+ this.rootLanguageLayer.buildHighlightIterator(),
+ ...this.injectionsMarkerLayer.getMarkers().map(m => m.languageLayer.buildHighlightIterator())
+ ]
+ return new HighlightIterator(this, layerIterators)
}
onDidChangeHighlighting (callback) {
@@ -189,14 +185,17 @@ class TreeSitterLanguageMode {
return this.getFoldableRangesAtIndentLevel(null)
}
+ /**
+ * TODO: Make this method generate folds for nested languages (currently,
+ * folds are only generated for the root language layer).
+ */
getFoldableRangesAtIndentLevel (goalLevel) {
- this.ensureParseTree()
let result = []
let stack = [{node: this.tree.rootNode, level: 0}]
while (stack.length > 0) {
const {node, level} = stack.pop()
- const range = this.getFoldableRangeForNode(node)
+ const range = this.getFoldableRangeForNode(node, this.grammar)
if (range) {
if (goalLevel == null || level === goalLevel) {
let updatedExistingRange = false
@@ -236,25 +235,49 @@ class TreeSitterLanguageMode {
}
getFoldableRangeContainingPoint (point, tabLength, existenceOnly = false) {
- this.ensureParseTree()
- let node = this.tree.rootNode.descendantForPosition(this.buffer.clipPosition(point))
- while (node) {
- if (existenceOnly && node.startPosition.row < point.row) break
- if (node.endPosition.row > point.row) {
- const range = this.getFoldableRangeForNode(node, existenceOnly)
- if (range) return range
+ if (!this.tree) return null
+
+ let smallestRange
+ this._forEachTreeWithRange(new Range(point, point), (tree, grammar) => {
+ let node = tree.rootNode.descendantForPosition(this.buffer.clipPosition(point))
+ while (node) {
+ if (existenceOnly && node.startPosition.row < point.row) return
+ if (node.endPosition.row > point.row) {
+ const range = this.getFoldableRangeForNode(node, grammar)
+ if (range && rangeIsSmaller(range, smallestRange)) {
+ smallestRange = range
+ return
+ }
+ }
+ node = node.parent
}
- node = node.parent
+ })
+
+ return existenceOnly
+ ? smallestRange && smallestRange.start.row === point.row
+ : smallestRange
+ }
+
+ _forEachTreeWithRange (range, callback) {
+ callback(this.rootLanguageLayer.tree, this.rootLanguageLayer.grammar)
+
+ const injectionMarkers = this.injectionsMarkerLayer.findMarkers({
+ intersectsRange: range
+ })
+
+ for (const injectionMarker of injectionMarkers) {
+ const {tree, grammar} = injectionMarker.languageLayer
+ if (tree) callback(tree, grammar)
}
}
- getFoldableRangeForNode (node, existenceOnly) {
+ getFoldableRangeForNode (node, grammar, existenceOnly) {
const {children, type: nodeType} = node
const childCount = children.length
let childTypes
- for (var i = 0, {length} = this.grammar.folds; i < length; i++) {
- const foldEntry = this.grammar.folds[i]
+ for (var i = 0, {length} = grammar.folds; i < length; i++) {
+ const foldEntry = grammar.folds[i]
if (foldEntry.type) {
if (typeof foldEntry.type === 'string') {
@@ -322,17 +345,24 @@ class TreeSitterLanguageMode {
}
/*
- Syntax Tree APIs
+ Section - Syntax Tree APIs
*/
getRangeForSyntaxNodeContainingRange (range) {
const startIndex = this.buffer.characterIndexForPosition(range.start)
const endIndex = this.buffer.characterIndexForPosition(range.end)
- let node = this.tree.rootNode.descendantForIndex(startIndex, endIndex - 1)
- while (node && node.startIndex === startIndex && node.endIndex === endIndex) {
- node = node.parent
- }
- if (node) return new Range(node.startPosition, node.endPosition)
+ const searchEndIndex = Math.max(0, endIndex - 1)
+
+ let smallestNode
+ this._forEachTreeWithRange(range, tree => {
+ let node = tree.rootNode.descendantForIndex(startIndex, searchEndIndex)
+ while (node && !nodeContainsIndices(node, startIndex, endIndex)) {
+ node = node.parent
+ }
+ if (nodeIsSmaller(node, smallestNode)) smallestNode = node
+ })
+
+ if (smallestNode) return rangeForNode(smallestNode)
}
bufferRangeForScopeAtPosition (position) {
@@ -358,53 +388,348 @@ class TreeSitterLanguageMode {
}
scopeDescriptorForPosition (point) {
- this.ensureParseTree()
+ if (!this.tree) return this.rootScopeDescriptor
point = Point.fromObject(point)
- let node = this.tree.rootNode.descendantForPosition(point)
- // Don't include anonymous token types like '(' because they prevent scope chains
- // from being parsed as CSS selectors by the `slick` parser. Other css selector
- // parsers like `postcss-selector-parser` do allow arbitrary quoted strings in
- // selectors.
- if (!node.isNamed) node = node.parent
+ const iterators = []
+ this._forEachTreeWithRange(new Range(point, point), tree => {
+ const rootStartIndex = tree.rootNode.startIndex
+ let node = tree.rootNode.descendantForPosition(point)
- const result = []
- while (node) {
- result.push(node.type)
- node = node.parent
+ // Don't include anonymous token types like '(' because they prevent scope chains
+ // from being parsed as CSS selectors by the `slick` parser. Other css selector
+ // parsers like `postcss-selector-parser` do allow arbitrary quoted strings in
+ // selectors.
+ if (!node.isNamed) node = node.parent
+ iterators.push({node, rootStartIndex})
+ })
+
+ iterators.sort(compareScopeDescriptorIterators)
+
+ const scopes = []
+ for (;;) {
+ const {length} = iterators
+ if (!length) break
+ const iterator = iterators[length - 1]
+ scopes.push(iterator.node.type)
+ iterator.node = iterator.node.parent
+ if (iterator.node) {
+ let i = length - 1
+ while (i > 0 && compareScopeDescriptorIterators(iterator, iterators[i - 1]) < 0) i--
+ if (i < length - 1) iterators.splice(i, 0, iterators.pop())
+ } else {
+ iterators.pop()
+ }
}
- result.push(this.grammar.id)
- return new ScopeDescriptor({scopes: result.reverse()})
- }
- hasTokenForSelector (scopeSelector) {
- return false
+ scopes.push(this.grammar.id)
+ return new ScopeDescriptor({scopes: scopes.reverse()})
}
getGrammar () {
return this.grammar
}
+
+ /*
+ Section - Private
+ */
+
+ grammarForLanguageString (languageString) {
+ return this.grammarRegistry.treeSitterGrammarForLanguageString(languageString)
+ }
+
+ emitRangeUpdate (range) {
+ const startRow = range.start.row
+ const endRow = range.end.row
+ for (let row = startRow; row < endRow; row++) {
+ this.isFoldableCache[row] = undefined
+ }
+ this.emitter.emit('did-change-highlighting', range)
+ }
}
-class TreeSitterHighlightIterator {
- constructor (languageMode, treeCursor) {
+class LanguageLayer {
+ constructor (languageMode, grammar, contentChildTypes) {
this.languageMode = languageMode
+ this.grammar = grammar
+ this.tree = null
+ this.currentParsePromise = null
+ this.patchSinceCurrentParseStarted = null
+ this.contentChildTypes = contentChildTypes
+ }
+
+ buildHighlightIterator () {
+ if (this.tree) {
+ return new LayerHighlightIterator(this, this.tree.walk())
+ } else {
+ return new NullHighlightIterator()
+ }
+ }
+
+ handleTextChange ({oldRange, newRange, oldText, newText}) {
+ if (this.tree) {
+ this.tree.edit(this._treeEditForBufferChange(
+ oldRange.start, oldRange.end, newRange.end, oldText, newText
+ ))
+
+ if (this.editedRange) {
+ if (newRange.start.isLessThan(this.editedRange.start)) {
+ this.editedRange.start = newRange.start
+ }
+ if (oldRange.end.isLessThan(this.editedRange.end)) {
+ this.editedRange.end = newRange.end.traverse(this.editedRange.end.traversalFrom(oldRange.end))
+ } else {
+ this.editedRange.end = newRange.end
+ }
+ } else {
+ this.editedRange = newRange.copy()
+ }
+ }
+
+ if (this.patchSinceCurrentParseStarted) {
+ this.patchSinceCurrentParseStarted.splice(
+ oldRange.start,
+ oldRange.end,
+ newRange.end,
+ oldText,
+ newText
+ )
+ }
+ }
+
+ destroy () {
+ for (const marker of this.languageMode.injectionsMarkerLayer.getMarkers()) {
+ if (marker.parentLanguageLayer === this) {
+ marker.languageLayer.destroy()
+ marker.destroy()
+ }
+ }
+ }
+
+ async update (nodeRangeSet) {
+ if (!this.currentParsePromise) {
+ do {
+ this.currentParsePromise = this._performUpdate(nodeRangeSet)
+ await this.currentParsePromise
+ } while (this.tree && this.tree.rootNode.hasChanges())
+ this.currentParsePromise = null
+ }
+ }
+
+ updateInjections (grammar) {
+ if (grammar.injectionRegExp) {
+ if (!this.currentParsePromise) this.currentParsePromise = Promise.resolve()
+ this.currentParsePromise = this.currentParsePromise.then(async () => {
+ await this._populateInjections(MAX_RANGE, null)
+ this.currentParsePromise = null
+ })
+ }
+ }
+
+ async _performUpdate (nodeRangeSet) {
+ let includedRanges = null
+ if (nodeRangeSet) {
+ includedRanges = nodeRangeSet.getRanges()
+ if (includedRanges.length === 0) {
+ this.tree = null
+ return
+ }
+ }
+
+ let affectedRange = this.editedRange
+ this.editedRange = null
+
+ this.patchSinceCurrentParseStarted = new Patch()
+ const tree = await this.languageMode.parse(
+ this.grammar.languageModule,
+ this.tree,
+ includedRanges
+ )
+ tree.buffer = this.languageMode.buffer
+
+ const changes = this.patchSinceCurrentParseStarted.getChanges()
+ this.patchSinceCurrentParseStarted = null
+ for (let i = changes.length - 1; i >= 0; i--) {
+ const {oldStart, oldEnd, newEnd, oldText, newText} = changes[i]
+ tree.edit(this._treeEditForBufferChange(
+ oldStart, oldEnd, newEnd, oldText, newText
+ ))
+ }
+
+ if (this.tree) {
+ const rangesWithSyntaxChanges = this.tree.getChangedRanges(tree)
+ this.tree = tree
+
+ if (!affectedRange) return
+ if (rangesWithSyntaxChanges.length > 0) {
+ for (const range of rangesWithSyntaxChanges) {
+ this.languageMode.emitRangeUpdate(rangeForNode(range))
+ }
+
+ affectedRange = affectedRange.union(new Range(
+ rangesWithSyntaxChanges[0].startPosition,
+ last(rangesWithSyntaxChanges).endPosition
+ ))
+ } else {
+ this.languageMode.emitRangeUpdate(affectedRange)
+ }
+ } else {
+ this.tree = tree
+ this.languageMode.emitRangeUpdate(rangeForNode(tree.rootNode))
+ if (includedRanges) {
+ affectedRange = new Range(includedRanges[0].startPosition, last(includedRanges).endPosition)
+ } else {
+ affectedRange = MAX_RANGE
+ }
+ }
+
+ await this._populateInjections(affectedRange, nodeRangeSet)
+ }
+
+ _populateInjections (range, nodeRangeSet) {
+ const {injectionsMarkerLayer, grammarForLanguageString} = this.languageMode
+
+ const existingInjectionMarkers = injectionsMarkerLayer
+ .findMarkers({intersectsRange: range})
+ .filter(marker => marker.parentLanguageLayer === this)
+
+ if (existingInjectionMarkers.length > 0) {
+ range = range.union(new Range(
+ existingInjectionMarkers[0].getRange().start,
+ last(existingInjectionMarkers).getRange().end
+ ))
+ }
+
+ const markersToUpdate = new Map()
+ for (const injectionPoint of this.grammar.injectionPoints) {
+ const nodes = this.tree.rootNode.descendantsOfType(
+ injectionPoint.type,
+ range.start,
+ range.end
+ )
+
+ for (const node of nodes) {
+ const languageName = injectionPoint.language(node)
+ if (!languageName) continue
+
+ const grammar = grammarForLanguageString(languageName)
+ if (!grammar) continue
+
+ const contentNodes = injectionPoint.content(node)
+ if (!contentNodes) continue
+
+ const injectionNodes = [].concat(contentNodes)
+ if (!injectionNodes.length) continue
+
+ const injectionRange = rangeForNode(node)
+ let marker = existingInjectionMarkers.find(m =>
+ m.getRange().isEqual(injectionRange) &&
+ m.languageLayer.grammar === grammar
+ )
+ if (!marker) {
+ marker = injectionsMarkerLayer.markRange(injectionRange)
+ marker.languageLayer = new LanguageLayer(this.languageMode, grammar, injectionPoint.contentChildTypes)
+ marker.parentLanguageLayer = this
+ }
+
+ markersToUpdate.set(marker, new NodeRangeSet(nodeRangeSet, injectionNodes))
+ }
+ }
+
+ for (const marker of existingInjectionMarkers) {
+ if (!markersToUpdate.has(marker)) {
+ marker.languageLayer.destroy()
+ this.languageMode.emitRangeUpdate(marker.getRange())
+ marker.destroy()
+ }
+ }
+
+ const promises = []
+ for (const [marker, nodeRangeSet] of markersToUpdate) {
+ promises.push(marker.languageLayer.update(nodeRangeSet))
+ }
+ return Promise.all(promises)
+ }
+
+ _treeEditForBufferChange (start, oldEnd, newEnd, oldText, newText) {
+ const startIndex = this.languageMode.buffer.characterIndexForPosition(start)
+ return {
+ startIndex,
+ oldEndIndex: startIndex + oldText.length,
+ newEndIndex: startIndex + newText.length,
+ startPosition: start,
+ oldEndPosition: oldEnd,
+ newEndPosition: newEnd
+ }
+ }
+}
+
+class HighlightIterator {
+ constructor (languageMode, iterators) {
+ this.languageMode = languageMode
+ this.iterators = iterators.sort((a, b) => b.getIndex() - a.getIndex())
+ }
+
+ seek (targetPosition) {
+ const containingTags = []
+ const containingTagStartIndices = []
+ const targetIndex = this.languageMode.buffer.characterIndexForPosition(targetPosition)
+ for (let i = this.iterators.length - 1; i >= 0; i--) {
+ this.iterators[i].seek(targetIndex, containingTags, containingTagStartIndices)
+ }
+ this.iterators.sort((a, b) => b.getIndex() - a.getIndex())
+ return containingTags
+ }
+
+ moveToSuccessor () {
+ const lastIndex = this.iterators.length - 1
+ const leader = this.iterators[lastIndex]
+ leader.moveToSuccessor()
+ const leaderCharIndex = leader.getIndex()
+ let i = lastIndex
+ while (i > 0 && this.iterators[i - 1].getIndex() < leaderCharIndex) i--
+ if (i < lastIndex) this.iterators.splice(i, 0, this.iterators.pop())
+ }
+
+ getPosition () {
+ return last(this.iterators).getPosition()
+ }
+
+ getCloseScopeIds () {
+ return last(this.iterators).getCloseScopeIds()
+ }
+
+ getOpenScopeIds () {
+ return last(this.iterators).getOpenScopeIds()
+ }
+
+ logState () {
+ const iterator = last(this.iterators)
+ if (iterator.treeCursor) {
+ console.log(
+ iterator.getPosition(),
+ iterator.treeCursor.nodeType,
+ new Range(
+ iterator.languageLayer.tree.rootNode.startPosition,
+ iterator.languageLayer.tree.rootNode.endPosition
+ ).toString()
+ )
+ }
+ }
+}
+
+class LayerHighlightIterator {
+ constructor (languageLayer, treeCursor) {
+ this.languageLayer = languageLayer
this.treeCursor = treeCursor
+ this.atEnd = false
// In order to determine which selectors match its current node, the iterator maintains
// a list of the current node's ancestors. Because the selectors can use the `:nth-child`
// pseudo-class, each node's child index is also stored.
this.containingNodeTypes = []
this.containingNodeChildIndices = []
-
- // Conceptually, the iterator represents a single position in the text. It stores this
- // position both as a character index and as a `Point`. This position corresponds to a
- // leaf node of the syntax tree, which either contains or follows the iterator's
- // textual position. The `treeCursor` property points at that leaf node, and
- // `currentChildIndex` represents the child index of that leaf node within its parent.
- this.currentIndex = null
- this.currentPosition = null
- this.currentChildIndex = null
+ this.containingNodeEndIndices = []
// At any given position, the iterator exposes the list of class names that should be
// *ended* at its current position and the list of class names that should be *started*
@@ -413,113 +738,159 @@ class TreeSitterHighlightIterator {
this.openTags = []
}
- seek (targetPosition) {
+ seek (targetIndex, containingTags, containingTagStartIndices) {
while (this.treeCursor.gotoParent()) {}
- const containingTags = []
-
+ this.done = false
+ this.atEnd = true
this.closeTags.length = 0
this.openTags.length = 0
this.containingNodeTypes.length = 0
this.containingNodeChildIndices.length = 0
- this.currentPosition = targetPosition
- this.currentIndex = this.languageMode.buffer.characterIndexForPosition(targetPosition)
+ this.containingNodeEndIndices.length = 0
- // Descend from the root of the tree to the smallest node that spans the given position.
- // Keep track of any nodes along the way that are associated with syntax highlighting
- // tags. These tags must be returned.
- var childIndex = -1
- var nodeContainsTarget = true
+ const containingTagEndIndices = []
+
+ if (targetIndex >= this.treeCursor.endIndex) {
+ this.done = true
+ return
+ }
+
+ let childIndex = -1
for (;;) {
- this.currentChildIndex = childIndex
- if (!nodeContainsTarget) break
this.containingNodeTypes.push(this.treeCursor.nodeType)
this.containingNodeChildIndices.push(childIndex)
+ this.containingNodeEndIndices.push(this.treeCursor.endIndex)
const scopeName = this.currentScopeName()
if (scopeName) {
- const id = this.languageMode.grammar.idForScope(scopeName)
- if (this.currentIndex === this.treeCursor.startIndex) {
- this.openTags.push(id)
+ const id = this.idForScope(scopeName)
+ if (this.treeCursor.startIndex < targetIndex) {
+ insertContainingTag(id, this.treeCursor.startIndex, containingTags, containingTagStartIndices)
+ containingTagEndIndices.push(this.treeCursor.endIndex)
} else {
- containingTags.push(id)
+ this.atEnd = false
+ this.openTags.push(id)
+ while (this.treeCursor.gotoFirstChild()) {
+ this.containingNodeTypes.push(this.treeCursor.nodeType)
+ this.containingNodeChildIndices.push(0)
+ const scopeName = this.currentScopeName()
+ if (scopeName) {
+ this.openTags.push(this.idForScope(scopeName))
+ }
+ }
+ break
}
}
- const nextChildIndex = this.treeCursor.gotoFirstChildForIndex(this.currentIndex)
- if (nextChildIndex == null) break
- if (this.treeCursor.startIndex > this.currentIndex) nodeContainsTarget = false
- childIndex = nextChildIndex
+ childIndex = this.treeCursor.gotoFirstChildForIndex(targetIndex)
+ if (childIndex === null) break
+ if (this.treeCursor.startIndex >= targetIndex) this.atEnd = false
+ }
+
+ if (this.atEnd) {
+ const currentIndex = this.treeCursor.endIndex
+ for (let i = 0, {length} = containingTags; i < length; i++) {
+ if (containingTagEndIndices[i] === currentIndex) {
+ this.closeTags.push(containingTags[i])
+ }
+ }
}
return containingTags
}
moveToSuccessor () {
+ let didMove = false
this.closeTags.length = 0
this.openTags.length = 0
- // Step forward through the leaves of the tree to find the next place where one or more
- // syntax highlighting tags begin, end, or both.
- do {
- // If the iterator is before the beginning of the current node, advance it to the
- // beginning of then node and then walk down into the node's children, marking
- // open tags as needed.
- if (this.currentIndex < this.treeCursor.startIndex) {
- this.currentIndex = this.treeCursor.startIndex
- this.currentPosition = this.treeCursor.startPosition
- this.pushOpenTag()
- this.descendLeft()
+ if (this.done) return
- // If the iterator is within the current node, advance it to the end of the node
- // and then walk up the tree until the next sibling is found, marking close tags
- // as needed.
- //
- } else if (this.currentIndex < this.treeCursor.endIndex) {
- /* eslint-disable no-labels */
- ascendingLoop:
- do {
- this.currentIndex = this.treeCursor.endIndex
- this.currentPosition = this.treeCursor.endPosition
- this.pushCloseTag()
+ while (true) {
+ if (this.atEnd) {
+ if (this.treeCursor.gotoNextSibling()) {
+ didMove = true
+ this.atEnd = false
+ const depth = this.containingNodeTypes.length
+ this.containingNodeTypes[depth - 1] = this.treeCursor.nodeType
+ this.containingNodeChildIndices[depth - 1]++
+ this.containingNodeEndIndices[depth - 1] = this.treeCursor.endIndex
- // Stop walking upward when we reach a node with a next sibling.
- while (this.treeCursor.gotoNextSibling()) {
- this.currentChildIndex++
-
- // If the next sibling has a size of zero (e.g. something like an `automatic_semicolon`,
- // an `indent`, or a `MISSING` node inserted by the parser during error recovery),
- // then skip it. These nodes play no role in syntax highlighting.
- if (this.treeCursor.endIndex === this.currentIndex) continue
-
- // If the next sibling starts right at the end of the current node (i.e. there is
- // no whitespace in between), then before returning, also mark any open tags associated
- // with this point in the tree.
- if (this.treeCursor.startIndex === this.currentIndex) {
- this.pushOpenTag()
- this.descendLeft()
+ while (true) {
+ const {startIndex} = this.treeCursor
+ const scopeName = this.currentScopeName()
+ if (scopeName) {
+ this.openTags.push(this.idForScope(scopeName))
}
- break ascendingLoop
+ if (this.treeCursor.gotoFirstChild()) {
+ if ((this.closeTags.length || this.openTags.length) &&
+ this.treeCursor.startIndex > startIndex) {
+ this.treeCursor.gotoParent()
+ break
+ }
+
+ this.containingNodeTypes.push(this.treeCursor.nodeType)
+ this.containingNodeChildIndices.push(0)
+ this.containingNodeEndIndices.push(this.treeCursor.endIndex)
+ } else {
+ break
+ }
}
+ } else if (this.treeCursor.gotoParent()) {
+ this.atEnd = false
+ this.containingNodeTypes.pop()
+ this.containingNodeChildIndices.pop()
+ this.containingNodeEndIndices.pop()
+ } else {
+ this.done = true
+ break
+ }
+ } else {
+ this.atEnd = true
+ didMove = true
- this.currentChildIndex = last(this.containingNodeChildIndices)
- } while (this.treeCursor.gotoParent())
- /* eslint-disable no-labels */
+ const scopeName = this.currentScopeName()
+ if (scopeName) {
+ this.closeTags.push(this.idForScope(scopeName))
+ }
- // If the iterator is at the end of a node, advance to the node's next sibling. If
- // it has no next sibing, then the iterator has reached the end of the tree.
- } else if (!this.treeCursor.gotoNextSibling()) {
- this.currentPosition = {row: Infinity, column: Infinity}
- break
+ const endIndex = this.treeCursor.endIndex
+ let depth = this.containingNodeEndIndices.length
+ while (depth > 1 && this.containingNodeEndIndices[depth - 2] === endIndex) {
+ this.treeCursor.gotoParent()
+ this.containingNodeTypes.pop()
+ this.containingNodeChildIndices.pop()
+ this.containingNodeEndIndices.pop()
+ --depth
+ const scopeName = this.currentScopeName()
+ if (scopeName) this.closeTags.push(this.idForScope(scopeName))
+ }
}
- } while (this.closeTags.length === 0 && this.openTags.length === 0)
- return true
+ if (didMove && (this.closeTags.length || this.openTags.length)) break
+ }
}
getPosition () {
- return this.currentPosition
+ if (this.done) {
+ return Point.INFINITY
+ } else if (this.atEnd) {
+ return this.treeCursor.endPosition
+ } else {
+ return this.treeCursor.startPosition
+ }
+ }
+
+ getIndex () {
+ if (this.done) {
+ return Infinity
+ } else if (this.atEnd) {
+ return this.treeCursor.endIndex
+ } else {
+ return this.treeCursor.startIndex
+ }
}
getCloseScopeIds () {
@@ -532,34 +903,137 @@ class TreeSitterHighlightIterator {
// Private methods
- descendLeft () {
- while (this.treeCursor.gotoFirstChild()) {
- this.currentChildIndex = 0
- this.pushOpenTag()
- }
- }
-
currentScopeName () {
- return this.languageMode.grammar.scopeMap.get(
+ return this.languageLayer.grammar.scopeMap.get(
this.containingNodeTypes,
this.containingNodeChildIndices,
this.treeCursor.nodeIsNamed
)
}
- pushCloseTag () {
- const scopeName = this.currentScopeName()
- if (scopeName) this.closeTags.push(this.languageMode.grammar.idForScope(scopeName))
- this.containingNodeTypes.pop()
- this.containingNodeChildIndices.pop()
+ idForScope (scopeName) {
+ return this.languageLayer.languageMode.grammar.idForScope(scopeName)
+ }
+}
+
+class NullHighlightIterator {
+ seek () { return [] }
+ moveToSuccessor () {}
+ getIndex () { return Infinity }
+ getPosition () { return Point.INFINITY }
+ getOpenScopeIds () { return [] }
+ getCloseScopeIds () { return [] }
+}
+
+class NodeRangeSet {
+ constructor (previous, nodes) {
+ this.previous = previous
+ this.nodes = nodes
}
- pushOpenTag () {
- this.containingNodeTypes.push(this.treeCursor.nodeType)
- this.containingNodeChildIndices.push(this.currentChildIndex)
- const scopeName = this.currentScopeName()
- if (scopeName) this.openTags.push(this.languageMode.grammar.idForScope(scopeName))
+ getRanges () {
+ const previousRanges = this.previous && this.previous.getRanges()
+ const result = []
+
+ for (const node of this.nodes) {
+ let position = node.startPosition
+ let index = node.startIndex
+
+ for (const child of node.children) {
+ const nextPosition = child.startPosition
+ const nextIndex = child.startIndex
+ if (nextIndex > index) {
+ this._pushRange(previousRanges, result, {
+ startIndex: index,
+ endIndex: nextIndex,
+ startPosition: position,
+ endPosition: nextPosition
+ })
+ }
+ position = child.endPosition
+ index = child.endIndex
+ }
+
+ if (node.endIndex > index) {
+ this._pushRange(previousRanges, result, {
+ startIndex: index,
+ endIndex: node.endIndex,
+ startPosition: position,
+ endPosition: node.endPosition
+ })
+ }
+ }
+
+ return result
}
+
+ _pushRange (previousRanges, newRanges, newRange) {
+ if (!previousRanges) {
+ newRanges.push(newRange)
+ return
+ }
+
+ for (const previousRange of previousRanges) {
+ if (previousRange.endIndex <= newRange.startIndex) continue
+ if (previousRange.startIndex >= newRange.endIndex) break
+ newRanges.push({
+ startIndex: Math.max(previousRange.startIndex, newRange.startIndex),
+ endIndex: Math.min(previousRange.endIndex, newRange.endIndex),
+ startPosition: Point.max(previousRange.startPosition, newRange.startPosition),
+ endPosition: Point.min(previousRange.endPosition, newRange.endPosition)
+ })
+ }
+ }
+}
+
+function insertContainingTag (tag, index, tags, indices) {
+ const i = indices.findIndex(existingIndex => existingIndex > index)
+ if (i === -1) {
+ tags.push(tag)
+ indices.push(index)
+ } else {
+ tags.splice(i, 0, tag)
+ indices.splice(i, 0, index)
+ }
+}
+
+// Return true iff `mouse` is smaller than `house`. Only correct if
+// mouse and house overlap.
+//
+// * `mouse` {Range}
+// * `house` {Range}
+function rangeIsSmaller (mouse, house) {
+ if (!house) return true
+ const mvec = vecFromRange(mouse)
+ const hvec = vecFromRange(house)
+ return Point.min(mvec, hvec) === mvec
+}
+
+function vecFromRange ({start, end}) {
+ return end.translate(start.negate())
+}
+
+function rangeForNode (node) {
+ return new Range(node.startPosition, node.endPosition)
+}
+
+function nodeContainsIndices (node, start, end) {
+ if (node.startIndex < start) return node.endIndex >= end
+ if (node.startIndex === start) return node.endIndex > end
+ return false
+}
+
+function nodeIsSmaller (left, right) {
+ if (!left) return false
+ if (!right) return true
+ return left.endIndex - left.startIndex < right.endIndex - right.startIndex
+}
+
+function compareScopeDescriptorIterators (a, b) {
+ return (
+ a.node.startIndex - b.node.startIndex ||
+ a.rootStartIndex - b.rootStartIndex
+ )
}
function pointIsGreater (left, right) {
@@ -579,5 +1053,9 @@ function last (array) {
'decreaseNextIndentRegexForScopeDescriptor',
'regexForPattern'
].forEach(methodName => {
- module.exports.prototype[methodName] = TextMateLanguageMode.prototype[methodName]
+ TreeSitterLanguageMode.prototype[methodName] = TextMateLanguageMode.prototype[methodName]
})
+
+TreeSitterLanguageMode.LanguageLayer = LanguageLayer
+
+module.exports = TreeSitterLanguageMode