Path: blob/main/src/vs/workbench/common/editor/textEditorModel.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 { ITextModel, ITextBufferFactory, ITextSnapshot, ModelConstants } from '../../../editor/common/model.js';6import { EditorModel } from './editorModel.js';7import { ILanguageSupport } from '../../services/textfile/common/textfiles.js';8import { URI } from '../../../base/common/uri.js';9import { ITextEditorModel, IResolvedTextEditorModel } from '../../../editor/common/services/resolverService.js';10import { ILanguageService, ILanguageSelection } from '../../../editor/common/languages/language.js';11import { IModelService } from '../../../editor/common/services/model.js';12import { MutableDisposable } from '../../../base/common/lifecycle.js';13import { PLAINTEXT_LANGUAGE_ID } from '../../../editor/common/languages/modesRegistry.js';14import { ILanguageDetectionService, LanguageDetectionLanguageEventSource } from '../../services/languageDetection/common/languageDetectionWorkerService.js';15import { ThrottledDelayer } from '../../../base/common/async.js';16import { IAccessibilityService } from '../../../platform/accessibility/common/accessibility.js';17import { localize } from '../../../nls.js';18import { IMarkdownString } from '../../../base/common/htmlContent.js';19import { TextModelEditSource } from '../../../editor/common/textModelEditSource.js';2021/**22* The base text editor model leverages the code editor model. This class is only intended to be subclassed and not instantiated.23*/24export class BaseTextEditorModel extends EditorModel implements ITextEditorModel, ILanguageSupport {2526private static readonly AUTO_DETECT_LANGUAGE_THROTTLE_DELAY = 600;2728protected textEditorModelHandle: URI | undefined = undefined;2930private createdEditorModel: boolean | undefined;3132private readonly modelDisposeListener = this._register(new MutableDisposable());33private readonly autoDetectLanguageThrottler = this._register(new ThrottledDelayer<void>(BaseTextEditorModel.AUTO_DETECT_LANGUAGE_THROTTLE_DELAY));3435constructor(36@IModelService protected modelService: IModelService,37@ILanguageService protected languageService: ILanguageService,38@ILanguageDetectionService private readonly languageDetectionService: ILanguageDetectionService,39@IAccessibilityService private readonly accessibilityService: IAccessibilityService,40textEditorModelHandle?: URI41) {42super();4344if (textEditorModelHandle) {45this.handleExistingModel(textEditorModelHandle);46}47}4849private handleExistingModel(textEditorModelHandle: URI): void {5051// We need the resource to point to an existing model52const model = this.modelService.getModel(textEditorModelHandle);53if (!model) {54throw new Error(`Document with resource ${textEditorModelHandle.toString(true)} does not exist`);55}5657this.textEditorModelHandle = textEditorModelHandle;5859// Make sure we clean up when this model gets disposed60this.registerModelDisposeListener(model);61}6263private registerModelDisposeListener(model: ITextModel): void {64this.modelDisposeListener.value = model.onWillDispose(() => {65this.textEditorModelHandle = undefined; // make sure we do not dispose code editor model again66this.dispose();67});68}6970get textEditorModel(): ITextModel | null {71return this.textEditorModelHandle ? this.modelService.getModel(this.textEditorModelHandle) : null;72}7374isReadonly(): boolean | IMarkdownString {75return true;76}7778private _blockLanguageChangeListener = false;79private _languageChangeSource: 'user' | 'api' | undefined = undefined;80get languageChangeSource() { return this._languageChangeSource; }81get hasLanguageSetExplicitly() {82// This is technically not 100% correct, because 'api' can also be83// set as source if a model is resolved as text first and then84// transitions into the resolved language. But to preserve the current85// behaviour, we do not change this property. Rather, `languageChangeSource`86// can be used to get more fine grained information.87return typeof this._languageChangeSource === 'string';88}8990setLanguageId(languageId: string, source?: string): void {9192// Remember that an explicit language was set93this._languageChangeSource = 'user';9495this.setLanguageIdInternal(languageId, source);96}9798private setLanguageIdInternal(languageId: string, source?: string): void {99if (!this.isResolved()) {100return;101}102103if (!languageId || languageId === this.textEditorModel.getLanguageId()) {104return;105}106107this._blockLanguageChangeListener = true;108try {109this.textEditorModel.setLanguage(this.languageService.createById(languageId), source);110} finally {111this._blockLanguageChangeListener = false;112}113}114115protected installModelListeners(model: ITextModel): void {116117// Setup listener for lower level language changes118const disposable = this._register(model.onDidChangeLanguage(e => {119if (120e.source === LanguageDetectionLanguageEventSource ||121this._blockLanguageChangeListener122) {123return;124}125126this._languageChangeSource = 'api';127disposable.dispose();128}));129}130131getLanguageId(): string | undefined {132return this.textEditorModel?.getLanguageId();133}134135protected autoDetectLanguage(): Promise<void> {136return this.autoDetectLanguageThrottler.trigger(() => this.doAutoDetectLanguage());137}138139private async doAutoDetectLanguage(): Promise<void> {140if (141this.hasLanguageSetExplicitly || // skip detection when the user has made an explicit choice on the language142!this.textEditorModelHandle || // require a URI to run the detection for143!this.languageDetectionService.isEnabledForLanguage(this.getLanguageId() ?? PLAINTEXT_LANGUAGE_ID) // require a valid language that is enlisted for detection144) {145return;146}147148const lang = await this.languageDetectionService.detectLanguage(this.textEditorModelHandle);149const prevLang = this.getLanguageId();150if (lang && lang !== prevLang && !this.isDisposed()) {151this.setLanguageIdInternal(lang, LanguageDetectionLanguageEventSource);152const languageName = this.languageService.getLanguageName(lang);153this.accessibilityService.alert(localize('languageAutoDetected', "Language {0} was automatically detected and set as the language mode.", languageName ?? lang));154}155}156157/**158* Creates the text editor model with the provided value, optional preferred language159* (can be comma separated for multiple values) and optional resource URL.160*/161protected createTextEditorModel(value: ITextBufferFactory, resource: URI | undefined, preferredLanguageId?: string): ITextModel {162const firstLineText = this.getFirstLineText(value);163const languageSelection = this.getOrCreateLanguage(resource, this.languageService, preferredLanguageId, firstLineText);164165return this.doCreateTextEditorModel(value, languageSelection, resource);166}167168private doCreateTextEditorModel(value: ITextBufferFactory, languageSelection: ILanguageSelection, resource: URI | undefined): ITextModel {169let model = resource && this.modelService.getModel(resource);170if (!model) {171model = this.modelService.createModel(value, languageSelection, resource);172this.createdEditorModel = true;173174// Make sure we clean up when this model gets disposed175this.registerModelDisposeListener(model);176} else {177this.updateTextEditorModel(value, languageSelection.languageId);178}179180this.textEditorModelHandle = model.uri;181182return model;183}184185protected getFirstLineText(value: ITextBufferFactory | ITextModel): string {186187// text buffer factory188const textBufferFactory = value as ITextBufferFactory;189if (typeof textBufferFactory.getFirstLineText === 'function') {190return textBufferFactory.getFirstLineText(ModelConstants.FIRST_LINE_DETECTION_LENGTH_LIMIT);191}192193// text model194const textSnapshot = value as ITextModel;195return textSnapshot.getLineContent(1).substr(0, ModelConstants.FIRST_LINE_DETECTION_LENGTH_LIMIT);196}197198/**199* Gets the language for the given identifier. Subclasses can override to provide their own implementation of this lookup.200*201* @param firstLineText optional first line of the text buffer to set the language on. This can be used to guess a language from content.202*/203protected getOrCreateLanguage(resource: URI | undefined, languageService: ILanguageService, preferredLanguage: string | undefined, firstLineText?: string): ILanguageSelection {204205// lookup language via resource path if the provided language is unspecific206if (!preferredLanguage || preferredLanguage === PLAINTEXT_LANGUAGE_ID) {207return languageService.createByFilepathOrFirstLine(resource ?? null, firstLineText);208}209210// otherwise take the preferred language for granted211return languageService.createById(preferredLanguage);212}213214/**215* Updates the text editor model with the provided value. If the value is the same as the model has, this is a no-op.216*/217updateTextEditorModel(newValue?: ITextBufferFactory, preferredLanguageId?: string, reason?: TextModelEditSource): void {218if (!this.isResolved()) {219return;220}221222// contents223if (newValue) {224this.modelService.updateModel(this.textEditorModel, newValue, reason);225}226227// language (only if specific and changed)228if (preferredLanguageId && preferredLanguageId !== PLAINTEXT_LANGUAGE_ID && this.textEditorModel.getLanguageId() !== preferredLanguageId) {229this.textEditorModel.setLanguage(this.languageService.createById(preferredLanguageId));230}231}232233createSnapshot(this: IResolvedTextEditorModel): ITextSnapshot;234createSnapshot(this: ITextEditorModel): ITextSnapshot | null;235createSnapshot(): ITextSnapshot | null {236if (!this.textEditorModel) {237return null;238}239240return this.textEditorModel.createSnapshot(true /* preserve BOM */);241}242243override isResolved(): this is IResolvedTextEditorModel {244return !!this.textEditorModelHandle;245}246247override dispose(): void {248this.modelDisposeListener.dispose(); // dispose this first because it will trigger another dispose() otherwise249250if (this.textEditorModelHandle && this.createdEditorModel) {251this.modelService.destroyModel(this.textEditorModelHandle);252}253254this.textEditorModelHandle = undefined;255this.createdEditorModel = false;256257super.dispose();258}259}260261262