Path: blob/main/src/vs/editor/common/services/modelService.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, DisposableStore } from '../../../base/common/lifecycle.js';7import * as platform from '../../../base/common/platform.js';8import { URI } from '../../../base/common/uri.js';9import { EditOperation, ISingleEditOperation } from '../core/editOperation.js';10import { Range } from '../core/range.js';11import { DefaultEndOfLine, EndOfLinePreference, EndOfLineSequence, ITextBuffer, ITextBufferFactory, ITextModel, ITextModelCreationOptions } from '../model.js';12import { TextModel, createTextBuffer } from '../model/textModel.js';13import { EDITOR_MODEL_DEFAULTS } from '../core/misc/textModelDefaults.js';14import { IModelLanguageChangedEvent } from '../textModelEvents.js';15import { PLAINTEXT_LANGUAGE_ID } from '../languages/modesRegistry.js';16import { ILanguageSelection } from '../languages/language.js';17import { IModelService } from './model.js';18import { ITextResourcePropertiesService } from './textResourceConfiguration.js';19import { IConfigurationChangeEvent, IConfigurationService } from '../../../platform/configuration/common/configuration.js';20import { IUndoRedoService, ResourceEditStackSnapshot } from '../../../platform/undoRedo/common/undoRedo.js';21import { StringSHA1 } from '../../../base/common/hash.js';22import { isEditStackElement } from '../model/editStack.js';23import { Schemas } from '../../../base/common/network.js';24import { equals } from '../../../base/common/objects.js';25import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';26import { EditSources, TextModelEditSource } from '../textModelEditSource.js';2728function MODEL_ID(resource: URI): string {29return resource.toString();30}3132class ModelData implements IDisposable {3334private readonly _modelEventListeners = new DisposableStore();3536constructor(37public readonly model: TextModel,38onWillDispose: (model: ITextModel) => void,39onDidChangeLanguage: (model: ITextModel, e: IModelLanguageChangedEvent) => void40) {41this.model = model;42this._modelEventListeners.add(model.onWillDispose(() => onWillDispose(model)));43this._modelEventListeners.add(model.onDidChangeLanguage((e) => onDidChangeLanguage(model, e)));44}4546public dispose(): void {47this._modelEventListeners.dispose();48}49}5051interface IRawEditorConfig {52tabSize?: any;53indentSize?: any;54insertSpaces?: any;55detectIndentation?: any;56trimAutoWhitespace?: any;57creationOptions?: any;58largeFileOptimizations?: any;59bracketPairColorization?: any;60}6162interface IRawConfig {63eol?: any;64editor?: IRawEditorConfig;65}6667const DEFAULT_EOL = (platform.isLinux || platform.isMacintosh) ? DefaultEndOfLine.LF : DefaultEndOfLine.CRLF;6869class DisposedModelInfo {70constructor(71public readonly uri: URI,72public readonly initialUndoRedoSnapshot: ResourceEditStackSnapshot | null,73public readonly time: number,74public readonly sharesUndoRedoStack: boolean,75public readonly heapSize: number,76public readonly sha1: string,77public readonly versionId: number,78public readonly alternativeVersionId: number,79) { }80}8182export class ModelService extends Disposable implements IModelService {8384public static MAX_MEMORY_FOR_CLOSED_FILES_UNDO_STACK = 20 * 1024 * 1024;8586public _serviceBrand: undefined;8788private readonly _onModelAdded: Emitter<ITextModel> = this._register(new Emitter<ITextModel>());89public readonly onModelAdded: Event<ITextModel> = this._onModelAdded.event;9091private readonly _onModelRemoved: Emitter<ITextModel> = this._register(new Emitter<ITextModel>());92public readonly onModelRemoved: Event<ITextModel> = this._onModelRemoved.event;9394private readonly _onModelModeChanged = this._register(new Emitter<{ model: ITextModel; oldLanguageId: string }>());95public readonly onModelLanguageChanged = this._onModelModeChanged.event;9697private _modelCreationOptionsByLanguageAndResource: { [languageAndResource: string]: ITextModelCreationOptions };9899/**100* All the models known in the system.101*/102private readonly _models: { [modelId: string]: ModelData };103private readonly _disposedModels: Map<string, DisposedModelInfo>;104private _disposedModelsHeapSize: number;105106constructor(107@IConfigurationService private readonly _configurationService: IConfigurationService,108@ITextResourcePropertiesService private readonly _resourcePropertiesService: ITextResourcePropertiesService,109@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,110@IInstantiationService private readonly _instantiationService: IInstantiationService111) {112super();113this._modelCreationOptionsByLanguageAndResource = Object.create(null);114this._models = {};115this._disposedModels = new Map<string, DisposedModelInfo>();116this._disposedModelsHeapSize = 0;117118this._register(this._configurationService.onDidChangeConfiguration(e => this._updateModelOptions(e)));119this._updateModelOptions(undefined);120}121122private static _readModelOptions(config: IRawConfig, isForSimpleWidget: boolean): ITextModelCreationOptions {123let tabSize = EDITOR_MODEL_DEFAULTS.tabSize;124if (config.editor && typeof config.editor.tabSize !== 'undefined') {125const parsedTabSize = parseInt(config.editor.tabSize, 10);126if (!isNaN(parsedTabSize)) {127tabSize = parsedTabSize;128}129if (tabSize < 1) {130tabSize = 1;131}132}133134let indentSize: number | 'tabSize' = 'tabSize';135if (config.editor && typeof config.editor.indentSize !== 'undefined' && config.editor.indentSize !== 'tabSize') {136const parsedIndentSize = parseInt(config.editor.indentSize, 10);137if (!isNaN(parsedIndentSize)) {138indentSize = Math.max(parsedIndentSize, 1);139}140}141142let insertSpaces = EDITOR_MODEL_DEFAULTS.insertSpaces;143if (config.editor && typeof config.editor.insertSpaces !== 'undefined') {144insertSpaces = (config.editor.insertSpaces === 'false' ? false : Boolean(config.editor.insertSpaces));145}146147let newDefaultEOL = DEFAULT_EOL;148const eol = config.eol;149if (eol === '\r\n') {150newDefaultEOL = DefaultEndOfLine.CRLF;151} else if (eol === '\n') {152newDefaultEOL = DefaultEndOfLine.LF;153}154155let trimAutoWhitespace = EDITOR_MODEL_DEFAULTS.trimAutoWhitespace;156if (config.editor && typeof config.editor.trimAutoWhitespace !== 'undefined') {157trimAutoWhitespace = (config.editor.trimAutoWhitespace === 'false' ? false : Boolean(config.editor.trimAutoWhitespace));158}159160let detectIndentation = EDITOR_MODEL_DEFAULTS.detectIndentation;161if (config.editor && typeof config.editor.detectIndentation !== 'undefined') {162detectIndentation = (config.editor.detectIndentation === 'false' ? false : Boolean(config.editor.detectIndentation));163}164165let largeFileOptimizations = EDITOR_MODEL_DEFAULTS.largeFileOptimizations;166if (config.editor && typeof config.editor.largeFileOptimizations !== 'undefined') {167largeFileOptimizations = (config.editor.largeFileOptimizations === 'false' ? false : Boolean(config.editor.largeFileOptimizations));168}169let bracketPairColorizationOptions = EDITOR_MODEL_DEFAULTS.bracketPairColorizationOptions;170if (config.editor?.bracketPairColorization && typeof config.editor.bracketPairColorization === 'object') {171bracketPairColorizationOptions = {172enabled: !!config.editor.bracketPairColorization.enabled,173independentColorPoolPerBracketType: !!config.editor.bracketPairColorization.independentColorPoolPerBracketType174};175}176177return {178isForSimpleWidget: isForSimpleWidget,179tabSize: tabSize,180indentSize: indentSize,181insertSpaces: insertSpaces,182detectIndentation: detectIndentation,183defaultEOL: newDefaultEOL,184trimAutoWhitespace: trimAutoWhitespace,185largeFileOptimizations: largeFileOptimizations,186bracketPairColorizationOptions187};188}189190private _getEOL(resource: URI | undefined, language: string): string {191if (resource) {192return this._resourcePropertiesService.getEOL(resource, language);193}194const eol = this._configurationService.getValue('files.eol', { overrideIdentifier: language });195if (eol && typeof eol === 'string' && eol !== 'auto') {196return eol;197}198return platform.OS === platform.OperatingSystem.Linux || platform.OS === platform.OperatingSystem.Macintosh ? '\n' : '\r\n';199}200201private _shouldRestoreUndoStack(): boolean {202const result = this._configurationService.getValue('files.restoreUndoStack');203if (typeof result === 'boolean') {204return result;205}206return true;207}208209public getCreationOptions(languageIdOrSelection: string | ILanguageSelection, resource: URI | undefined, isForSimpleWidget: boolean): ITextModelCreationOptions {210const language = (typeof languageIdOrSelection === 'string' ? languageIdOrSelection : languageIdOrSelection.languageId);211let creationOptions = this._modelCreationOptionsByLanguageAndResource[language + resource];212if (!creationOptions) {213const editor = this._configurationService.getValue<IRawEditorConfig>('editor', { overrideIdentifier: language, resource });214const eol = this._getEOL(resource, language);215creationOptions = ModelService._readModelOptions({ editor, eol }, isForSimpleWidget);216this._modelCreationOptionsByLanguageAndResource[language + resource] = creationOptions;217}218return creationOptions;219}220221private _updateModelOptions(e: IConfigurationChangeEvent | undefined): void {222const oldOptionsByLanguageAndResource = this._modelCreationOptionsByLanguageAndResource;223this._modelCreationOptionsByLanguageAndResource = Object.create(null);224225// Update options on all models226const keys = Object.keys(this._models);227for (let i = 0, len = keys.length; i < len; i++) {228const modelId = keys[i];229const modelData = this._models[modelId];230const language = modelData.model.getLanguageId();231const uri = modelData.model.uri;232233if (e && !e.affectsConfiguration('editor', { overrideIdentifier: language, resource: uri }) && !e.affectsConfiguration('files.eol', { overrideIdentifier: language, resource: uri })) {234continue; // perf: skip if this model is not affected by configuration change235}236237const oldOptions = oldOptionsByLanguageAndResource[language + uri];238const newOptions = this.getCreationOptions(language, uri, modelData.model.isForSimpleWidget);239ModelService._setModelOptionsForModel(modelData.model, newOptions, oldOptions);240}241}242243private static _setModelOptionsForModel(model: ITextModel, newOptions: ITextModelCreationOptions, currentOptions: ITextModelCreationOptions): void {244if (currentOptions && currentOptions.defaultEOL !== newOptions.defaultEOL && model.getLineCount() === 1) {245model.setEOL(newOptions.defaultEOL === DefaultEndOfLine.LF ? EndOfLineSequence.LF : EndOfLineSequence.CRLF);246}247248if (currentOptions249&& (currentOptions.detectIndentation === newOptions.detectIndentation)250&& (currentOptions.insertSpaces === newOptions.insertSpaces)251&& (currentOptions.tabSize === newOptions.tabSize)252&& (currentOptions.indentSize === newOptions.indentSize)253&& (currentOptions.trimAutoWhitespace === newOptions.trimAutoWhitespace)254&& equals(currentOptions.bracketPairColorizationOptions, newOptions.bracketPairColorizationOptions)255) {256// Same indent opts, no need to touch the model257return;258}259260if (newOptions.detectIndentation) {261model.detectIndentation(newOptions.insertSpaces, newOptions.tabSize);262model.updateOptions({263trimAutoWhitespace: newOptions.trimAutoWhitespace,264bracketColorizationOptions: newOptions.bracketPairColorizationOptions265});266} else {267model.updateOptions({268insertSpaces: newOptions.insertSpaces,269tabSize: newOptions.tabSize,270indentSize: newOptions.indentSize,271trimAutoWhitespace: newOptions.trimAutoWhitespace,272bracketColorizationOptions: newOptions.bracketPairColorizationOptions273});274}275}276277// --- begin IModelService278279private _insertDisposedModel(disposedModelData: DisposedModelInfo): void {280this._disposedModels.set(MODEL_ID(disposedModelData.uri), disposedModelData);281this._disposedModelsHeapSize += disposedModelData.heapSize;282}283284private _removeDisposedModel(resource: URI): DisposedModelInfo | undefined {285const disposedModelData = this._disposedModels.get(MODEL_ID(resource));286if (disposedModelData) {287this._disposedModelsHeapSize -= disposedModelData.heapSize;288}289this._disposedModels.delete(MODEL_ID(resource));290return disposedModelData;291}292293private _ensureDisposedModelsHeapSize(maxModelsHeapSize: number): void {294if (this._disposedModelsHeapSize > maxModelsHeapSize) {295// we must remove some old undo stack elements to free up some memory296const disposedModels: DisposedModelInfo[] = [];297this._disposedModels.forEach(entry => {298if (!entry.sharesUndoRedoStack) {299disposedModels.push(entry);300}301});302disposedModels.sort((a, b) => a.time - b.time);303while (disposedModels.length > 0 && this._disposedModelsHeapSize > maxModelsHeapSize) {304const disposedModel = disposedModels.shift()!;305this._removeDisposedModel(disposedModel.uri);306if (disposedModel.initialUndoRedoSnapshot !== null) {307this._undoRedoService.restoreSnapshot(disposedModel.initialUndoRedoSnapshot);308}309}310}311}312313private _createModelData(value: string | ITextBufferFactory, languageIdOrSelection: string | ILanguageSelection, resource: URI | undefined, isForSimpleWidget: boolean): ModelData {314// create & save the model315const options = this.getCreationOptions(languageIdOrSelection, resource, isForSimpleWidget);316const model: TextModel = this._instantiationService.createInstance(TextModel,317value,318languageIdOrSelection,319options,320resource321);322if (resource && this._disposedModels.has(MODEL_ID(resource))) {323const disposedModelData = this._removeDisposedModel(resource)!;324const elements = this._undoRedoService.getElements(resource);325const sha1Computer = this._getSHA1Computer();326const sha1IsEqual = (327sha1Computer.canComputeSHA1(model)328? sha1Computer.computeSHA1(model) === disposedModelData.sha1329: false330);331if (sha1IsEqual || disposedModelData.sharesUndoRedoStack) {332for (const element of elements.past) {333if (isEditStackElement(element) && element.matchesResource(resource)) {334element.setModel(model);335}336}337for (const element of elements.future) {338if (isEditStackElement(element) && element.matchesResource(resource)) {339element.setModel(model);340}341}342this._undoRedoService.setElementsValidFlag(resource, true, (element) => (isEditStackElement(element) && element.matchesResource(resource)));343if (sha1IsEqual) {344model._overwriteVersionId(disposedModelData.versionId);345model._overwriteAlternativeVersionId(disposedModelData.alternativeVersionId);346model._overwriteInitialUndoRedoSnapshot(disposedModelData.initialUndoRedoSnapshot);347}348} else {349if (disposedModelData.initialUndoRedoSnapshot !== null) {350this._undoRedoService.restoreSnapshot(disposedModelData.initialUndoRedoSnapshot);351}352}353}354const modelId = MODEL_ID(model.uri);355356if (this._models[modelId]) {357// There already exists a model with this id => this is a programmer error358throw new Error('ModelService: Cannot add model because it already exists!');359}360361const modelData = new ModelData(362model,363(model) => this._onWillDispose(model),364(model, e) => this._onDidChangeLanguage(model, e)365);366this._models[modelId] = modelData;367368return modelData;369}370371public updateModel(model: ITextModel, value: string | ITextBufferFactory, reason: TextModelEditSource = EditSources.unknown({ name: 'updateModel' })): void {372const options = this.getCreationOptions(model.getLanguageId(), model.uri, model.isForSimpleWidget);373const { textBuffer, disposable } = createTextBuffer(value, options.defaultEOL);374375// Return early if the text is already set in that form376if (model.equalsTextBuffer(textBuffer)) {377disposable.dispose();378return;379}380381// Otherwise find a diff between the values and update model382model.pushStackElement();383model.pushEOL(textBuffer.getEOL() === '\r\n' ? EndOfLineSequence.CRLF : EndOfLineSequence.LF);384model.pushEditOperations(385[],386ModelService._computeEdits(model, textBuffer),387() => [],388undefined,389reason390);391model.pushStackElement();392disposable.dispose();393}394395private static _commonPrefix(a: ITextModel, aLen: number, aDelta: number, b: ITextBuffer, bLen: number, bDelta: number): number {396const maxResult = Math.min(aLen, bLen);397398let result = 0;399for (let i = 0; i < maxResult && a.getLineContent(aDelta + i) === b.getLineContent(bDelta + i); i++) {400result++;401}402return result;403}404405private static _commonSuffix(a: ITextModel, aLen: number, aDelta: number, b: ITextBuffer, bLen: number, bDelta: number): number {406const maxResult = Math.min(aLen, bLen);407408let result = 0;409for (let i = 0; i < maxResult && a.getLineContent(aDelta + aLen - i) === b.getLineContent(bDelta + bLen - i); i++) {410result++;411}412return result;413}414415/**416* Compute edits to bring `model` to the state of `textSource`.417*/418public static _computeEdits(model: ITextModel, textBuffer: ITextBuffer): ISingleEditOperation[] {419const modelLineCount = model.getLineCount();420const textBufferLineCount = textBuffer.getLineCount();421const commonPrefix = this._commonPrefix(model, modelLineCount, 1, textBuffer, textBufferLineCount, 1);422423if (modelLineCount === textBufferLineCount && commonPrefix === modelLineCount) {424// equality case425return [];426}427428const commonSuffix = this._commonSuffix(model, modelLineCount - commonPrefix, commonPrefix, textBuffer, textBufferLineCount - commonPrefix, commonPrefix);429430let oldRange: Range;431let newRange: Range;432if (commonSuffix > 0) {433oldRange = new Range(commonPrefix + 1, 1, modelLineCount - commonSuffix + 1, 1);434newRange = new Range(commonPrefix + 1, 1, textBufferLineCount - commonSuffix + 1, 1);435} else if (commonPrefix > 0) {436oldRange = new Range(commonPrefix, model.getLineMaxColumn(commonPrefix), modelLineCount, model.getLineMaxColumn(modelLineCount));437newRange = new Range(commonPrefix, 1 + textBuffer.getLineLength(commonPrefix), textBufferLineCount, 1 + textBuffer.getLineLength(textBufferLineCount));438} else {439oldRange = new Range(1, 1, modelLineCount, model.getLineMaxColumn(modelLineCount));440newRange = new Range(1, 1, textBufferLineCount, 1 + textBuffer.getLineLength(textBufferLineCount));441}442443return [EditOperation.replaceMove(oldRange, textBuffer.getValueInRange(newRange, EndOfLinePreference.TextDefined))];444}445446public createModel(value: string | ITextBufferFactory, languageSelection: ILanguageSelection | null, resource?: URI, isForSimpleWidget: boolean = false): ITextModel {447let modelData: ModelData;448449if (languageSelection) {450modelData = this._createModelData(value, languageSelection, resource, isForSimpleWidget);451} else {452modelData = this._createModelData(value, PLAINTEXT_LANGUAGE_ID, resource, isForSimpleWidget);453}454455this._onModelAdded.fire(modelData.model);456457return modelData.model;458}459460public destroyModel(resource: URI): void {461// We need to support that not all models get disposed through this service (i.e. model.dispose() should work!)462const modelData = this._models[MODEL_ID(resource)];463if (!modelData) {464return;465}466modelData.model.dispose();467}468469public getModels(): ITextModel[] {470const ret: ITextModel[] = [];471472const keys = Object.keys(this._models);473for (let i = 0, len = keys.length; i < len; i++) {474const modelId = keys[i];475ret.push(this._models[modelId].model);476}477478return ret;479}480481public getModel(resource: URI): ITextModel | null {482const modelId = MODEL_ID(resource);483const modelData = this._models[modelId];484if (!modelData) {485return null;486}487return modelData.model;488}489490// --- end IModelService491492protected _schemaShouldMaintainUndoRedoElements(resource: URI) {493return (494resource.scheme === Schemas.file495|| resource.scheme === Schemas.vscodeRemote496|| resource.scheme === Schemas.vscodeUserData497|| resource.scheme === Schemas.vscodeNotebookCell498|| resource.scheme === 'fake-fs' // for tests499);500}501502private _onWillDispose(model: ITextModel): void {503const modelId = MODEL_ID(model.uri);504const modelData = this._models[modelId];505506const sharesUndoRedoStack = (this._undoRedoService.getUriComparisonKey(model.uri) !== model.uri.toString());507let maintainUndoRedoStack = false;508let heapSize = 0;509if (sharesUndoRedoStack || (this._shouldRestoreUndoStack() && this._schemaShouldMaintainUndoRedoElements(model.uri))) {510const elements = this._undoRedoService.getElements(model.uri);511if (elements.past.length > 0 || elements.future.length > 0) {512for (const element of elements.past) {513if (isEditStackElement(element) && element.matchesResource(model.uri)) {514maintainUndoRedoStack = true;515heapSize += element.heapSize(model.uri);516element.setModel(model.uri); // remove reference from text buffer instance517}518}519for (const element of elements.future) {520if (isEditStackElement(element) && element.matchesResource(model.uri)) {521maintainUndoRedoStack = true;522heapSize += element.heapSize(model.uri);523element.setModel(model.uri); // remove reference from text buffer instance524}525}526}527}528529const maxMemory = ModelService.MAX_MEMORY_FOR_CLOSED_FILES_UNDO_STACK;530const sha1Computer = this._getSHA1Computer();531if (!maintainUndoRedoStack) {532if (!sharesUndoRedoStack) {533const initialUndoRedoSnapshot = modelData.model.getInitialUndoRedoSnapshot();534if (initialUndoRedoSnapshot !== null) {535this._undoRedoService.restoreSnapshot(initialUndoRedoSnapshot);536}537}538} else if (!sharesUndoRedoStack && (heapSize > maxMemory || !sha1Computer.canComputeSHA1(model))) {539// the undo stack for this file would never fit in the configured memory or the file is very large, so don't bother with it.540const initialUndoRedoSnapshot = modelData.model.getInitialUndoRedoSnapshot();541if (initialUndoRedoSnapshot !== null) {542this._undoRedoService.restoreSnapshot(initialUndoRedoSnapshot);543}544} else {545this._ensureDisposedModelsHeapSize(maxMemory - heapSize);546// We only invalidate the elements, but they remain in the undo-redo service.547this._undoRedoService.setElementsValidFlag(model.uri, false, (element) => (isEditStackElement(element) && element.matchesResource(model.uri)));548this._insertDisposedModel(new DisposedModelInfo(model.uri, modelData.model.getInitialUndoRedoSnapshot(), Date.now(), sharesUndoRedoStack, heapSize, sha1Computer.computeSHA1(model), model.getVersionId(), model.getAlternativeVersionId()));549}550551delete this._models[modelId];552modelData.dispose();553554// clean up cache555delete this._modelCreationOptionsByLanguageAndResource[model.getLanguageId() + model.uri];556557this._onModelRemoved.fire(model);558}559560private _onDidChangeLanguage(model: ITextModel, e: IModelLanguageChangedEvent): void {561const oldLanguageId = e.oldLanguage;562const newLanguageId = model.getLanguageId();563const oldOptions = this.getCreationOptions(oldLanguageId, model.uri, model.isForSimpleWidget);564const newOptions = this.getCreationOptions(newLanguageId, model.uri, model.isForSimpleWidget);565ModelService._setModelOptionsForModel(model, newOptions, oldOptions);566this._onModelModeChanged.fire({ model, oldLanguageId: oldLanguageId });567}568569protected _getSHA1Computer(): ITextModelSHA1Computer {570return new DefaultModelSHA1Computer();571}572}573574export interface ITextModelSHA1Computer {575canComputeSHA1(model: ITextModel): boolean;576computeSHA1(model: ITextModel): string;577}578579export class DefaultModelSHA1Computer implements ITextModelSHA1Computer {580581public static MAX_MODEL_SIZE = 10 * 1024 * 1024; // takes 200ms to compute a sha1 on a 10MB model on a new machine582583canComputeSHA1(model: ITextModel): boolean {584return (model.getValueLength() <= DefaultModelSHA1Computer.MAX_MODEL_SIZE);585}586587computeSHA1(model: ITextModel): string {588// compute the sha1589const shaComputer = new StringSHA1();590const snapshot = model.createSnapshot();591let text: string | null;592while ((text = snapshot.read())) {593shaComputer.update(text);594}595return shaComputer.digest();596}597}598599600