Skip to content

Commit 1ac5ea4

Browse files
authored
Implements handled undo stack in merge editor (microsoft#166606)
1 parent 0d4e84d commit 1ac5ea4

File tree

7 files changed

+240
-64
lines changed

7 files changed

+240
-64
lines changed

src/vs/editor/common/model.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { IModelContentChange, IModelContentChangedEvent, IModelDecorationsChange
2121
import { IGuidesTextModelPart } from 'vs/editor/common/textModelGuides';
2222
import { ITokenizationTextModelPart } from 'vs/editor/common/tokenizationTextModelPart';
2323
import { ThemeColor } from 'vs/platform/theme/common/themeService';
24+
import { UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo';
2425

2526
/**
2627
* Vertical Lane in the overview ruler of the editor.
@@ -1023,6 +1024,10 @@ export interface ITextModel {
10231024
* @return The cursor state returned by the `cursorStateComputer`.
10241025
*/
10251026
pushEditOperations(beforeCursorState: Selection[] | null, editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): Selection[] | null;
1027+
/**
1028+
* @internal
1029+
*/
1030+
pushEditOperations(beforeCursorState: Selection[] | null, editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer, group?: UndoRedoGroup): Selection[] | null;
10261031

10271032
/**
10281033
* Change the end of line sequence. This is the preferred way of

src/vs/editor/common/model/editStack.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { onUnexpectedError } from 'vs/base/common/errors';
88
import { Selection } from 'vs/editor/common/core/selection';
99
import { EndOfLineSequence, ICursorStateComputer, IValidEditOperation, ITextModel } from 'vs/editor/common/model';
1010
import { TextModel } from 'vs/editor/common/model/textModel';
11-
import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorkspaceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo';
11+
import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorkspaceUndoRedoElement, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo';
1212
import { URI } from 'vs/base/common/uri';
1313
import { TextChange, compressConsecutiveTextChanges } from 'vs/editor/common/core/textChange';
1414
import * as buffer from 'vs/base/common/buffer';
@@ -408,24 +408,24 @@ export class EditStack {
408408
this._undoRedoService.removeElements(this._model.uri);
409409
}
410410

411-
private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null): EditStackElement {
411+
private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null, group: UndoRedoGroup | undefined): EditStackElement {
412412
const lastElement = this._undoRedoService.getLastElement(this._model.uri);
413413
if (isEditStackElement(lastElement) && lastElement.canAppend(this._model)) {
414414
return lastElement;
415415
}
416416
const newElement = new SingleModelEditStackElement(nls.localize('edit', "Typing"), 'undoredo.textBufferEdit', this._model, beforeCursorState);
417-
this._undoRedoService.pushElement(newElement);
417+
this._undoRedoService.pushElement(newElement, group);
418418
return newElement;
419419
}
420420

421421
public pushEOL(eol: EndOfLineSequence): void {
422-
const editStackElement = this._getOrCreateEditStackElement(null);
422+
const editStackElement = this._getOrCreateEditStackElement(null, undefined);
423423
this._model.setEOL(eol);
424424
editStackElement.append(this._model, [], getModelEOL(this._model), this._model.getAlternativeVersionId(), null);
425425
}
426426

427-
public pushEditOperation(beforeCursorState: Selection[] | null, editOperations: ISingleEditOperation[], cursorStateComputer: ICursorStateComputer | null): Selection[] | null {
428-
const editStackElement = this._getOrCreateEditStackElement(beforeCursorState);
427+
public pushEditOperation(beforeCursorState: Selection[] | null, editOperations: ISingleEditOperation[], cursorStateComputer: ICursorStateComputer | null, group?: UndoRedoGroup): Selection[] | null {
428+
const editStackElement = this._getOrCreateEditStackElement(beforeCursorState, group);
429429
const inverseEditOperations = this._model.applyEdits(editOperations, true);
430430
const afterCursorState = EditStack._computeCursorState(cursorStateComputer, inverseEditOperations);
431431
const textChanges = inverseEditOperations.map((op, index) => ({ index: index, textChange: op.textChange }));

src/vs/editor/common/model/textModel.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelOptions
4242
import { IGuidesTextModelPart } from 'vs/editor/common/textModelGuides';
4343
import { ITokenizationTextModelPart } from 'vs/editor/common/tokenizationTextModelPart';
4444
import { IColorTheme, ThemeColor } from 'vs/platform/theme/common/themeService';
45-
import { IUndoRedoService, ResourceEditStackSnapshot } from 'vs/platform/undoRedo/common/undoRedo';
45+
import { IUndoRedoService, ResourceEditStackSnapshot, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo';
4646

4747
export function createTextBufferFactory(text: string): model.ITextBufferFactory {
4848
const builder = new PieceTreeTextBufferBuilder();
@@ -1242,18 +1242,18 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
12421242
return result;
12431243
}
12441244

1245-
public pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.IIdentifiedSingleEditOperation[], cursorStateComputer: model.ICursorStateComputer | null): Selection[] | null {
1245+
public pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.IIdentifiedSingleEditOperation[], cursorStateComputer: model.ICursorStateComputer | null, group?: UndoRedoGroup): Selection[] | null {
12461246
try {
12471247
this._onDidChangeDecorations.beginDeferredEmit();
12481248
this._eventEmitter.beginDeferredEmit();
1249-
return this._pushEditOperations(beforeCursorState, this._validateEditOperations(editOperations), cursorStateComputer);
1249+
return this._pushEditOperations(beforeCursorState, this._validateEditOperations(editOperations), cursorStateComputer, group);
12501250
} finally {
12511251
this._eventEmitter.endDeferredEmit();
12521252
this._onDidChangeDecorations.endDeferredEmit();
12531253
}
12541254
}
12551255

1256-
private _pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.ValidAnnotatedEditOperation[], cursorStateComputer: model.ICursorStateComputer | null): Selection[] | null {
1256+
private _pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.ValidAnnotatedEditOperation[], cursorStateComputer: model.ICursorStateComputer | null, group?: UndoRedoGroup): Selection[] | null {
12571257
if (this._options.trimAutoWhitespace && this._trimAutoWhitespaceLines) {
12581258
// Go through each saved line number and insert a trim whitespace edit
12591259
// 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
13401340
if (this._initialUndoRedoSnapshot === null) {
13411341
this._initialUndoRedoSnapshot = this._undoRedoService.createSnapshot(this.uri);
13421342
}
1343-
return this._commandManager.pushEditOperation(beforeCursorState, editOperations, cursorStateComputer);
1343+
return this._commandManager.pushEditOperation(beforeCursorState, editOperations, cursorStateComputer, group);
13441344
}
13451345

13461346
_applyUndo(changes: TextChange[], eol: model.EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void {

src/vs/workbench/contrib/mergeEditor/browser/model/editing.ts

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { equals } from 'vs/base/common/arrays';
77
import { Range } from 'vs/editor/common/core/range';
8-
import { ITextModel } from 'vs/editor/common/model';
8+
import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model';
99
import { LineRange } from './lineRange';
1010

1111
/**
@@ -22,8 +22,8 @@ export class LineRangeEdit {
2222
return this.range.equals(other.range) && equals(this.newLines, other.newLines);
2323
}
2424

25-
public apply(model: ITextModel): void {
26-
new LineEdits([this]).apply(model);
25+
public toEdits(modelLineCount: number): IIdentifiedSingleEditOperation[] {
26+
return new LineEdits([this]).toEdits(modelLineCount);
2727
}
2828
}
2929

@@ -41,30 +41,26 @@ export class RangeEdit {
4141
export class LineEdits {
4242
constructor(public readonly edits: readonly LineRangeEdit[]) { }
4343

44-
public apply(model: ITextModel): void {
45-
model.pushEditOperations(
46-
null,
47-
this.edits.map((e) => {
48-
if (e.range.endLineNumberExclusive <= model.getLineCount()) {
49-
return {
50-
range: new Range(e.range.startLineNumber, 1, e.range.endLineNumberExclusive, 1),
51-
text: e.newLines.map(s => s + '\n').join(''),
52-
};
53-
}
54-
55-
if (e.range.startLineNumber === 1) {
56-
return {
57-
range: new Range(1, 1, model.getLineCount(), Number.MAX_SAFE_INTEGER),
58-
text: e.newLines.join('\n'),
59-
};
60-
}
44+
public toEdits(modelLineCount: number): IIdentifiedSingleEditOperation[] {
45+
return this.edits.map((e) => {
46+
if (e.range.endLineNumberExclusive <= modelLineCount) {
47+
return {
48+
range: new Range(e.range.startLineNumber, 1, e.range.endLineNumberExclusive, 1),
49+
text: e.newLines.map(s => s + '\n').join(''),
50+
};
51+
}
6152

53+
if (e.range.startLineNumber === 1) {
6254
return {
63-
range: new Range(e.range.startLineNumber - 1, Number.MAX_SAFE_INTEGER, model.getLineCount(), Number.MAX_SAFE_INTEGER),
64-
text: e.newLines.map(s => '\n' + s).join(''),
55+
range: new Range(1, 1, modelLineCount, Number.MAX_SAFE_INTEGER),
56+
text: e.newLines.join('\n'),
6557
};
66-
}),
67-
() => null
68-
);
58+
}
59+
60+
return {
61+
range: new Range(e.range.startLineNumber - 1, Number.MAX_SAFE_INTEGER, modelLineCount, Number.MAX_SAFE_INTEGER),
62+
text: e.newLines.map(s => '\n' + s).join(''),
63+
};
64+
});
6965
}
7066
}

src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66
import { CompareResult, equals } from 'vs/base/common/arrays';
77
import { BugIndicatingError } from 'vs/base/common/errors';
88
import { autorunHandleChanges, derived, IObservable, IReader, ISettableObservable, ITransaction, keepAlive, observableValue, transaction, waitForState } from 'vs/base/common/observable';
9+
import { URI } from 'vs/base/common/uri';
910
import { Range } from 'vs/editor/common/core/range';
1011
import { ILanguageService } from 'vs/editor/common/languages/language';
1112
import { ITextModel } from 'vs/editor/common/model';
1213
import { IModelService } from 'vs/editor/common/services/model';
14+
import { localize } from 'vs/nls';
15+
import { IResourceUndoRedoElement, IUndoRedoService, UndoRedoElementType, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo';
1316
import { EditorModel } from 'vs/workbench/common/editor/editorModel';
1417
import { IMergeDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer';
1518
import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange';
@@ -58,6 +61,7 @@ export class MergeEditorModel extends EditorModel {
5861
public readonly telemetry: MergeEditorTelemetry,
5962
@IModelService private readonly modelService: IModelService,
6063
@ILanguageService private readonly languageService: ILanguageService,
64+
@IUndoRedoService private readonly undoRedoService: IUndoRedoService,
6165
) {
6266
super();
6367

@@ -405,9 +409,9 @@ export class MergeEditorModel extends EditorModel {
405409
public setState(
406410
baseRange: ModifiedBaseRange,
407411
state: ModifiedBaseRangeState,
408-
markInputAsHandled: boolean | InputNumber,
409-
transaction: ITransaction,
410-
pushStackElement: boolean = false
412+
_markInputAsHandled: boolean | InputNumber,
413+
tx: ITransaction,
414+
_pushStackElement: boolean = false
411415
): void {
412416
if (!this.isUpToDate.get()) {
413417
throw new BugIndicatingError('Cannot set state while updating');
@@ -421,29 +425,36 @@ export class MergeEditorModel extends EditorModel {
421425
const conflictingDiffs = this.resultTextModelDiffs.findTouchingDiffs(
422426
baseRange.baseRange
423427
);
428+
const group = new UndoRedoGroup();
424429
if (conflictingDiffs) {
425-
this.resultTextModelDiffs.removeDiffs(conflictingDiffs, transaction);
430+
this.resultTextModelDiffs.removeDiffs(conflictingDiffs, tx, group);
426431
}
427432

428433
const { edit, effectiveState } = baseRange.getEditForBase(state);
429434

430-
existingState.accepted.set(effectiveState, transaction);
435+
existingState.accepted.set(effectiveState, tx);
431436
existingState.previousNonDiffingState = undefined;
432437
existingState.computedFromDiffing = false;
433438

439+
const input1Handled = existingState.handledInput1.get();
440+
const input2Handled = existingState.handledInput2.get();
441+
442+
if (!input1Handled || !input2Handled) {
443+
this.undoRedoService.pushElement(
444+
new MarkAsHandledUndoRedoElement(this.resultTextModel.uri, new WeakRef(this), new WeakRef(existingState), input1Handled, input2Handled),
445+
group
446+
);
447+
}
448+
434449
if (edit) {
435-
if (pushStackElement) {
436-
this.resultTextModel.pushStackElement();
437-
}
438-
this.resultTextModelDiffs.applyEditRelativeToOriginal(edit, transaction);
439-
if (pushStackElement) {
440-
this.resultTextModel.pushStackElement();
441-
}
450+
this.resultTextModel.pushStackElement();
451+
this.resultTextModelDiffs.applyEditRelativeToOriginal(edit, tx, group);
452+
this.resultTextModel.pushStackElement();
442453
}
443454

444455
// always set conflict as handled
445-
existingState.handledInput1.set(true, transaction);
446-
existingState.handledInput2.set(true, transaction);
456+
existingState.handledInput1.set(true, tx);
457+
existingState.handledInput2.set(true, tx);
447458
}
448459

449460
public resetDirtyConflictsToBase(): void {
@@ -474,6 +485,42 @@ export class MergeEditorModel extends EditorModel {
474485
return;
475486
}
476487

488+
const dataRef = new WeakRef(ModifiedBaseRangeData);
489+
const modelRef = new WeakRef(this);
490+
491+
this.undoRedoService.pushElement({
492+
type: UndoRedoElementType.Resource,
493+
resource: this.resultTextModel.uri,
494+
code: 'setInputHandled',
495+
label: localize('setInputHandled', "Set Input Handled"),
496+
redo() {
497+
const model = modelRef.deref();
498+
const data = dataRef.deref();
499+
if (model && !model.isDisposed() && data) {
500+
transaction(tx => {
501+
if (inputNumber === 1) {
502+
state.handledInput1.set(handled, tx);
503+
} else {
504+
state.handledInput2.set(handled, tx);
505+
}
506+
});
507+
}
508+
},
509+
undo() {
510+
const model = modelRef.deref();
511+
const data = dataRef.deref();
512+
if (model && !model.isDisposed() && data) {
513+
transaction(tx => {
514+
if (inputNumber === 1) {
515+
state.handledInput1.set(!handled, tx);
516+
} else {
517+
state.handledInput2.set(!handled, tx);
518+
}
519+
});
520+
}
521+
},
522+
});
523+
477524
if (inputNumber === 1) {
478525
state.handledInput1.set(handled, tx);
479526
} else {
@@ -723,3 +770,43 @@ export const enum MergeEditorModelState {
723770
upToDate = 2,
724771
updating = 3,
725772
}
773+
774+
class MarkAsHandledUndoRedoElement implements IResourceUndoRedoElement {
775+
public readonly code = 'undoMarkAsHandled';
776+
public readonly label = localize('undoMarkAsHandled', 'Undo Mark As Handled');
777+
778+
public readonly type = UndoRedoElementType.Resource;
779+
780+
constructor(
781+
public readonly resource: URI,
782+
private readonly mergeEditorModelRef: WeakRef<MergeEditorModel>,
783+
private readonly stateRef: WeakRef<ModifiedBaseRangeData>,
784+
private readonly input1Handled: boolean,
785+
private readonly input2Handled: boolean,
786+
) { }
787+
788+
public redo() {
789+
const mergeEditorModel = this.mergeEditorModelRef.deref();
790+
if (!mergeEditorModel || mergeEditorModel.isDisposed()) {
791+
return;
792+
}
793+
const state = this.stateRef.deref();
794+
if (!state) { return; }
795+
transaction(tx => {
796+
state.handledInput1.set(true, tx);
797+
state.handledInput2.set(true, tx);
798+
});
799+
}
800+
public undo() {
801+
const mergeEditorModel = this.mergeEditorModelRef.deref();
802+
if (!mergeEditorModel || mergeEditorModel.isDisposed()) {
803+
return;
804+
}
805+
const state = this.stateRef.deref();
806+
if (!state) { return; }
807+
transaction(tx => {
808+
state.handledInput1.set(this.input1Handled, tx);
809+
state.handledInput2.set(this.input2Handled, tx);
810+
});
811+
}
812+
}

src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRa
1313
import { ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/utils';
1414
import { IMergeDiffComputer } from './diffComputer';
1515
import { autorun, IObservable, IReader, ITransaction, observableSignal, observableValue, transaction } from 'vs/base/common/observable';
16+
import { UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo';
1617

1718
export class TextModelDiffs extends Disposable {
1819
private recomputeCount = 0;
@@ -120,7 +121,7 @@ export class TextModelDiffs extends Disposable {
120121
}
121122
}
122123

123-
public removeDiffs(diffToRemoves: DetailedLineRangeMapping[], transaction: ITransaction | undefined): void {
124+
public removeDiffs(diffToRemoves: DetailedLineRangeMapping[], transaction: ITransaction | undefined, group?: UndoRedoGroup): void {
124125
this.ensureUpToDate();
125126

126127
diffToRemoves.sort(compareBy((d) => d.inputRange.startLineNumber, numberComparator));
@@ -137,7 +138,8 @@ export class TextModelDiffs extends Disposable {
137138
}
138139

139140
this.barrier.runExclusivelyOrThrow(() => {
140-
diffToRemove.getReverseLineEdit().apply(this.textModel);
141+
const edits = diffToRemove.getReverseLineEdit().toEdits(this.textModel.getLineCount());
142+
this.textModel.pushEditOperations(null, edits, () => null, group);
141143
});
142144

143145
diffs = diffs.map((d) =>
@@ -153,7 +155,7 @@ export class TextModelDiffs extends Disposable {
153155
/**
154156
* Edit must be conflict free.
155157
*/
156-
public applyEditRelativeToOriginal(edit: LineRangeEdit, transaction: ITransaction | undefined): void {
158+
public applyEditRelativeToOriginal(edit: LineRangeEdit, transaction: ITransaction | undefined, group?: UndoRedoGroup): void {
157159
this.ensureUpToDate();
158160

159161
const editMapping = new DetailedLineRangeMapping(
@@ -191,7 +193,8 @@ export class TextModelDiffs extends Disposable {
191193
}
192194

193195
this.barrier.runExclusivelyOrThrow(() => {
194-
new LineRangeEdit(edit.range.delta(delta), edit.newLines).apply(this.textModel);
196+
const edits = new LineRangeEdit(edit.range.delta(delta), edit.newLines).toEdits(this.textModel.getLineCount());
197+
this.textModel.pushEditOperations(null, edits, () => null, group);
195198
});
196199
this._diffs.set(newDiffs, transaction, TextModelDiffChangeReason.other);
197200
}

0 commit comments

Comments
 (0)