Skip to content

Commit e72d8d1

Browse files
committed
Fixes microsoft#46314: Make sure model change events reach the view models first
1 parent 4e7a681 commit e72d8d1

4 files changed

Lines changed: 57 additions & 8 deletions

File tree

src/vs/editor/common/model.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,13 @@ export interface ITextModel {
10021002
*/
10031003
redo(): Selection[];
10041004

1005+
/**
1006+
* @deprecated Please use `onDidChangeContent` instead.
1007+
* An event emitted when the contents of the model have changed.
1008+
* @internal
1009+
* @event
1010+
*/
1011+
onDidChangeRawContentFast(listener: (e: ModelRawContentChangedEvent) => void): IDisposable;
10051012
/**
10061013
* @deprecated Please use `onDidChangeContent` instead.
10071014
* An event emitted when the contents of the model have changed.

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -211,11 +211,14 @@ export class TextModel extends Disposable implements model.ITextModel {
211211
public readonly onDidChangeOptions: Event<IModelOptionsChangedEvent> = this._onDidChangeOptions.event;
212212

213213
private readonly _eventEmitter: DidChangeContentEmitter = this._register(new DidChangeContentEmitter());
214+
public onDidChangeRawContentFast(listener: (e: ModelRawContentChangedEvent) => void): IDisposable {
215+
return this._eventEmitter.fastEvent((e: InternalModelContentChangeEvent) => listener(e.rawContentChangedEvent));
216+
}
214217
public onDidChangeRawContent(listener: (e: ModelRawContentChangedEvent) => void): IDisposable {
215-
return this._eventEmitter.event((e: InternalModelContentChangeEvent) => listener(e.rawContentChangedEvent));
218+
return this._eventEmitter.slowEvent((e: InternalModelContentChangeEvent) => listener(e.rawContentChangedEvent));
216219
}
217220
public onDidChangeContent(listener: (e: IModelContentChangedEvent) => void): IDisposable {
218-
return this._eventEmitter.event((e: InternalModelContentChangeEvent) => listener(e.contentChangedEvent));
221+
return this._eventEmitter.slowEvent((e: InternalModelContentChangeEvent) => listener(e.contentChangedEvent));
219222
}
220223
//#endregion
221224

@@ -2600,8 +2603,13 @@ export class DidChangeDecorationsEmitter extends Disposable {
26002603

26012604
export class DidChangeContentEmitter extends Disposable {
26022605

2603-
private readonly _actual: Emitter<InternalModelContentChangeEvent> = this._register(new Emitter<InternalModelContentChangeEvent>());
2604-
public readonly event: Event<InternalModelContentChangeEvent> = this._actual.event;
2606+
/**
2607+
* Both `fastEvent` and `slowEvent` work the same way and contain the same events, but first we invoke `fastEvent` and then `slowEvent`.
2608+
*/
2609+
private readonly _fastEmitter: Emitter<InternalModelContentChangeEvent> = this._register(new Emitter<InternalModelContentChangeEvent>());
2610+
public readonly fastEvent: Event<InternalModelContentChangeEvent> = this._fastEmitter.event;
2611+
private readonly _slowEmitter: Emitter<InternalModelContentChangeEvent> = this._register(new Emitter<InternalModelContentChangeEvent>());
2612+
public readonly slowEvent: Event<InternalModelContentChangeEvent> = this._slowEmitter.event;
26052613

26062614
private _deferredCnt: number;
26072615
private _deferredEvent: InternalModelContentChangeEvent;
@@ -2622,7 +2630,8 @@ export class DidChangeContentEmitter extends Disposable {
26222630
if (this._deferredEvent !== null) {
26232631
const e = this._deferredEvent;
26242632
this._deferredEvent = null;
2625-
this._actual.fire(e);
2633+
this._fastEmitter.fire(e);
2634+
this._slowEmitter.fire(e);
26262635
}
26272636
}
26282637
}
@@ -2636,6 +2645,7 @@ export class DidChangeContentEmitter extends Disposable {
26362645
}
26372646
return;
26382647
}
2639-
this._actual.fire(e);
2648+
this._fastEmitter.fire(e);
2649+
this._slowEmitter.fire(e);
26402650
}
26412651
}

src/vs/editor/common/viewModel/viewModelImpl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel
173173

174174
private _registerModelEvents(): void {
175175

176-
this._register(this.model.onDidChangeRawContent((e) => {
176+
this._register(this.model.onDidChangeRawContentFast((e) => {
177177
try {
178178
const eventsCollector = this._beginEmit();
179179

src/vs/editor/test/browser/controller/cursor.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@ import { IndentAction, IndentationRule } from 'vs/editor/common/modes/languageCo
1717
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
1818
import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration';
1919
import { MockMode } from 'vs/editor/test/common/mocks/mockMode';
20-
import { LanguageIdentifier } from 'vs/editor/common/modes';
20+
import { LanguageIdentifier, ITokenizationSupport, IState, TokenizationRegistry } from 'vs/editor/common/modes';
2121
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
2222
import { CoreNavigationCommands, CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands';
2323
import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
2424
import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl';
25+
import { NULL_STATE } from 'vs/editor/common/modes/nullMode';
26+
import { TokenizationResult2 } from 'vs/editor/common/core/token';
27+
2528
let H = Handler;
2629

2730
// --------- utils
@@ -2048,6 +2051,35 @@ suite('Editor Controller - Regression tests', () => {
20482051

20492052
model.dispose();
20502053
});
2054+
2055+
test('issue #46314: ViewModel is out of sync with Model!', () => {
2056+
2057+
const tokenizationSupport: ITokenizationSupport = {
2058+
getInitialState: () => NULL_STATE,
2059+
tokenize: undefined,
2060+
tokenize2: (line: string, state: IState): TokenizationResult2 => {
2061+
return new TokenizationResult2(null, state);
2062+
}
2063+
};
2064+
2065+
const LANGUAGE_ID = 'modelModeTest1';
2066+
const languageRegistration = TokenizationRegistry.register(LANGUAGE_ID, tokenizationSupport);
2067+
let model = TextModel.createFromString('Just text', undefined, new LanguageIdentifier(LANGUAGE_ID, 0));
2068+
2069+
withTestCodeEditor(null, { model: model }, (editor1, cursor1) => {
2070+
withTestCodeEditor(null, { model: model }, (editor2, cursor2) => {
2071+
2072+
editor1.onDidChangeCursorPosition(() => {
2073+
model.tokenizeIfCheap(1);
2074+
});
2075+
2076+
model.applyEdits([{ range: new Range(1, 1, 1, 1), text: '-' }]);
2077+
});
2078+
});
2079+
2080+
languageRegistration.dispose();
2081+
model.dispose();
2082+
});
20512083
});
20522084

20532085
suite('Editor Controller - Cursor Configuration', () => {

0 commit comments

Comments
 (0)