mirror of
https://github.com/atom/atom.git
synced 2026-01-26 07:19:06 -05:00
Fix propogation of included ranges for injections within injections
Co-Authored-By: Ashi Krishnan <queerviolet@github.com>
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user