Skip to content

Commit fa484b6

Browse files
committed
Fixes #29036: Create an undo stop when alternating between constructive edits and destructive edits
1 parent a608c4a commit fa484b6

File tree

6 files changed

+288
-44
lines changed

6 files changed

+288
-44
lines changed

src/vs/editor/common/controller/coreCommands.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { Position } from 'vs/editor/common/core/position';
99
import { Range } from 'vs/editor/common/core/range';
1010
import * as editorCommon from 'vs/editor/common/editorCommon';
11-
import { CursorState, ICursors, RevealTarget, IColumnSelectData, CursorContext } from 'vs/editor/common/controller/cursorCommon';
11+
import { CursorState, ICursors, RevealTarget, IColumnSelectData, CursorContext, EditOperationType } from 'vs/editor/common/controller/cursorCommon';
1212
import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents';
1313
import { CursorMoveCommands, CursorMove as CursorMove_ } from 'vs/editor/common/controller/cursorMoveCommands';
1414
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
@@ -1614,11 +1614,13 @@ export namespace CoreEditingCommands {
16141614
}
16151615

16161616
public runEditorCommand(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor, args: any): void {
1617-
const [shouldPushStackElementBefore, commands] = DeleteOperations.deleteLeft(editor._getCursorConfiguration(), editor.getModel(), editor.getSelections());
1617+
const cursors = editor._getCursors();
1618+
const [shouldPushStackElementBefore, commands] = DeleteOperations.deleteLeft(cursors.getPrevEditOperationType(), editor._getCursorConfiguration(), editor.getModel(), editor.getSelections());
16181619
if (shouldPushStackElementBefore) {
16191620
editor.pushUndoStop();
16201621
}
16211622
editor.executeCommands(this.id, commands);
1623+
cursors.setPrevEditOperationType(EditOperationType.DeletingLeft);
16221624
}
16231625
});
16241626

@@ -1637,11 +1639,13 @@ export namespace CoreEditingCommands {
16371639
}
16381640

16391641
public runEditorCommand(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor, args: any): void {
1640-
const [shouldPushStackElementBefore, commands] = DeleteOperations.deleteRight(editor._getCursorConfiguration(), editor.getModel(), editor.getSelections());
1642+
const cursors = editor._getCursors();
1643+
const [shouldPushStackElementBefore, commands] = DeleteOperations.deleteRight(cursors.getPrevEditOperationType(), editor._getCursorConfiguration(), editor.getModel(), editor.getSelections());
16411644
if (shouldPushStackElementBefore) {
16421645
editor.pushUndoStop();
16431646
}
16441647
editor.executeCommands(this.id, commands);
1648+
cursors.setPrevEditOperationType(EditOperationType.DeletingRight);
16451649
}
16461650
});
16471651

src/vs/editor/common/controller/cursor.ts

+21-6
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Position } from 'vs/editor/common/core/position';
1212
import { Range } from 'vs/editor/common/core/range';
1313
import { Selection, SelectionDirection, ISelection } from 'vs/editor/common/core/selection';
1414
import * as editorCommon from 'vs/editor/common/editorCommon';
15-
import { CursorColumns, CursorConfiguration, EditOperationResult, CursorContext, CursorState, RevealTarget, IColumnSelectData, ICursors } from 'vs/editor/common/controller/cursorCommon';
15+
import { CursorColumns, CursorConfiguration, EditOperationResult, CursorContext, CursorState, RevealTarget, IColumnSelectData, ICursors, EditOperationType } from 'vs/editor/common/controller/cursorCommon';
1616
import { DeleteOperations } from 'vs/editor/common/controller/cursorDeleteOperations';
1717
import { TypeOperations } from 'vs/editor/common/controller/cursorTypeOperations';
1818
import { TextModelEventType, ModelRawContentChangedEvent, RawContentChangedType } from 'vs/editor/common/model/textModelEvents';
@@ -98,6 +98,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
9898
private _isHandling: boolean;
9999
private _isDoingComposition: boolean;
100100
private _columnSelectData: IColumnSelectData;
101+
private _prevEditOperationType: EditOperationType;
101102

