未验证 提交 46dbb797 编写于 作者: A Alex Dima

Compress consecutive edits in undo stack

上级 4fc25c28
......@@ -15,6 +15,7 @@ import { SearchData } from 'vs/editor/common/model/textModelSearch';
import { LanguageId, LanguageIdentifier, FormattingOptions } from 'vs/editor/common/modes';
import { ThemeColor } from 'vs/platform/theme/common/themeService';
import { MultilineTokens, MultilineTokens2 } from 'vs/editor/common/model/tokensStore';
import { TextChange } from 'vs/editor/common/model/textChange';
/**
* Vertical Lane in the overview ruler of the editor.
......@@ -373,21 +374,13 @@ export interface IValidEditOperation {
*/
range: Range;
/**
* The text to replace with. This can be null to emulate a simple delete.
* The text to replace with. This can be empty to emulate a simple delete.
*/
text: string | null;
text: string;
/**
* This indicates that this operation has "insert" semantics.
* i.e. forceMoveMarkers = true => if `range` is collapsed, all markers at the position will be moved.
*/
forceMoveMarkers: boolean;
}
/**
* @internal
*/
export interface IValidEditOperations {
operations: IValidEditOperation[];
textChange: TextChange;
}
/**
......@@ -1106,7 +1099,12 @@ export interface ITextModel {
/**
* @internal
*/
_applyUndoRedoEdits(edits: IValidEditOperations[], eol: EndOfLineSequence, isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): IValidEditOperations[];
_applyUndo(changes: TextChange[], eol: EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void;
/**
* @internal
*/
_applyRedo(changes: TextChange[], eol: EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void;
/**
* Undo edit operations until the first previous stop point created by `pushStackElement`.
......@@ -1285,7 +1283,7 @@ export interface ITextBuffer {
getLineLastNonWhitespaceColumn(lineNumber: number): number;
setEOL(newEOL: '\r\n' | '\n'): void;
applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean): ApplyEditsResult;
applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean, computeUndoEdits: boolean): ApplyEditsResult;
findMatchesLineByLine(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[];
}
......
......@@ -6,11 +6,12 @@
import * as nls from 'vs/nls';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Selection } from 'vs/editor/common/core/selection';
import { EndOfLineSequence, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation, ITextModel, IValidEditOperations } from 'vs/editor/common/model';
import { EndOfLineSequence, ICursorStateComputer, IIdentifiedSingleEditOperation, 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 { URI } from 'vs/base/common/uri';
import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources';
import { TextChange, compressConsecutiveTextChanges } from 'vs/editor/common/model/textChange';
export class SingleModelEditStackElement implements IResourceUndoRedoElement {
......@@ -24,7 +25,7 @@ export class SingleModelEditStackElement implements IResourceUndoRedoElement {
private _afterVersionId: number;
private _afterEOL: EndOfLineSequence;
private _afterCursorState: Selection[] | null;
private _edits: IValidEditOperations[];
private _changes: TextChange[];
public get resource(): URI {
return this.model.uri;
......@@ -40,7 +41,7 @@ export class SingleModelEditStackElement implements IResourceUndoRedoElement {
this._afterVersionId = this._beforeVersionId;
this._afterEOL = this._beforeEOL;
this._afterCursorState = this._beforeCursorState;
this._edits = [];
this._changes = [];
}
public setModel(model: ITextModel): void {
......@@ -53,7 +54,7 @@ export class SingleModelEditStackElement implements IResourceUndoRedoElement {
public append(model: ITextModel, operations: IValidEditOperation[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void {
if (operations.length > 0) {
this._edits.push({ operations: operations });
this._changes = compressConsecutiveTextChanges(this._changes, operations.map(op => op.textChange));
}
this._afterEOL = afterEOL;
this._afterVersionId = afterVersionId;
......@@ -66,13 +67,11 @@ export class SingleModelEditStackElement implements IResourceUndoRedoElement {
public undo(): void {
this._isOpen = false;
this._edits.reverse();
this._edits = this.model._applyUndoRedoEdits(this._edits, this._beforeEOL, true, false, this._beforeVersionId, this._beforeCursorState);
this.model._applyUndo(this._changes, this._beforeEOL, this._beforeVersionId, this._beforeCursorState);
}
public redo(): void {
this._edits.reverse();
this._edits = this.model._applyUndoRedoEdits(this._edits, this._afterEOL, false, true, this._afterVersionId, this._afterCursorState);
this.model._applyRedo(this._changes, this._afterEOL, this._afterVersionId, this._afterCursorState);
}
}
......
......@@ -10,6 +10,7 @@ import { ApplyEditsResult, EndOfLinePreference, FindMatch, IInternalModelContent
import { PieceTreeBase, StringBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase';
import { SearchData } from 'vs/editor/common/model/textModelSearch';
import { countEOL, StringEOL } from 'vs/editor/common/model/tokensStore';
import { TextChange } from 'vs/editor/common/model/textChange';
export interface IValidatedEditOperation {
sortIndex: number;
......@@ -17,7 +18,7 @@ export interface IValidatedEditOperation {
range: Range;
rangeOffset: number;
rangeLength: number;
text: string | null;
text: string;
eolCount: number;
firstLineLength: number;
lastLineLength: number;
......@@ -205,7 +206,7 @@ export class PieceTreeTextBuffer implements ITextBuffer {
this._pieceTree.setEOL(newEOL);
}
public applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean): ApplyEditsResult {
public applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean, computeUndoEdits: boolean): ApplyEditsResult {
let mightContainRTL = this._mightContainRTL;
let mightContainNonBasicASCII = this._mightContainNonBasicASCII;
let canReduceOperations = true;
......@@ -225,7 +226,7 @@ export class PieceTreeTextBuffer implements ITextBuffer {
mightContainNonBasicASCII = !strings.isBasicASCII(op.text);
}
let validText: string | null = null;
let validText = '';
let eolCount = 0;
let firstLineLength = 0;
let lastLineLength = 0;
......@@ -260,33 +261,19 @@ export class PieceTreeTextBuffer implements ITextBuffer {
// Sort operations ascending
operations.sort(PieceTreeTextBuffer._sortOpsAscending);
let hasTouchingRanges = false;
for (let i = 0, count = operations.length - 1; i < count; i++) {
let rangeEnd = operations[i].range.getEndPosition();
let nextRangeStart = operations[i + 1].range.getStartPosition();
if (nextRangeStart.isBeforeOrEqual(rangeEnd)) {
if (nextRangeStart.isBefore(rangeEnd)) {
// overlapping ranges
throw new Error('Overlapping ranges are not allowed!');
}
hasTouchingRanges = true;
}
}
if (canReduceOperations) {
operations = this._reduceOperations(operations);
}
// Delta encode operations
let reverseRanges = PieceTreeTextBuffer._getInverseEditRanges(operations);
let reverseRanges = (computeUndoEdits || recordTrimAutoWhitespace ? PieceTreeTextBuffer._getInverseEditRanges(operations) : []);
let newTrimAutoWhitespaceCandidates: { lineNumber: number, oldContent: string }[] = [];
if (recordTrimAutoWhitespace) {
for (let i = 0; i < operations.length; i++) {
let op = operations[i];
let reverseRange = reverseRanges[i];
if (recordTrimAutoWhitespace && op.isAutoWhitespaceEdit && op.range.isEmpty()) {
if (op.isAutoWhitespaceEdit && op.range.isEmpty()) {
// Record already the future line numbers that might be auto whitespace removal candidates on next edit
for (let lineNumber = reverseRange.startLineNumber; lineNumber <= reverseRange.endLineNumber; lineNumber++) {
let currentLineContent = '';
......@@ -300,18 +287,39 @@ export class PieceTreeTextBuffer implements ITextBuffer {
}
}
}
}
let reverseOperations: IReverseSingleEditOperation[] = [];
if (computeUndoEdits) {
let hasTouchingRanges = false;
for (let i = 0, count = operations.length - 1; i < count; i++) {
let rangeEnd = operations[i].range.getEndPosition();
let nextRangeStart = operations[i + 1].range.getStartPosition();
if (nextRangeStart.isBeforeOrEqual(rangeEnd)) {
if (nextRangeStart.isBefore(rangeEnd)) {
// overlapping ranges
throw new Error('Overlapping ranges are not allowed!');
}
hasTouchingRanges = true;
}
}
let reverseRangeDeltaOffset = 0;
for (let i = 0; i < operations.length; i++) {
let op = operations[i];
let reverseRange = reverseRanges[i];
const op = operations[i];
const reverseRange = reverseRanges[i];
const bufferText = this.getValueInRange(op.range);
const reverseRangeOffset = op.rangeOffset + reverseRangeDeltaOffset;
reverseRangeDeltaOffset += (op.text.length - bufferText.length);
reverseOperations[i] = {
sortIndex: op.sortIndex,
identifier: op.identifier,
range: reverseRange,
text: this.getValueInRange(op.range),
forceMoveMarkers: op.forceMoveMarkers
text: bufferText,
textChange: new TextChange(op.rangeOffset, bufferText, reverseRangeOffset, op.text)
};
}
......@@ -319,6 +327,8 @@ export class PieceTreeTextBuffer implements ITextBuffer {
if (!hasTouchingRanges) {
reverseOperations.sort((a, b) => a.sortIndex - b.sortIndex);
}
}
this._mightContainRTL = mightContainRTL;
this._mightContainNonBasicASCII = mightContainNonBasicASCII;
......@@ -393,7 +403,7 @@ export class PieceTreeTextBuffer implements ITextBuffer {
result.push(this.getValueInRange(new Range(lastEndLineNumber, lastEndColumn, range.startLineNumber, range.startColumn)));
// (2) -- Push new text
if (operation.text) {
if (operation.text.length > 0) {
result.push(operation.text);
}
......@@ -433,17 +443,15 @@ export class PieceTreeTextBuffer implements ITextBuffer {
const endLineNumber = op.range.endLineNumber;
const endColumn = op.range.endColumn;
if (startLineNumber === endLineNumber && startColumn === endColumn && (!op.text || op.text.length === 0)) {
if (startLineNumber === endLineNumber && startColumn === endColumn && op.text.length === 0) {
// no-op
continue;
}
const text = op.text ? op.text : '';
if (text) {
if (op.text) {
// replacement
this._pieceTree.delete(op.rangeOffset, op.rangeLength);
this._pieceTree.insert(op.rangeOffset, text, true);
this._pieceTree.insert(op.rangeOffset, op.text, true);
} else {
// deletion
......@@ -454,7 +462,7 @@ export class PieceTreeTextBuffer implements ITextBuffer {
contentChanges.push({
range: contentChangeRange,
rangeLength: op.rangeLength,
text: text,
text: op.text,
rangeOffset: op.rangeOffset,
forceMoveMarkers: op.forceMoveMarkers
});
......@@ -503,7 +511,7 @@ export class PieceTreeTextBuffer implements ITextBuffer {
let resultRange: Range;
if (op.text && op.text.length > 0) {
if (op.text.length > 0) {
// the operation inserts something
const lineCount = op.eolCount + 1;
......
......@@ -4,35 +4,33 @@
*--------------------------------------------------------------------------------------------*/
export class TextChange {
public readonly oldPosition: number;
public readonly oldLength: number;
public readonly oldEnd: number;
public readonly oldText: string;
public readonly newPosition: number;
public readonly newLength: number;
public readonly newEnd: number;
public readonly newText: string;
constructor(
oldPosition: number,
oldText: string,
newPosition: number,
newText: string
) {
this.oldPosition = oldPosition;
this.oldLength = oldText.length;
this.oldEnd = this.oldPosition + this.oldLength;
this.oldText = oldText;
this.newPosition = newPosition;
this.newLength = newText.length;
this.newEnd = this.newPosition + this.newLength;
this.newText = newText;
public get oldLength(): number {
return this.oldText.length;
}
public get oldEnd(): number {
return this.oldPosition + this.oldText.length;
}
public get newLength(): number {
return this.newText.length;
}
public get newEnd(): number {
return this.newPosition + this.newText.length;
}
constructor(
public readonly oldPosition: number,
public readonly oldText: string,
public readonly newPosition: number,
public readonly newText: string
) { }
}
export function compressConsecutiveTextChanges(prevEdits: TextChange[] | null, currEdits: TextChange[]): TextChange[] {
if (prevEdits === null) {
if (prevEdits === null || prevEdits.length === 0) {
return currEdits;
}
const compressor = new TextChangeCompressor(prevEdits, currEdits);
......
......@@ -37,6 +37,7 @@ import { Color } from 'vs/base/common/color';
import { Constants } from 'vs/base/common/uint';
import { EditorTheme } from 'vs/editor/common/view/viewContext';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { TextChange } from 'vs/editor/common/model/textChange';
function createTextBufferBuilder() {
return new PieceTreeTextBufferBuilder();
......@@ -1283,19 +1284,39 @@ export class TextModel extends Disposable implements model.ITextModel {
return this._commandManager.pushEditOperation(beforeCursorState, editOperations, cursorStateComputer);
}
_applyUndoRedoEdits(edits: model.IValidEditOperations[], eol: model.EndOfLineSequence, isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): model.IValidEditOperations[] {
_applyUndo(changes: TextChange[], eol: model.EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void {
const edits = changes.map<model.IIdentifiedSingleEditOperation>((change) => {
const rangeStart = this.getPositionAt(change.newPosition);
const rangeEnd = this.getPositionAt(change.newEnd);
return {
range: new Range(rangeStart.lineNumber, rangeStart.column, rangeEnd.lineNumber, rangeEnd.column),
text: change.oldText
};
});
this._applyUndoRedoEdits(edits, eol, true, false, resultingAlternativeVersionId, resultingSelection);
}
_applyRedo(changes: TextChange[], eol: model.EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void {
const edits = changes.map<model.IIdentifiedSingleEditOperation>((change) => {
const rangeStart = this.getPositionAt(change.oldPosition);
const rangeEnd = this.getPositionAt(change.oldEnd);
return {
range: new Range(rangeStart.lineNumber, rangeStart.column, rangeEnd.lineNumber, rangeEnd.column),
text: change.newText
};
});
this._applyUndoRedoEdits(edits, eol, false, true, resultingAlternativeVersionId, resultingSelection);
}
private _applyUndoRedoEdits(edits: model.IIdentifiedSingleEditOperation[], eol: model.EndOfLineSequence, isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void {
try {
this._onDidChangeDecorations.beginDeferredEmit();
this._eventEmitter.beginDeferredEmit();
this._isUndoing = isUndoing;
this._isRedoing = isRedoing;
let reverseEdits: model.IValidEditOperations[] = [];
for (let i = 0, len = edits.length; i < len; i++) {
reverseEdits[i] = { operations: this.applyEdits(edits[i].operations) };
}
this.applyEdits(edits, false);
this.setEOL(eol);
this._overwriteAlternativeVersionId(resultingAlternativeVersionId);
return reverseEdits;
} finally {
this._isUndoing = false;
this._isRedoing = false;
......@@ -1304,21 +1325,21 @@ export class TextModel extends Disposable implements model.ITextModel {
}
}
public applyEdits(rawOperations: model.IIdentifiedSingleEditOperation[]): model.IValidEditOperation[] {
public applyEdits(rawOperations: model.IIdentifiedSingleEditOperation[], computeUndoEdits: boolean = true): model.IValidEditOperation[] {
try {
this._onDidChangeDecorations.beginDeferredEmit();
this._eventEmitter.beginDeferredEmit();
return this._doApplyEdits(this._validateEditOperations(rawOperations));
return this._doApplyEdits(this._validateEditOperations(rawOperations), computeUndoEdits);
} finally {
this._eventEmitter.endDeferredEmit();
this._onDidChangeDecorations.endDeferredEmit();
}
}
private _doApplyEdits(rawOperations: model.ValidAnnotatedEditOperation[]): model.IValidEditOperation[] {
private _doApplyEdits(rawOperations: model.ValidAnnotatedEditOperation[], computeUndoEdits: boolean): model.IValidEditOperation[] {
const oldLineCount = this._buffer.getLineCount();
const result = this._buffer.applyEdits(rawOperations, this._options.trimAutoWhitespace);
const result = this._buffer.applyEdits(rawOperations, this._options.trimAutoWhitespace, computeUndoEdits);
const newLineCount = this._buffer.getLineCount();
const contentChanges = result.changes;
......
......@@ -1348,7 +1348,7 @@ suite('Editor Controller - Regression tests', () => {
CoreEditingCommands.Undo.runEditorCommand(null, editor, null);
assert.equal(model.getLineContent(1), 'Hello world ');
assertCursor(cursor, new Position(1, 13));
assertCursor(cursor, new Selection(1, 12, 1, 13));
CoreEditingCommands.Undo.runEditorCommand(null, editor, null);
assert.equal(model.getLineContent(1), 'Hello world');
......
......@@ -54,7 +54,7 @@ for (let fileSize of fileSizes) {
fn: (textBuffer) => {
// for line model, this loop doesn't reflect the real situation.
for (const edit of edits) {
textBuffer.applyEdits([edit], false);
textBuffer.applyEdits([edit], false, false);
}
}
});
......@@ -67,7 +67,7 @@ for (let fileSize of fileSizes) {
},
preCycle: (textBuffer) => {
for (const edit of edits) {
textBuffer.applyEdits([edit], false);
textBuffer.applyEdits([edit], false, false);
}
return textBuffer;
},
......@@ -91,7 +91,7 @@ for (let fileSize of fileSizes) {
},
preCycle: (textBuffer) => {
for (const edit of edits) {
textBuffer.applyEdits([edit], false);
textBuffer.applyEdits([edit], false, false);
}
return textBuffer;
},
......@@ -121,7 +121,7 @@ for (let fileSize of fileSizes) {
},
preCycle: (textBuffer) => {
for (const edit of edits) {
textBuffer.applyEdits([edit], false);
textBuffer.applyEdits([edit], false, false);
}
return textBuffer;
},
......
......@@ -41,7 +41,7 @@ for (let fileSize of fileSizes) {
return textBuffer;
},
fn: (textBuffer) => {
textBuffer.applyEdits(edits.slice(0, i), false);
textBuffer.applyEdits(edits.slice(0, i), false, false);
}
});
}
......
......@@ -36,8 +36,8 @@ export function testApplyEditsWithSyncedModels(original: string[], edits: IIdent
identifier: edit.identifier,
range: edit.range,
text: edit.text,
forceMoveMarkers: edit.forceMoveMarkers,
isAutoWhitespaceEdit: edit.isAutoWhitespaceEdit
forceMoveMarkers: edit.forceMoveMarkers || false,
isAutoWhitespaceEdit: edit.isAutoWhitespaceEdit || false
};
};
// Assert the inverse of the inverse edits are the original edits
......
......@@ -18,7 +18,7 @@ suite('PieceTreeTextBuffer._getInverseEdits', () => {
range: new Range(startLineNumber, startColumn, endLineNumber, endColumn),
rangeOffset: 0,
rangeLength: 0,
text: text ? text.join('\n') : null,
text: text ? text.join('\n') : '',
eolCount: text ? text.length - 1 : 0,
firstLineLength: text ? text[0].length : 0,
lastLineLength: text ? text[text.length - 1].length : 0,
......@@ -272,7 +272,7 @@ suite('PieceTreeTextBuffer._toSingleEditOperation', () => {
range: new Range(startLineNumber, startColumn, endLineNumber, endColumn),
rangeOffset: rangeOffset,
rangeLength: rangeLength,
text: text ? text.join('\n') : null,
text: text ? text.join('\n') : '',
eolCount: text ? text.length - 1 : 0,
firstLineLength: text ? text[0].length : 0,
lastLineLength: text ? text[text.length - 1].length : 0,
......
......@@ -71,8 +71,8 @@ suite('Editor Model - Model Edit Operation', () => {
identifier: edit.identifier,
range: edit.range,
text: edit.text,
forceMoveMarkers: edit.forceMoveMarkers,
isAutoWhitespaceEdit: edit.isAutoWhitespaceEdit
forceMoveMarkers: edit.forceMoveMarkers || false,
isAutoWhitespaceEdit: edit.isAutoWhitespaceEdit || false
};
};
assert.deepEqual(originalOp.map(simplifyEdit), editOp.map(simplifyEdit));
......
......@@ -1552,14 +1552,9 @@ declare namespace monaco.editor {
*/
range: Range;
/**
* The text to replace with. This can be null to emulate a simple delete.
* The text to replace with. This can be empty to emulate a simple delete.
*/
text: string | null;
/**
* This indicates that this operation has "insert" semantics.
* i.e. forceMoveMarkers = true => if `range` is collapsed, all markers at the position will be moved.
*/
forceMoveMarkers: boolean;
text: string;
}
/**
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册