Atom/src/tree-sitter-grammar.js

192 lines
5.3 KiB
JavaScript

const path = require('path');
const SyntaxScopeMap = require('./syntax-scope-map');
const Module = require('module');
module.exports = class TreeSitterGrammar {
constructor(registry, filePath, params) {
this.registry = registry;
this.name = params.name;
this.scopeName = params.scopeName;
// TODO - Remove the `RegExp` spelling and only support `Regex`, once all of the existing
// Tree-sitter grammars are updated to spell it `Regex`.
this.contentRegex = buildRegex(params.contentRegex || params.contentRegExp);
this.injectionRegex = buildRegex(
params.injectionRegex || params.injectionRegExp
);
this.firstLineRegex = buildRegex(params.firstLineRegex);
this.folds = params.folds || [];
this.folds.forEach(normalizeFoldSpecification);
this.commentStrings = {
commentStartString: params.comments && params.comments.start,
commentEndString: params.comments && params.comments.end
};
const scopeSelectors = {};
for (const key in params.scopes || {}) {
const classes = preprocessScopes(params.scopes[key]);
const selectors = key.split(/,\s+/);
for (let selector of selectors) {
selector = selector.trim();
if (!selector) continue;
if (scopeSelectors[selector]) {
scopeSelectors[selector] = [].concat(
scopeSelectors[selector],
classes
);
} else {
scopeSelectors[selector] = classes;
}
}
}
this.scopeMap = new SyntaxScopeMap(scopeSelectors);
this.fileTypes = params.fileTypes || [];
this.injectionPointsByType = {};
for (const injectionPoint of params.injectionPoints || []) {
this.addInjectionPoint(injectionPoint);
}
// TODO - When we upgrade to a new enough version of node, use `require.resolve`
// with the new `paths` option instead of this private API.
const languageModulePath = Module._resolveFilename(params.parser, {
id: filePath,
filename: filePath,
paths: Module._nodeModulePaths(path.dirname(filePath))
});
this.languageModule = require(languageModulePath);
this.classNamesById = new Map();
this.scopeNamesById = new Map();
this.idsByScope = Object.create(null);
this.nextScopeId = 256 + 1;
this.registration = null;
}
inspect() {
return `TreeSitterGrammar {scopeName: ${this.scopeName}}`;
}
idForScope(scopeName) {
let id = this.idsByScope[scopeName];
if (!id) {
id = this.nextScopeId += 2;
const className = scopeName
.split('.')
.map(s => `syntax--${s}`)
.join(' ');
this.idsByScope[scopeName] = id;
this.classNamesById.set(id, className);
this.scopeNamesById.set(id, scopeName);
}
return id;
}
classNameForScopeId(id) {
return this.classNamesById.get(id);
}
scopeNameForScopeId(id) {
return this.scopeNamesById.get(id);
}
activate() {
this.registration = this.registry.addGrammar(this);
}
deactivate() {
if (this.registration) this.registration.dispose();
}
addInjectionPoint(injectionPoint) {
let injectionPoints = this.injectionPointsByType[injectionPoint.type];
if (!injectionPoints) {
injectionPoints = this.injectionPointsByType[injectionPoint.type] = [];
}
injectionPoints.push(injectionPoint);
}
removeInjectionPoint(injectionPoint) {
const injectionPoints = this.injectionPointsByType[injectionPoint.type];
if (injectionPoints) {
const index = injectionPoints.indexOf(injectionPoint);
if (index !== -1) injectionPoints.splice(index, 1);
if (injectionPoints.length === 0) {
delete this.injectionPointsByType[injectionPoint.type];
}
}
}
/*
Section - Backward compatibility shims
*/
onDidUpdate(callback) {
// do nothing
}
tokenizeLines(text, compatibilityMode = true) {
return text.split('\n').map(line => this.tokenizeLine(line, null, false));
}
tokenizeLine(line, ruleStack, firstLine) {
return {
value: line,
scopes: [this.scopeName]
};
}
};
const preprocessScopes = value =>
typeof value === 'string'
? value
: Array.isArray(value)
? value.map(preprocessScopes)
: value.match
? { match: new RegExp(value.match), scopes: preprocessScopes(value.scopes) }
: Object.assign({}, value, { scopes: preprocessScopes(value.scopes) });
const NODE_NAME_REGEX = /[\w_]+/;
function matcherForSpec(spec) {
if (typeof spec === 'string') {
if (spec[0] === '"' && spec[spec.length - 1] === '"') {
return {
type: spec.substr(1, spec.length - 2),
named: false
};
}
if (!NODE_NAME_REGEX.test(spec)) {
return { type: spec, named: false };
}
return { type: spec, named: true };
}
return spec;
}
function normalizeFoldSpecification(spec) {
if (spec.type) {
if (Array.isArray(spec.type)) {
spec.matchers = spec.type.map(matcherForSpec);
} else {
spec.matchers = [matcherForSpec(spec.type)];
}
}
if (spec.start) normalizeFoldSpecification(spec.start);
if (spec.end) normalizeFoldSpecification(spec.end);
}
function buildRegex(value) {
// Allow multiple alternatives to be specified via an array, for
// readability of the grammar file
if (Array.isArray(value)) value = value.map(_ => `(${_})`).join('|');
if (typeof value === 'string') return new RegExp(value);
return null;
}