Merge pull request #17721 from atom/get-range-for-syntax-node-with-selector

Add TreeSitterLanguageMode::getSyntaxNodeContainingRange and TreeSitterLanguageMode::getSyntaxNodeAtPosition
This commit is contained in:
Ashi Krishnan
2018-07-24 16:00:54 -04:00
committed by GitHub
4 changed files with 271 additions and 14 deletions

View File

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

View File

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

View File

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