mirror of https://github.com/atom/atom.git
192 lines
5.3 KiB
JavaScript
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;
|
|
}
|