123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832 |
- const _ = require('underscore-plus');
- const Grim = require('grim');
- const CSON = require('season');
- const SecondMate = require('second-mate');
- const { Disposable, CompositeDisposable, Emitter } = require('event-kit');
- const TextMateLanguageMode = require('./text-mate-language-mode');
- const NodeTreeSitterLanguageMode = require('./tree-sitter-language-mode');
- const WASMTreeSitterLanguageMode = require('./wasm-tree-sitter-language-mode');
- const TreeSitterGrammar = require('./tree-sitter-grammar');
- const WASMTreeSitterGrammar = require('./wasm-tree-sitter-grammar');
- const ScopeDescriptor = require('./scope-descriptor');
- const Token = require('./token');
- const fs = require('fs-plus');
- const { Point, Range } = require('text-buffer');
- const PATH_SPLIT_REGEX = new RegExp('[/.]');
- // Extended: This class holds the grammars used for tokenizing.
- //
- // An instance of this class is always available as the `atom.grammars` global.
- module.exports = class GrammarRegistry {
- constructor({ config } = {}) {
- this.config = config;
- this.subscriptions = new CompositeDisposable();
- this.textmateRegistry = new SecondMate.GrammarRegistry({
- maxTokensPerLine: 100,
- maxLineLength: 1000
- });
- this.emitter = new Emitter();
- this.clear();
- }
- clear() {
- this.textmateRegistry.clear();
- this.wasmTreeSitterGrammarsById = {};
- this.treeSitterGrammarsById = {};
- if (this.subscriptions) this.subscriptions.dispose();
- this.subscriptions = new CompositeDisposable();
- this.languageOverridesByBufferId = new Map();
- this.grammarScoresByBuffer = new Map();
- this.textMateScopeNamesByTreeSitterLanguageId = new Map();
- this.treeSitterLanguageIdsByTextMateScopeName = new Map();
- const grammarAddedOrUpdated = this.grammarAddedOrUpdated.bind(this);
- this.textmateRegistry.onDidAddGrammar(grammarAddedOrUpdated);
- this.textmateRegistry.onDidUpdateGrammar(grammarAddedOrUpdated);
- let onLanguageModeChange = () => {
- this.grammarScoresByBuffer.forEach((_score, buffer) => {
- if (!this.languageOverridesByBufferId.has(buffer.id)) {
- this.autoAssignLanguageMode(buffer);
- }
- });
- };
- this.subscriptions.add(
- this.config.onDidChange('core.useTreeSitterParsers', onLanguageModeChange),
- this.config.onDidChange('core.useLegacyTreeSitter', onLanguageModeChange)
- );
- }
- serialize() {
- const languageOverridesByBufferId = {};
- this.languageOverridesByBufferId.forEach((languageId, bufferId) => {
- languageOverridesByBufferId[bufferId] = languageId;
- });
- return { languageOverridesByBufferId };
- }
- deserialize(params) {
- for (const bufferId in params.languageOverridesByBufferId || {}) {
- this.languageOverridesByBufferId.set(
- bufferId,
- params.languageOverridesByBufferId[bufferId]
- );
- }
- }
- createToken(value, scopes) {
- return new Token({ value, scopes });
- }
- // Extended: set a {TextBuffer}'s language mode based on its path and content,
- // and continue to update its language mode as grammars are added or updated, or
- // the buffer's file path changes.
- //
- // * `buffer` The {TextBuffer} whose language mode will be maintained.
- //
- // Returns a {Disposable} that can be used to stop updating the buffer's
- // language mode.
- maintainLanguageMode(buffer) {
- this.grammarScoresByBuffer.set(buffer, null);
- const languageOverride = this.languageOverridesByBufferId.get(buffer.id);
- if (languageOverride) {
- this.assignLanguageMode(buffer, languageOverride);
- } else {
- this.autoAssignLanguageMode(buffer);
- }
- const pathChangeSubscription = buffer.onDidChangePath(() => {
- this.grammarScoresByBuffer.delete(buffer);
- if (!this.languageOverridesByBufferId.has(buffer.id)) {
- this.autoAssignLanguageMode(buffer);
- }
- });
- const destroySubscription = buffer.onDidDestroy(() => {
- this.grammarScoresByBuffer.delete(buffer);
- this.languageOverridesByBufferId.delete(buffer.id);
- this.subscriptions.remove(destroySubscription);
- this.subscriptions.remove(pathChangeSubscription);
- });
- this.subscriptions.add(pathChangeSubscription, destroySubscription);
- return new Disposable(() => {
- destroySubscription.dispose();
- pathChangeSubscription.dispose();
- this.subscriptions.remove(pathChangeSubscription);
- this.subscriptions.remove(destroySubscription);
- this.grammarScoresByBuffer.delete(buffer);
- this.languageOverridesByBufferId.delete(buffer.id);
- });
- }
- // Extended: Force a {TextBuffer} to use a different grammar than the
- // one that would otherwise be selected for it.
- //
- // * `buffer` The {TextBuffer} whose grammar will be set.
- // * `languageId` The {String} id of the desired language.
- //
- // Returns a {Boolean} that indicates whether the language was successfully
- // found.
- assignLanguageMode(buffer, languageId) {
- if (buffer.getBuffer) buffer = buffer.getBuffer();
- let grammar = null;
- if (languageId != null) {
- grammar = this.grammarForId(languageId);
- if (!grammar || !grammar.scopeName) return false;
- this.languageOverridesByBufferId.set(buffer.id, languageId);
- } else {
- this.languageOverridesByBufferId.set(buffer.id, null);
- grammar = this.textmateRegistry.nullGrammar;
- }
- this.grammarScoresByBuffer.set(buffer, null);
- if (grammar !== buffer.getLanguageMode().grammar) {
- buffer.setLanguageMode(
- this.languageModeForGrammarAndBuffer(grammar, buffer)
- );
- }
- return true;
- }
- // Extended: Force a {TextBuffer} to use a different grammar than the
- // one that would otherwise be selected for it.
- //
- // * `buffer` The {TextBuffer} whose grammar will be set.
- // * `grammar` The desired {Grammar}.
- //
- // Returns a {Boolean} that indicates whether the assignment was successful
- assignGrammar(buffer, grammar) {
- if (!grammar) return false;
- if (buffer.getBuffer) buffer = buffer.getBuffer();
- this.languageOverridesByBufferId.set(buffer.id, grammar.scopeName || null);
- this.grammarScoresByBuffer.set(buffer, null);
- if (grammar !== buffer.getLanguageMode().grammar) {
- buffer.setLanguageMode(
- this.languageModeForGrammarAndBuffer(grammar, buffer)
- );
- }
- return true;
- }
- // Extended: Get the `languageId` that has been explicitly assigned to
- // the given buffer, if any.
- //
- // Returns a {String} id of the language
- getAssignedLanguageId(buffer) {
- return this.languageOverridesByBufferId.get(buffer.id);
- }
- // Extended: Remove any language mode override that has been set for the
- // given {TextBuffer}. This will assign to the buffer the best language
- // mode available.
- //
- // * `buffer` The {TextBuffer}.
- autoAssignLanguageMode(buffer) {
- const result = this.selectGrammarWithScore(
- buffer.getPath(),
- getGrammarSelectionContent(buffer)
- );
- this.languageOverridesByBufferId.delete(buffer.id);
- this.grammarScoresByBuffer.set(buffer, result.score);
- if (result.grammar !== buffer.getLanguageMode().grammar) {
- buffer.setLanguageMode(
- this.languageModeForGrammarAndBuffer(result.grammar, buffer)
- );
- }
- }
- languageModeForGrammarAndBuffer(grammar, buffer) {
- if (grammar instanceof WASMTreeSitterGrammar) {
- return new WASMTreeSitterLanguageMode({
- grammar,
- buffer,
- config: this.config,
- grammars: this
- });
- } else if (grammar instanceof TreeSitterGrammar) {
- return new NodeTreeSitterLanguageMode({
- grammar,
- buffer,
- config: this.config,
- grammars: this
- });
- } else {
- return new TextMateLanguageMode({ grammar, buffer, config: this.config });
- }
- }
- // Extended: Select a grammar for the given file path and file contents.
- //
- // This picks the best match by checking the file path and contents against
- // each grammar.
- //
- // * `filePath` A {String} file path.
- // * `fileContents` A {String} of text for the file path.
- //
- // Returns a {Grammar}, never null.
- selectGrammar(filePath, fileContents) {
- return this.selectGrammarWithScore(filePath, fileContents).grammar;
- }
- selectGrammarWithScore(filePath, fileContents) {
- let bestMatch = null;
- let highestScore = -Infinity;
- this.forEachGrammar(grammar => {
- const score = this.getGrammarScore(grammar, filePath, fileContents);
- if (score > highestScore || bestMatch == null) {
- bestMatch = grammar;
- highestScore = score;
- }
- });
- return { grammar: bestMatch, score: highestScore };
- }
- getLanguageParserForScope(scope) {
- if (typeof scope === 'string') {
- scope = new ScopeDescriptor({ scopes: [scope] })
- }
- let useTreeSitterParsers = this.config.get('core.useTreeSitterParsers', { scope });
- let useLegacyTreeSitter = this.config.get('core.useLegacyTreeSitter', { scope });
- if (!useTreeSitterParsers) return 'textmate';
- return useLegacyTreeSitter ? 'node-tree-sitter' : 'wasm-tree-sitter';
- }
- // Extended: Evaluates a grammar's fitness for use for a certain file.
- //
- // By analyzing the file's extension and contents — plus other criteria, like
- // the user's configuration — Pulsar will assign a score to this grammar that
- // represents how suitable it is for the given file.
- //
- // Ultimately, whichever grammar scores highest for this file will be used
- // to highlight it.
- //
- // * `grammar`: A given {Grammar}.
- // * `filePath`: A {String} path to the file.
- // * `contents`: The {String} contents of the file.
- //
- // Returns a {Number}.
- getGrammarScore(grammar, filePath, contents) {
- if (contents == null && fs.isFileSync(filePath)) {
- contents = fs.readFileSync(filePath, 'utf8');
- }
- // Initially identify matching grammars based on the filename and the first
- // line of the file.
- let score = this.getGrammarPathScore(grammar, filePath);
- if (this.grammarMatchesPrefix(grammar, contents)) score += 0.5;
- // If multiple grammars match by one of the above criteria, break ties.
- if (score > 0) {
- const isNewTreeSitter = grammar instanceof WASMTreeSitterGrammar;
- const isOldTreeSitter = grammar instanceof TreeSitterGrammar;
- const isTreeSitter = isNewTreeSitter || isOldTreeSitter;
- let scope = new ScopeDescriptor({ scopes: [grammar.scopeName] });
- let parserConfig = this.getLanguageParserForScope(scope);
- // Prefer either TextMate or Tree-sitter grammars based on the user's
- // settings.
- //
- // TODO: This logic is a bit convoluted temporarily as we transition away
- // from legacy tree-sitter grammars; it can be vastly simplified once the
- // transition is complete.
- if (isNewTreeSitter) {
- if (parserConfig === 'wasm-tree-sitter') {
- score += 0.1;
- } else if (parserConfig === 'textmate') {
- score = -1;
- }
- } else if (isOldTreeSitter) {
- if (parserConfig === 'node-tree-sitter') {
- score += 0.1;
- } else if (parserConfig === 'wasm-tree-sitter') {
- // If `useLegacyTreeSitter` isn't checked, we probably still prefer a
- // legacy Tree-sitter grammar over a TextMate-style grammar. Bump the
- // score, but just a bit less than we'd bump it if this were a
- // modern Tree-sitter grammar.
- score += 0.09;
- } else if (parserConfig === 'textmate') {
- score = -1;
- }
- }
- // Prefer grammars with matching content regexes. Prefer a grammar with
- // no content regex over one with a non-matching content regex.
- if (grammar.contentRegex) {
- const contentMatch = isTreeSitter
- ? grammar.contentRegex.test(contents)
- : grammar.contentRegex.findNextMatchSync(contents);
- if (contentMatch) {
- score += 0.05;
- } else {
- score -= 0.05;
- }
- }
- // Prefer grammars that the user has manually installed over bundled
- // grammars.
- if (!grammar.bundledPackage) score += 0.01;
- }
- return score;
- }
- getGrammarPathScore(grammar, filePath) {
- if (!filePath) return -1;
- if (process.platform === 'win32') {
- filePath = filePath.replace(/\\/g, '/');
- }
- const pathComponents = filePath.toLowerCase().split(PATH_SPLIT_REGEX);
- let pathScore = 0;
- let customFileTypes;
- if (this.config.get('core.customFileTypes')) {
- customFileTypes = this.config.get('core.customFileTypes')[
- grammar.scopeName
- ];
- }
- let { fileTypes } = grammar;
- if (customFileTypes) {
- fileTypes = fileTypes.concat(customFileTypes);
- }
- for (let i = 0; i < fileTypes.length; i++) {
- const fileType = fileTypes[i];
- const fileTypeComponents = fileType.toLowerCase().split(PATH_SPLIT_REGEX);
- const pathSuffix = pathComponents.slice(-fileTypeComponents.length);
- if (_.isEqual(pathSuffix, fileTypeComponents)) {
- pathScore = Math.max(pathScore, fileType.length);
- if (i >= grammar.fileTypes.length) {
- pathScore += 0.5;
- }
- }
- }
- return pathScore;
- }
- grammarMatchesPrefix(grammar, contents) {
- if (contents && grammar.firstLineRegex) {
- let escaped = false;
- let numberOfNewlinesInRegex = 0;
- for (let character of grammar.firstLineRegex.source) {
- switch (character) {
- case '\\':
- escaped = !escaped;
- break;
- case 'n':
- if (escaped) {
- numberOfNewlinesInRegex++;
- }
- escaped = false;
- break;
- default:
- escaped = false;
- }
- }
- const prefix = contents
- .split('\n')
- .slice(0, numberOfNewlinesInRegex + 1)
- .join('\n');
- if (grammar.firstLineRegex.findNextMatchSync) {
- return grammar.firstLineRegex.findNextMatchSync(prefix);
- } else {
- return grammar.firstLineRegex.test(prefix);
- }
- } else {
- return false;
- }
- }
- forEachGrammar(callback) {
- this.getGrammars({ includeTreeSitter: true }).forEach(callback);
- }
- grammarForId(languageId) {
- if (!languageId) return null;
- const config = this.getLanguageParserForScope(
- new ScopeDescriptor({ scopes: [languageId] })
- );
- let getTreeSitterGrammar = (table, languageId) => {
- let grammar = table[languageId];
- if (grammar?.scopeName) {
- return grammar;
- }
- return null;
- };
- if (config === 'wasm-tree-sitter') {
- return (
- getTreeSitterGrammar(
- this.wasmTreeSitterGrammarsById,
- languageId
- ) ||
- this.textmateRegistry.grammarForScopeName(languageId)
- );
- } else if (config === 'node-tree-sitter') {
- return (
- getTreeSitterGrammar(
- this.treeSitterGrammarsById,
- languageId
- ) ||
- this.textmateRegistry.grammarForScopeName(languageId)
- );
- } else {
- return (
- this.textmateRegistry.grammarForScopeName(languageId) ||
- this.wasmTreeSitterGrammarsById[languageId] ||
- this.treeSitterGrammarsById[languageId]
- );
- }
- }
- // Deprecated: Get the grammar override for the given file path.
- //
- // * `filePath` A {String} file path.
- //
- // Returns a {String} such as `"source.js"`.
- grammarOverrideForPath(filePath) {
- Grim.deprecate('Use buffer.getLanguageMode().getLanguageId() instead');
- const buffer = atom.project.findBufferForPath(filePath);
- if (buffer) return this.getAssignedLanguageId(buffer);
- }
- // Deprecated: Set the grammar override for the given file path.
- //
- // * `filePath` A non-empty {String} file path.
- // * `languageId` A {String} such as `"source.js"`.
- //
- // Returns undefined.
- setGrammarOverrideForPath(filePath, languageId) {
- Grim.deprecate(
- 'Use atom.grammars.assignLanguageMode(buffer, languageId) instead'
- );
- const buffer = atom.project.findBufferForPath(filePath);
- if (buffer) {
- const grammar = this.grammarForScopeName(languageId);
- if (grammar)
- this.languageOverridesByBufferId.set(buffer.id, grammar.name);
- }
- }
- // Remove the grammar override for the given file path.
- //
- // * `filePath` A {String} file path.
- //
- // Returns undefined.
- clearGrammarOverrideForPath(filePath) {
- Grim.deprecate('Use atom.grammars.autoAssignLanguageMode(buffer) instead');
- const buffer = atom.project.findBufferForPath(filePath);
- if (buffer) this.languageOverridesByBufferId.delete(buffer.id);
- }
- grammarAddedOrUpdated(grammar) {
- if (grammar.scopeName && !grammar.id) grammar.id = grammar.scopeName;
- this.grammarScoresByBuffer.forEach((score, buffer) => {
- const languageMode = buffer.getLanguageMode();
- const languageOverride = this.languageOverridesByBufferId.get(buffer.id);
- if (
- grammar === buffer.getLanguageMode().grammar ||
- grammar === this.grammarForId(languageOverride)
- ) {
- buffer.setLanguageMode(
- this.languageModeForGrammarAndBuffer(grammar, buffer)
- );
- return;
- } else if (!languageOverride) {
- const score = this.getGrammarScore(
- grammar,
- buffer.getPath(),
- getGrammarSelectionContent(buffer)
- );
- const currentScore = this.grammarScoresByBuffer.get(buffer);
- if (currentScore == null || score > currentScore) {
- buffer.setLanguageMode(
- this.languageModeForGrammarAndBuffer(grammar, buffer)
- );
- this.grammarScoresByBuffer.set(buffer, score);
- return;
- }
- }
- languageMode.updateForInjection(grammar);
- });
- }
- // Extended: Invoke the given callback when a grammar is added to the registry.
- //
- // * `callback` {Function} to call when a grammar is added.
- // * `grammar` {Grammar} that was added.
- //
- // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidAddGrammar(callback) {
- let disposable = new CompositeDisposable();
- disposable.add(
- this.textmateRegistry.onDidAddGrammar(callback),
- this.emitter.on('did-add-grammar', callback)
- );
- return disposable;
- }
- // Extended: Invoke the given callback when a grammar is updated due to a grammar
- // it depends on being added or removed from the registry.
- //
- // * `callback` {Function} to call when a grammar is updated.
- // * `grammar` {Grammar} that was updated.
- //
- // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidUpdateGrammar(callback) {
- let disposable = new CompositeDisposable();
- disposable.add(
- this.textmateRegistry.onDidUpdateGrammar(callback),
- this.emitter.on('did-update-grammar', callback)
- );
- return disposable;
- }
- // Public: 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' `injectionRegex` 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 or array of syntax nodes that contain the embedded source
- // code. Depending on the settings below, the content node(s) will be converted into a
- // series of buffer ranges; when this injection is parsed, anything not inside those
- // ranges will be invisible to the parser.
- // * `includeChildren` A {Boolean} that indicates whether the children (and, in fact, all
- // descendants) of the nodes returned by `content` should be included in the injection's
- // buffer range(s). Defaults to `false`.
- // * `newlinesBetween` A {Boolean} that indicates whether each node returned from `content`
- // should be separated by at least one newline character so that the parser understands
- // them to be logically separated. Embedded languages like ERB and EJS need this. Defaults
- // to {false}.
- // * `languageScope` A {String} or {Function} that returns the desired scope name to apply
- // to each of the injection's buffer ranges. Defaults to the injected grammar's own language
- // scope — e.g., `source.js` for the JavaScript grammar. Set to `null` if the language scope
- // should be omitted. If a {Function}, will be called with the grammar instance as an
- // argument, and should return either a {String} or `null`.
- // * `coverShallowerScopes` A {Boolean} that indicates whether this injection should prevent
- // shallower layers (including the layer that created this injection) from adding scopes
- // within any of this injection's buffer ranges. Useful for injecting languages into
- // themselves — for instance, injecting Rust into Rust macro definitions.
- // * `includeAdjacentWhitespace` A {Boolean} that indicates whether the injection's buffer
- // range(s) should include whitespace that occurs between two adjacent ranges. Defaults to
- // `false`. When `true`, if two consecutive injection buffer ranges are separated _only_ by
- // whitespace, those ranges will be consolidated into one range along with that whitespace.
- //
- addInjectionPoint(grammarId, injectionPoint, { only = null } = {}) {
- let grammarsToDispose = [];
- const addOrCreateInjectionPoint = (table, grammarId) => {
- let grammar = table[grammarId];
- if (grammar) {
- if (grammar.addInjectionPoint) {
- grammar.addInjectionPoint(injectionPoint);
- // This is a grammar that's already loaded — not just a stub. Editors
- // that already use this grammar will want to know that we added an
- // injection.
- this.emitter.emit('did-update-grammar', grammar);
- } else {
- grammar.injectionPoints.push(injectionPoint);
- }
- grammarsToDispose.push(grammar);
- } else {
- table[grammarId] = { injectionPoints: [injectionPoint] }
- }
- };
- // TEMP: By default, an injection point will be added for both kinds of
- // tree-sitter grammars, but the optional keyword argument `only` lets us
- // target one or the other. We'll only need this option until we transition
- // away from legacy tree-sitter.
- if (!only || only === 'legacy') {
- addOrCreateInjectionPoint(this.treeSitterGrammarsById, grammarId);
- }
- if (!only || only === 'modern') {
- addOrCreateInjectionPoint(this.wasmTreeSitterGrammarsById, grammarId);
- }
- return new Disposable(() => {
- for (let grammar of grammarsToDispose) {
- grammar.removeInjectionPoint(injectionPoint);
- }
- });
- }
- get nullGrammar() {
- return this.textmateRegistry.nullGrammar;
- }
- get grammars() {
- return this.getGrammars();
- }
- decodeTokens() {
- return this.textmateRegistry.decodeTokens.apply(
- this.textmateRegistry,
- arguments
- );
- }
- grammarForScopeName(scopeName) {
- return this.grammarForId(scopeName);
- }
- addGrammar(grammar) {
- if (grammar instanceof WASMTreeSitterGrammar) {
- const existingParams =
- this.wasmTreeSitterGrammarsById[grammar.scopeName] || {};
- if (grammar.scopeName)
- this.wasmTreeSitterGrammarsById[grammar.scopeName] = grammar;
- if (existingParams.injectionPoints) {
- for (const injectionPoint of existingParams.injectionPoints) {
- grammar.addInjectionPoint(injectionPoint);
- }
- }
- this.grammarAddedOrUpdated(grammar);
- this.emitter.emit('did-add-grammar', grammar);
- return new Disposable(() => this.removeGrammar(grammar));
- } else if (grammar instanceof TreeSitterGrammar) {
- const existingParams =
- this.treeSitterGrammarsById[grammar.scopeName] || {};
- if (grammar.scopeName)
- this.treeSitterGrammarsById[grammar.scopeName] = grammar;
- if (existingParams.injectionPoints) {
- for (const injectionPoint of existingParams.injectionPoints) {
- grammar.addInjectionPoint(injectionPoint);
- }
- }
- this.grammarAddedOrUpdated(grammar);
- this.emitter.emit('did-add-grammar', grammar);
- return new Disposable(() => this.removeGrammar(grammar));
- } else {
- return this.textmateRegistry.addGrammar(grammar);
- }
- }
- removeGrammar(grammar) {
- if (grammar instanceof WASMTreeSitterGrammar) {
- delete this.wasmTreeSitterGrammarsById[grammar.scopeName];
- } else if (grammar instanceof TreeSitterGrammar) {
- delete this.treeSitterGrammarsById[grammar.scopeName];
- } else {
- return this.textmateRegistry.removeGrammar(grammar);
- }
- }
- removeGrammarForScopeName(scopeName) {
- return this.textmateRegistry.removeGrammarForScopeName(scopeName);
- }
- // Extended: Read a grammar asynchronously and add it to the registry.
- //
- // * `grammarPath` A {String} absolute file path to a grammar file.
- // * `callback` A {Function} to call when loaded with the following arguments:
- // * `error` An {Error}, may be null.
- // * `grammar` A {Grammar} or null if an error occurred.
- loadGrammar(grammarPath, callback) {
- this.readGrammar(grammarPath, (error, grammar) => {
- if (error) return callback(error);
- this.addGrammar(grammar);
- callback(null, grammar);
- });
- }
- // Extended: Read a grammar synchronously and add it to this registry.
- //
- // * `grammarPath` A {String} absolute file path to a grammar file.
- //
- // Returns a {Grammar}.
- loadGrammarSync(grammarPath) {
- const grammar = this.readGrammarSync(grammarPath);
- this.addGrammar(grammar);
- return grammar;
- }
- // Extended: Read a grammar asynchronously but don't add it to the registry.
- //
- // * `grammarPath` A {String} absolute file path to a grammar file.
- // * `callback` A {Function} to call when read with the following arguments:
- // * `error` An {Error}, may be null.
- // * `grammar` A {Grammar} or null if an error occurred.
- //
- // Returns undefined.
- readGrammar(grammarPath, callback) {
- if (!callback) callback = () => {};
- CSON.readFile(grammarPath, (error, params = {}) => {
- if (error) return callback(error);
- try {
- callback(null, this.createGrammar(grammarPath, params));
- } catch (error) {
- callback(error);
- }
- });
- }
- // Extended: Read a grammar synchronously but don't add it to the registry.
- //
- // * `grammarPath` A {String} absolute file path to a grammar file.
- //
- // Returns a {Grammar}.
- readGrammarSync(grammarPath) {
- return this.createGrammar(
- grammarPath,
- CSON.readFileSync(grammarPath) || {}
- );
- }
- createGrammar(grammarPath, params) {
- if (params.type === 'modern-tree-sitter') {
- return new WASMTreeSitterGrammar(this, grammarPath, params)
- } else if (params.type === 'tree-sitter') {
- return new TreeSitterGrammar(this, grammarPath, params);
- } else {
- if (
- typeof params.scopeName !== 'string' ||
- params.scopeName.length === 0
- ) {
- throw new Error(
- `Grammar missing required scopeName property: ${grammarPath}`
- );
- }
- return this.textmateRegistry.createGrammar(grammarPath, params);
- }
- }
- // Extended: Get all the grammars in this registry.
- //
- // * `options` (optional) {Object}
- // * `includeTreeSitter` (optional) {Boolean} Set to include
- // [Tree-sitter](https://github.blog/2018-10-31-atoms-new-parsing-system/) grammars
- //
- // Returns a non-empty {Array} of {Grammar} instances.
- getGrammars(params) {
- let result = this.textmateRegistry.getGrammars();
- if (!(params && params.includeTreeSitter)) return result;
- let modernTsGrammars = Object.values(this.wasmTreeSitterGrammarsById)
- .filter(g => g.scopeName);
- result = result.concat(modernTsGrammars);
- // We must include all legacy Tree-sitter grammars here just in case the
- // user has opted into `useTreeSitterGrammars` via a scope-specific
- // setting.
- const legacyTsGrammars = Object.values(this.treeSitterGrammarsById)
- .filter(g => g.scopeName);
- result = result.concat(legacyTsGrammars);
- return result;
- }
- scopeForId(id) {
- return this.textmateRegistry.scopeForId(id);
- }
- // Match up a language string (of the sort generated by an injection point)
- // with a grammar. Checks the `injectionRegex` property on grammars and
- // returns the one with the longest match.
- treeSitterGrammarForLanguageString(languageString, type = 'wasm') {
- let longestMatchLength = 0;
- let grammarWithLongestMatch = null;
- let table = type === 'original' ? this.treeSitterGrammarsById : this.wasmTreeSitterGrammarsById;
- for (const id in table) {
- const grammar = table[id];
- if (grammar.injectionRegex) {
- const match = languageString.match(grammar.injectionRegex);
- if (match) {
- const { length } = match[0];
- if (length > longestMatchLength) {
- grammarWithLongestMatch = grammar;
- longestMatchLength = length;
- }
- }
- }
- }
- return grammarWithLongestMatch;
- }
- };
- function getGrammarSelectionContent(buffer) {
- return buffer.getTextInRange(
- Range(Point(0, 0), buffer.positionForCharacterIndex(1024))
- );
- }
|