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