diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js
index 6d1381a93..37413a9d5 100644
--- a/spec/tree-sitter-language-mode-spec.js
+++ b/spec/tree-sitter-language-mode-spec.js
@@ -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(`
+
+
+
+ `)
+
+ 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(`
+
+
+
+ `)
+
+ 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('\\${person\\.name}'))
+ })
+
+ 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(`
+
+
+
+ `)
+
+ 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, {
diff --git a/src/selectors.js b/src/selectors.js
new file mode 100644
index 000000000..ce03b80b4
--- /dev/null
+++ b/src/selectors.js
@@ -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} specifying what to match
+// Returns selector parts, an {Array}.
+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} scopes
+// Returns {Boolean} true if any scope matches the selector.
+function selectorMatchesAnyScope (selector, scopes) {
+ return !selector || scopes.some(matcherForSelector(selector))
+}
diff --git a/src/text-mate-language-mode.js b/src/text-mate-language-mode.js
index 9abe55ecb..471af9af2 100644
--- a/src/text-mate-language-mode.js
+++ b/src/text-mate-language-mode.js
@@ -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
diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js
index bd0b8b1b3..9c9b02f6c 100644
--- a/src/tree-sitter-language-mode.js
+++ b/src/tree-sitter-language-mode.js
@@ -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
}
/*