102103
constructor(configuration: editorCommon.IConfiguration, model: editorCommon.IModel, viewModel: IViewModel) {
103104
super();
@@ -110,6 +111,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
110111
this._isHandling = false;
111112
this._isDoingComposition = false;
112113
this._columnSelectData = null;
114+
this._prevEditOperationType = EditOperationType.Other;
113115

114116
this._register(this._model.addBulkListener((events) => {
115117
if (this._isHandling) {
@@ -278,6 +280,9 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
278280
}
279281

280282
private _onModelContentChanged(hadFlushEvent: boolean): void {
283+
284+
this._prevEditOperationType = EditOperationType.Other;
285+
281286
if (hadFlushEvent) {
282287
// a model.setValue() was called
283288
this._cursors.dispose();
@@ -322,6 +327,14 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
322327
this.setStates(source, CursorChangeReason.NotSet, CursorState.fromModelSelections(selections));
323328
}
324329

330+
public getPrevEditOperationType(): EditOperationType {
331+
return this._prevEditOperationType;
332+
}
333+
334+
public setPrevEditOperationType(type: EditOperationType): void {
335+
this._prevEditOperationType = type;
336+
}
337+
325338
// ------ auxiliary handling logic
326339

327340
private _executeEditOperation(opResult: EditOperationResult): void {
@@ -344,6 +357,8 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
344357
if (result) {
345358
// The commands were applied correctly
346359
this._interpretCommandResult(result);
360+
361+
this._prevEditOperationType = opResult.type;
347362
}
348363

349364
if (opResult.shouldPushStackElementAfter) {
@@ -515,16 +530,16 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
515530
}
516531

517532
// Here we must interpret each typed character individually, that's why we create a new context
518-
this._executeEditOperation(TypeOperations.typeWithInterceptors(this.context.config, this.context.model, this.getSelections(), chr));
533+
this._executeEditOperation(TypeOperations.typeWithInterceptors(this._prevEditOperationType, this.context.config, this.context.model, this.getSelections(), chr));
519534
}
520535

521536
} else {
522-
this._executeEditOperation(TypeOperations.typeWithoutInterceptors(this.context.config, this.context.model, this.getSelections(), text));
537+
this._executeEditOperation(TypeOperations.typeWithoutInterceptors(this._prevEditOperationType, this.context.config, this.context.model, this.getSelections(), text));
523538
}
524539
}
525540

526541
private _replacePreviousChar(text: string, replaceCharCnt: number): void {
527-
this._executeEditOperation(TypeOperations.replacePreviousChar(this.context.config, this.context.model, this.getSelections(), text, replaceCharCnt));
542+
this._executeEditOperation(TypeOperations.replacePreviousChar(this._prevEditOperationType, this.context.config, this.context.model, this.getSelections(), text, replaceCharCnt));
528543
}
529544

530545
private _paste(text: string, pasteOnNewLine: boolean): void {
@@ -538,14 +553,14 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
538553
private _externalExecuteCommand(command: editorCommon.ICommand): void {
539554
this._cursors.killSecondaryCursors();
540555

541-
this._executeEditOperation(new EditOperationResult([command], {
556+
this._executeEditOperation(new EditOperationResult(EditOperationType.Other, [command], {
542557
shouldPushStackElementBefore: false,
543558
shouldPushStackElementAfter: false
544559
}));
545560
}
546561

547562
private _externalExecuteCommands(commands: editorCommon.ICommand[]): void {
548-
this._executeEditOperation(new EditOperationResult(commands, {
563+
this._executeEditOperation(new EditOperationResult(EditOperationType.Other, commands, {
549564
shouldPushStackElementBefore: false,
550565
shouldPushStackElementAfter: false
551566
}));

src/vs/editor/common/controller/cursorCommon.ts

+17
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ export const enum RevealTarget {
3131
BottomMost = 2
3232
}
3333

34+
/**
35+
* This is an operation type that will be recorded for undo/redo purposes.
36+
* The goal is to introduce an undo stop when the controller switches between different operation types.
37+
*/
38+
export const enum EditOperationType {
39+
Other = 0,
40+
Typing = 1,
41+
DeletingLeft = 2,
42+
DeletingRight = 3
43+
}
44+
3445
export interface ICursors {
3546
readonly context: CursorContext;
3647
getPrimaryCursor(): CursorState;
@@ -45,6 +56,9 @@ export interface ICursors {
4556
revealRange(revealHorizontal: boolean, viewRange: Range, verticalType: VerticalRevealType, scrollType: ScrollType): void;
4657

4758
scrollTo(desiredScrollTop: number): void;
59+
60+
getPrevEditOperationType(): EditOperationType;
61+
setPrevEditOperationType(type: EditOperationType): void;
4862
}
4963

5064
export interface CharacterMap {
@@ -422,17 +436,20 @@ export class CursorState {
422436
export class EditOperationResult {
423437
_editOperationResultBrand: void;
424438

439+
readonly type: EditOperationType;
425440
readonly commands: ICommand[];
426441
readonly shouldPushStackElementBefore: boolean;
427442
readonly shouldPushStackElementAfter: boolean;
428443

429444
constructor(
445+
type: EditOperationType,
430446
commands: ICommand[],
431447
opts: {
432448
shouldPushStackElementBefore: boolean;
433449
shouldPushStackElementAfter: boolean;
434450
}
435451
) {
452+
this.type = type;
436453
this.commands = commands;
437454
this.shouldPushStackElementBefore = opts.shouldPushStackElementBefore;
438455
this.shouldPushStackElementAfter = opts.shouldPushStackElementAfter;

src/vs/editor/common/controller/cursorDeleteOperations.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
'use strict';
66

77
import { ReplaceCommand } from 'vs/editor/common/commands/replaceCommand';
8-
import { CursorColumns, CursorConfiguration, ICursorSimpleModel, EditOperationResult } from 'vs/editor/common/controller/cursorCommon';
8+
import { CursorColumns, CursorConfiguration, ICursorSimpleModel, EditOperationResult, EditOperationType } from 'vs/editor/common/controller/cursorCommon';
99
import { Range } from 'vs/editor/common/core/range';
1010
import { Selection } from 'vs/editor/common/core/selection';
1111
import { MoveOperations } from 'vs/editor/common/controller/cursorMoveOperations';
@@ -14,9 +14,9 @@ import { ICommand } from 'vs/editor/common/editorCommon';
1414

1515
export class DeleteOperations {
1616

17-
public static deleteRight(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): [boolean, ICommand[]] {
17+
public static deleteRight(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): [boolean, ICommand[]] {
1818
let commands: ICommand[] = [];
19-
let shouldPushStackElementBefore = false;
19+
let shouldPushStackElementBefore = (prevEditOperationType !== EditOperationType.DeletingRight);
2020
for (let i = 0, len = selections.length; i < len; i++) {
2121
const selection = selections[i];
2222

@@ -94,14 +94,14 @@ export class DeleteOperations {
9494
return [true, commands];
9595
}
9696

97-
public static deleteLeft(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): [boolean, ICommand[]] {
97+
public static deleteLeft(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): [boolean, ICommand[]] {
9898

9999
if (this._isAutoClosingPairDelete(config, model, selections)) {
100100
return this._runAutoClosingPairDelete(config, model, selections);
101101
}
102102

103103
let commands: ICommand[] = [];
104-
let shouldPushStackElementBefore = false;
104+
let shouldPushStackElementBefore = (prevEditOperationType !== EditOperationType.DeletingLeft);
105105
for (let i = 0, len = selections.length; i < len; i++) {
106106
const selection = selections[i];
107107

@@ -210,7 +210,7 @@ export class DeleteOperations {
210210
commands[i] = new ReplaceCommand(selection, '');
211211
}
212212
}
213-
return new EditOperationResult(commands, {
213+
return new EditOperationResult(EditOperationType.Other, commands, {
214214
shouldPushStackElementBefore: true,
215215
shouldPushStackElementAfter: true
216216
});

0 commit comments

Comments
 (0)