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 }