Add tests and docs for addInjectionPoint

Also, replace `addInjectionPattern` API with a single `injectionRegExp` 
field on the grammar.

Co-Authored-By: Ashi Krishnan <queerviolet@github.com>
This commit is contained in:
Max Brunsfeld
2018-06-27 12:39:29 -07:00
parent 930691c689
commit 4d3916f74e
5 changed files with 67 additions and 17 deletions

View File

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

View File

@@ -324,9 +324,7 @@ describe('TreeSitterLanguageMode', () => {
tag_name: 'tag',
attribute_name: 'attr'
},
injections: [
name => name.toLowerCase().includes('html')
]
injectionRegExp: 'html'
})
atom.grammars.addGrammar(jsGrammar)

View File

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

View File

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

View File

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