Path: blob/main/src/vs/editor/common/languages/languageConfigurationRegistry.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter, Event } from '../../../base/common/event.js';6import { Disposable, IDisposable, markAsSingleton, toDisposable } from '../../../base/common/lifecycle.js';7import * as strings from '../../../base/common/strings.js';8import { ITextModel } from '../model.js';9import { DEFAULT_WORD_REGEXP, ensureValidWordDefinition } from '../core/wordHelper.js';10import { EnterAction, FoldingRules, IAutoClosingPair, IndentationRule, LanguageConfiguration, AutoClosingPairs, CharacterPair, ExplicitLanguageConfiguration } from './languageConfiguration.js';11import { CharacterPairSupport } from './supports/characterPair.js';12import { BracketElectricCharacterSupport } from './supports/electricCharacter.js';13import { IndentRulesSupport } from './supports/indentRules.js';14import { OnEnterSupport } from './supports/onEnter.js';15import { RichEditBrackets } from './supports/richEditBrackets.js';16import { EditorAutoIndentStrategy } from '../config/editorOptions.js';17import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';18import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';19import { ILanguageService } from './language.js';20import { InstantiationType, registerSingleton } from '../../../platform/instantiation/common/extensions.js';21import { PLAINTEXT_LANGUAGE_ID } from './modesRegistry.js';22import { LanguageBracketsConfiguration } from './supports/languageBracketsConfiguration.js';2324/**25* Interface used to support insertion of mode specific comments.26*/27export interface ICommentsConfiguration {28lineCommentToken?: string;29lineCommentNoIndent?: boolean;30blockCommentStartToken?: string;31blockCommentEndToken?: string;32}3334export interface ILanguageConfigurationService {35readonly _serviceBrand: undefined;3637onDidChange: Event<LanguageConfigurationServiceChangeEvent>;3839/**40* @param priority Use a higher number for higher priority41*/42register(languageId: string, configuration: LanguageConfiguration, priority?: number): IDisposable;4344getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration;4546}4748export class LanguageConfigurationServiceChangeEvent {49constructor(public readonly languageId: string | undefined) { }5051public affects(languageId: string): boolean {52return !this.languageId ? true : this.languageId === languageId;53}54}5556export const ILanguageConfigurationService = createDecorator<ILanguageConfigurationService>('languageConfigurationService');5758export class LanguageConfigurationService extends Disposable implements ILanguageConfigurationService {59_serviceBrand: undefined;6061private readonly _registry = this._register(new LanguageConfigurationRegistry());6263private readonly onDidChangeEmitter = this._register(new Emitter<LanguageConfigurationServiceChangeEvent>());64public readonly onDidChange = this.onDidChangeEmitter.event;6566private readonly configurations = new Map<string, ResolvedLanguageConfiguration>();6768constructor(69@IConfigurationService private readonly configurationService: IConfigurationService,70@ILanguageService private readonly languageService: ILanguageService71) {72super();7374const languageConfigKeys = new Set(Object.values(customizedLanguageConfigKeys));7576this._register(this.configurationService.onDidChangeConfiguration((e) => {77const globalConfigChanged = e.change.keys.some((k) =>78languageConfigKeys.has(k)79);80const localConfigChanged = e.change.overrides81.filter(([overrideLangName, keys]) =>82keys.some((k) => languageConfigKeys.has(k))83)84.map(([overrideLangName]) => overrideLangName);8586if (globalConfigChanged) {87this.configurations.clear();88this.onDidChangeEmitter.fire(new LanguageConfigurationServiceChangeEvent(undefined));89} else {90for (const languageId of localConfigChanged) {91if (this.languageService.isRegisteredLanguageId(languageId)) {92this.configurations.delete(languageId);93this.onDidChangeEmitter.fire(new LanguageConfigurationServiceChangeEvent(languageId));94}95}96}97}));9899this._register(this._registry.onDidChange((e) => {100this.configurations.delete(e.languageId);101this.onDidChangeEmitter.fire(new LanguageConfigurationServiceChangeEvent(e.languageId));102}));103}104105public register(languageId: string, configuration: LanguageConfiguration, priority?: number): IDisposable {106return this._registry.register(languageId, configuration, priority);107}108109public getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration {110let result = this.configurations.get(languageId);111if (!result) {112result = computeConfig(languageId, this._registry, this.configurationService, this.languageService);113this.configurations.set(languageId, result);114}115return result;116}117}118119function computeConfig(120languageId: string,121registry: LanguageConfigurationRegistry,122configurationService: IConfigurationService,123languageService: ILanguageService,124): ResolvedLanguageConfiguration {125let languageConfig = registry.getLanguageConfiguration(languageId);126127if (!languageConfig) {128if (!languageService.isRegisteredLanguageId(languageId)) {129// this happens for the null language, which can be returned by monarch.130// Instead of throwing an error, we just return a default config.131return new ResolvedLanguageConfiguration(languageId, {});132}133languageConfig = new ResolvedLanguageConfiguration(languageId, {});134}135136const customizedConfig = getCustomizedLanguageConfig(languageConfig.languageId, configurationService);137const data = combineLanguageConfigurations([languageConfig.underlyingConfig, customizedConfig]);138const config = new ResolvedLanguageConfiguration(languageConfig.languageId, data);139return config;140}141142const customizedLanguageConfigKeys = {143brackets: 'editor.language.brackets',144colorizedBracketPairs: 'editor.language.colorizedBracketPairs'145};146147function getCustomizedLanguageConfig(languageId: string, configurationService: IConfigurationService): LanguageConfiguration {148const brackets = configurationService.getValue(customizedLanguageConfigKeys.brackets, {149overrideIdentifier: languageId,150});151152const colorizedBracketPairs = configurationService.getValue(customizedLanguageConfigKeys.colorizedBracketPairs, {153overrideIdentifier: languageId,154});155156return {157brackets: validateBracketPairs(brackets),158colorizedBracketPairs: validateBracketPairs(colorizedBracketPairs),159};160}161162function validateBracketPairs(data: unknown): CharacterPair[] | undefined {163if (!Array.isArray(data)) {164return undefined;165}166return data.map(pair => {167if (!Array.isArray(pair) || pair.length !== 2) {168return undefined;169}170return [pair[0], pair[1]] as CharacterPair;171}).filter((p): p is CharacterPair => !!p);172}173174export function getIndentationAtPosition(model: ITextModel, lineNumber: number, column: number): string {175const lineText = model.getLineContent(lineNumber);176let indentation = strings.getLeadingWhitespace(lineText);177if (indentation.length > column - 1) {178indentation = indentation.substring(0, column - 1);179}180return indentation;181}182183class ComposedLanguageConfiguration {184private readonly _entries: LanguageConfigurationContribution[];185private _order: number;186private _resolved: ResolvedLanguageConfiguration | null = null;187188constructor(public readonly languageId: string) {189this._entries = [];190this._order = 0;191this._resolved = null;192}193194public register(195configuration: LanguageConfiguration,196priority: number197): IDisposable {198const entry = new LanguageConfigurationContribution(199configuration,200priority,201++this._order202);203this._entries.push(entry);204this._resolved = null;205return markAsSingleton(toDisposable(() => {206for (let i = 0; i < this._entries.length; i++) {207if (this._entries[i] === entry) {208this._entries.splice(i, 1);209this._resolved = null;210break;211}212}213}));214}215216public getResolvedConfiguration(): ResolvedLanguageConfiguration | null {217if (!this._resolved) {218const config = this._resolve();219if (config) {220this._resolved = new ResolvedLanguageConfiguration(221this.languageId,222config223);224}225}226return this._resolved;227}228229private _resolve(): LanguageConfiguration | null {230if (this._entries.length === 0) {231return null;232}233this._entries.sort(LanguageConfigurationContribution.cmp);234return combineLanguageConfigurations(this._entries.map(e => e.configuration));235}236}237238function combineLanguageConfigurations(configs: LanguageConfiguration[]): LanguageConfiguration {239let result: ExplicitLanguageConfiguration = {240comments: undefined,241brackets: undefined,242wordPattern: undefined,243indentationRules: undefined,244onEnterRules: undefined,245autoClosingPairs: undefined,246surroundingPairs: undefined,247autoCloseBefore: undefined,248folding: undefined,249colorizedBracketPairs: undefined,250__electricCharacterSupport: undefined,251};252for (const entry of configs) {253result = {254comments: entry.comments || result.comments,255brackets: entry.brackets || result.brackets,256wordPattern: entry.wordPattern || result.wordPattern,257indentationRules: entry.indentationRules || result.indentationRules,258onEnterRules: entry.onEnterRules || result.onEnterRules,259autoClosingPairs: entry.autoClosingPairs || result.autoClosingPairs,260surroundingPairs: entry.surroundingPairs || result.surroundingPairs,261autoCloseBefore: entry.autoCloseBefore || result.autoCloseBefore,262folding: entry.folding || result.folding,263colorizedBracketPairs: entry.colorizedBracketPairs || result.colorizedBracketPairs,264__electricCharacterSupport: entry.__electricCharacterSupport || result.__electricCharacterSupport,265};266}267268return result;269}270271class LanguageConfigurationContribution {272constructor(273public readonly configuration: LanguageConfiguration,274public readonly priority: number,275public readonly order: number276) { }277278public static cmp(a: LanguageConfigurationContribution, b: LanguageConfigurationContribution) {279if (a.priority === b.priority) {280// higher order last281return a.order - b.order;282}283// higher priority last284return a.priority - b.priority;285}286}287288export class LanguageConfigurationChangeEvent {289constructor(public readonly languageId: string) { }290}291292export class LanguageConfigurationRegistry extends Disposable {293private readonly _entries = new Map<string, ComposedLanguageConfiguration>();294295private readonly _onDidChange = this._register(new Emitter<LanguageConfigurationChangeEvent>());296public readonly onDidChange: Event<LanguageConfigurationChangeEvent> = this._onDidChange.event;297298constructor() {299super();300this._register(this.register(PLAINTEXT_LANGUAGE_ID, {301brackets: [302['(', ')'],303['[', ']'],304['{', '}'],305],306surroundingPairs: [307{ open: '{', close: '}' },308{ open: '[', close: ']' },309{ open: '(', close: ')' },310{ open: '<', close: '>' },311{ open: '\"', close: '\"' },312{ open: '\'', close: '\'' },313{ open: '`', close: '`' },314],315colorizedBracketPairs: [],316folding: {317offSide: true318}319}, 0));320}321322/**323* @param priority Use a higher number for higher priority324*/325public register(languageId: string, configuration: LanguageConfiguration, priority: number = 0): IDisposable {326let entries = this._entries.get(languageId);327if (!entries) {328entries = new ComposedLanguageConfiguration(languageId);329this._entries.set(languageId, entries);330}331332const disposable = entries.register(configuration, priority);333this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageId));334335return markAsSingleton(toDisposable(() => {336disposable.dispose();337this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageId));338}));339}340341public getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration | null {342const entries = this._entries.get(languageId);343return entries?.getResolvedConfiguration() || null;344}345}346347/**348* Immutable.349*/350export class ResolvedLanguageConfiguration {351private _brackets: RichEditBrackets | null;352private _electricCharacter: BracketElectricCharacterSupport | null;353private readonly _onEnterSupport: OnEnterSupport | null;354355public readonly comments: ICommentsConfiguration | null;356public readonly characterPair: CharacterPairSupport;357public readonly wordDefinition: RegExp;358public readonly indentRulesSupport: IndentRulesSupport | null;359public readonly indentationRules: IndentationRule | undefined;360public readonly foldingRules: FoldingRules;361public readonly bracketsNew: LanguageBracketsConfiguration;362363constructor(364public readonly languageId: string,365public readonly underlyingConfig: LanguageConfiguration366) {367this._brackets = null;368this._electricCharacter = null;369this._onEnterSupport =370this.underlyingConfig.brackets ||371this.underlyingConfig.indentationRules ||372this.underlyingConfig.onEnterRules373? new OnEnterSupport(this.underlyingConfig)374: null;375this.comments = ResolvedLanguageConfiguration._handleComments(this.underlyingConfig);376this.characterPair = new CharacterPairSupport(this.underlyingConfig);377378this.wordDefinition = this.underlyingConfig.wordPattern || DEFAULT_WORD_REGEXP;379this.indentationRules = this.underlyingConfig.indentationRules;380if (this.underlyingConfig.indentationRules) {381this.indentRulesSupport = new IndentRulesSupport(382this.underlyingConfig.indentationRules383);384} else {385this.indentRulesSupport = null;386}387this.foldingRules = this.underlyingConfig.folding || {};388389this.bracketsNew = new LanguageBracketsConfiguration(390languageId,391this.underlyingConfig392);393}394395public getWordDefinition(): RegExp {396return ensureValidWordDefinition(this.wordDefinition);397}398399public get brackets(): RichEditBrackets | null {400if (!this._brackets && this.underlyingConfig.brackets) {401this._brackets = new RichEditBrackets(402this.languageId,403this.underlyingConfig.brackets404);405}406return this._brackets;407}408409public get electricCharacter(): BracketElectricCharacterSupport | null {410if (!this._electricCharacter) {411this._electricCharacter = new BracketElectricCharacterSupport(412this.brackets413);414}415return this._electricCharacter;416}417418public onEnter(419autoIndent: EditorAutoIndentStrategy,420previousLineText: string,421beforeEnterText: string,422afterEnterText: string423): EnterAction | null {424if (!this._onEnterSupport) {425return null;426}427return this._onEnterSupport.onEnter(428autoIndent,429previousLineText,430beforeEnterText,431afterEnterText432);433}434435public getAutoClosingPairs(): AutoClosingPairs {436return new AutoClosingPairs(this.characterPair.getAutoClosingPairs());437}438439public getAutoCloseBeforeSet(forQuotes: boolean): string {440return this.characterPair.getAutoCloseBeforeSet(forQuotes);441}442443public getSurroundingPairs(): IAutoClosingPair[] {444return this.characterPair.getSurroundingPairs();445}446447private static _handleComments(448conf: LanguageConfiguration449): ICommentsConfiguration | null {450const commentRule = conf.comments;451if (!commentRule) {452return null;453}454455// comment configuration456const comments: ICommentsConfiguration = {};457458if (commentRule.lineComment) {459if (typeof commentRule.lineComment === 'string') {460comments.lineCommentToken = commentRule.lineComment;461} else {462comments.lineCommentToken = commentRule.lineComment.comment;463comments.lineCommentNoIndent = commentRule.lineComment.noIndent;464}465}466if (commentRule.blockComment) {467const [blockStart, blockEnd] = commentRule.blockComment;468comments.blockCommentStartToken = blockStart;469comments.blockCommentEndToken = blockEnd;470}471472return comments;473}474}475476registerSingleton(ILanguageConfigurationService, LanguageConfigurationService, InstantiationType.Delayed);477478479