Path: blob/main/src/vs/editor/common/model/editStack.ts
3294 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 * as nls from '../../../nls.js';6import { onUnexpectedError } from '../../../base/common/errors.js';7import { Selection } from '../core/selection.js';8import { EndOfLineSequence, ICursorStateComputer, IValidEditOperation, ITextModel } from '../model.js';9import { TextModel } from './textModel.js';10import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorkspaceUndoRedoElement, UndoRedoGroup } from '../../../platform/undoRedo/common/undoRedo.js';11import { URI } from '../../../base/common/uri.js';12import { TextChange, compressConsecutiveTextChanges } from '../core/textChange.js';13import * as buffer from '../../../base/common/buffer.js';14import { IDisposable } from '../../../base/common/lifecycle.js';15import { basename } from '../../../base/common/resources.js';16import { ISingleEditOperation } from '../core/editOperation.js';17import { EditSources, TextModelEditSource } from '../textModelEditSource.js';1819function uriGetComparisonKey(resource: URI): string {20return resource.toString();21}2223export class SingleModelEditStackData {2425public static create(model: ITextModel, beforeCursorState: Selection[] | null): SingleModelEditStackData {26const alternativeVersionId = model.getAlternativeVersionId();27const eol = getModelEOL(model);28return new SingleModelEditStackData(29alternativeVersionId,30alternativeVersionId,31eol,32eol,33beforeCursorState,34beforeCursorState,35[]36);37}3839constructor(40public readonly beforeVersionId: number,41public afterVersionId: number,42public readonly beforeEOL: EndOfLineSequence,43public afterEOL: EndOfLineSequence,44public readonly beforeCursorState: Selection[] | null,45public afterCursorState: Selection[] | null,46public changes: TextChange[]47) { }4849public append(model: ITextModel, textChanges: TextChange[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void {50if (textChanges.length > 0) {51this.changes = compressConsecutiveTextChanges(this.changes, textChanges);52}53this.afterEOL = afterEOL;54this.afterVersionId = afterVersionId;55this.afterCursorState = afterCursorState;56}5758private static _writeSelectionsSize(selections: Selection[] | null): number {59return 4 + 4 * 4 * (selections ? selections.length : 0);60}6162private static _writeSelections(b: Uint8Array, selections: Selection[] | null, offset: number): number {63buffer.writeUInt32BE(b, (selections ? selections.length : 0), offset); offset += 4;64if (selections) {65for (const selection of selections) {66buffer.writeUInt32BE(b, selection.selectionStartLineNumber, offset); offset += 4;67buffer.writeUInt32BE(b, selection.selectionStartColumn, offset); offset += 4;68buffer.writeUInt32BE(b, selection.positionLineNumber, offset); offset += 4;69buffer.writeUInt32BE(b, selection.positionColumn, offset); offset += 4;70}71}72return offset;73}7475private static _readSelections(b: Uint8Array, offset: number, dest: Selection[]): number {76const count = buffer.readUInt32BE(b, offset); offset += 4;77for (let i = 0; i < count; i++) {78const selectionStartLineNumber = buffer.readUInt32BE(b, offset); offset += 4;79const selectionStartColumn = buffer.readUInt32BE(b, offset); offset += 4;80const positionLineNumber = buffer.readUInt32BE(b, offset); offset += 4;81const positionColumn = buffer.readUInt32BE(b, offset); offset += 4;82dest.push(new Selection(selectionStartLineNumber, selectionStartColumn, positionLineNumber, positionColumn));83}84return offset;85}8687public serialize(): ArrayBuffer {88let necessarySize = (89+ 4 // beforeVersionId90+ 4 // afterVersionId91+ 1 // beforeEOL92+ 1 // afterEOL93+ SingleModelEditStackData._writeSelectionsSize(this.beforeCursorState)94+ SingleModelEditStackData._writeSelectionsSize(this.afterCursorState)95+ 4 // change count96);97for (const change of this.changes) {98necessarySize += change.writeSize();99}100101const b = new Uint8Array(necessarySize);102let offset = 0;103buffer.writeUInt32BE(b, this.beforeVersionId, offset); offset += 4;104buffer.writeUInt32BE(b, this.afterVersionId, offset); offset += 4;105buffer.writeUInt8(b, this.beforeEOL, offset); offset += 1;106buffer.writeUInt8(b, this.afterEOL, offset); offset += 1;107offset = SingleModelEditStackData._writeSelections(b, this.beforeCursorState, offset);108offset = SingleModelEditStackData._writeSelections(b, this.afterCursorState, offset);109buffer.writeUInt32BE(b, this.changes.length, offset); offset += 4;110for (const change of this.changes) {111offset = change.write(b, offset);112}113return b.buffer;114}115116public static deserialize(source: ArrayBuffer): SingleModelEditStackData {117const b = new Uint8Array(source);118let offset = 0;119const beforeVersionId = buffer.readUInt32BE(b, offset); offset += 4;120const afterVersionId = buffer.readUInt32BE(b, offset); offset += 4;121const beforeEOL = buffer.readUInt8(b, offset); offset += 1;122const afterEOL = buffer.readUInt8(b, offset); offset += 1;123const beforeCursorState: Selection[] = [];124offset = SingleModelEditStackData._readSelections(b, offset, beforeCursorState);125const afterCursorState: Selection[] = [];126offset = SingleModelEditStackData._readSelections(b, offset, afterCursorState);127const changeCount = buffer.readUInt32BE(b, offset); offset += 4;128const changes: TextChange[] = [];129for (let i = 0; i < changeCount; i++) {130offset = TextChange.read(b, offset, changes);131}132return new SingleModelEditStackData(133beforeVersionId,134afterVersionId,135beforeEOL,136afterEOL,137beforeCursorState,138afterCursorState,139changes140);141}142}143144export interface IUndoRedoDelegate {145prepareUndoRedo(element: MultiModelEditStackElement): Promise<IDisposable> | IDisposable | void;146}147148export class SingleModelEditStackElement implements IResourceUndoRedoElement {149150public model: ITextModel | URI;151private _data: SingleModelEditStackData | ArrayBuffer;152153public get type(): UndoRedoElementType.Resource {154return UndoRedoElementType.Resource;155}156157public get resource(): URI {158if (URI.isUri(this.model)) {159return this.model;160}161return this.model.uri;162}163164constructor(165public readonly label: string,166public readonly code: string,167model: ITextModel,168beforeCursorState: Selection[] | null169) {170this.model = model;171this._data = SingleModelEditStackData.create(model, beforeCursorState);172}173174public toString(): string {175const data = (this._data instanceof SingleModelEditStackData ? this._data : SingleModelEditStackData.deserialize(this._data));176return data.changes.map(change => change.toString()).join(', ');177}178179public matchesResource(resource: URI): boolean {180const uri = (URI.isUri(this.model) ? this.model : this.model.uri);181return (uri.toString() === resource.toString());182}183184public setModel(model: ITextModel | URI): void {185this.model = model;186}187188public canAppend(model: ITextModel): boolean {189return (this.model === model && this._data instanceof SingleModelEditStackData);190}191192public append(model: ITextModel, textChanges: TextChange[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void {193if (this._data instanceof SingleModelEditStackData) {194this._data.append(model, textChanges, afterEOL, afterVersionId, afterCursorState);195}196}197198public close(): void {199if (this._data instanceof SingleModelEditStackData) {200this._data = this._data.serialize();201}202}203204public open(): void {205if (!(this._data instanceof SingleModelEditStackData)) {206this._data = SingleModelEditStackData.deserialize(this._data);207}208}209210public undo(): void {211if (URI.isUri(this.model)) {212// don't have a model213throw new Error(`Invalid SingleModelEditStackElement`);214}215if (this._data instanceof SingleModelEditStackData) {216this._data = this._data.serialize();217}218const data = SingleModelEditStackData.deserialize(this._data);219this.model._applyUndo(data.changes, data.beforeEOL, data.beforeVersionId, data.beforeCursorState);220}221222public redo(): void {223if (URI.isUri(this.model)) {224// don't have a model225throw new Error(`Invalid SingleModelEditStackElement`);226}227if (this._data instanceof SingleModelEditStackData) {228this._data = this._data.serialize();229}230const data = SingleModelEditStackData.deserialize(this._data);231this.model._applyRedo(data.changes, data.afterEOL, data.afterVersionId, data.afterCursorState);232}233234public heapSize(): number {235if (this._data instanceof SingleModelEditStackData) {236this._data = this._data.serialize();237}238return this._data.byteLength + 168/*heap overhead*/;239}240}241242export class MultiModelEditStackElement implements IWorkspaceUndoRedoElement {243244public readonly type = UndoRedoElementType.Workspace;245private _isOpen: boolean;246247private readonly _editStackElementsArr: SingleModelEditStackElement[];248private readonly _editStackElementsMap: Map<string, SingleModelEditStackElement>;249250private _delegate: IUndoRedoDelegate | null;251252public get resources(): readonly URI[] {253return this._editStackElementsArr.map(editStackElement => editStackElement.resource);254}255256constructor(257public readonly label: string,258public readonly code: string,259editStackElements: SingleModelEditStackElement[]260) {261this._isOpen = true;262this._editStackElementsArr = editStackElements.slice(0);263this._editStackElementsMap = new Map<string, SingleModelEditStackElement>();264for (const editStackElement of this._editStackElementsArr) {265const key = uriGetComparisonKey(editStackElement.resource);266this._editStackElementsMap.set(key, editStackElement);267}268this._delegate = null;269}270271public setDelegate(delegate: IUndoRedoDelegate): void {272this._delegate = delegate;273}274275public prepareUndoRedo(): Promise<IDisposable> | IDisposable | void {276if (this._delegate) {277return this._delegate.prepareUndoRedo(this);278}279}280281public getMissingModels(): URI[] {282const result: URI[] = [];283for (const editStackElement of this._editStackElementsArr) {284if (URI.isUri(editStackElement.model)) {285result.push(editStackElement.model);286}287}288return result;289}290291public matchesResource(resource: URI): boolean {292const key = uriGetComparisonKey(resource);293return (this._editStackElementsMap.has(key));294}295296public setModel(model: ITextModel | URI): void {297const key = uriGetComparisonKey(URI.isUri(model) ? model : model.uri);298if (this._editStackElementsMap.has(key)) {299this._editStackElementsMap.get(key)!.setModel(model);300}301}302303public canAppend(model: ITextModel): boolean {304if (!this._isOpen) {305return false;306}307const key = uriGetComparisonKey(model.uri);308if (this._editStackElementsMap.has(key)) {309const editStackElement = this._editStackElementsMap.get(key)!;310return editStackElement.canAppend(model);311}312return false;313}314315public append(model: ITextModel, textChanges: TextChange[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void {316const key = uriGetComparisonKey(model.uri);317const editStackElement = this._editStackElementsMap.get(key)!;318editStackElement.append(model, textChanges, afterEOL, afterVersionId, afterCursorState);319}320321public close(): void {322this._isOpen = false;323}324325public open(): void {326// cannot reopen327}328329public undo(): void {330this._isOpen = false;331332for (const editStackElement of this._editStackElementsArr) {333editStackElement.undo();334}335}336337public redo(): void {338for (const editStackElement of this._editStackElementsArr) {339editStackElement.redo();340}341}342343public heapSize(resource: URI): number {344const key = uriGetComparisonKey(resource);345if (this._editStackElementsMap.has(key)) {346const editStackElement = this._editStackElementsMap.get(key)!;347return editStackElement.heapSize();348}349return 0;350}351352public split(): IResourceUndoRedoElement[] {353return this._editStackElementsArr;354}355356public toString(): string {357const result: string[] = [];358for (const editStackElement of this._editStackElementsArr) {359result.push(`${basename(editStackElement.resource)}: ${editStackElement}`);360}361return `{${result.join(', ')}}`;362}363}364365export type EditStackElement = SingleModelEditStackElement | MultiModelEditStackElement;366367function getModelEOL(model: ITextModel): EndOfLineSequence {368const eol = model.getEOL();369if (eol === '\n') {370return EndOfLineSequence.LF;371} else {372return EndOfLineSequence.CRLF;373}374}375376export function isEditStackElement(element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null): element is EditStackElement {377if (!element) {378return false;379}380return ((element instanceof SingleModelEditStackElement) || (element instanceof MultiModelEditStackElement));381}382383export class EditStack {384385private readonly _model: TextModel;386private readonly _undoRedoService: IUndoRedoService;387388constructor(model: TextModel, undoRedoService: IUndoRedoService) {389this._model = model;390this._undoRedoService = undoRedoService;391}392393public pushStackElement(): void {394const lastElement = this._undoRedoService.getLastElement(this._model.uri);395if (isEditStackElement(lastElement)) {396lastElement.close();397}398}399400public popStackElement(): void {401const lastElement = this._undoRedoService.getLastElement(this._model.uri);402if (isEditStackElement(lastElement)) {403lastElement.open();404}405}406407public clear(): void {408this._undoRedoService.removeElements(this._model.uri);409}410411private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null, group: UndoRedoGroup | undefined): EditStackElement {412const lastElement = this._undoRedoService.getLastElement(this._model.uri);413if (isEditStackElement(lastElement) && lastElement.canAppend(this._model)) {414return lastElement;415}416const newElement = new SingleModelEditStackElement(nls.localize('edit', "Typing"), 'undoredo.textBufferEdit', this._model, beforeCursorState);417this._undoRedoService.pushElement(newElement, group);418return newElement;419}420421public pushEOL(eol: EndOfLineSequence): void {422const editStackElement = this._getOrCreateEditStackElement(null, undefined);423this._model.setEOL(eol);424editStackElement.append(this._model, [], getModelEOL(this._model), this._model.getAlternativeVersionId(), null);425}426427public pushEditOperation(beforeCursorState: Selection[] | null, editOperations: ISingleEditOperation[], cursorStateComputer: ICursorStateComputer | null, group?: UndoRedoGroup, reason: TextModelEditSource = EditSources.unknown({ name: 'pushEditOperation' })): Selection[] | null {428const editStackElement = this._getOrCreateEditStackElement(beforeCursorState, group);429const inverseEditOperations = this._model.applyEdits(editOperations, true, reason);430const afterCursorState = EditStack._computeCursorState(cursorStateComputer, inverseEditOperations);431const textChanges = inverseEditOperations.map((op, index) => ({ index: index, textChange: op.textChange }));432textChanges.sort((a, b) => {433if (a.textChange.oldPosition === b.textChange.oldPosition) {434return a.index - b.index;435}436return a.textChange.oldPosition - b.textChange.oldPosition;437});438editStackElement.append(this._model, textChanges.map(op => op.textChange), getModelEOL(this._model), this._model.getAlternativeVersionId(), afterCursorState);439return afterCursorState;440}441442private static _computeCursorState(cursorStateComputer: ICursorStateComputer | null, inverseEditOperations: IValidEditOperation[]): Selection[] | null {443try {444return cursorStateComputer ? cursorStateComputer(inverseEditOperations) : null;445} catch (e) {446onUnexpectedError(e);447return null;448}449}450}451452453