/ src / tree-sitter-grammar.js
tree-sitter-grammar.js
  1  const path = require('path');
  2  const SyntaxScopeMap = require('./syntax-scope-map');
  3  const Module = require('module');
  4  
  5  module.exports = class TreeSitterGrammar {
  6    constructor(registry, filePath, params) {
  7      this.registry = registry;
  8      this.name = params.name;
  9      this.scopeName = params.scopeName;
 10  
 11      // TODO - Remove the `RegExp` spelling and only support `Regex`, once all of the existing
 12      // Tree-sitter grammars are updated to spell it `Regex`.
 13      this.contentRegex = buildRegex(params.contentRegex || params.contentRegExp);
 14      this.injectionRegex = buildRegex(
 15        params.injectionRegex || params.injectionRegExp
 16      );
 17      this.firstLineRegex = buildRegex(params.firstLineRegex);
 18  
 19      this.folds = params.folds || [];
 20      this.folds.forEach(normalizeFoldSpecification);
 21  
 22      this.commentStrings = {
 23        commentStartString: params.comments && params.comments.start,
 24        commentEndString: params.comments && params.comments.end
 25      };
 26  
 27      const scopeSelectors = {};
 28      for (const key in params.scopes || {}) {
 29        const classes = preprocessScopes(params.scopes[key]);
 30        const selectors = key.split(/,\s+/);
 31        for (let selector of selectors) {
 32          selector = selector.trim();
 33          if (!selector) continue;
 34          if (scopeSelectors[selector]) {
 35            scopeSelectors[selector] = [].concat(
 36              scopeSelectors[selector],
 37              classes
 38            );
 39          } else {
 40            scopeSelectors[selector] = classes;
 41          }
 42        }
 43      }
 44  
 45      this.scopeMap = new SyntaxScopeMap(scopeSelectors);
 46      this.fileTypes = params.fileTypes || [];
 47      this.injectionPointsByType = {};
 48  
 49      for (const injectionPoint of params.injectionPoints || []) {
 50        this.addInjectionPoint(injectionPoint);
 51      }
 52  
 53      // TODO - When we upgrade to a new enough version of node, use `require.resolve`
 54      // with the new `paths` option instead of this private API.
 55      const languageModulePath = Module._resolveFilename(params.parser, {
 56        id: filePath,
 57        filename: filePath,
 58        paths: Module._nodeModulePaths(path.dirname(filePath))
 59      });
 60  
 61      this.languageModule = require(languageModulePath);
 62      this.classNamesById = new Map();
 63      this.scopeNamesById = new Map();
 64      this.idsByScope = Object.create(null);
 65      this.nextScopeId = 256 + 1;
 66      this.registration = null;
 67    }
 68  
 69    inspect() {
 70      return `TreeSitterGrammar {scopeName: ${this.scopeName}}`;
 71    }
 72  
 73    idForScope(scopeName) {
 74      let id = this.idsByScope[scopeName];
 75      if (!id) {
 76        id = this.nextScopeId += 2;
 77        const className = scopeName
 78          .split('.')
 79          .map(s => `syntax--${s}`)
 80          .join(' ');
 81        this.idsByScope[scopeName] = id;
 82        this.classNamesById.set(id, className);
 83        this.scopeNamesById.set(id, scopeName);
 84      }
 85      return id;
 86    }
 87  
 88    classNameForScopeId(id) {
 89      return this.classNamesById.get(id);
 90    }
 91  
 92    scopeNameForScopeId(id) {
 93      return this.scopeNamesById.get(id);
 94    }
 95  
 96    activate() {
 97      this.registration = this.registry.addGrammar(this);
 98    }
 99  
100    deactivate() {
101      if (this.registration) this.registration.dispose();
102    }
103  
104    addInjectionPoint(injectionPoint) {
105      let injectionPoints = this.injectionPointsByType[injectionPoint.type];
106      if (!injectionPoints) {
107        injectionPoints = this.injectionPointsByType[injectionPoint.type] = [];
108      }
109      injectionPoints.push(injectionPoint);
110    }
111  
112    removeInjectionPoint(injectionPoint) {
113      const injectionPoints = this.injectionPointsByType[injectionPoint.type];
114      if (injectionPoints) {
115        const index = injectionPoints.indexOf(injectionPoint);
116        if (index !== -1) injectionPoints.splice(index, 1);
117        if (injectionPoints.length === 0) {
118          delete this.injectionPointsByType[injectionPoint.type];
119        }
120      }
121    }
122  
123    /*
124    Section - Backward compatibility shims
125    */
126  
127    onDidUpdate(callback) {
128      // do nothing
129    }
130  
131    tokenizeLines(text, compatibilityMode = true) {
132      return text.split('\n').map(line => this.tokenizeLine(line, null, false));
133    }
134  
135    tokenizeLine(line, ruleStack, firstLine) {
136      return {
137        value: line,
138        scopes: [this.scopeName]
139      };
140    }
141  };
142  
143  const preprocessScopes = value =>
144    typeof value === 'string'
145      ? value
146      : Array.isArray(value)
147      ? value.map(preprocessScopes)
148      : value.match
149      ? { match: new RegExp(value.match), scopes: preprocessScopes(value.scopes) }
150      : Object.assign({}, value, { scopes: preprocessScopes(value.scopes) });
151  
152  const NODE_NAME_REGEX = /[\w_]+/;
153  
154  function matcherForSpec(spec) {
155    if (typeof spec === 'string') {
156      if (spec[0] === '"' && spec[spec.length - 1] === '"') {
157        return {
158          type: spec.substr(1, spec.length - 2),
159          named: false
160        };
161      }
162  
163      if (!NODE_NAME_REGEX.test(spec)) {
164        return { type: spec, named: false };
165      }
166  
167      return { type: spec, named: true };
168    }
169    return spec;
170  }
171  
172  function normalizeFoldSpecification(spec) {
173    if (spec.type) {
174      if (Array.isArray(spec.type)) {
175        spec.matchers = spec.type.map(matcherForSpec);
176      } else {
177        spec.matchers = [matcherForSpec(spec.type)];
178      }
179    }
180  
181    if (spec.start) normalizeFoldSpecification(spec.start);
182    if (spec.end) normalizeFoldSpecification(spec.end);
183  }
184  
185  function buildRegex(value) {
186    // Allow multiple alternatives to be specified via an array, for
187    // readability of the grammar file
188    if (Array.isArray(value)) value = value.map(_ => `(${_})`).join('|');
189    if (typeof value === 'string') return new RegExp(value);
190    return null;
191  }