From 4d3916f74e350691ab85e63ac8b1ad5bb851339b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Jun 2018 12:39:29 -0700 Subject: [PATCH] Add tests and docs for addInjectionPoint Also, replace `addInjectionPattern` API with a single `injectionRegExp` field on the grammar. Co-Authored-By: Ashi Krishnan --- spec/grammar-registry-spec.js | 28 +++++++++++++++++++ spec/tree-sitter-language-mode-spec.js | 4 +-- src/grammar-registry.js | 38 +++++++++++++++++++++----- src/tree-sitter-grammar.js | 2 +- src/tree-sitter-language-mode.js | 12 ++++---- 5 files changed, 67 insertions(+), 17 deletions(-) diff --git a/spec/grammar-registry-spec.js b/spec/grammar-registry-spec.js index abb3b189a..bcd57f3a2 100644 --- a/spec/grammar-registry-spec.js +++ b/spec/grammar-registry-spec.js @@ -525,6 +525,34 @@ describe('GrammarRegistry', () => { }) }) + describe('.addInjectionPoint(languageId, {type, language, content})', () => { + const injectionPoint = { + type: 'some_node_type', + language() { return 'some_language_name' }, + content(node) { return node } + } + + beforeEach(() => { + atom.config.set('core.useTreeSitterParsers', true) + }) + + it('adds an injection point to the grammar with the given id', async () => { + await atom.packages.activatePackage('language-javascript') + atom.grammars.addInjectionPoint('javascript', injectionPoint) + const grammar = atom.grammars.grammarForId('javascript') + expect(grammar.injectionPoints).toContain(injectionPoint) + }) + + describe('when called before a grammar with the given id is loaded', () => { + it('adds the injection point once the grammar is loaded', async () => { + atom.grammars.addInjectionPoint('javascript', injectionPoint) + await atom.packages.activatePackage('language-javascript') + const grammar = atom.grammars.grammarForId('javascript') + expect(grammar.injectionPoints).toContain(injectionPoint) + }) + }) + }) + describe('serialization', () => { it('persists editors\' grammar overrides', async () => { const buffer1 = new TextBuffer() diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js index 0b510d408..aa82810c4 100644 --- a/spec/tree-sitter-language-mode-spec.js +++ b/spec/tree-sitter-language-mode-spec.js @@ -324,9 +324,7 @@ describe('TreeSitterLanguageMode', () => { tag_name: 'tag', attribute_name: 'attr' }, - injections: [ - name => name.toLowerCase().includes('html') - ] + injectionRegExp: 'html' }) atom.grammars.addGrammar(jsGrammar) diff --git a/src/grammar-registry.js b/src/grammar-registry.js index b7972a1eb..e12a03da9 100644 --- a/src/grammar-registry.js +++ b/src/grammar-registry.js @@ -391,6 +391,32 @@ class GrammarRegistry { return this.textmateRegistry.onDidUpdateGrammar(callback) } + // Experimental: Specify a type of syntax node that may embed other languages. + // + // * `grammarId` The {String} id of the parent language + // * `injectionPoint` An {Object} with the following keys: + // * `type` The {String} type of syntax node that may embed other languages + // * `language` A {Function} that is called with syntax nodes of the specified `type` and + // returns a {String} that will be tested against other grammars' `injectionRegExp` in + // order to determine what language should be embedded. + // * `content` A {Function} that is called with syntax nodes of the specified `type` and + // returns another syntax node that contains the embedded source code. + addInjectionPoint (grammarId, injectionPoint) { + const grammar = this.treeSitterGrammarsById[grammarId] + if (grammar) { + grammar.injectionPoints.push(injectionPoint) + } else { + this.treeSitterGrammarsById[grammarId] = { + injectionPoints: [injectionPoint] + } + } + return new Disposable(() => { + const grammar = this.treeSitterGrammarsById[grammarId] + const index = grammar.injectionPoints.indexOf(injectionPoint) + if (index !== -1) grammar.injectionPoints.splice(index, 1) + }) + } + get nullGrammar () { return this.textmateRegistry.nullGrammar } @@ -409,12 +435,14 @@ class GrammarRegistry { addGrammar (grammar) { if (grammar instanceof TreeSitterGrammar) { + const existingParams = this.treeSitterGrammarsById[grammar.id] || {} this.treeSitterGrammarsById[grammar.id] = grammar if (grammar.legacyScopeName) { this.config.setLegacyScopeAliasForNewScope(grammar.id, grammar.legacyScopeName) this.textMateScopeNamesByTreeSitterLanguageId.set(grammar.id, grammar.legacyScopeName) this.treeSitterLanguageIdsByTextMateScopeName.set(grammar.legacyScopeName, grammar.id) } + if (existingParams.injectionPoints) grammar.injectionPoints.push(...existingParams.injectionPoints) this.grammarAddedOrUpdated(grammar) return new Disposable(() => this.removeGrammar(grammar)) } else { @@ -517,13 +545,9 @@ class GrammarRegistry { treeSitterGrammarForLanguageString (languageString) { for (const id in this.treeSitterGrammarsById) { - const grammar = this.treeSitterGrammarsById[id]; - if (grammar.injections) { - for (const injection of grammar.injections) { - if (injection(languageString)) { - return grammar - } - } + const grammar = this.treeSitterGrammarsById[id] + if (grammar.injectionRegExp && grammar.injectionRegExp.test(languageString)) { + return grammar } } } diff --git a/src/tree-sitter-grammar.js b/src/tree-sitter-grammar.js index fb54bfdb8..acea24213 100644 --- a/src/tree-sitter-grammar.js +++ b/src/tree-sitter-grammar.js @@ -10,6 +10,7 @@ class TreeSitterGrammar { this.name = params.name this.legacyScopeName = params.legacyScopeName if (params.contentRegExp) this.contentRegExp = new RegExp(params.contentRegExp) + if (params.injectionRegExp) this.injectionRegExp = new RegExp(params.injectionRegExp) this.folds = params.folds || [] @@ -29,7 +30,6 @@ class TreeSitterGrammar { this.scopeMap = new SyntaxScopeMap(scopeSelectors) this.fileTypes = params.fileTypes this.injectionPoints = params.injectionPoints || [] - this.injections = params.injections || [] // TODO - When we upgrade to a new enough version of node, use `require.resolve` // with the new `paths` option instead of this private API. diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 26c976df4..9e875de18 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -30,7 +30,7 @@ class TreeSitterLanguageMode { this.emitRangeUpdate = this.emitRangeUpdate.bind(this) this.subscription = this.buffer.onDidChangeText(({changes}) => { - for (let i = changes.length; i --> 0;) { + for (let i = changes.length - 1; i >= 0; i--) { const {oldRange, newRange} = changes[i] const startRow = oldRange.start.row const oldEndRow = oldRange.end.row @@ -421,7 +421,7 @@ class LanguageLayer { } } - destroy() { + destroy () { for (const marker of this.languageMode.injectionsMarkerLayer.getMarkers()) { if (marker.parentLanguageLayer === this) { marker.languageLayer.destroy() @@ -439,7 +439,7 @@ class LanguageLayer { if (this.patchSinceCurrentParseStarted) { const changes = this.patchSinceCurrentParseStarted.getChanges() - for (let i = changes.length; i --> 0;) { + for (let i = changes.length - 1; i >= 0; i--) { const {oldStart, oldEnd, newEnd, oldText, newText} = changes[i] this.tree.edit(this._treeEditForBufferChange( oldStart, oldEnd, newEnd, oldText, newText @@ -474,8 +474,8 @@ class LanguageLayer { let affectedRange let existingInjectionMarkers if (this.tree) { - const changedTokenRange = this.tree.getEditedRange() - affectedRange = new Range(changedTokenRange.startPosition, changedTokenRange.endPosition) + const editedRange = this.tree.getEditedRange() + affectedRange = new Range(editedRange.startPosition, editedRange.endPosition) const rangesWithSyntaxChanges = this.tree.getChangedRanges(tree) for (const range of rangesWithSyntaxChanges) { @@ -506,7 +506,7 @@ class LanguageLayer { injectionPoint.type, affectedRange.start, affectedRange.end - ); + ) for (const node of nodes) { const languageName = injectionPoint.language(node, getNodeText)