From f8d2e4b8e0bf958587de25b843e5fd8abba778f9 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 17 Nov 2022 17:33:45 +0100 Subject: [PATCH] Implements handled undo stack in merge editor --- src/vs/editor/common/model.ts | 5 + src/vs/editor/common/model/editStack.ts | 12 +- src/vs/editor/common/model/textModel.ts | 10 +- .../mergeEditor/browser/model/editing.ts | 46 ++++--- .../browser/model/mergeEditorModel.ts | 115 +++++++++++++++--- .../browser/model/textModelDiffs.ts | 11 +- .../mergeEditor/browser/view/viewModel.ts | 105 ++++++++++++++-- 7 files changed, 240 insertions(+), 64 deletions(-) diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 0d2d8f3576fc0..861ad5b583252 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -21,6 +21,7 @@ import { IModelContentChange, IModelContentChangedEvent, IModelDecorationsChange import { IGuidesTextModelPart } from 'vs/editor/common/textModelGuides'; import { ITokenizationTextModelPart } from 'vs/editor/common/tokenizationTextModelPart'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; +import { UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; /** * Vertical Lane in the overview ruler of the editor. @@ -1023,6 +1024,10 @@ export interface ITextModel { * @return The cursor state returned by the `cursorStateComputer`. */ pushEditOperations(beforeCursorState: Selection[] | null, editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): Selection[] | null; + /** + * @internal + */ + pushEditOperations(beforeCursorState: Selection[] | null, editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer, group?: UndoRedoGroup): Selection[] | null; /** * Change the end of line sequence. This is the preferred way of diff --git a/src/vs/editor/common/model/editStack.ts b/src/vs/editor/common/model/editStack.ts index c1fd64e880d1d..8c9f1c4d6e2e7 100644 --- a/src/vs/editor/common/model/editStack.ts +++ b/src/vs/editor/common/model/editStack.ts @@ -8,7 +8,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { Selection } from 'vs/editor/common/core/selection'; import { EndOfLineSequence, ICursorStateComputer, IValidEditOperation, ITextModel } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorkspaceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; +import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorkspaceUndoRedoElement, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { TextChange, compressConsecutiveTextChanges } from 'vs/editor/common/core/textChange'; import * as buffer from 'vs/base/common/buffer'; @@ -408,24 +408,24 @@ export class EditStack { this._undoRedoService.removeElements(this._model.uri); } - private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null): EditStackElement { + private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null, group: UndoRedoGroup | undefined): EditStackElement { const lastElement = this._undoRedoService.getLastElement(this._model.uri); if (isEditStackElement(lastElement) && lastElement.canAppend(this._model)) { return lastElement; } const newElement = new SingleModelEditStackElement(nls.localize('edit', "Typing"), 'undoredo.textBufferEdit', this._model, beforeCursorState); - this._undoRedoService.pushElement(newElement); + this._undoRedoService.pushElement(newElement, group); return newElement; } public pushEOL(eol: EndOfLineSequence): void { - const editStackElement = this._getOrCreateEditStackElement(null); + const editStackElement = this._getOrCreateEditStackElement(null, undefined); this._model.setEOL(eol); editStackElement.append(this._model, [], getModelEOL(this._model), this._model.getAlternativeVersionId(), null); } - public pushEditOperation(beforeCursorState: Selection[] | null, editOperations: ISingleEditOperation[], cursorStateComputer: ICursorStateComputer | null): Selection[] | null { - const editStackElement = this._getOrCreateEditStackElement(beforeCursorState); + public pushEditOperation(beforeCursorState: Selection[] | null, editOperations: ISingleEditOperation[], cursorStateComputer: ICursorStateComputer | null, group?: UndoRedoGroup): Selection[] | null { + const editStackElement = this._getOrCreateEditStackElement(beforeCursorState, group); const inverseEditOperations = this._model.applyEdits(editOperations, true); const afterCursorState = EditStack._computeCursorState(cursorStateComputer, inverseEditOperations); const textChanges = inverseEditOperations.map((op, index) => ({ index: index, textChange: op.textChange })); diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index f22215997fb27..9447ad435700e 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -42,7 +42,7 @@ import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelOptions import { IGuidesTextModelPart } from 'vs/editor/common/textModelGuides'; import { ITokenizationTextModelPart } from 'vs/editor/common/tokenizationTextModelPart'; import { IColorTheme, ThemeColor } from 'vs/platform/theme/common/themeService'; -import { IUndoRedoService, ResourceEditStackSnapshot } from 'vs/platform/undoRedo/common/undoRedo'; +import { IUndoRedoService, ResourceEditStackSnapshot, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; export function createTextBufferFactory(text: string): model.ITextBufferFactory { const builder = new PieceTreeTextBufferBuilder(); @@ -1242,18 +1242,18 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return result; } - public pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.IIdentifiedSingleEditOperation[], cursorStateComputer: model.ICursorStateComputer | null): Selection[] | null { + public pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.IIdentifiedSingleEditOperation[], cursorStateComputer: model.ICursorStateComputer | null, group?: UndoRedoGroup): Selection[] | null { try { this._onDidChangeDecorations.beginDeferredEmit(); this._eventEmitter.beginDeferredEmit(); - return this._pushEditOperations(beforeCursorState, this._validateEditOperations(editOperations), cursorStateComputer); + return this._pushEditOperations(beforeCursorState, this._validateEditOperations(editOperations), cursorStateComputer, group); } finally { this._eventEmitter.endDeferredEmit(); this._onDidChangeDecorations.endDeferredEmit(); } } - private _pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.ValidAnnotatedEditOperation[], cursorStateComputer: model.ICursorStateComputer | null): Selection[] | null { + private _pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.ValidAnnotatedEditOperation[], cursorStateComputer: model.ICursorStateComputer | null, group?: UndoRedoGroup): Selection[] | null { if (this._options.trimAutoWhitespace && this._trimAutoWhitespaceLines) { // Go through each saved line number and insert a trim whitespace edit // if it is safe to do so (no conflicts with other edits). @@ -1340,7 +1340,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati if (this._initialUndoRedoSnapshot === null) { this._initialUndoRedoSnapshot = this._undoRedoService.createSnapshot(this.uri); } - return this._commandManager.pushEditOperation(beforeCursorState, editOperations, cursorStateComputer); + return this._commandManager.pushEditOperation(beforeCursorState, editOperations, cursorStateComputer, group); } _applyUndo(changes: TextChange[], eol: model.EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/editing.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/editing.ts index de2e9e2c6df84..57775b6e7aa5b 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/editing.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/editing.ts @@ -5,7 +5,7 @@ import { equals } from 'vs/base/common/arrays'; import { Range } from 'vs/editor/common/core/range'; -import { ITextModel } from 'vs/editor/common/model'; +import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { LineRange } from './lineRange'; /** @@ -22,8 +22,8 @@ export class LineRangeEdit { return this.range.equals(other.range) && equals(this.newLines, other.newLines); } - public apply(model: ITextModel): void { - new LineEdits([this]).apply(model); + public toEdits(modelLineCount: number): IIdentifiedSingleEditOperation[] { + return new LineEdits([this]).toEdits(modelLineCount); } } @@ -41,30 +41,26 @@ export class RangeEdit { export class LineEdits { constructor(public readonly edits: readonly LineRangeEdit[]) { } - public apply(model: ITextModel): void { - model.pushEditOperations( - null, - this.edits.map((e) => { - if (e.range.endLineNumberExclusive <= model.getLineCount()) { - return { - range: new Range(e.range.startLineNumber, 1, e.range.endLineNumberExclusive, 1), - text: e.newLines.map(s => s + '\n').join(''), - }; - } - - if (e.range.startLineNumber === 1) { - return { - range: new Range(1, 1, model.getLineCount(), Number.MAX_SAFE_INTEGER), - text: e.newLines.join('\n'), - }; - } + public toEdits(modelLineCount: number): IIdentifiedSingleEditOperation[] { + return this.edits.map((e) => { + if (e.range.endLineNumberExclusive <= modelLineCount) { + return { + range: new Range(e.range.startLineNumber, 1, e.range.endLineNumberExclusive, 1), + text: e.newLines.map(s => s + '\n').join(''), + }; + } + if (e.range.startLineNumber === 1) { return { - range: new Range(e.range.startLineNumber - 1, Number.MAX_SAFE_INTEGER, model.getLineCount(), Number.MAX_SAFE_INTEGER), - text: e.newLines.map(s => '\n' + s).join(''), + range: new Range(1, 1, modelLineCount, Number.MAX_SAFE_INTEGER), + text: e.newLines.join('\n'), }; - }), - () => null - ); + } + + return { + range: new Range(e.range.startLineNumber - 1, Number.MAX_SAFE_INTEGER, modelLineCount, Number.MAX_SAFE_INTEGER), + text: e.newLines.map(s => '\n' + s).join(''), + }; + }); } } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts index e3bbbd7c81f1e..7d8f404b794f6 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts @@ -6,10 +6,13 @@ import { CompareResult, equals } from 'vs/base/common/arrays'; import { BugIndicatingError } from 'vs/base/common/errors'; import { autorunHandleChanges, derived, IObservable, IReader, ISettableObservable, ITransaction, keepAlive, observableValue, transaction, waitForState } from 'vs/base/common/observable'; +import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; +import { localize } from 'vs/nls'; +import { IResourceUndoRedoElement, IUndoRedoService, UndoRedoElementType, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { IMergeDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer'; import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; @@ -58,6 +61,7 @@ export class MergeEditorModel extends EditorModel { public readonly telemetry: MergeEditorTelemetry, @IModelService private readonly modelService: IModelService, @ILanguageService private readonly languageService: ILanguageService, + @IUndoRedoService private readonly undoRedoService: IUndoRedoService, ) { super(); @@ -405,9 +409,9 @@ export class MergeEditorModel extends EditorModel { public setState( baseRange: ModifiedBaseRange, state: ModifiedBaseRangeState, - markInputAsHandled: boolean | InputNumber, - transaction: ITransaction, - pushStackElement: boolean = false + _markInputAsHandled: boolean | InputNumber, + tx: ITransaction, + _pushStackElement: boolean = false ): void { if (!this.isUpToDate.get()) { throw new BugIndicatingError('Cannot set state while updating'); @@ -421,29 +425,36 @@ export class MergeEditorModel extends EditorModel { const conflictingDiffs = this.resultTextModelDiffs.findTouchingDiffs( baseRange.baseRange ); + const group = new UndoRedoGroup(); if (conflictingDiffs) { - this.resultTextModelDiffs.removeDiffs(conflictingDiffs, transaction); + this.resultTextModelDiffs.removeDiffs(conflictingDiffs, tx, group); } const { edit, effectiveState } = baseRange.getEditForBase(state); - existingState.accepted.set(effectiveState, transaction); + existingState.accepted.set(effectiveState, tx); existingState.previousNonDiffingState = undefined; existingState.computedFromDiffing = false; + const input1Handled = existingState.handledInput1.get(); + const input2Handled = existingState.handledInput2.get(); + + if (!input1Handled || !input2Handled) { + this.undoRedoService.pushElement( + new MarkAsHandledUndoRedoElement(this.resultTextModel.uri, new WeakRef(this), new WeakRef(existingState), input1Handled, input2Handled), + group + ); + } + if (edit) { - if (pushStackElement) { - this.resultTextModel.pushStackElement(); - } - this.resultTextModelDiffs.applyEditRelativeToOriginal(edit, transaction); - if (pushStackElement) { - this.resultTextModel.pushStackElement(); - } + this.resultTextModel.pushStackElement(); + this.resultTextModelDiffs.applyEditRelativeToOriginal(edit, tx, group); + this.resultTextModel.pushStackElement(); } // always set conflict as handled - existingState.handledInput1.set(true, transaction); - existingState.handledInput2.set(true, transaction); + existingState.handledInput1.set(true, tx); + existingState.handledInput2.set(true, tx); } public resetDirtyConflictsToBase(): void { @@ -474,6 +485,42 @@ export class MergeEditorModel extends EditorModel { return; } + const dataRef = new WeakRef(ModifiedBaseRangeData); + const modelRef = new WeakRef(this); + + this.undoRedoService.pushElement({ + type: UndoRedoElementType.Resource, + resource: this.resultTextModel.uri, + code: 'setInputHandled', + label: localize('setInputHandled', "Set Input Handled"), + redo() { + const model = modelRef.deref(); + const data = dataRef.deref(); + if (model && !model.isDisposed() && data) { + transaction(tx => { + if (inputNumber === 1) { + state.handledInput1.set(handled, tx); + } else { + state.handledInput2.set(handled, tx); + } + }); + } + }, + undo() { + const model = modelRef.deref(); + const data = dataRef.deref(); + if (model && !model.isDisposed() && data) { + transaction(tx => { + if (inputNumber === 1) { + state.handledInput1.set(!handled, tx); + } else { + state.handledInput2.set(!handled, tx); + } + }); + } + }, + }); + if (inputNumber === 1) { state.handledInput1.set(handled, tx); } else { @@ -723,3 +770,43 @@ export const enum MergeEditorModelState { upToDate = 2, updating = 3, } + +class MarkAsHandledUndoRedoElement implements IResourceUndoRedoElement { + public readonly code = 'undoMarkAsHandled'; + public readonly label = localize('undoMarkAsHandled', 'Undo Mark As Handled'); + + public readonly type = UndoRedoElementType.Resource; + + constructor( + public readonly resource: URI, + private readonly mergeEditorModelRef: WeakRef, + private readonly stateRef: WeakRef, + private readonly input1Handled: boolean, + private readonly input2Handled: boolean, + ) { } + + public redo() { + const mergeEditorModel = this.mergeEditorModelRef.deref(); + if (!mergeEditorModel || mergeEditorModel.isDisposed()) { + return; + } + const state = this.stateRef.deref(); + if (!state) { return; } + transaction(tx => { + state.handledInput1.set(true, tx); + state.handledInput2.set(true, tx); + }); + } + public undo() { + const mergeEditorModel = this.mergeEditorModelRef.deref(); + if (!mergeEditorModel || mergeEditorModel.isDisposed()) { + return; + } + const state = this.stateRef.deref(); + if (!state) { return; } + transaction(tx => { + state.handledInput1.set(this.input1Handled, tx); + state.handledInput2.set(this.input2Handled, tx); + }); + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts index b618ffd82bcdc..526ca3d408580 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts @@ -13,6 +13,7 @@ import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRa import { ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { IMergeDiffComputer } from './diffComputer'; import { autorun, IObservable, IReader, ITransaction, observableSignal, observableValue, transaction } from 'vs/base/common/observable'; +import { UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; export class TextModelDiffs extends Disposable { private recomputeCount = 0; @@ -120,7 +121,7 @@ export class TextModelDiffs extends Disposable { } } - public removeDiffs(diffToRemoves: DetailedLineRangeMapping[], transaction: ITransaction | undefined): void { + public removeDiffs(diffToRemoves: DetailedLineRangeMapping[], transaction: ITransaction | undefined, group?: UndoRedoGroup): void { this.ensureUpToDate(); diffToRemoves.sort(compareBy((d) => d.inputRange.startLineNumber, numberComparator)); @@ -137,7 +138,8 @@ export class TextModelDiffs extends Disposable { } this.barrier.runExclusivelyOrThrow(() => { - diffToRemove.getReverseLineEdit().apply(this.textModel); + const edits = diffToRemove.getReverseLineEdit().toEdits(this.textModel.getLineCount()); + this.textModel.pushEditOperations(null, edits, () => null, group); }); diffs = diffs.map((d) => @@ -153,7 +155,7 @@ export class TextModelDiffs extends Disposable { /** * Edit must be conflict free. */ - public applyEditRelativeToOriginal(edit: LineRangeEdit, transaction: ITransaction | undefined): void { + public applyEditRelativeToOriginal(edit: LineRangeEdit, transaction: ITransaction | undefined, group?: UndoRedoGroup): void { this.ensureUpToDate(); const editMapping = new DetailedLineRangeMapping( @@ -191,7 +193,8 @@ export class TextModelDiffs extends Disposable { } this.barrier.runExclusivelyOrThrow(() => { - new LineRangeEdit(edit.range.delta(delta), edit.newLines).apply(this.textModel); + const edits = new LineRangeEdit(edit.range.delta(delta), edit.newLines).toEdits(this.textModel.getLineCount()); + this.textModel.pushEditOperations(null, edits, () => null, group); }); this._diffs.set(newDiffs, transaction, TextModelDiffChangeReason.other); } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts index 69bdccfa99ebc..75a2bf5751388 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts @@ -8,6 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { derived, derivedObservableWithWritableCache, IObservable, IReader, ITransaction, observableFromEvent, observableValue, transaction } from 'vs/base/common/observable'; import { Range } from 'vs/editor/common/core/range'; import { ScrollType } from 'vs/editor/common/editorCommon'; +import { ITextModel } from 'vs/editor/common/model'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -24,6 +25,8 @@ export class MergeEditorViewModel extends Disposable { { range: ModifiedBaseRange | undefined; counter: number } >('manuallySetActiveModifiedBaseRange', { range: undefined, counter: 0 }); + private readonly attachedHistory = this._register(new AttachedHistory(this.model.resultTextModel)); + constructor( public readonly model: MergeEditorModel, public readonly inputCodeEditorView1: InputCodeEditorView, @@ -32,24 +35,53 @@ export class MergeEditorViewModel extends Disposable { public readonly baseCodeEditorView: IObservable, public readonly showNonConflictingChanges: IObservable, @IConfigurationService private readonly configurationService: IConfigurationService, - @INotificationService private readonly notificationService: INotificationService + @INotificationService private readonly notificationService: INotificationService, ) { super(); this._register(resultCodeEditorView.editor.onDidChangeModelContent(e => { - if (this.model.isApplyingEditInResult) { + if (this.model.isApplyingEditInResult || e.isRedoing || e.isUndoing) { return; } - transaction(tx => { - /** @description Mark conflicts touched by manual edits as handled */ - for (const change of e.changes) { - const rangeInBase = this.model.translateResultRangeToBase(Range.lift(change.range)); - const baseRanges = this.model.findModifiedBaseRangesInRange(new LineRange(rangeInBase.startLineNumber, rangeInBase.endLineNumber - rangeInBase.startLineNumber)); - if (baseRanges.length === 1) { - this.model.setHandled(baseRanges[0], true, tx); + + const baseRangeStates: ModifiedBaseRange[] = []; + + for (const change of e.changes) { + const rangeInBase = this.model.translateResultRangeToBase(Range.lift(change.range)); + const baseRanges = this.model.findModifiedBaseRangesInRange(new LineRange(rangeInBase.startLineNumber, rangeInBase.endLineNumber - rangeInBase.startLineNumber)); + if (baseRanges.length === 1) { + const isHandled = this.model.isHandled(baseRanges[0]).get(); + if (!isHandled) { + baseRangeStates.push(baseRanges[0]); } } - }); + } + + if (baseRangeStates.length === 0) { + return; + } + + const element = { + model: this.model, + redo() { + transaction(tx => { + /** @description Mark conflicts touched by manual edits as handled */ + for (const r of baseRangeStates) { + this.model.setHandled(r, true, tx); + } + }); + }, + undo() { + transaction(tx => { + /** @description Mark conflicts touched by manual edits as handled */ + for (const r of baseRangeStates) { + this.model.setHandled(r, false, tx); + } + }); + }, + }; + this.attachedHistory.pushAttachedHistoryElement(element); + element.redo(); })); } @@ -255,3 +287,56 @@ export class MergeEditorViewModel extends Disposable { }); } } + +class AttachedHistory extends Disposable { + private readonly attachedHistory: { element: IAttachedHistoryElement; altId: number }[] = []; + private previousAltId: number = this.model.getAlternativeVersionId(); + + constructor(private readonly model: ITextModel) { + super(); + + this._register(model.onDidChangeContent((e) => { + const currentAltId = model.getAlternativeVersionId(); + + if (e.isRedoing) { + for (const item of this.attachedHistory) { + if (this.previousAltId < item.altId && item.altId <= currentAltId) { + item.element.redo(); + } + } + } else if (e.isUndoing) { + for (let i = this.attachedHistory.length - 1; i >= 0; i--) { + const item = this.attachedHistory[i]; + if (currentAltId < item.altId && item.altId <= this.previousAltId) { + item.element.undo(); + } + } + + } else { + // The user destroyed the redo stack by performing a non redo/undo operation. + // Thus we also need to remove all history elements after the last version id. + while ( + this.attachedHistory.length > 0 + && this.attachedHistory[this.attachedHistory.length - 1]!.altId > this.previousAltId + ) { + this.attachedHistory.pop(); + } + } + + this.previousAltId = currentAltId; + })); + } + + /** + * Pushes an history item that is tied to the last text edit (or an extension of it). + * When the last text edit is undone/redone, so is is this history item. + */ + public pushAttachedHistoryElement(element: IAttachedHistoryElement): void { + this.attachedHistory.push({ altId: this.model.getAlternativeVersionId(), element }); + } +} + +interface IAttachedHistoryElement { + undo(): void; + redo(): void; +}