Skip to content

Commit 46dbb79

Browse files
committed
Compress consecutive edits in undo stack
1 parent 4fc25c2 commit 46dbb79

12 files changed

Lines changed: 150 additions & 131 deletions

File tree

src/vs/editor/common/model.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { SearchData } from 'vs/editor/common/model/textModelSearch';
1515
import { LanguageId, LanguageIdentifier, FormattingOptions } from 'vs/editor/common/modes';
1616
import { ThemeColor } from 'vs/platform/theme/common/themeService';
1717
import { MultilineTokens, MultilineTokens2 } from 'vs/editor/common/model/tokensStore';
18+
import { TextChange } from 'vs/editor/common/model/textChange';
1819

1920
/**
2021
* Vertical Lane in the overview ruler of the editor.
@@ -373,21 +374,13 @@ export interface IValidEditOperation {
373374
*/
374375
range: Range;
375376
/**
376-
* The text to replace with. This can be null to emulate a simple delete.
377+
* The text to replace with. This can be empty to emulate a simple delete.
377378
*/
378-
text: string | null;
379+
text: string;
379380
/**
380-
* This indicates that this operation has "insert" semantics.
381-
* i.e. forceMoveMarkers = true => if `range` is collapsed, all markers at the position will be moved.
381+
* @internal
382382
*/
383-
forceMoveMarkers: boolean;
384-
}
385-
386-
/**
387-
* @internal
388-
*/
389-
export interface IValidEditOperations {
390-
operations: IValidEditOperation[];
383+
textChange: TextChange;
391384
}
392385

