diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 03bbb10f077b0..497ab6d2e3801 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -14,7 +14,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { IIdentifiedSingleEditOperation, IModelDecoration, IModelDeltaDecoration, ITextModel, ICursorStateComputer, PositionAffinity } from 'vs/editor/common/model'; import { IWordAtPosition } from 'vs/editor/common/core/wordHelper'; -import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent } from 'vs/editor/common/textModelEvents'; +import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents'; import { OverviewRulerZone } from 'vs/editor/common/viewModel/overviewZoneManager'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IEditorWhitespace, IViewModel } from 'vs/editor/common/viewModel'; @@ -531,6 +531,11 @@ export interface ICodeEditor extends editorCommon.IEditor { * @event */ readonly onDidChangeModelDecorations: Event; + /** + * An event emitted when the tokens of the current model have changed. + * @internal + */ + readonly onDidChangeModelTokens: Event; /** * An event emitted when the text inside this editor gained focus (i.e. cursor starts blinking). * @event diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 13f69b7d04346..e6f64a9ed2aaf 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -37,7 +37,7 @@ import { EndOfLinePreference, IIdentifiedSingleEditOperation, IModelDecoration, import { IWordAtPosition } from 'vs/editor/common/core/wordHelper'; import { ClassName } from 'vs/editor/common/model/intervalTree'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent } from 'vs/editor/common/textModelEvents'; +import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents'; import { editorUnnecessaryCodeBorder, editorUnnecessaryCodeOpacity } from 'vs/editor/common/core/editorColorRegistry'; import { editorErrorBorder, editorErrorForeground, editorHintBorder, editorHintForeground, editorInfoBorder, editorInfoForeground, editorWarningBorder, editorWarningForeground, editorForeground, editorErrorBackground, editorInfoBackground, editorWarningBackground } from 'vs/platform/theme/common/colorRegistry'; import { VerticalRevealType } from 'vs/editor/common/viewEvents'; @@ -134,6 +134,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE private readonly _onDidChangeModelDecorations: Emitter = this._register(new Emitter()); public readonly onDidChangeModelDecorations: Event = this._onDidChangeModelDecorations.event; + private readonly _onDidChangeModelTokens: Emitter = this._register(new Emitter()); + public readonly onDidChangeModelTokens: Event = this._onDidChangeModelTokens.event; + private readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); public readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; @@ -1587,14 +1590,6 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._themeService ); - listenersToRemove.push(model.onDidChangeDecorations((e) => this._onDidChangeModelDecorations.fire(e))); - listenersToRemove.push(model.onDidChangeLanguage((e) => { - this._domElement.setAttribute('data-mode-id', model.getLanguageId()); - this._onDidChangeModelLanguage.fire(e); - })); - listenersToRemove.push(model.onDidChangeLanguageConfiguration((e) => this._onDidChangeModelLanguageConfiguration.fire(e))); - listenersToRemove.push(model.onDidChangeContent((e) => this._onDidChangeModelContent.fire(e))); - listenersToRemove.push(model.onDidChangeOptions((e) => this._onDidChangeModelOptions.fire(e))); // Someone might destroy the model from under the editor, so prevent any exceptions by setting a null model listenersToRemove.push(model.onWillDispose(() => this.setModel(null))); @@ -1649,6 +1644,25 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE break; } + case OutgoingViewModelEventKind.ModelDecorationsChanged: + this._onDidChangeModelDecorations.fire(e.event); + break; + case OutgoingViewModelEventKind.ModelLanguageChanged: + this._domElement.setAttribute('data-mode-id', model.getLanguageId()); + this._onDidChangeModelLanguage.fire(e.event); + break; + case OutgoingViewModelEventKind.ModelLanguageConfigurationChanged: + this._onDidChangeModelLanguageConfiguration.fire(e.event); + break; + case OutgoingViewModelEventKind.ModelContentChanged: + this._onDidChangeModelContent.fire(e.event); + break; + case OutgoingViewModelEventKind.ModelOptionsChanged: + this._onDidChangeModelOptions.fire(e.event); + break; + case OutgoingViewModelEventKind.ModelTokensChanged: + this._onDidChangeModelTokens.fire(e.event); + break; } })); diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index f11cb2a3242ec..c36e1d11e8130 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -35,7 +35,7 @@ import { ILineBreaksComputer, ILineBreaksComputerFactory, InjectedText } from 'v import { ViewEventHandler } from 'vs/editor/common/viewEventHandler'; import { ICoordinatesConverter, IViewModel, IWhitespaceChangeAccessor, MinimapLinesRenderingData, OverviewRulerDecorationsGroup, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from 'vs/editor/common/viewModel'; import { ViewModelDecorations } from 'vs/editor/common/viewModel/viewModelDecorations'; -import { FocusChangedEvent, OutgoingViewModelEvent, ReadOnlyEditAttemptEvent, ScrollChangedEvent, ViewModelEventDispatcher, ViewModelEventsCollector, ViewZonesChangedEvent } from 'vs/editor/common/viewModelEventDispatcher'; +import { FocusChangedEvent, ModelContentChangedEvent, ModelDecorationsChangedEvent, ModelLanguageChangedEvent, ModelLanguageConfigurationChangedEvent, ModelOptionsChangedEvent, ModelTokensChangedEvent, OutgoingViewModelEvent, ReadOnlyEditAttemptEvent, ScrollChangedEvent, ViewModelEventDispatcher, ViewModelEventsCollector, ViewZonesChangedEvent } from 'vs/editor/common/viewModelEventDispatcher'; import { IViewModelLines, ViewModelLinesFromModelAsIs, ViewModelLinesFromProjectedModel } from 'vs/editor/common/viewModel/viewModelLines'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -399,6 +399,10 @@ export class ViewModel extends Disposable implements IViewModel { this._tokenizeViewportSoon.schedule(); })); + this._register(this.model.onDidChangeContent((e) => { + this._eventDispatcher.emitOutgoingEvent(new ModelContentChangedEvent(e)); + })); + this._register(this.model.onDidChangeTokens((e) => { const viewRanges: { fromLineNumber: number; toLineNumber: number }[] = []; for (let j = 0, lenJ = e.ranges.length; j < lenJ; j++) { @@ -415,17 +419,20 @@ export class ViewModel extends Disposable implements IViewModel { if (e.tokenizationSupportChanged) { this._tokenizeViewportSoon.schedule(); } + this._eventDispatcher.emitOutgoingEvent(new ModelTokensChangedEvent(e)); })); this._register(this.model.onDidChangeLanguageConfiguration((e) => { this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewLanguageConfigurationEvent()); this.cursorConfig = new CursorConfiguration(this.model.getLanguageId(), this.model.getOptions(), this._configuration, this.languageConfigurationService); this._cursor.updateConfiguration(this.cursorConfig); + this._eventDispatcher.emitOutgoingEvent(new ModelLanguageConfigurationChangedEvent(e)); })); this._register(this.model.onDidChangeLanguage((e) => { this.cursorConfig = new CursorConfiguration(this.model.getLanguageId(), this.model.getOptions(), this._configuration, this.languageConfigurationService); this._cursor.updateConfiguration(this.cursorConfig); + this._eventDispatcher.emitOutgoingEvent(new ModelLanguageChangedEvent(e)); })); this._register(this.model.onDidChangeOptions((e) => { @@ -447,11 +454,14 @@ export class ViewModel extends Disposable implements IViewModel { this.cursorConfig = new CursorConfiguration(this.model.getLanguageId(), this.model.getOptions(), this._configuration, this.languageConfigurationService); this._cursor.updateConfiguration(this.cursorConfig); + + this._eventDispatcher.emitOutgoingEvent(new ModelOptionsChangedEvent(e)); })); this._register(this.model.onDidChangeDecorations((e) => { this._decorations.onModelDecorationsChanged(); this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewDecorationsChangedEvent(e)); + this._eventDispatcher.emitOutgoingEvent(new ModelDecorationsChangedEvent(e)); })); } diff --git a/src/vs/editor/common/viewModelEventDispatcher.ts b/src/vs/editor/common/viewModelEventDispatcher.ts index cbdc94107764d..49d467a7afe38 100644 --- a/src/vs/editor/common/viewModelEventDispatcher.ts +++ b/src/vs/editor/common/viewModelEventDispatcher.ts @@ -10,6 +10,7 @@ import { Emitter } from 'vs/base/common/event'; import { Selection } from 'vs/editor/common/core/selection'; import { Disposable } from 'vs/base/common/lifecycle'; import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; +import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents'; export class ViewModelEventDispatcher extends Disposable { @@ -40,8 +41,9 @@ export class ViewModelEventDispatcher extends Disposable { private _addOutgoingEvent(e: OutgoingViewModelEvent): void { for (let i = 0, len = this._outgoingEvents.length; i < len; i++) { - if (this._outgoingEvents[i].kind === e.kind) { - this._outgoingEvents[i] = this._outgoingEvents[i].merge(e); + const mergeResult = (this._outgoingEvents[i].kind === e.kind ? this._outgoingEvents[i].attemptToMerge(e) : null); + if (mergeResult) { + this._outgoingEvents[i] = mergeResult; return; } } @@ -179,6 +181,12 @@ export type OutgoingViewModelEvent = ( | HiddenAreasChangedEvent | ReadOnlyEditAttemptEvent | CursorStateChangedEvent + | ModelDecorationsChangedEvent + | ModelLanguageChangedEvent + | ModelLanguageConfigurationChangedEvent + | ModelContentChangedEvent + | ModelOptionsChangedEvent + | ModelTokensChangedEvent ); export const enum OutgoingViewModelEventKind { @@ -189,6 +197,12 @@ export const enum OutgoingViewModelEventKind { HiddenAreasChanged, ReadOnlyEditAttempt, CursorStateChanged, + ModelDecorationsChanged, + ModelLanguageChanged, + ModelLanguageConfigurationChanged, + ModelContentChanged, + ModelOptionsChanged, + ModelTokensChanged, } export class ContentSizeChangedEvent implements IContentSizeChangedEvent { @@ -216,10 +230,9 @@ export class ContentSizeChangedEvent implements IContentSizeChangedEvent { return (!this.contentWidthChanged && !this.contentHeightChanged); } - - public merge(other: OutgoingViewModelEvent): ContentSizeChangedEvent { - if (other.kind !== OutgoingViewModelEventKind.ContentSizeChanged) { - return this; + public attemptToMerge(other: OutgoingViewModelEvent): OutgoingViewModelEvent | null { + if (other.kind !== this.kind) { + return null; } return new ContentSizeChangedEvent(this._oldContentWidth, this._oldContentHeight, other.contentWidth, other.contentHeight); } @@ -241,9 +254,9 @@ export class FocusChangedEvent { return (this.oldHasFocus === this.hasFocus); } - public merge(other: OutgoingViewModelEvent): FocusChangedEvent { - if (other.kind !== OutgoingViewModelEventKind.FocusChanged) { - return this; + public attemptToMerge(other: OutgoingViewModelEvent): OutgoingViewModelEvent | null { + if (other.kind !== this.kind) { + return null; } return new FocusChangedEvent(this.oldHasFocus, other.hasFocus); } @@ -292,9 +305,9 @@ export class ScrollChangedEvent { return (!this.scrollWidthChanged && !this.scrollLeftChanged && !this.scrollHeightChanged && !this.scrollTopChanged); } - public merge(other: OutgoingViewModelEvent): ScrollChangedEvent { - if (other.kind !== OutgoingViewModelEventKind.ScrollChanged) { - return this; + public attemptToMerge(other: OutgoingViewModelEvent): OutgoingViewModelEvent | null { + if (other.kind !== this.kind) { + return null; } return new ScrollChangedEvent( this._oldScrollWidth, this._oldScrollLeft, this._oldScrollHeight, this._oldScrollTop, @@ -314,7 +327,10 @@ export class ViewZonesChangedEvent { return false; } - public merge(other: OutgoingViewModelEvent): ViewZonesChangedEvent { + public attemptToMerge(other: OutgoingViewModelEvent): OutgoingViewModelEvent | null { + if (other.kind !== this.kind) { + return null; + } return this; } } @@ -330,7 +346,10 @@ export class HiddenAreasChangedEvent { return false; } - public merge(other: OutgoingViewModelEvent): HiddenAreasChangedEvent { + public attemptToMerge(other: OutgoingViewModelEvent): OutgoingViewModelEvent | null { + if (other.kind !== this.kind) { + return null; + } return this; } } @@ -384,9 +403,9 @@ export class CursorStateChangedEvent { ); } - public merge(other: OutgoingViewModelEvent): CursorStateChangedEvent { - if (other.kind !== OutgoingViewModelEventKind.CursorStateChanged) { - return this; + public attemptToMerge(other: OutgoingViewModelEvent): OutgoingViewModelEvent | null { + if (other.kind !== this.kind) { + return null; } return new CursorStateChangedEvent( this.oldSelections, other.selections, this.oldModelVersionId, other.modelVersionId, other.source, other.reason, this.reachedMaxCursorCount || other.reachedMaxCursorCount @@ -405,7 +424,106 @@ export class ReadOnlyEditAttemptEvent { return false; } - public merge(other: OutgoingViewModelEvent): ReadOnlyEditAttemptEvent { + public attemptToMerge(other: OutgoingViewModelEvent): OutgoingViewModelEvent | null { + if (other.kind !== this.kind) { + return null; + } return this; } } + +export class ModelDecorationsChangedEvent { + public readonly kind = OutgoingViewModelEventKind.ModelDecorationsChanged; + + constructor( + public readonly event: IModelDecorationsChangedEvent + ) { } + + public isNoOp(): boolean { + return false; + } + + public attemptToMerge(other: OutgoingViewModelEvent): OutgoingViewModelEvent | null { + return null; + } +} + +export class ModelLanguageChangedEvent { + public readonly kind = OutgoingViewModelEventKind.ModelLanguageChanged; + + constructor( + public readonly event: IModelLanguageChangedEvent + ) { } + + public isNoOp(): boolean { + return false; + } + + public attemptToMerge(other: OutgoingViewModelEvent): OutgoingViewModelEvent | null { + return null; + } +} + +export class ModelLanguageConfigurationChangedEvent { + public readonly kind = OutgoingViewModelEventKind.ModelLanguageConfigurationChanged; + + constructor( + public readonly event: IModelLanguageConfigurationChangedEvent + ) { } + + public isNoOp(): boolean { + return false; + } + + public attemptToMerge(other: OutgoingViewModelEvent): OutgoingViewModelEvent | null { + return null; + } +} + +export class ModelContentChangedEvent { + public readonly kind = OutgoingViewModelEventKind.ModelContentChanged; + + constructor( + public readonly event: IModelContentChangedEvent + ) { } + + public isNoOp(): boolean { + return false; + } + + public attemptToMerge(other: OutgoingViewModelEvent): OutgoingViewModelEvent | null { + return null; + } +} + +export class ModelOptionsChangedEvent { + public readonly kind = OutgoingViewModelEventKind.ModelOptionsChanged; + + constructor( + public readonly event: IModelOptionsChangedEvent + ) { } + + public isNoOp(): boolean { + return false; + } + + public attemptToMerge(other: OutgoingViewModelEvent): OutgoingViewModelEvent | null { + return null; + } +} + +export class ModelTokensChangedEvent { + public readonly kind = OutgoingViewModelEventKind.ModelTokensChanged; + + constructor( + public readonly event: IModelTokensChangedEvent + ) { } + + public isNoOp(): boolean { + return false; + } + + public attemptToMerge(other: OutgoingViewModelEvent): OutgoingViewModelEvent | null { + return null; + } +} diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index 6bbab59a76494..4757ff43be02d 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -119,11 +119,11 @@ export interface TestCodeEditorInstantiationOptions extends TestCodeEditorCreati serviceCollection?: ServiceCollection; } -export function withTestCodeEditor(text: ITextModel | string | string[] | ITextBufferFactory, options: TestCodeEditorInstantiationOptions, callback: (editor: ITestCodeEditor, viewModel: ViewModel, instantiationService: IInstantiationService) => void): void { +export function withTestCodeEditor(text: ITextModel | string | string[] | ITextBufferFactory, options: TestCodeEditorInstantiationOptions, callback: (editor: ITestCodeEditor, viewModel: ViewModel, instantiationService: TestInstantiationService) => void): void { return _withTestCodeEditor(text, options, callback); } -export async function withAsyncTestCodeEditor(text: ITextModel | string | string[] | ITextBufferFactory, options: TestCodeEditorInstantiationOptions, callback: (editor: ITestCodeEditor, viewModel: ViewModel, instantiationService: IInstantiationService) => Promise): Promise { +export async function withAsyncTestCodeEditor(text: ITextModel | string | string[] | ITextBufferFactory, options: TestCodeEditorInstantiationOptions, callback: (editor: ITestCodeEditor, viewModel: ViewModel, instantiationService: TestInstantiationService) => Promise): Promise { return _withTestCodeEditor(text, options, callback); } @@ -131,9 +131,9 @@ function isTextModel(arg: ITextModel | string | string[] | ITextBufferFactory): return Boolean(arg && (arg as ITextModel).uri); } -function _withTestCodeEditor(arg: ITextModel | string | string[] | ITextBufferFactory, options: TestCodeEditorInstantiationOptions, callback: (editor: ITestCodeEditor, viewModel: ViewModel, instantiationService: IInstantiationService) => void): void; -function _withTestCodeEditor(arg: ITextModel | string | string[] | ITextBufferFactory, options: TestCodeEditorInstantiationOptions, callback: (editor: ITestCodeEditor, viewModel: ViewModel, instantiationService: IInstantiationService) => Promise): Promise; -function _withTestCodeEditor(arg: ITextModel | string | string[] | ITextBufferFactory, options: TestCodeEditorInstantiationOptions, callback: (editor: ITestCodeEditor, viewModel: ViewModel, instantiationService: IInstantiationService) => Promise | void): Promise | void { +function _withTestCodeEditor(arg: ITextModel | string | string[] | ITextBufferFactory, options: TestCodeEditorInstantiationOptions, callback: (editor: ITestCodeEditor, viewModel: ViewModel, instantiationService: TestInstantiationService) => void): void; +function _withTestCodeEditor(arg: ITextModel | string | string[] | ITextBufferFactory, options: TestCodeEditorInstantiationOptions, callback: (editor: ITestCodeEditor, viewModel: ViewModel, instantiationService: TestInstantiationService) => Promise): Promise; +function _withTestCodeEditor(arg: ITextModel | string | string[] | ITextBufferFactory, options: TestCodeEditorInstantiationOptions, callback: (editor: ITestCodeEditor, viewModel: ViewModel, instantiationService: TestInstantiationService) => Promise | void): Promise | void { const disposables = new DisposableStore(); const instantiationService = createCodeEditorServices(disposables, options.serviceCollection); delete options.serviceCollection; diff --git a/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts b/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts new file mode 100644 index 0000000000000..72fc637074981 --- /dev/null +++ b/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Selection } from 'vs/editor/common/core/selection'; +import { Range } from 'vs/editor/common/core/range'; +import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; +import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; + +suite('CodeEditorWidget', () => { + + test('onDidChangeModelDecorations', () => { + withTestCodeEditor('', {}, (editor, viewModel) => { + const disposables = new DisposableStore(); + + let invoked = false; + disposables.add(editor.onDidChangeModelDecorations((e) => { + invoked = true; + })); + + viewModel.model.deltaDecorations([], [{ range: new Range(1, 1, 1, 1), options: { description: 'test' } }]); + + assert.deepStrictEqual(invoked, true); + + disposables.dispose(); + }); + }); + + test('onDidChangeModelLanguage', () => { + withTestCodeEditor('', {}, (editor, viewModel) => { + const disposables = new DisposableStore(); + disposables.add(ModesRegistry.registerLanguage({ id: 'testMode' })); + + let invoked = false; + disposables.add(editor.onDidChangeModelLanguage((e) => { + invoked = true; + })); + + viewModel.model.setMode('testMode'); + + assert.deepStrictEqual(invoked, true); + + disposables.dispose(); + }); + }); + + test('onDidChangeModelLanguageConfiguration', () => { + withTestCodeEditor('', {}, (editor, viewModel, instantiationService) => { + const disposables = new DisposableStore(); + disposables.add(ModesRegistry.registerLanguage({ id: 'testMode' })); + viewModel.model.setMode('testMode'); + + let invoked = false; + disposables.add(editor.onDidChangeModelLanguageConfiguration((e) => { + invoked = true; + })); + + disposables.add(LanguageConfigurationRegistry.register('testMode', { + brackets: [['(', ')']] + })); + + assert.deepStrictEqual(invoked, true); + + disposables.dispose(); + }); + }); + + test('onDidChangeModelContent', () => { + withTestCodeEditor('', {}, (editor, viewModel) => { + const disposables = new DisposableStore(); + + let invoked = false; + disposables.add(editor.onDidChangeModelContent((e) => { + invoked = true; + })); + + viewModel.type('hello', 'test'); + + assert.deepStrictEqual(invoked, true); + + disposables.dispose(); + }); + }); + + test('onDidChangeModelOptions', () => { + withTestCodeEditor('', {}, (editor, viewModel) => { + const disposables = new DisposableStore(); + + let invoked = false; + disposables.add(editor.onDidChangeModelOptions((e) => { + invoked = true; + })); + + viewModel.model.updateOptions({ + tabSize: 3 + }); + + assert.deepStrictEqual(invoked, true); + + disposables.dispose(); + }); + }); + + test('issue #145872 - Model change events are emitted before the selection updates', () => { + withTestCodeEditor('', {}, (editor, viewModel) => { + const disposables = new DisposableStore(); + + let observedSelection: Selection | null = null; + disposables.add(editor.onDidChangeModelContent((e) => { + observedSelection = editor.getSelection(); + })); + + viewModel.type('hello', 'test'); + + assert.deepStrictEqual(observedSelection, new Selection(1, 6, 1, 6)); + + disposables.dispose(); + }); + }); + +});