Keep view model in sync with model (#44805)

This commit is contained in:
Alex Dima 2018-03-01 16:02:32 +01:00
parent dda0911086
commit 47497b7c5c
4 changed files with 85 additions and 14 deletions

View file

@ -1018,12 +1018,12 @@ export class TextModel extends Disposable implements model.ITextModel {
public pushEditOperations(beforeCursorState: Selection[], editOperations: model.IIdentifiedSingleEditOperation[], cursorStateComputer: model.ICursorStateComputer): Selection[] {
try {
this._eventEmitter.beginDeferredEmit();
this._onDidChangeDecorations.beginDeferredEmit();
this._eventEmitter.beginDeferredEmit();
return this._pushEditOperations(beforeCursorState, editOperations, cursorStateComputer);
} finally {
this._onDidChangeDecorations.endDeferredEmit();
this._eventEmitter.endDeferredEmit();
this._onDidChangeDecorations.endDeferredEmit();
}
}
@ -1108,12 +1108,12 @@ export class TextModel extends Disposable implements model.ITextModel {
public applyEdits(rawOperations: model.IIdentifiedSingleEditOperation[]): model.IIdentifiedSingleEditOperation[] {
try {
this._eventEmitter.beginDeferredEmit();
this._onDidChangeDecorations.beginDeferredEmit();
this._eventEmitter.beginDeferredEmit();
return this._applyEdits(rawOperations);
} finally {
this._onDidChangeDecorations.endDeferredEmit();
this._eventEmitter.endDeferredEmit();
this._onDidChangeDecorations.endDeferredEmit();
}
}
@ -1250,12 +1250,12 @@ export class TextModel extends Disposable implements model.ITextModel {
public undo(): Selection[] {
try {
this._eventEmitter.beginDeferredEmit();
this._onDidChangeDecorations.beginDeferredEmit();
this._eventEmitter.beginDeferredEmit();
return this._undo();
} finally {
this._onDidChangeDecorations.endDeferredEmit();
this._eventEmitter.endDeferredEmit();
this._onDidChangeDecorations.endDeferredEmit();
}
}
@ -1275,12 +1275,12 @@ export class TextModel extends Disposable implements model.ITextModel {
public redo(): Selection[] {
try {
this._eventEmitter.beginDeferredEmit();
this._onDidChangeDecorations.beginDeferredEmit();
this._eventEmitter.beginDeferredEmit();
return this._redo();
} finally {
this._onDidChangeDecorations.endDeferredEmit();
this._eventEmitter.endDeferredEmit();
this._onDidChangeDecorations.endDeferredEmit();
}
}
@ -2469,12 +2469,12 @@ export class DidChangeContentEmitter extends Disposable {
public readonly event: Event<InternalModelContentChangeEvent> = this._actual.event;
private _deferredCnt: number;
private _deferredEvents: InternalModelContentChangeEvent[];
private _deferredEvent: InternalModelContentChangeEvent;
constructor() {
super();
this._deferredCnt = 0;
this._deferredEvents = [];
this._deferredEvent = null;
}
public beginDeferredEmit(): void {
@ -2484,15 +2484,21 @@ export class DidChangeContentEmitter extends Disposable {
public endDeferredEmit(): void {
this._deferredCnt--;
if (this._deferredCnt === 0) {
while (this._deferredEvents.length > 0) {
this._actual.fire(this._deferredEvents.shift());
if (this._deferredEvent !== null) {
const e = this._deferredEvent;
this._deferredEvent = null;
this._actual.fire(e);
}
}
}
public fire(e: InternalModelContentChangeEvent): void {
if (this._deferredCnt > 0) {
this._deferredEvents.push(e);
if (this._deferredEvent) {
this._deferredEvent = this._deferredEvent.merge(e);
} else {
this._deferredEvent = e;
}
return;
}
this._actual.fire(e);

View file

@ -234,6 +234,14 @@ export class ModelRawContentChangedEvent {
}
return false;
}
public static merge(a: ModelRawContentChangedEvent, b: ModelRawContentChangedEvent): ModelRawContentChangedEvent {
const changes = [].concat(a.changes).concat(b.changes);
const versionId = b.versionId;
const isUndoing = (a.isUndoing || b.isUndoing);
const isRedoing = (a.isRedoing || b.isRedoing);
return new ModelRawContentChangedEvent(changes, versionId, isUndoing, isRedoing);
}
}
/**
@ -244,4 +252,27 @@ export class InternalModelContentChangeEvent {
public readonly rawContentChangedEvent: ModelRawContentChangedEvent,
public readonly contentChangedEvent: IModelContentChangedEvent,
) { }
public merge(other: InternalModelContentChangeEvent): InternalModelContentChangeEvent {
const rawContentChangedEvent = ModelRawContentChangedEvent.merge(this.rawContentChangedEvent, other.rawContentChangedEvent);
const contentChangedEvent = InternalModelContentChangeEvent._mergeChangeEvents(this.contentChangedEvent, other.contentChangedEvent);
return new InternalModelContentChangeEvent(rawContentChangedEvent, contentChangedEvent);
}
private static _mergeChangeEvents(a: IModelContentChangedEvent, b: IModelContentChangedEvent): IModelContentChangedEvent {
const changes = [].concat(a.changes).concat(b.changes);
const eol = b.eol;
const versionId = b.versionId;
const isUndoing = (a.isUndoing || b.isUndoing);
const isRedoing = (a.isRedoing || b.isRedoing);
const isFlush = (a.isFlush || b.isFlush);
return {
changes: changes,
eol: eol,
versionId: versionId,
isUndoing: isUndoing,
isRedoing: isRedoing,
isFlush: isFlush
};
}
}

View file

@ -180,7 +180,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
let modelVersion = this.model.getVersionId();
if (modelVersion !== this._validModelVersionId) {
// This is pretty bad, it means we lost track of the model...
this._constructLines(false);
throw new Error(`ViewModel is out of sync with Model!`);
}
}

View file

@ -40,6 +40,40 @@ suite('ViewModel', () => {
});
});
test('issue #44805: SplitLinesCollection: attempt to access a \'newer\' model', () => {
const text = [''];
testViewModel(text, {}, (viewModel, model) => {
assert.equal(viewModel.getLineCount(), 1);
model.pushEditOperations([], [{
range: new Range(1, 1, 1, 1),
text: '\ninsert1'
}], () => ([]));
model.pushEditOperations([], [{
range: new Range(1, 1, 1, 1),
text: '\ninsert2'
}], () => ([]));
model.pushEditOperations([], [{
range: new Range(1, 1, 1, 1),
text: '\ninsert3'
}], () => ([]));
let viewLineCount: number[] = [];
viewLineCount.push(viewModel.getLineCount());
viewModel.addEventListener((events) => {
// Access the view model
viewLineCount.push(viewModel.getLineCount());
});
model.undo();
viewLineCount.push(viewModel.getLineCount());
assert.deepEqual(viewLineCount, [4, 1, 1, 1]);
});
});
function assertGetPlainTextToCopy(text: string[], ranges: Range[], emptySelectionClipboard: boolean, expected: string | string[]): void {
testViewModel(text, {}, (viewModel, model) => {
let actual = viewModel.getPlainTextToCopy(ranges, emptySelectionClipboard);