393386
/**
@@ -1106,7 +1099,12 @@ export interface ITextModel {
11061099
/**
11071100
* @internal
11081101
*/
1109-
_applyUndoRedoEdits(edits: IValidEditOperations[], eol: EndOfLineSequence, isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): IValidEditOperations[];
1102+
_applyUndo(changes: TextChange[], eol: EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void;
1103+
1104+
/**
1105+
* @internal
1106+
*/
1107+
_applyRedo(changes: TextChange[], eol: EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void;
11101108

11111109
/**
11121110
* Undo edit operations until the first previous stop point created by `pushStackElement`.
@@ -1285,7 +1283,7 @@ export interface ITextBuffer {
12851283
getLineLastNonWhitespaceColumn(lineNumber: number): number;
12861284

12871285
setEOL(newEOL: '\r\n' | '\n'): void;
1288-
applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean): ApplyEditsResult;
1286+
applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean, computeUndoEdits: boolean): ApplyEditsResult;
12891287
findMatchesLineByLine(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[];
12901288
}
12911289

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
import * as nls from 'vs/nls';
77
import { onUnexpectedError } from 'vs/base/common/errors';
88
import { Selection } from 'vs/editor/common/core/selection';
9-
import { EndOfLineSequence, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation, ITextModel, IValidEditOperations } from 'vs/editor/common/model';
9+
import { EndOfLineSequence, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation, ITextModel } from 'vs/editor/common/model';
1010
import { TextModel } from 'vs/editor/common/model/textModel';
1111
import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorkspaceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo';
1212
import { URI } from 'vs/base/common/uri';
1313
import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources';
14+
import { TextChange, compressConsecutiveTextChanges } from 'vs/editor/common/model/textChange';
1415

1516
export class SingleModelEditStackElement implements IResourceUndoRedoElement {
1617

@@ -24,7 +25,7 @@ export class SingleModelEditStackElement implements IResourceUndoRedoElement {
2425
private _afterVersionId: number;
2526
private _afterEOL: EndOfLineSequence;
2627
private _afterCursorState: Selection[] | null;
27-
private _edits: IValidEditOperations[];
28+
private _changes: TextChange[];
2829

2930
public get resource(): URI {
3031
return this.model.uri;
@@ -40,7 +41,7 @@ export class SingleModelEditStackElement implements IResourceUndoRedoElement {
4041
this._afterVersionId = this._beforeVersionId;
4142
this._afterEOL = this._beforeEOL;
4243
this._afterCursorState = this._beforeCursorState;
43-
this._edits = [];
44+
this._changes = [];
4445
}
4546

4647
public setModel(model: ITextModel): void {
@@ -53,7 +54,7 @@ export class SingleModelEditStackElement implements IResourceUndoRedoElement {
5354

5455
public append(model: ITextModel, operations: IValidEditOperation[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void {
5556
if (operations.length > 0) {
56-
this._edits.push({ operations: operations });
57+
this._changes = compressConsecutiveTextChanges(this._changes, operations.map(op => op.textChange));
5758
}
5859
this._afterEOL = afterEOL;
5960
this._afterVersionId = afterVersionId;
@@ -66,13 +67,11 @@ export class SingleModelEditStackElement implements IResourceUndoRedoElement {
6667

6768
public undo(): void {
6869
this._isOpen = false;
69-
this._edits.reverse();
70-
this._edits = this.model._applyUndoRedoEdits(this._edits, this._beforeEOL, true, false, this._beforeVersionId, this._beforeCursorState);
70+
this.model._applyUndo(this._changes, this._beforeEOL, this._beforeVersionId, this._beforeCursorState);
7171
}
7272

7373
public redo(): void {
74-
this._edits.reverse();
75-
this._edits = this.model._applyUndoRedoEdits(this._edits, this._afterEOL, false, true, this._afterVersionId, this._afterCursorState);
74+
this.model._applyRedo(this._changes, this._afterEOL, this._afterVersionId, this._afterCursorState);
7675
}
7776
}
7877

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

Lines changed: 62 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import { ApplyEditsResult, EndOfLinePreference, FindMatch, IInternalModelContent
1010
import { PieceTreeBase, StringBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase';
1111
import { SearchData } from 'vs/editor/common/model/textModelSearch';
1212
import { countEOL, StringEOL } from 'vs/editor/common/model/tokensStore';
13+
import { TextChange } from 'vs/editor/common/model/textChange';
1314

1415
export interface IValidatedEditOperation {
1516
sortIndex: number;
1617
identifier: ISingleEditOperationIdentifier | null;
1718
range: Range;
1819
rangeOffset: number;
1920
rangeLength: number;
20-
text: string | null;
21+
text: string;
2122
eolCount: number;
2223
firstLineLength: number;
2324
lastLineLength: number;
@@ -205,7 +206,7 @@ export class PieceTreeTextBuffer implements ITextBuffer {
205206
this._pieceTree.setEOL(newEOL);
206207
}
207208

208-
public applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean): ApplyEditsResult {
209+
public applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean, computeUndoEdits: boolean): ApplyEditsResult {
209210
let mightContainRTL = this._mightContainRTL;
210211
let mightContainNonBasicASCII = this._mightContainNonBasicASCII;
211212
let canReduceOperations = true;
@@ -225,7 +226,7 @@ export class PieceTreeTextBuffer implements ITextBuffer {
225226
mightContainNonBasicASCII = !strings.isBasicASCII(op.text);
226227
}
227228

228-
let validText: string | null = null;
229+
let validText = '';
229230
let eolCount = 0;
230231
let firstLineLength = 0;
231232
let lastLineLength = 0;
@@ -260,66 +261,75 @@ export class PieceTreeTextBuffer implements ITextBuffer {
260261
// Sort operations ascending
261262
operations.sort(PieceTreeTextBuffer._sortOpsAscending);
262263

263-
let hasTouchingRanges = false;
264-
for (let i = 0, count = operations.length - 1; i < count; i++) {
265-
let rangeEnd = operations[i].range.getEndPosition();
266-
let nextRangeStart = operations[i + 1].range.getStartPosition();
267-
268-
if (nextRangeStart.isBeforeOrEqual(rangeEnd)) {
269-
if (nextRangeStart.isBefore(rangeEnd)) {
270-
// overlapping ranges
271-
throw new Error('Overlapping ranges are not allowed!');
272-
}
273-
hasTouchingRanges = true;
274-
}
275-
}
276-
277264
if (canReduceOperations) {
278265
operations = this._reduceOperations(operations);
279266
}
280267

281268
// Delta encode operations
282-
let reverseRanges = PieceTreeTextBuffer._getInverseEditRanges(operations);
269+
let reverseRanges = (computeUndoEdits || recordTrimAutoWhitespace ? PieceTreeTextBuffer._getInverseEditRanges(operations) : []);
283270
let newTrimAutoWhitespaceCandidates: { lineNumber: number, oldContent: string }[] = [];
284-
285-
for (let i = 0; i < operations.length; i++) {
286-
let op = operations[i];
287-
let reverseRange = reverseRanges[i];
288-
289-
if (recordTrimAutoWhitespace && op.isAutoWhitespaceEdit && op.range.isEmpty()) {
290-
// Record already the future line numbers that might be auto whitespace removal candidates on next edit
291-
for (let lineNumber = reverseRange.startLineNumber; lineNumber <= reverseRange.endLineNumber; lineNumber++) {
292-
let currentLineContent = '';
293-
if (lineNumber === reverseRange.startLineNumber) {
294-
currentLineContent = this.getLineContent(op.range.startLineNumber);
295-
if (strings.firstNonWhitespaceIndex(currentLineContent) !== -1) {
296-
continue;
271+
if (recordTrimAutoWhitespace) {
272+
for (let i = 0; i < operations.length; i++) {
273+
let op = operations[i];
274+
let reverseRange = reverseRanges[i];
275+
276+
if (op.isAutoWhitespaceEdit && op.range.isEmpty()) {
277+
// Record already the future line numbers that might be auto whitespace removal candidates on next edit
278+
for (let lineNumber = reverseRange.startLineNumber; lineNumber <= reverseRange.endLineNumber; lineNumber++) {
279+
let currentLineContent = '';
280+
if (lineNumber === reverseRange.startLineNumber) {
281+
currentLineContent = this.getLineContent(op.range.startLineNumber);
282+
if (strings.firstNonWhitespaceIndex(currentLineContent) !== -1) {
283+
continue;
284+
}
297285
}
286+
newTrimAutoWhitespaceCandidates.push({ lineNumber: lineNumber, oldContent: currentLineContent });
298287
}
299-
newTrimAutoWhitespaceCandidates.push({ lineNumber: lineNumber, oldContent: currentLineContent });
300288
}
301289
}
302290
}
303291

304292
let reverseOperations: IReverseSingleEditOperation[] = [];
305-
for (let i = 0; i < operations.length; i++) {
306-
let op = operations[i];
307-
let reverseRange = reverseRanges[i];
293+
if (computeUndoEdits) {
308294

309-
reverseOperations[i] = {
310-
sortIndex: op.sortIndex,
311-
identifier: op.identifier,
312-
range: reverseRange,
313-
text: this.getValueInRange(op.range),
314-
forceMoveMarkers: op.forceMoveMarkers
315-
};
316-
}
295+
let hasTouchingRanges = false;
296+
for (let i = 0, count = operations.length - 1; i < count; i++) {
297+
let rangeEnd = operations[i].range.getEndPosition();
298+
let nextRangeStart = operations[i + 1].range.getStartPosition();
299+
300+
if (nextRangeStart.isBeforeOrEqual(rangeEnd)) {
301+
if (nextRangeStart.isBefore(rangeEnd)) {
302+
// overlapping ranges
303+
throw new Error('Overlapping ranges are not allowed!');
304+
}
305+
hasTouchingRanges = true;
306+
}
307+
}
317308

318-
// Can only sort reverse operations when the order is not significant
319-
if (!hasTouchingRanges) {
320-
reverseOperations.sort((a, b) => a.sortIndex - b.sortIndex);
309+
let reverseRangeDeltaOffset = 0;
310+
for (let i = 0; i < operations.length; i++) {
311+
const op = operations[i];
312+
const reverseRange = reverseRanges[i];
313+
const bufferText = this.getValueInRange(op.range);
314+
const reverseRangeOffset = op.rangeOffset + reverseRangeDeltaOffset;
315+
reverseRangeDeltaOffset += (op.text.length - bufferText.length);
316+
317+
reverseOperations[i] = {
318+
sortIndex: op.sortIndex,
319+
identifier: op.identifier,
320+
range: reverseRange,
321+
text: bufferText,
322+
textChange: new TextChange(op.rangeOffset, bufferText, reverseRangeOffset, op.text)
323+
};
324+
}
325+
326+
// Can only sort reverse operations when the order is not significant
327+
if (!hasTouchingRanges) {
328+
reverseOperations.sort((a, b) => a.sortIndex - b.sortIndex);
329+
}
321330
}
322331

332+
323333
this._mightContainRTL = mightContainRTL;
324334
this._mightContainNonBasicASCII = mightContainNonBasicASCII;
325335

@@ -393,7 +403,7 @@ export class PieceTreeTextBuffer implements ITextBuffer {
393403
result.push(this.getValueInRange(new Range(lastEndLineNumber, lastEndColumn, range.startLineNumber, range.startColumn)));
394404

395405
// (2) -- Push new text
396-
if (operation.text) {
406+
if (operation.text.length > 0) {
397407
result.push(operation.text);
398408
}
399409

@@ -433,17 +443,15 @@ export class PieceTreeTextBuffer implements ITextBuffer {
433443
const endLineNumber = op.range.endLineNumber;
434444
const endColumn = op.range.endColumn;
435445

436-
if (startLineNumber === endLineNumber && startColumn === endColumn && (!op.text || op.text.length === 0)) {
446+
if (startLineNumber === endLineNumber && startColumn === endColumn && op.text.length === 0) {
437447
// no-op
438448
continue;
439449
}
440450

441-
const text = op.text ? op.text : '';
442-
443-
if (text) {
451+
if (op.text) {
444452
// replacement
445453
this._pieceTree.delete(op.rangeOffset, op.rangeLength);
446-
this._pieceTree.insert(op.rangeOffset, text, true);
454+
this._pieceTree.insert(op.rangeOffset, op.text, true);
447455

448456
} else {
449457
// deletion
@@ -454,7 +462,7 @@ export class PieceTreeTextBuffer implements ITextBuffer {
454462
contentChanges.push({
455463
range: contentChangeRange,
456464
rangeLength: op.rangeLength,
457-
text: text,
465+
text: op.text,
458466
rangeOffset: op.rangeOffset,
459467
forceMoveMarkers: op.forceMoveMarkers
460468
});
@@ -503,7 +511,7 @@ export class PieceTreeTextBuffer implements ITextBuffer {
503511

504512
let resultRange: Range;
505513

506-
if (op.text && op.text.length > 0) {
514+
if (op.text.length > 0) {
507515
// the operation inserts something
508516
const lineCount = op.eolCount + 1;
509517

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

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,33 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
export class TextChange {
7-
public readonly oldPosition: number;
8-
public readonly oldLength: number;
9-
public readonly oldEnd: number;
10-
public readonly oldText: string;
11-
public readonly newPosition: number;
12-
public readonly newLength: number;
13-
public readonly newEnd: number;
14-
public readonly newText: string;
157

16-
constructor(
17-
oldPosition: number,
18-
oldText: string,
19-
newPosition: number,
20-
newText: string
21-
) {
22-
this.oldPosition = oldPosition;
23-
this.oldLength = oldText.length;
24-
this.oldEnd = this.oldPosition + this.oldLength;
25-
this.oldText = oldText;
26-
27-
this.newPosition = newPosition;
28-
this.newLength = newText.length;
29-
this.newEnd = this.newPosition + this.newLength;
30-
this.newText = newText;
8+
public get oldLength(): number {
9+
return this.oldText.length;
10+
}
11+
12+
public get oldEnd(): number {
13+
return this.oldPosition + this.oldText.length;
14+
}
15+
16+
public get newLength(): number {
17+
return this.newText.length;
3118
}
19+
20+
public get newEnd(): number {
21+
return this.newPosition + this.newText.length;
22+
}
23+
24+
constructor(
25+
public readonly oldPosition: number,
26+
public readonly oldText: string,
27+
public readonly newPosition: number,
28+
public readonly newText: string
29+
) { }
3230
}
3331

3432
export function compressConsecutiveTextChanges(prevEdits: TextChange[] | null, currEdits: TextChange[]): TextChange[] {
35-
if (prevEdits === null) {
33+
if (prevEdits === null || prevEdits.length === 0) {
3634
return currEdits;
3735
}
3836
const compressor = new TextChangeCompressor(prevEdits, currEdits);

0 commit comments

Comments
 (0)