mirror of
https://github.com/atom/atom.git
synced 2026-01-25 23:08:18 -05:00
Merge pull request #17721 from atom/get-range-for-syntax-node-with-selector
Add TreeSitterLanguageMode::getSyntaxNodeContainingRange and TreeSitterLanguageMode::getSyntaxNodeAtPosition
This commit is contained in:
@@ -1304,6 +1304,206 @@ describe('TreeSitterLanguageMode', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('.bufferRangeForScopeAtPosition(selector?, position)', () => {
|
||||
describe('when selector = null', () => {
|
||||
it('returns the range of the smallest node at position', 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}))
|
||||
await nextHighlightingUpdate(buffer.getLanguageMode())
|
||||
expect(editor.bufferRangeForScopeAtPosition(null, [0, 6])).toEqual(
|
||||
[[0, 5], [0, 8]]
|
||||
)
|
||||
expect(editor.bufferRangeForScopeAtPosition(null, [0, 9])).toEqual(
|
||||
[[0, 8], [0, 9]]
|
||||
)
|
||||
})
|
||||
|
||||
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(`
|
||||
<div>
|
||||
<script>
|
||||
html \`
|
||||
<span>\${person.name}</span>
|
||||
\`
|
||||
</script>
|
||||
</div>
|
||||
`)
|
||||
|
||||
const languageMode = new TreeSitterLanguageMode({buffer, grammar: htmlGrammar, grammars: atom.grammars})
|
||||
buffer.setLanguageMode(languageMode)
|
||||
await nextHighlightingUpdate(languageMode)
|
||||
await nextHighlightingUpdate(languageMode)
|
||||
await nextHighlightingUpdate(languageMode)
|
||||
|
||||
const nameProperty = buffer.findSync('name')
|
||||
const {start} = nameProperty
|
||||
const position = Object.assign({}, start, {column: start.column + 2})
|
||||
expect(languageMode.bufferRangeForScopeAtPosition(null, position))
|
||||
.toEqual(nameProperty)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a selector', () => {
|
||||
it('returns the range of the smallest matching node at position', 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}))
|
||||
await nextHighlightingUpdate(buffer.getLanguageMode())
|
||||
expect(editor.bufferRangeForScopeAtPosition('.property_identifier', [0, 6])).toEqual(
|
||||
buffer.findSync('bar')
|
||||
)
|
||||
expect(editor.bufferRangeForScopeAtPosition('.call_expression', [0, 6])).toEqual(
|
||||
[[0, 0], [0, buffer.getText().length - 1]]
|
||||
)
|
||||
expect(editor.bufferRangeForScopeAtPosition('.object', [0, 9])).toEqual(
|
||||
buffer.findSync('{bar: baz}')
|
||||
)
|
||||
})
|
||||
|
||||
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(`
|
||||
<div>
|
||||
<script>
|
||||
html \`
|
||||
<span>\${person.name}</span>
|
||||
\`
|
||||
</script>
|
||||
</div>
|
||||
`)
|
||||
|
||||
const languageMode = new TreeSitterLanguageMode({buffer, grammar: htmlGrammar, grammars: atom.grammars})
|
||||
buffer.setLanguageMode(languageMode)
|
||||
await nextHighlightingUpdate(languageMode)
|
||||
await nextHighlightingUpdate(languageMode)
|
||||
await nextHighlightingUpdate(languageMode)
|
||||
|
||||
const nameProperty = buffer.findSync('name')
|
||||
const {start} = nameProperty
|
||||
const position = Object.assign({}, start, {column: start.column + 2})
|
||||
expect(languageMode.bufferRangeForScopeAtPosition('.property_identifier', position))
|
||||
.toEqual(nameProperty)
|
||||
expect(languageMode.bufferRangeForScopeAtPosition('.element', position))
|
||||
.toEqual(buffer.findSync('<span>\\${person\\.name}</span>'))
|
||||
})
|
||||
|
||||
it('accepts node-matching functions as selectors', 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(`
|
||||
<div>
|
||||
<script>
|
||||
html \`
|
||||
<span>\${person.name}</span>
|
||||
\`
|
||||
</script>
|
||||
</div>
|
||||
`)
|
||||
|
||||
const languageMode = new TreeSitterLanguageMode({buffer, grammar: htmlGrammar, grammars: atom.grammars})
|
||||
buffer.setLanguageMode(languageMode)
|
||||
await nextHighlightingUpdate(languageMode)
|
||||
await nextHighlightingUpdate(languageMode)
|
||||
await nextHighlightingUpdate(languageMode)
|
||||
|
||||
const nameProperty = buffer.findSync('name')
|
||||
const {start} = nameProperty
|
||||
const position = Object.assign({}, start, {column: start.column + 2})
|
||||
const templateStringInCallExpression = node =>
|
||||
node.type === 'template_string' && node.parent.type === 'call_expression'
|
||||
expect(languageMode.bufferRangeForScopeAtPosition(templateStringInCallExpression, position))
|
||||
.toEqual([[3, 19], [5, 15]])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.getSyntaxNodeAtPosition(position, where?)', () => {
|
||||
it('returns the range of the smallest matching node at position', async () => {
|
||||
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
|
||||
id: 'javascript',
|
||||
parser: 'tree-sitter-javascript'
|
||||
})
|
||||
|
||||
buffer.setText('foo(bar({x: 2}));')
|
||||
const languageMode = new TreeSitterLanguageMode({buffer, grammar})
|
||||
buffer.setLanguageMode(languageMode)
|
||||
await nextHighlightingUpdate(languageMode)
|
||||
expect(languageMode.getSyntaxNodeAtPosition([0, 6]).range).toEqual(
|
||||
buffer.findSync('bar')
|
||||
)
|
||||
const findFoo = node =>
|
||||
node.type === 'call_expression' && node.firstChild.text === 'foo'
|
||||
expect(languageMode.getSyntaxNodeAtPosition([0, 6], findFoo).range).toEqual(
|
||||
[[0, 0], [0, buffer.getText().length - 1]]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('TextEditor.selectLargerSyntaxNode and .selectSmallerSyntaxNode', () => {
|
||||
it('expands and contracts the selection based on the syntax tree', async () => {
|
||||
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
|
||||
|
||||
38
src/selectors.js
Normal file
38
src/selectors.js
Normal file
@@ -0,0 +1,38 @@
|
||||
module.exports = {selectorMatchesAnyScope, matcherForSelector}
|
||||
|
||||
const {isSubset} = require('underscore-plus')
|
||||
|
||||
// Private: Parse a selector into parts.
|
||||
// If already parsed, returns the selector unmodified.
|
||||
//
|
||||
// * `selector` a {String|Array<String>} specifying what to match
|
||||
// Returns selector parts, an {Array<String>}.
|
||||
function parse (selector) {
|
||||
return typeof selector === 'string'
|
||||
? selector.replace(/^\./, '').split('.')
|
||||
: selector
|
||||
}
|
||||
|
||||
const always = scope => true
|
||||
|
||||
// Essential: Return a matcher function for a selector.
|
||||
//
|
||||
// * selector, a {String} selector
|
||||
// Returns {(scope: String) -> Boolean}, a matcher function returning
|
||||
// true iff the scope matches the selector.
|
||||
function matcherForSelector (selector) {
|
||||
const parts = parse(selector)
|
||||
if (typeof parts === 'function') return parts
|
||||
return selector
|
||||
? scope => isSubset(parts, parse(scope))
|
||||
: always
|
||||
}
|
||||
|
||||
// Essential: Return true iff the selector matches any provided scope.
|
||||
//
|
||||
// * {String} selector
|
||||
// * {Array<String>} scopes
|
||||
// Returns {Boolean} true if any scope matches the selector.
|
||||
function selectorMatchesAnyScope (selector, scopes) {
|
||||
return !selector || scopes.some(matcherForSelector(selector))
|
||||
}
|
||||
@@ -7,6 +7,7 @@ const ScopeDescriptor = require('./scope-descriptor')
|
||||
const NullGrammar = require('./null-grammar')
|
||||
const {OnigRegExp} = require('oniguruma')
|
||||
const {toFirstMateScopeId, fromFirstMateScopeId} = require('./first-mate-helpers')
|
||||
const {selectorMatchesAnyScope} = require('./selectors')
|
||||
|
||||
const NON_WHITESPACE_REGEX = /\S/
|
||||
|
||||
@@ -726,14 +727,6 @@ class TextMateLanguageMode {
|
||||
|
||||
TextMateLanguageMode.prototype.chunkSize = 50
|
||||
|
||||
function selectorMatchesAnyScope (selector, scopes) {
|
||||
const targetClasses = selector.replace(/^\./, '').split('.')
|
||||
return scopes.some((scope) => {
|
||||
const scopeClasses = scope.split('.')
|
||||
return _.isSubset(targetClasses, scopeClasses)
|
||||
})
|
||||
}
|
||||
|
||||
class TextMateHighlightIterator {
|
||||
constructor (languageMode) {
|
||||
this.languageMode = languageMode
|
||||
|
||||
@@ -5,6 +5,7 @@ const {Emitter, Disposable} = require('event-kit')
|
||||
const ScopeDescriptor = require('./scope-descriptor')
|
||||
const TokenizedLine = require('./tokenized-line')
|
||||
const TextMateLanguageMode = require('./text-mate-language-mode')
|
||||
const {matcherForSelector} = require('./selectors')
|
||||
|
||||
let nextId = 0
|
||||
const MAX_RANGE = new Range(Point.ZERO, Point.INFINITY).freeze()
|
||||
@@ -20,6 +21,13 @@ class TreeSitterLanguageMode {
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!Parser.SyntaxNode.prototype.hasOwnProperty('range')) {
|
||||
Object.defineProperty(Parser.SyntaxNode.prototype, 'range', {
|
||||
get () {
|
||||
return rangeForNode(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
constructor ({buffer, grammar, config, grammars}) {
|
||||
@@ -334,7 +342,7 @@ class TreeSitterLanguageMode {
|
||||
Section - Syntax Tree APIs
|
||||
*/
|
||||
|
||||
getRangeForSyntaxNodeContainingRange (range) {
|
||||
getSyntaxNodeContainingRange (range, where = _ => true) {
|
||||
const startIndex = this.buffer.characterIndexForPosition(range.start)
|
||||
const endIndex = this.buffer.characterIndexForPosition(range.end)
|
||||
const searchEndIndex = Math.max(0, endIndex - 1)
|
||||
@@ -342,17 +350,35 @@ class TreeSitterLanguageMode {
|
||||
let smallestNode
|
||||
this._forEachTreeWithRange(range, tree => {
|
||||
let node = tree.rootNode.descendantForIndex(startIndex, searchEndIndex)
|
||||
while (node && !nodeContainsIndices(node, startIndex, endIndex)) {
|
||||
while (node) {
|
||||
if (nodeContainsIndices(node, startIndex, endIndex) && where(node)) {
|
||||
if (nodeIsSmaller(node, smallestNode)) smallestNode = node
|
||||
break
|
||||
}
|
||||
node = node.parent
|
||||
}
|
||||
if (nodeIsSmaller(node, smallestNode)) smallestNode = node
|
||||
})
|
||||
|
||||
if (smallestNode) return rangeForNode(smallestNode)
|
||||
return smallestNode
|
||||
}
|
||||
|
||||
bufferRangeForScopeAtPosition (position) {
|
||||
return this.getRangeForSyntaxNodeContainingRange(new Range(position, position))
|
||||
getRangeForSyntaxNodeContainingRange (range, where) {
|
||||
const node = this.getSyntaxNodeContainingRange(range, where)
|
||||
return node && node.range
|
||||
}
|
||||
|
||||
getSyntaxNodeAtPosition (position, where) {
|
||||
return this.getSyntaxNodeContainingRange(new Range(position, position), where)
|
||||
}
|
||||
|
||||
bufferRangeForScopeAtPosition (selector, position) {
|
||||
if (typeof selector === 'string') {
|
||||
const match = matcherForSelector(selector)
|
||||
selector = ({type}) => match(type)
|
||||
}
|
||||
if (selector === null) selector = undefined
|
||||
const node = this.getSyntaxNodeAtPosition(position, selector)
|
||||
return node && node.range
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user