Fix propogation of included ranges for injections within injections

Co-Authored-By: Ashi Krishnan <queerviolet@github.com>
This commit is contained in:
Max Brunsfeld
2018-07-06 13:53:13 -07:00
parent a9da395f60
commit 670371c376
2 changed files with 166 additions and 59 deletions

View File

@@ -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('<body>\n<script>\nb(<%= c.d %>)\n</script>\n</body>')
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()
}

View File

@@ -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
}