diff --git a/src/vs/editor/common/model/bracketPairColorizer/bracketPairColorizer.ts b/src/vs/editor/common/model/bracketPairColorizer/bracketPairColorizer.ts index 096db5d943e..7c5cc158702 100644 --- a/src/vs/editor/common/model/bracketPairColorizer/bracketPairColorizer.ts +++ b/src/vs/editor/common/model/bracketPairColorizer/bracketPairColorizer.ts @@ -13,7 +13,7 @@ import { DecorationProvider } from 'vs/editor/common/model/decorationProvider'; import { BackgroundTokenizationState, TextModel } from 'vs/editor/common/model/textModel'; import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; import { LanguageId } from 'vs/editor/common/modes'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { ILanguageConfigurationService, ResolvedLanguageConfiguration } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { editorBracketHighlightingForeground1, editorBracketHighlightingForeground2, editorBracketHighlightingForeground3, editorBracketHighlightingForeground4, editorBracketHighlightingForeground5, editorBracketHighlightingForeground6, editorBracketHighlightingUnexpectedBracketForeground } from 'vs/editor/common/view/editorColorRegistry'; @@ -45,18 +45,14 @@ export class BracketPairColorizer extends Disposable implements DecorationProvid private bracketsRequested = false; private options: BracketPairColorizationOptions; - constructor(private readonly textModel: TextModel) { + constructor( + private readonly textModel: TextModel, + private readonly languageConfigurationService: ILanguageConfigurationService + ) { super(); this.options = textModel.getOptions().bracketPairColorizationOptions; - this._register(LanguageConfigurationRegistry.onDidChange((e) => { - if (this.cache.value?.object.didLanguageChange(e.languageIdentifier.id)) { - this.cache.clear(); - this.updateCache(); - } - })); - this._register(textModel.onDidChangeOptions(e => { this.options = textModel.getOptions().bracketPairColorizationOptions; @@ -78,7 +74,24 @@ export class BracketPairColorizer extends Disposable implements DecorationProvid if (this.bracketsRequested || (this.textModel.isAttachedToEditor() && this.isDocumentSupported && this.options.enabled)) { if (!this.cache.value) { const store = new DisposableStore(); - this.cache.value = createDisposableRef(store.add(new BracketPairColorizerImpl(this.textModel)), store); + const watchedIds = new Set(); + + this.cache.value = createDisposableRef( + store.add( + new BracketPairColorizerImpl(this.textModel, (languageId) => { + if (!watchedIds.has(languageId)) { + watchedIds.add(languageId); + store.add(this.languageConfigurationService.onLanguageConfigurationDidChange(languageId, this.textModel.uri, () => { + this.cache.clear(); + this.updateCache(); + })); + } + + return this.languageConfigurationService.getLanguageConfiguration(languageId, this.textModel.uri); + }) + ), + store + ); store.add(this.cache.value.object.onDidChangeDecorations(e => this.didChangeDecorationsEmitter.fire(e))); this.didChangeDecorationsEmitter.fire(); } @@ -151,7 +164,7 @@ class BracketPairColorizerImpl extends Disposable implements DecorationProvider, private astWithTokens: AstNode | undefined; private readonly denseKeyProvider = new DenseKeyProvider(); - private readonly brackets = new LanguageAgnosticBracketTokens(this.denseKeyProvider); + private readonly brackets = new LanguageAgnosticBracketTokens(this.denseKeyProvider, this.getLanguageConfiguration); public didLanguageChange(languageId: LanguageId): boolean { return this.brackets.didLanguageChange(languageId); @@ -159,7 +172,10 @@ class BracketPairColorizerImpl extends Disposable implements DecorationProvider, readonly onDidChangeDecorations = this.didChangeDecorationsEmitter.event; - constructor(private readonly textModel: TextModel) { + constructor( + private readonly textModel: TextModel, + private readonly getLanguageConfiguration: (languageId: LanguageId) => ResolvedLanguageConfiguration + ) { super(); this._register(textModel.onBackgroundTokenizationStateChanged(() => { diff --git a/src/vs/editor/common/model/bracketPairColorizer/brackets.ts b/src/vs/editor/common/model/bracketPairColorizer/brackets.ts index ddd6e61ffa6..b35f94266dd 100644 --- a/src/vs/editor/common/model/bracketPairColorizer/brackets.ts +++ b/src/vs/editor/common/model/bracketPairColorizer/brackets.ts @@ -2,31 +2,34 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { toLength } from 'vs/editor/common/model/bracketPairColorizer/length'; import { SmallImmutableSet, DenseKeyProvider, identityKeyProvider } from 'vs/editor/common/model/bracketPairColorizer/smallImmutableSet'; import { LanguageId } from 'vs/editor/common/modes'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { ResolvedLanguageConfiguration } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { BracketAstNode } from './ast'; import { OpeningBracketId, Token, TokenKind } from './tokenizer'; export class BracketTokens { - static createFromLanguage(languageId: LanguageId, denseKeyProvider: DenseKeyProvider): BracketTokens { + static createFromLanguage(configuration: ResolvedLanguageConfiguration, denseKeyProvider: DenseKeyProvider): BracketTokens { function getId(languageId: LanguageId, openingText: string): OpeningBracketId { return denseKeyProvider.getKey(`${languageId}:::${openingText}`); } - const brackets = [...(LanguageConfigurationRegistry.getColorizedBracketPairs(languageId))]; + const brackets = configuration.characterPair.getColorizedBrackets(); const closingBrackets = new Map, first: OpeningBracketId }>(); const openingBrackets = new Set(); for (const [openingText, closingText] of brackets) { + if (openingText === '' || closingText === '') { + continue; + } + openingBrackets.add(openingText); let info = closingBrackets.get(closingText); - const openingTextId = getId(languageId, openingText); + const openingTextId = getId(configuration.languageIdentifier.id, openingText); if (!info) { info = { openingBrackets: SmallImmutableSet.getEmpty(), first: openingTextId }; closingBrackets.set(closingText, info); @@ -49,7 +52,7 @@ export class BracketTokens { for (const openingText of openingBrackets) { const length = toLength(0, openingText.length); - const openingTextId = getId(languageId, openingText); + const openingTextId = getId(configuration.languageIdentifier.id, openingText); map.set(openingText, new Token( length, TokenKind.OpeningBracket, @@ -104,7 +107,10 @@ export class BracketTokens { export class LanguageAgnosticBracketTokens { private readonly languageIdToBracketTokens: Map = new Map(); - constructor(private readonly denseKeyProvider: DenseKeyProvider) { + constructor( + private readonly denseKeyProvider: DenseKeyProvider, + private readonly getLanguageConfiguration: (languageId: LanguageId) => ResolvedLanguageConfiguration, + ) { } public didLanguageChange(languageId: LanguageId): boolean { @@ -112,14 +118,14 @@ export class LanguageAgnosticBracketTokens { if (!existing) { return false; } - const newRegExpStr = BracketTokens.createFromLanguage(languageId, this.denseKeyProvider).getRegExpStr(); + const newRegExpStr = BracketTokens.createFromLanguage(this.getLanguageConfiguration(languageId), this.denseKeyProvider).getRegExpStr(); return existing.getRegExpStr() !== newRegExpStr; } getSingleLanguageBracketTokens(languageId: LanguageId): BracketTokens { let singleLanguageBracketTokens = this.languageIdToBracketTokens.get(languageId); if (!singleLanguageBracketTokens) { - singleLanguageBracketTokens = BracketTokens.createFromLanguage(languageId, this.denseKeyProvider); + singleLanguageBracketTokens = BracketTokens.createFromLanguage(this.getLanguageConfiguration(languageId), this.denseKeyProvider); this.languageIdToBracketTokens.set(languageId, singleLanguageBracketTokens); } return singleLanguageBracketTokens; diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 8ba37b6a3bb..afef70bd1f4 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -25,7 +25,7 @@ import { SearchData, SearchParams, TextModelSearch } from 'vs/editor/common/mode import { TextModelTokenization } from 'vs/editor/common/model/textModelTokens'; import { getWordAtText } from 'vs/editor/common/model/wordHelper'; import { LanguageId, LanguageIdentifier, FormattingOptions } from 'vs/editor/common/modes'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { ILanguageConfigurationService, ResolvedLanguageConfiguration } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { NULL_LANGUAGE_IDENTIFIER } from 'vs/editor/common/modes/nullMode'; import { ignoreBracketsInToken } from 'vs/editor/common/modes/supports'; import { BracketsUtils, RichEditBracket, RichEditBrackets } from 'vs/editor/common/modes/supports/richEditBrackets'; @@ -334,15 +334,20 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private readonly _onBackgroundTokenizationStateChanged = this._register(new Emitter()); public readonly onBackgroundTokenizationStateChanged: Event = this._onBackgroundTokenizationStateChanged.event; + private readonly _languageConfigurationService: ILanguageConfigurationService; + constructor( source: string | model.ITextBufferFactory, creationOptions: model.ITextModelCreationOptions, languageIdentifier: LanguageIdentifier | null, associatedResource: URI | null = null, - undoRedoService: IUndoRedoService + undoRedoService: IUndoRedoService, + languageConfigurationService: ILanguageConfigurationService ) { super(); + this._languageConfigurationService = languageConfigurationService; + this._register(this._eventEmitter.fastEvent((e: InternalModelContentChangeEvent) => { this._onDidChangeContentOrInjectedText.fire(e.rawContentChangedEvent); })); @@ -391,11 +396,13 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._languageIdentifier = languageIdentifier || NULL_LANGUAGE_IDENTIFIER; - this._languageRegistryListener = LanguageConfigurationRegistry.onDidChange((e) => { - if (e.languageIdentifier.id === this._languageIdentifier.id) { + this._languageRegistryListener = this._languageConfigurationService.onLanguageConfigurationDidChange( + this._languageIdentifier.id, + this._associatedResource, + () => { this._onDidChangeLanguageConfiguration.fire({}); } - }); + ); this._instanceId = strings.singleLetterHash(MODEL_ID); this._lastDecorationId = 0; @@ -411,7 +418,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._tokens2 = new TokensStore2(); this._tokenization = new TextModelTokenization(this); - this._bracketPairColorizer = this._register(new BracketPairColorizer(this)); + this._bracketPairColorizer = this._register(new BracketPairColorizer(this, this._languageConfigurationService)); this._decorationProvider = this._bracketPairColorizer; this._register(this._decorationProvider.onDidChangeDecorations(() => { @@ -2142,6 +2149,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return lineTokens.getLanguageId(lineTokens.findTokenIndexAtOffset(position.column - 1)); } + private getLanguageConfiguration(languageId: LanguageId): ResolvedLanguageConfiguration { + return this._languageConfigurationService.getLanguageConfiguration(languageId, this.uri); + } + // Having tokens allows implementing additional helper methods public getWordAtPosition(_position: IPosition): model.IWordAtPosition | null { @@ -2155,7 +2166,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const [rbStartOffset, rbEndOffset] = TextModel._findLanguageBoundaries(lineTokens, tokenIndex); const rightBiasedWord = getWordAtText( position.column, - LanguageConfigurationRegistry.getWordDefinition(lineTokens.getLanguageId(tokenIndex)), + this.getLanguageConfiguration(lineTokens.getLanguageId(tokenIndex)).getWordDefinition(), lineContent.substring(rbStartOffset, rbEndOffset), rbStartOffset ); @@ -2170,7 +2181,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const [lbStartOffset, lbEndOffset] = TextModel._findLanguageBoundaries(lineTokens, tokenIndex - 1); const leftBiasedWord = getWordAtText( position.column, - LanguageConfigurationRegistry.getWordDefinition(lineTokens.getLanguageId(tokenIndex - 1)), + this.getLanguageConfiguration(lineTokens.getLanguageId(tokenIndex - 1)).getWordDefinition(), lineContent.substring(lbStartOffset, lbEndOffset), lbStartOffset ); @@ -2223,7 +2234,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati let lineTokens = this._getLineTokens(position.lineNumber); let languageId = lineTokens.getLanguageId(lineTokens.findTokenIndexAtOffset(position.column - 1)); - let bracketsSupport = LanguageConfigurationRegistry.getBracketsSupport(languageId); + let bracketsSupport = this.getLanguageConfiguration(languageId).brackets; if (!bracketsSupport) { return null; @@ -2284,7 +2295,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati if (tokenIndex < 0) { return null; } - const currentModeBrackets = LanguageConfigurationRegistry.getBracketsSupport(lineTokens.getLanguageId(tokenIndex)); + const currentModeBrackets = this.getLanguageConfiguration(lineTokens.getLanguageId(tokenIndex)).brackets; // check that the token is not to be ignored if (currentModeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))) { @@ -2324,7 +2335,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati // If position is in between two tokens, try also looking in the previous token if (tokenIndex > 0 && lineTokens.getStartOffset(tokenIndex) === position.column - 1) { const prevTokenIndex = tokenIndex - 1; - const prevModeBrackets = LanguageConfigurationRegistry.getBracketsSupport(lineTokens.getLanguageId(prevTokenIndex)); + const prevModeBrackets = this.getLanguageConfiguration(lineTokens.getLanguageId(prevTokenIndex)).brackets; // check that previous token is not to be ignored if (prevModeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(prevTokenIndex))) { @@ -2567,7 +2578,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); if (languageId !== tokenLanguageId) { languageId = tokenLanguageId; - modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId); + modeBrackets = this.getLanguageConfiguration(languageId).brackets; } } @@ -2585,7 +2596,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati prevSearchInToken = false; } languageId = tokenLanguageId; - modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId); + modeBrackets = this.getLanguageConfiguration(languageId).brackets; } const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); @@ -2645,7 +2656,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); if (languageId !== tokenLanguageId) { languageId = tokenLanguageId; - modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId); + modeBrackets = this.getLanguageConfiguration(languageId).brackets; } } @@ -2663,7 +2674,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati prevSearchInToken = false; } languageId = tokenLanguageId; - modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId); + modeBrackets = this.getLanguageConfiguration(languageId).brackets; } const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); @@ -2774,7 +2785,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); if (languageId !== tokenLanguageId) { languageId = tokenLanguageId; - modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId); + modeBrackets = this.getLanguageConfiguration(languageId).brackets; resetCounts(languageId, modeBrackets); } } @@ -2793,7 +2804,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati prevSearchInToken = false; } languageId = tokenLanguageId; - modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId); + modeBrackets = this.getLanguageConfiguration(languageId).brackets; resetCounts(languageId, modeBrackets); } @@ -2894,7 +2905,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati throw new Error('Illegal value for lineNumber'); } - const foldingRules = LanguageConfigurationRegistry.getFoldingRules(this._languageIdentifier.id); + const foldingRules = this.getLanguageConfiguration(this._languageIdentifier.id).foldingRules; const offSide = Boolean(foldingRules && foldingRules.offSide); let up_aboveContentLineIndex = -2; /* -2 is a marker for not having computed it */ @@ -3175,7 +3186,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati throw new Error('Illegal value for endLineNumber'); } - const foldingRules = LanguageConfigurationRegistry.getFoldingRules(this._languageIdentifier.id); + const foldingRules = this.getLanguageConfiguration(this._languageIdentifier.id).foldingRules; const offSide = Boolean(foldingRules && foldingRules.offSide); let result: number[] = new Array(endLineNumber - startLineNumber + 1); diff --git a/src/vs/editor/common/modes/languageConfigurationRegistry.ts b/src/vs/editor/common/modes/languageConfigurationRegistry.ts index 62632cd8ac2..acba9f015ab 100644 --- a/src/vs/editor/common/modes/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/modes/languageConfigurationRegistry.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { Range } from 'vs/editor/common/core/range'; @@ -19,6 +19,11 @@ import { IndentConsts, IndentRulesSupport } from 'vs/editor/common/modes/support import { OnEnterSupport } from 'vs/editor/common/modes/supports/onEnter'; import { RichEditBrackets } from 'vs/editor/common/modes/supports/richEditBrackets'; import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { URI } from 'vs/base/common/uri'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; /** * Interface used to support insertion of mode specific comments. @@ -42,172 +47,171 @@ export interface IIndentConverter { normalizeIndentation?(indentation: string): string; } -export class RichEditSupport { +export interface ILanguageConfigurationService { + readonly _serviceBrand: undefined; - private readonly _conf: LanguageConfiguration; - private readonly _languageIdentifier: LanguageIdentifier; - private _brackets: RichEditBrackets | null; - private _electricCharacter: BracketElectricCharacterSupport | null; - private readonly _onEnterSupport: OnEnterSupport | null; + onLanguageConfigurationDidChange(languageId: LanguageId, resource: URI | undefined, onChangeHandler: () => void): IDisposable + getLanguageConfiguration(languageId: LanguageId, resource: URI | undefined): ResolvedLanguageConfiguration; +} - public readonly comments: ICommentsConfiguration | null; - public readonly characterPair: CharacterPairSupport; - public readonly wordDefinition: RegExp; - public readonly indentRulesSupport: IndentRulesSupport | null; - public readonly indentationRules: IndentationRule | undefined; - public readonly foldingRules: FoldingRules; +export const ILanguageConfigurationService = createDecorator('languageConfigurationService'); - constructor(languageIdentifier: LanguageIdentifier, rawConf: LanguageConfiguration) { - this._languageIdentifier = languageIdentifier; - this._brackets = null; - this._electricCharacter = null; - this._conf = rawConf; - this._onEnterSupport = (this._conf.brackets || this._conf.indentationRules || this._conf.onEnterRules ? new OnEnterSupport(this._conf) : null); - this.comments = RichEditSupport._handleComments(this._conf); - this.characterPair = new CharacterPairSupport(this._conf); - this.wordDefinition = this._conf.wordPattern || DEFAULT_WORD_REGEXP; - this.indentationRules = this._conf.indentationRules; - if (this._conf.indentationRules) { - this.indentRulesSupport = new IndentRulesSupport(this._conf.indentationRules); - } else { - this.indentRulesSupport = null; +function getCacheKey(languageId: LanguageId, resource?: URI): string { + let result = `${languageId}`; + if (resource) { + result += `:${resource.toString()}`; + } + return result; +} + +function getCustomizedLanguageConfig(languageIdentifier: LanguageIdentifier, resource: URI | undefined, configurationService: IConfigurationService): LanguageConfiguration { + // TODO how should these keys be registered to the JSON schema? + + const brackets = configurationService.getValue('editor.language.brackets', { + resource, + overrideIdentifier: languageIdentifier.language, + }); + + const colorizedBracketPairs = configurationService.getValue('editor.language.colorizedBracketPairs', { + resource, + overrideIdentifier: languageIdentifier.language, + }); + + return { + brackets: validateBracketPairs(brackets), + colorizedBracketPairs: validateBracketPairs(colorizedBracketPairs), + }; +} + +function validateBracketPairs(data: unknown): CharacterPair[] | undefined { + if (!Array.isArray(data)) { + return undefined; + } + return data.map(pair => { + if (!Array.isArray(pair) || pair.length !== 2) { + return undefined; } - this.foldingRules = this._conf.folding || {}; + return [pair[0], pair[1]] as CharacterPair; + }).filter((p): p is CharacterPair => !!p); +} + +class CustomizedConfigurationWatcher extends Disposable { + public config!: ResolvedLanguageConfiguration; + + private readonly onChangeEmitter = new Emitter({ onLastListenerRemove: () => this.onUnsubscribe() }); + public readonly onDidChange = this.onChangeEmitter.event; + + constructor( + public readonly languageId: LanguageId, + public readonly resource: URI | undefined, + public readonly configurationService: IConfigurationService, + public readonly modeService: IModeService, + public readonly onUnsubscribe: () => void + ) { + super(); + + this.config = computeConfig(this.languageId, this.resource, this.configurationService, this.modeService); + + this._register(this.configurationService.onDidChangeConfiguration((e) => { + this.update(); + })); + this._register(LanguageConfigurationRegistry.onDidChange((e) => { + if (e.languageIdentifier.id === languageId) { + this.update(); + } + })); } - public get brackets(): RichEditBrackets | null { - if (!this._brackets && this._conf.brackets) { - this._brackets = new RichEditBrackets(this._languageIdentifier, this._conf.brackets); + private update(): void { + const oldConfig = this.config; + this.config = computeConfig(this.languageId, this.resource, this.configurationService, this.modeService); + if (!oldConfig.equals(this.config)) { + this.onChangeEmitter.fire(); } - return this._brackets; + } +} + +function computeConfig( + languageId: LanguageId, + resource: URI | undefined, + configurationService: IConfigurationService, + modeService: IModeService, +): ResolvedLanguageConfiguration { + let languageConfig = LanguageConfigurationRegistry.getLanguageConfiguration(languageId); + + if (!languageConfig) { + const languageIdentifier = modeService.getLanguageIdentifier(languageId); + if (!languageIdentifier) { + throw new Error('Unexpected languageId'); + } + languageConfig = new ResolvedLanguageConfiguration(languageIdentifier, {}); } - public get electricCharacter(): BracketElectricCharacterSupport | null { - if (!this._electricCharacter) { - this._electricCharacter = new BracketElectricCharacterSupport(this.brackets); + const customizedConfig = getCustomizedLanguageConfig(languageConfig.languageIdentifier, resource, configurationService); + const data = combineLanguageConfigurations([languageConfig.underlyingConfig, customizedConfig]); + const config = new ResolvedLanguageConfiguration(languageConfig.languageIdentifier, data); + return config; +} + +export class LanguageConfigurationService extends Disposable implements ILanguageConfigurationService { + private readonly watchers = new Map(); + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @IModeService private readonly modeService: IModeService + ) { + super(); + + this._register(toDisposable(() => { + this.watchers.forEach(watcher => watcher.dispose()); + this.watchers.clear(); + })); + } + _serviceBrand: undefined; + + public onLanguageConfigurationDidChange(languageId: LanguageId, resource: URI | undefined, handler: () => void): IDisposable { + const cacheKey = getCacheKey(languageId, resource); + + let watcher = this.watchers.get(cacheKey); + + if (!watcher) { + watcher = new CustomizedConfigurationWatcher( + languageId, + resource, + this.configurationService, + this.modeService, + () => { + watcher!.dispose(); + this.watchers.delete(cacheKey); + } + ); + this.watchers.set(cacheKey, watcher); } - return this._electricCharacter; + + return watcher.onDidChange(handler); } - public onEnter(autoIndent: EditorAutoIndentStrategy, previousLineText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null { - if (!this._onEnterSupport) { - return null; - } - return this._onEnterSupport.onEnter(autoIndent, previousLineText, beforeEnterText, afterEnterText); - } + public getLanguageConfiguration(languageId: LanguageId, resource?: URI): ResolvedLanguageConfiguration { + const cacheKey = getCacheKey(languageId, resource); - private static _handleComments(conf: LanguageConfiguration): ICommentsConfiguration | null { - let commentRule = conf.comments; - if (!commentRule) { - return null; - } - - // comment configuration - let comments: ICommentsConfiguration = {}; - - if (commentRule.lineComment) { - comments.lineCommentToken = commentRule.lineComment; - } - if (commentRule.blockComment) { - let [blockStart, blockEnd] = commentRule.blockComment; - comments.blockCommentStartToken = blockStart; - comments.blockCommentEndToken = blockEnd; - } - - return comments; + let config = + this.watchers.get(cacheKey)?.config ?? + computeConfig( + languageId, + resource, + this.configurationService, + this.modeService + ); + return config; } } export class LanguageConfigurationChangeEvent { - constructor( - public readonly languageIdentifier: LanguageIdentifier - ) { } -} - -class LanguageConfigurationEntry { - - constructor( - public readonly configuration: LanguageConfiguration, - public readonly priority: number, - public readonly order: number - ) { } - - public static cmp(a: LanguageConfigurationEntry, b: LanguageConfigurationEntry) { - if (a.priority === b.priority) { - // higher order last - return a.order - b.order; - } - // higher priority last - return a.priority - b.priority; - } -} - -class LanguageConfigurationEntries { - - private readonly _entries: LanguageConfigurationEntry[]; - private _order: number; - private _resolved: RichEditSupport | null = null; - - constructor( - public readonly languageIdentifier: LanguageIdentifier - ) { - this._entries = []; - this._order = 0; - this._resolved = null; - } - - public register(configuration: LanguageConfiguration, priority: number): IDisposable { - const entry = new LanguageConfigurationEntry(configuration, priority, ++this._order); - this._entries.push(entry); - this._resolved = null; - return toDisposable(() => { - for (let i = 0; i < this._entries.length; i++) { - if (this._entries[i] === entry) { - this._entries.splice(i, 1); - this._resolved = null; - break; - } - } - }); - } - - public getRichEditSupport(): RichEditSupport | null { - if (!this._resolved) { - const config = this._resolve(); - if (config) { - this._resolved = new RichEditSupport(this.languageIdentifier, config); - } - } - return this._resolved; - } - - private _resolve(): LanguageConfiguration | null { - if (this._entries.length === 0) { - return null; - } - this._entries.sort(LanguageConfigurationEntry.cmp); - const result: LanguageConfiguration = {}; - for (const entry of this._entries) { - const conf = entry.configuration; - result.comments = conf.comments || result.comments; - result.brackets = conf.brackets || result.brackets; - result.wordPattern = conf.wordPattern || result.wordPattern; - result.indentationRules = conf.indentationRules || result.indentationRules; - result.onEnterRules = conf.onEnterRules || result.onEnterRules; - result.autoClosingPairs = conf.autoClosingPairs || result.autoClosingPairs; - result.surroundingPairs = conf.surroundingPairs || result.surroundingPairs; - result.autoCloseBefore = conf.autoCloseBefore || result.autoCloseBefore; - result.folding = conf.folding || result.folding; - result.colorizedBracketPairs = conf.colorizedBracketPairs || result.colorizedBracketPairs; - result.__electricCharacterSupport = conf.__electricCharacterSupport || result.__electricCharacterSupport; - } - return result; - } + constructor(public readonly languageIdentifier: LanguageIdentifier) { } } export class LanguageConfigurationRegistryImpl { - - private readonly _entries = new Map(); + private readonly _entries = new Map(); private readonly _onDidChange = new Emitter(); public readonly onDidChange: Event = this._onDidChange.event; @@ -218,7 +222,7 @@ export class LanguageConfigurationRegistryImpl { public register(languageIdentifier: LanguageIdentifier, configuration: LanguageConfiguration, priority: number = 0): IDisposable { let entries = this._entries.get(languageIdentifier.id); if (!entries) { - entries = new LanguageConfigurationEntries(languageIdentifier); + entries = new ComposedLanguageConfiguration(languageIdentifier); this._entries.set(languageIdentifier.id, entries); } @@ -231,20 +235,20 @@ export class LanguageConfigurationRegistryImpl { }); } - private _getRichEditSupport(languageId: LanguageId): RichEditSupport | null { - const entries = this._entries.get(languageId); - return entries ? entries.getRichEditSupport() : null; + public getLanguageConfiguration(languageId: LanguageId): ResolvedLanguageConfiguration | null { + let entries = this._entries.get(languageId); + return entries?.getResolvedConfiguration() || null; } public getIndentationRules(languageId: LanguageId): IndentationRule | null { - const value = this._getRichEditSupport(languageId); + const value = this.getLanguageConfiguration(languageId); return value ? value.indentationRules || null : null; } // begin electricCharacter private _getElectricCharacterSupport(languageId: LanguageId): BracketElectricCharacterSupport | null { - let value = this._getRichEditSupport(languageId); + let value = this.getLanguageConfiguration(languageId); if (!value) { return null; } @@ -274,7 +278,7 @@ export class LanguageConfigurationRegistryImpl { // end electricCharacter public getComments(languageId: LanguageId): ICommentsConfiguration | null { - let value = this._getRichEditSupport(languageId); + let value = this.getLanguageConfiguration(languageId); if (!value) { return null; } @@ -284,7 +288,7 @@ export class LanguageConfigurationRegistryImpl { // begin characterPair private _getCharacterPairSupport(languageId: LanguageId): CharacterPairSupport | null { - let value = this._getRichEditSupport(languageId); + let value = this.getLanguageConfiguration(languageId); if (!value) { return null; } @@ -320,7 +324,7 @@ export class LanguageConfigurationRegistryImpl { // end characterPair public getWordDefinition(languageId: LanguageId): RegExp { - let value = this._getRichEditSupport(languageId); + let value = this.getLanguageConfiguration(languageId); if (!value) { return ensureValidWordDefinition(null); } @@ -330,7 +334,7 @@ export class LanguageConfigurationRegistryImpl { public getWordDefinitions(): [LanguageId, RegExp][] { let result: [LanguageId, RegExp][] = []; for (const [language, entries] of this._entries) { - const value = entries.getRichEditSupport(); + const value = entries.getResolvedConfiguration(); if (value) { result.push([language, value.wordDefinition]); } @@ -339,7 +343,7 @@ export class LanguageConfigurationRegistryImpl { } public getFoldingRules(languageId: LanguageId): FoldingRules { - let value = this._getRichEditSupport(languageId); + let value = this.getLanguageConfiguration(languageId); if (!value) { return {}; } @@ -349,7 +353,7 @@ export class LanguageConfigurationRegistryImpl { // begin Indent Rules public getIndentRulesSupport(languageId: LanguageId): IndentRulesSupport | null { - let value = this._getRichEditSupport(languageId); + let value = this.getLanguageConfiguration(languageId); if (!value) { return null; } @@ -357,7 +361,7 @@ export class LanguageConfigurationRegistryImpl { } /** - * Get nearest preceiding line which doesn't match unIndentPattern or contains all whitespace. + * Get nearest preceding line which doesn't match unIndentPattern or contains all whitespace. * Result: * -1: run into the boundary of embedded languages * 0: every line above are invalid @@ -527,7 +531,7 @@ export class LanguageConfigurationRegistryImpl { return null; } - const richEditSupport = this._getRichEditSupport(languageId); + const richEditSupport = this.getLanguageConfiguration(languageId); if (!richEditSupport) { return null; } @@ -740,7 +744,7 @@ export class LanguageConfigurationRegistryImpl { public getEnterAction(autoIndent: EditorAutoIndentStrategy, model: ITextModel, range: Range): CompleteEnterAction | null { const scopedLineTokens = this.getScopedLineTokens(model, range.startLineNumber, range.startColumn); - const richEditSupport = this._getRichEditSupport(scopedLineTokens.languageId); + const richEditSupport = this.getLanguageConfiguration(scopedLineTokens.languageId); if (!richEditSupport) { return null; } @@ -822,16 +826,242 @@ export class LanguageConfigurationRegistryImpl { // end onEnter public getBracketsSupport(languageId: LanguageId): RichEditBrackets | null { - const value = this._getRichEditSupport(languageId); + const value = this.getLanguageConfiguration(languageId); if (!value) { return null; } return value.brackets || null; } - public getColorizedBracketPairs(languageId: LanguageId): CharacterPair[] { - return this._getRichEditSupport(languageId)?.characterPair.getColorizedBrackets() || []; + public getColorizedBracketPairs(languageId: LanguageId): readonly CharacterPair[] { + return this.getLanguageConfiguration(languageId)?.characterPair.getColorizedBrackets() || []; } } export const LanguageConfigurationRegistry = new LanguageConfigurationRegistryImpl(); + +class ComposedLanguageConfiguration { + private readonly _entries: LanguageConfigurationContribution[]; + private _order: number; + private _resolved: ResolvedLanguageConfiguration | null = null; + + constructor(public readonly languageIdentifier: LanguageIdentifier) { + this._entries = []; + this._order = 0; + this._resolved = null; + } + + public register( + configuration: LanguageConfiguration, + priority: number + ): IDisposable { + const entry = new LanguageConfigurationContribution( + configuration, + priority, + ++this._order + ); + this._entries.push(entry); + this._resolved = null; + return toDisposable(() => { + for (let i = 0; i < this._entries.length; i++) { + if (this._entries[i] === entry) { + this._entries.splice(i, 1); + this._resolved = null; + break; + } + } + }); + } + + public getResolvedConfiguration(): ResolvedLanguageConfiguration | null { + if (!this._resolved) { + const config = this._resolve(); + if (config) { + this._resolved = new ResolvedLanguageConfiguration( + this.languageIdentifier, + config + ); + } + } + return this._resolved; + } + + private _resolve(): LanguageConfiguration | null { + if (this._entries.length === 0) { + return null; + } + this._entries.sort(LanguageConfigurationContribution.cmp); + return combineLanguageConfigurations(this._entries.map(e => e.configuration)); + } +} + +function combineLanguageConfigurations(configs: LanguageConfiguration[]): LanguageConfiguration { + const result: LanguageConfiguration = {}; + for (const config of configs) { + for (const [key, value] of Object.entries(config)) { + if (value) { + result[key as keyof LanguageConfiguration] = value; + } + } + } + return result; +} + +class LanguageConfigurationContribution { + constructor( + public readonly configuration: LanguageConfiguration, + public readonly priority: number, + public readonly order: number + ) { } + + public static cmp(a: LanguageConfigurationContribution, b: LanguageConfigurationContribution) { + if (a.priority === b.priority) { + // higher order last + return a.order - b.order; + } + // higher priority last + return a.priority - b.priority; + } +} + +export class UnconfiguredLanguage { } + +/** + * Immutable. +*/ +export class ResolvedLanguageConfiguration { + private _brackets: RichEditBrackets | null; + private _electricCharacter: BracketElectricCharacterSupport | null; + private readonly _onEnterSupport: OnEnterSupport | null; + + public readonly comments: ICommentsConfiguration | null; + public readonly characterPair: CharacterPairSupport; + public readonly wordDefinition: RegExp; + public readonly indentRulesSupport: IndentRulesSupport | null; + public readonly indentationRules: IndentationRule | undefined; + public readonly foldingRules: FoldingRules; + + constructor( + public readonly languageIdentifier: LanguageIdentifier, + public readonly underlyingConfig: LanguageConfiguration + ) { + this._brackets = null; + this._electricCharacter = null; + this._onEnterSupport = + this.underlyingConfig.brackets || + this.underlyingConfig.indentationRules || + this.underlyingConfig.onEnterRules + ? new OnEnterSupport(this.underlyingConfig) + : null; + this.comments = ResolvedLanguageConfiguration._handleComments(this.underlyingConfig); + this.characterPair = new CharacterPairSupport(this.underlyingConfig); + + this.wordDefinition = this.underlyingConfig.wordPattern || DEFAULT_WORD_REGEXP; + this.indentationRules = this.underlyingConfig.indentationRules; + if (this.underlyingConfig.indentationRules) { + this.indentRulesSupport = new IndentRulesSupport( + this.underlyingConfig.indentationRules + ); + } else { + this.indentRulesSupport = null; + } + this.foldingRules = this.underlyingConfig.folding || {}; + } + + public getWordDefinition(): RegExp { + return ensureValidWordDefinition(this.wordDefinition); + } + + public get brackets(): RichEditBrackets | null { + if (!this._brackets && this.underlyingConfig.brackets) { + this._brackets = new RichEditBrackets( + this.languageIdentifier, + this.underlyingConfig.brackets + ); + } + return this._brackets; + } + + public get electricCharacter(): BracketElectricCharacterSupport | null { + if (!this._electricCharacter) { + this._electricCharacter = new BracketElectricCharacterSupport( + this.brackets + ); + } + return this._electricCharacter; + } + + public onEnter( + autoIndent: EditorAutoIndentStrategy, + previousLineText: string, + beforeEnterText: string, + afterEnterText: string + ): EnterAction | null { + if (!this._onEnterSupport) { + return null; + } + return this._onEnterSupport.onEnter( + autoIndent, + previousLineText, + beforeEnterText, + afterEnterText + ); + } + + public equals(other: ResolvedLanguageConfiguration): boolean { + return deepStrictEqual(this.underlyingConfig, other.underlyingConfig); + } + + private static _handleComments( + conf: LanguageConfiguration + ): ICommentsConfiguration | null { + let commentRule = conf.comments; + if (!commentRule) { + return null; + } + + // comment configuration + let comments: ICommentsConfiguration = {}; + + if (commentRule.lineComment) { + comments.lineCommentToken = commentRule.lineComment; + } + if (commentRule.blockComment) { + let [blockStart, blockEnd] = commentRule.blockComment; + comments.blockCommentStartToken = blockStart; + comments.blockCommentEndToken = blockEnd; + } + + return comments; + } +} + +function deepStrictEqual(a: unknown, b: unknown): boolean { + if (a === b) { + return true; + } + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!deepStrictEqual(a[i], b[i])) { + return false; + } + } + return true; + } else if (typeof a === 'object' && typeof b === 'object' && a && b) { + if (Object.keys(a).length !== Object.keys(b).length) { + return false; + } + for (const [key, value] of Object.entries(a)) { + if (!deepStrictEqual(value, (b as any)[key])) { + return false; + } + } + return true; + } + return false; +} + +registerSingleton(ILanguageConfigurationService, LanguageConfigurationService); diff --git a/src/vs/editor/common/modes/supports/characterPair.ts b/src/vs/editor/common/modes/supports/characterPair.ts index 4fc763d4b34..38d08948cf0 100644 --- a/src/vs/editor/common/modes/supports/characterPair.ts +++ b/src/vs/editor/common/modes/supports/characterPair.ts @@ -73,7 +73,7 @@ export class CharacterPairSupport { return this._surroundingPairs; } - public getColorizedBrackets(): CharacterPair[] { + public getColorizedBrackets(): readonly CharacterPair[] { return this._colorizedBracketPairs; } } diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 441a17d859d..819c0840724 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -31,6 +31,7 @@ import { Schemas } from 'vs/base/common/network'; import { SemanticTokensProviderStyling, toMultilineTokens2 } from 'vs/editor/common/services/semanticTokensProviderStyling'; import { getDocumentSemanticTokens, isSemanticTokens, isSemanticTokensEdits } from 'vs/editor/common/services/getSemanticTokens'; import { equals } from 'vs/base/common/objects'; +import { ILanguageConfigurationService } from 'vs/editor/common/modes/languageConfigurationRegistry'; export interface IEditorSemanticHighlightingOptions { enabled: true | false | 'configuredByTheme'; @@ -161,6 +162,7 @@ export class ModelServiceImpl extends Disposable implements IModelService { @IThemeService private readonly _themeService: IThemeService, @ILogService private readonly _logService: ILogService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService ) { super(); this._modelCreationOptionsByLanguageAndResource = Object.create(null); @@ -365,7 +367,14 @@ export class ModelServiceImpl extends Disposable implements IModelService { private _createModelData(value: string | ITextBufferFactory, languageIdentifier: LanguageIdentifier, resource: URI | undefined, isForSimpleWidget: boolean): ModelData { // create & save the model const options = this.getCreationOptions(languageIdentifier.language, resource, isForSimpleWidget); - const model: TextModel = new TextModel(value, options, languageIdentifier, resource, this._undoRedoService); + const model: TextModel = new TextModel( + value, + options, + languageIdentifier, + resource, + this._undoRedoService, + this._languageConfigurationService + ); if (resource && this._disposedModels.has(MODEL_ID(resource))) { const disposedModelData = this._removeDisposedModel(resource)!; const elements = this._undoRedoService.getElements(resource); diff --git a/src/vs/editor/contrib/find/test/findModel.test.ts b/src/vs/editor/contrib/find/test/findModel.test.ts index 899a2b19bbb..893d97ad866 100644 --- a/src/vs/editor/contrib/find/test/findModel.test.ts +++ b/src/vs/editor/contrib/find/test/findModel.test.ts @@ -14,6 +14,7 @@ import { TextModel } from 'vs/editor/common/model/textModel'; import { FindModelBoundToEditorModel } from 'vs/editor/contrib/find/findModel'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; @@ -44,9 +45,20 @@ suite('FindModel', () => { ptBuilder.acceptChunk(text.substr(94, 101)); ptBuilder.acceptChunk(text.substr(195, 59)); const factory = ptBuilder.finish(); - withTestCodeEditor([], + withTestCodeEditor( + [], { - model: new TextModel(factory, TextModel.DEFAULT_CREATION_OPTIONS, null, null, new UndoRedoService(new TestDialogService(), new TestNotificationService())) + model: new TextModel( + factory, + TextModel.DEFAULT_CREATION_OPTIONS, + null, + null, + new UndoRedoService( + new TestDialogService(), + new TestNotificationService() + ), + new TestLanguageConfigurationService() + ), }, (editor) => callback(editor as IActiveCodeEditor) ); diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts index bd4760fe1bd..a157a693b07 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts @@ -55,7 +55,7 @@ export abstract class ReferencesController implements IEditorContribution { @INotificationService private readonly _notificationService: INotificationService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IStorageService private readonly _storageService: IStorageService, - @IConfigurationService private readonly _configurationService: IConfigurationService, + @IConfigurationService private readonly _configurationService: IConfigurationService ) { this._referenceSearchVisible = ctxReferenceSearchVisible.bindTo(contextKeyService); diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts index adde0a13038..657fdfbc8c9 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts @@ -23,6 +23,7 @@ import { ScrollType } from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions, TextModel } from 'vs/editor/common/model/textModel'; import { Location } from 'vs/editor/common/modes'; +import { ILanguageConfigurationService } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { AccessibilityProvider, DataSource, Delegate, FileReferencesRenderer, IdentityProvider, OneReferenceRenderer, StringRepresentationProvider, TreeElement } from 'vs/editor/contrib/gotoSymbol/peek/referencesTree'; import * as peekView from 'vs/editor/contrib/peekView/peekView'; @@ -223,6 +224,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { @ILabelService private readonly _uriLabel: ILabelService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, @IKeybindingService private readonly _keybindingService: IKeybindingService, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, ) { super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true, supportOnTitleClick: true }, _instantiationService); @@ -311,7 +313,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { }; this._preview = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._previewContainer, options, this.editor); dom.hide(this._previewContainer); - this._previewNotAvailableMessage = new TextModel(nls.localize('missingPreviewMessage', "no preview available"), TextModel.DEFAULT_CREATION_OPTIONS, null, null, this._undoRedoService); + this._previewNotAvailableMessage = new TextModel(nls.localize('missingPreviewMessage', "no preview available"), TextModel.DEFAULT_CREATION_OPTIONS, null, null, this._undoRedoService, this._languageConfigurationService); // tree this._treeContainer = dom.append(containerElement, dom.$('div.ref-tree.inline')); diff --git a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts index b5596314ee6..67a1a01660b 100644 --- a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts @@ -15,6 +15,7 @@ import { provideSelectionRanges } from 'vs/editor/contrib/smartSelect/smartSelec import { WordSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/wordSelections'; import { MockMode, StaticLanguageSelector } from 'vs/editor/test/common/mocks/mockMode'; import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; @@ -61,7 +62,7 @@ suite('SmartSelect', () => { setup(() => { const configurationService = new TestConfigurationService(); const dialogService = new TestDialogService(); - modelService = new ModelServiceImpl(configurationService, new TestTextResourcePropertiesService(configurationService), new TestThemeService(), new NullLogService(), new UndoRedoService(dialogService, new TestNotificationService())); + modelService = new ModelServiceImpl(configurationService, new TestTextResourcePropertiesService(configurationService), new TestThemeService(), new NullLogService(), new UndoRedoService(dialogService, new TestNotificationService()), new TestLanguageConfigurationService()); mode = new MockJSMode(); }); diff --git a/src/vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch.ts b/src/vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch.ts index 9222a29a49e..f336cef6452 100644 --- a/src/vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch.ts +++ b/src/vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch.ts @@ -22,7 +22,7 @@ export class StandaloneReferencesController extends ReferencesController { @INotificationService notificationService: INotificationService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, - @IConfigurationService configurationService: IConfigurationService, + @IConfigurationService configurationService: IConfigurationService ) { super( true, diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 62623f967eb..402454baa48 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -54,6 +54,7 @@ import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { StandaloneQuickInputServiceImpl } from 'vs/editor/standalone/browser/quickInput/standaloneQuickInputServiceImpl'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { ILanguageConfigurationService, LanguageConfigurationService } from 'vs/editor/common/modes/languageConfigurationRegistry'; export interface IEditorOverrideServices { [index: string]: any; @@ -156,7 +157,20 @@ export module StaticServices { export const undoRedoService = define(IUndoRedoService, (o) => new UndoRedoService(dialogService.get(o), notificationService.get(o))); - export const modelService = define(IModelService, (o) => new ModelServiceImpl(configurationService.get(o), resourcePropertiesService.get(o), standaloneThemeService.get(o), logService.get(o), undoRedoService.get(o))); + export const languageConfigurationService = define(ILanguageConfigurationService, (o) => new LanguageConfigurationService(configurationService.get(o), modeService.get(o))); + + export const modelService = define( + IModelService, + (o) => + new ModelServiceImpl( + configurationService.get(o), + resourcePropertiesService.get(o), + standaloneThemeService.get(o), + logService.get(o), + undoRedoService.get(o), + languageConfigurationService.get(o) + ) + ); export const markerDecorationsService = define(IMarkerDecorationsService, (o) => new MarkerDecorationsService(modelService.get(o), markerService.get(o))); diff --git a/src/vs/editor/test/common/editorTestUtils.ts b/src/vs/editor/test/common/editorTestUtils.ts index 0c5fa61ac86..2a8210d9c6d 100644 --- a/src/vs/editor/test/common/editorTestUtils.ts +++ b/src/vs/editor/test/common/editorTestUtils.ts @@ -7,6 +7,7 @@ import { URI } from 'vs/base/common/uri'; import { BracketPairColorizationOptions, DefaultEndOfLine, ITextModelCreationOptions } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; import { LanguageIdentifier } from 'vs/editor/common/modes'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; @@ -44,5 +45,5 @@ export function createTextModel(text: string, _options: IRelaxedTextModelCreatio const dialogService = new TestDialogService(); const notificationService = new TestNotificationService(); const undoRedoService = new UndoRedoService(dialogService, notificationService); - return new TextModel(text, options, languageIdentifier, uri, undoRedoService); + return new TextModel(text, options, languageIdentifier, uri, undoRedoService, new TestLanguageConfigurationService()); } diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts index 62943389a2e..9ee5df3b758 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts @@ -10,6 +10,7 @@ import { SmallImmutableSet, DenseKeyProvider } from 'vs/editor/common/model/brac import { Token, TokenKind } from 'vs/editor/common/model/bracketPairColorizer/tokenizer'; import { LanguageIdentifier } from 'vs/editor/common/modes'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; suite('Bracket Pair Colorizer - Brackets', () => { test('Basic', () => { @@ -35,7 +36,8 @@ suite('Bracket Pair Colorizer - Brackets', () => { ] })); - const brackets = new LanguageAgnosticBracketTokens(denseKeyProvider); + const languageConfigService = new TestLanguageConfigurationService(); + const brackets = new LanguageAgnosticBracketTokens(denseKeyProvider, l => languageConfigService.getLanguageConfiguration(l, undefined)); const bracketsExpected = [ { text: '{', length: 1, kind: 'OpeningBracket', bracketId: getKey('{'), bracketIds: getImmutableSet(['{']) }, { text: '[', length: 1, kind: 'OpeningBracket', bracketId: getKey('['), bracketIds: getImmutableSet(['[']) }, diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts index 19f60716fb2..3ddf2c89ffc 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts @@ -14,6 +14,7 @@ import { TextModel } from 'vs/editor/common/model/textModel'; import { IState, ITokenizationSupport, LanguageId, LanguageIdentifier, MetadataConsts, StandardTokenType, TokenizationRegistry } from 'vs/editor/common/modes'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; suite('Bracket Pair Colorizer - Tokenizer', () => { test('Basic', () => { @@ -35,11 +36,12 @@ suite('Bracket Pair Colorizer - Tokenizer', () => { brackets: [['{', '}'], ['[', ']'], ['(', ')'], ['begin', 'end']], })); - const brackets = new LanguageAgnosticBracketTokens(denseKeyProvider); - const model = createTextModel(document.getText(), {}, mode1); model.forceTokenization(model.getLineCount()); + const languageConfigService = new TestLanguageConfigurationService(); + const brackets = new LanguageAgnosticBracketTokens(denseKeyProvider, l => languageConfigService.getLanguageConfiguration(l, undefined)); + const tokens = readAllTokens(new TextBufferTokenizer(model, brackets)); assert.deepStrictEqual(toArr(tokens, model, denseKeyProvider), [ diff --git a/src/vs/editor/test/common/modes/testLanguageConfigurationService.ts b/src/vs/editor/test/common/modes/testLanguageConfigurationService.ts new file mode 100644 index 00000000000..f51430bd145 --- /dev/null +++ b/src/vs/editor/test/common/modes/testLanguageConfigurationService.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { LanguageId, LanguageIdentifier } from 'vs/editor/common/modes'; +import { ILanguageConfigurationService, LanguageConfigurationRegistry, ResolvedLanguageConfiguration } from 'vs/editor/common/modes/languageConfigurationRegistry'; + +export class TestLanguageConfigurationService implements ILanguageConfigurationService { + _serviceBrand: undefined; + + onLanguageConfigurationDidChange(languageId: LanguageId, resource: URI | undefined, handler: () => void): IDisposable { + return toDisposable(() => { + return LanguageConfigurationRegistry.onDidChange(e => { + if (e.languageIdentifier.id === languageId) { + handler(); + } + }); + }); + } + + getLanguageConfiguration(languageId: LanguageId, resource?: URI): ResolvedLanguageConfiguration { + return LanguageConfigurationRegistry.getLanguageConfiguration(languageId) ?? + new ResolvedLanguageConfiguration(new LanguageIdentifier('unknown', languageId), {}); + } +} diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index 23e0f8f2a1d..88db3f83d68 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -31,6 +31,7 @@ import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; const GENERATE_TESTS = false; @@ -43,7 +44,7 @@ suite('ModelService', () => { configService.setUserConfiguration('files', { 'eol': '\r\n' }, URI.file(platform.isWindows ? 'c:\\myroot' : '/myroot')); const dialogService = new TestDialogService(); - modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), new UndoRedoService(dialogService, new TestNotificationService())); + modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), new UndoRedoService(dialogService, new TestNotificationService()), new TestLanguageConfigurationService()); }); teardown(() => { @@ -404,7 +405,8 @@ suite('ModelSemanticColoring', () => { new TestTextResourcePropertiesService(configService), themeService, new NullLogService(), - new UndoRedoService(new TestDialogService(), new TestNotificationService()) + new UndoRedoService(new TestDialogService(), new TestNotificationService()), + new TestLanguageConfigurationService() )); modeService = disposables.add(new ModeServiceImpl(false)); }); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts index 7d1348b25f8..d4985e9b0a8 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts @@ -27,6 +27,7 @@ import { URI } from 'vs/base/common/uri'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { Iterable } from 'vs/base/common/iterator'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; // --- VIEW MODEL @@ -214,7 +215,7 @@ export class BulkEditDataSource implements IAsyncDataSource { @@ -54,7 +55,14 @@ suite('MainThreadDocumentsAndEditors', () => { const dialogService = new TestDialogService(); const notificationService = new TestNotificationService(); const undoRedoService = new UndoRedoService(dialogService, notificationService); - modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService); + modelService = new ModelServiceImpl( + configService, + new TestTextResourcePropertiesService(configService), + new TestThemeService(), + new NullLogService(), + undoRedoService, + new TestLanguageConfigurationService() + ); codeEditorService = new TestCodeEditorService(); textFileService = new class extends mock() { override isDirty() { return false; } diff --git a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts index 514ca0b9fd0..968f745328a 100644 --- a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts @@ -53,6 +53,7 @@ import { ITextSnapshot } from 'vs/editor/common/model'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; suite('MainThreadEditors', () => { @@ -78,7 +79,14 @@ suite('MainThreadEditors', () => { const dialogService = new TestDialogService(); const notificationService = new TestNotificationService(); const undoRedoService = new UndoRedoService(dialogService, notificationService); - modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService); + modelService = new ModelServiceImpl( + configService, + new TestTextResourcePropertiesService(configService), + new TestThemeService(), + new NullLogService(), + undoRedoService, + new TestLanguageConfigurationService() + ); const services = new ServiceCollection(); diff --git a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts index ee9fc627b1d..831134b711d 100644 --- a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts @@ -12,6 +12,7 @@ import { URI } from 'vs/base/common/uri'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -72,22 +73,40 @@ suite.skip('TextSearch performance (integration)', () => { const dialogService = new TestDialogService(); const notificationService = new TestNotificationService(); const undoRedoService = new UndoRedoService(dialogService, notificationService); - const instantiationService = new InstantiationService(new ServiceCollection( - [ITelemetryService, telemetryService], - [IConfigurationService, configurationService], - [ITextResourcePropertiesService, textResourcePropertiesService], - [IDialogService, dialogService], - [INotificationService, notificationService], - [IUndoRedoService, undoRedoService], - [IModelService, new ModelServiceImpl(configurationService, textResourcePropertiesService, new TestThemeService(), logService, undoRedoService)], - [IWorkspaceContextService, new TestContextService(testWorkspace(URI.file(testWorkspacePath)))], - [IEditorService, new TestEditorService()], - [IEditorGroupsService, new TestEditorGroupsService()], - [IEnvironmentService, TestEnvironmentService], - [IUntitledTextEditorService, new SyncDescriptor(UntitledTextEditorService)], - [ISearchService, new SyncDescriptor(LocalSearchService)], - [ILogService, logService] - )); + const instantiationService = new InstantiationService( + new ServiceCollection( + [ITelemetryService, telemetryService], + [IConfigurationService, configurationService], + [ITextResourcePropertiesService, textResourcePropertiesService], + [IDialogService, dialogService], + [INotificationService, notificationService], + [IUndoRedoService, undoRedoService], + [ + IModelService, + new ModelServiceImpl( + configurationService, + textResourcePropertiesService, + new TestThemeService(), + logService, + undoRedoService, + new TestLanguageConfigurationService() + ), + ], + [ + IWorkspaceContextService, + new TestContextService(testWorkspace(URI.file(testWorkspacePath))), + ], + [IEditorService, new TestEditorService()], + [IEditorGroupsService, new TestEditorGroupsService()], + [IEnvironmentService, TestEnvironmentService], + [ + IUntitledTextEditorService, + new SyncDescriptor(UntitledTextEditorService), + ], + [ISearchService, new SyncDescriptor(LocalSearchService)], + [ILogService, logService] + ) + ); const queryOptions: ITextQueryBuilderOptions = { maxResults: 2048