diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js index 5606aec8e..be5ccc13d 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 @@ -530,6 +531,75 @@ describe('TreeSitterLanguageMode', () => { ], ]) }) + + 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']} + ], + ]) + }) }) }) @@ -965,6 +1035,15 @@ describe('TreeSitterLanguageMode', () => { }) }) +function nextHighlightingUpdate (languageMode) { + return new Promise(resolve => { + const subscription = languageMode.onDidChangeHighlighting(() => { + subscription.dispose() + resolve() + }) + }) +} + function getDisplayText (editor) { return editor.displayLayer.getText() } diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index cfc13d828..9d44ed88a 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -52,10 +52,10 @@ class TreeSitterLanguageMode { ) } - this.rootLanguageLayer.update() + this.rootLanguageLayer.update(NodeRangeSet.FULL) }) - this.rootLanguageLayer.update() + this.rootLanguageLayer.update(NodeRangeSet.FULL) // 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. @@ -435,10 +435,10 @@ class LanguageLayer { } } - async update (containingNodes) { + async update (nodeRangeSet) { if (this.currentParsePromise) return this.currentParsePromise - this.currentParsePromise = this._performUpdate(containingNodes) + this.currentParsePromise = this._performUpdate(nodeRangeSet) await this.currentParsePromise this.currentParsePromise = null @@ -451,7 +451,7 @@ class LanguageLayer { )) } this.patchSinceCurrentParseStarted = null - this.update(containingNodes) + this.update(nodeRangeSet) } } @@ -459,23 +459,17 @@ class LanguageLayer { if (!grammar.injectionRegExp) return if (!this.currentParsePromise) this.currentParsePromise = Promise.resolve() this.currentParsePromise = this.currentParsePromise.then(async () => { - await this._populateInjections(MAX_RANGE) - const markers = this.languageMode.injectionsMarkerLayer.getMarkers().filter(marker => - marker.parentLanguageLayer === this - ) - for (const marker of markers) { - await marker.languageLayer._populateInjections(MAX_RANGE) - } + await this._populateInjections(MAX_RANGE, NodeRangeSet.FULL) this.currentParsePromise = null }) } - async _performUpdate (containingNodes) { - let includedRanges = [] - if (containingNodes) { - for (const node of containingNodes) { - includedRanges.push(...this._rangesForInjectionNode(node)) - } + async _performUpdate (nodeRangeSet) { + let includedRanges + if (nodeRangeSet === NodeRangeSet.FULL) { + includedRanges = null + } else { + includedRanges = nodeRangeSet.getRanges() if (includedRanges.length === 0) return } @@ -494,6 +488,7 @@ class LanguageLayer { affectedRange = this._rangeForNode(editedRange) const rangesWithSyntaxChanges = this.tree.getChangedRanges(tree) + this.tree = tree if (rangesWithSyntaxChanges.length > 0) { for (const range of rangesWithSyntaxChanges) { this.languageMode.emitRangeUpdate(this._rangeForNode(range)) @@ -505,15 +500,15 @@ class LanguageLayer { )) } } else { + this.tree = tree this.languageMode.emitRangeUpdate(this._rangeForNode(tree.rootNode)) affectedRange = MAX_RANGE } - this.tree = tree - await this._populateInjections(affectedRange) + await this._populateInjections(affectedRange, nodeRangeSet) } - async _populateInjections (range) { + async _populateInjections (range, nodeRangeSet) { const {injectionsMarkerLayer, grammarForLanguageString} = this.languageMode const existingInjectionMarkers = injectionsMarkerLayer @@ -559,7 +554,7 @@ class LanguageLayer { marker.parentLanguageLayer = this } - markersToUpdate.set(marker, injectionNodes) + markersToUpdate.set(marker, nodeRangeSet.intersect(injectionNodes)) } } @@ -571,46 +566,11 @@ class LanguageLayer { } } - for (const [marker, injectionNodes] of markersToUpdate) { - await marker.languageLayer.update(injectionNodes) + for (const [marker, nodeRangeSet] of markersToUpdate) { + await marker.languageLayer.update(nodeRangeSet) } } - /** - * @param node {Parser.SyntaxNode} - */ - _rangesForInjectionNode (node) { - const result = [] - 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) { - result.push({ - startIndex: index, - endIndex: nextIndex, - startPosition: position, - endPosition: nextPosition - }) - } - position = child.endPosition - index = child.endIndex - } - - if (node.endIndex > index) { - result.push({ - startIndex: index, - endIndex: node.endIndex, - startPosition: position, - endPosition: node.endPosition - }) - } - - return result - } - _rangeForNode (node) { return new Range(node.startPosition, node.endPosition) } @@ -858,6 +818,74 @@ class NullHighlightIterator { getCloseScopeIds () { return [] } } +class NodeRangeSet { + constructor (previous, nodes) { + this.previous = previous + this.nodes = nodes + } + + intersect (nodes) { + return new NodeRangeSet(this, nodes) + } + + getRanges () { + const previousRanges = 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) { + 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) + }) + } + } +} + +class FullRangeSet extends NodeRangeSet { + getRanges () { + return [{startPosition: Point.ZERO, endPosition: Point.INFINITY, startIndex: 0, endIndex: Infinity}] + } +} + +NodeRangeSet.FULL = new FullRangeSet() + function pointIsLess (left, right) { return left.row < right.row || left.row === right.row && left.column < right.column }