improves obsCodeEditor helper (#214215)

* improves obsCodeEditor helper
This commit is contained in:
Henning Dieterichs 2024-06-04 16:17:50 +02:00 committed by GitHub
parent 0fba8ea5f4
commit 3bb57eb6ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 669 additions and 128 deletions

View File

@ -6,6 +6,10 @@
import * as arrays from 'vs/base/common/arrays';
export type EqualityComparer<T> = (a: T, b: T) => boolean;
/**
* Compares two items for equality using strict equality.
*/
export const strictEquals: EqualityComparer<any> = (a, b) => a === b;
/**
@ -30,11 +34,30 @@ export function itemEquals<T extends { equals(other: T): boolean }>(): EqualityC
return (a, b) => a.equals(b);
}
export function equalsIfDefined<T>(v1: T | undefined, v2: T | undefined, equals: EqualityComparer<T>): boolean {
if (!v1 || !v2) {
return v1 === v2;
/**
* Checks if two items are both null or undefined, or are equal according to the provided equality comparer.
*/
export function equalsIfDefined<T>(v1: T | undefined | null, v2: T | undefined | null, equals: EqualityComparer<T>): boolean;
/**
* Returns an equality comparer that checks if two items are both null or undefined, or are equal according to the provided equality comparer.
*/
export function equalsIfDefined<T>(equals: EqualityComparer<T>): EqualityComparer<T | undefined | null>;
export function equalsIfDefined<T>(equalsOrV1: EqualityComparer<T> | T, v2?: T | undefined | null, equals?: EqualityComparer<T>): EqualityComparer<T | undefined | null> | boolean {
if (equals !== undefined) {
const v1 = equalsOrV1 as T | undefined;
if (v1 === undefined || v1 === null || v2 === undefined || v2 === null) {
return v2 === v1;
}
return equals(v1, v2);
} else {
const equals = equalsOrV1 as EqualityComparer<T>;
return (v1, v2) => {
if (v1 === undefined || v1 === null || v2 === undefined || v2 === null) {
return v2 === v1;
}
return equals(v1, v2);
};
}
return equals(v1, v2);
}
/**

View File

@ -60,6 +60,9 @@ export {
waitForState,
derivedWithCancellationToken,
} from 'vs/base/common/observableInternal/promise';
export {
observableValueOpts
} from 'vs/base/common/observableInternal/api';
import { ConsoleObservableLogger, setLogger } from 'vs/base/common/observableInternal/logging';

View File

@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EqualityComparer, strictEquals } from 'vs/base/common/equals';
import { ISettableObservable } from 'vs/base/common/observable';
import { ObservableValue } from 'vs/base/common/observableInternal/base';
import { IDebugNameData, DebugNameData } from 'vs/base/common/observableInternal/debugName';
import { LazyObservableValue } from 'vs/base/common/observableInternal/lazyObservableValue';
export function observableValueOpts<T, TChange = void>(
options: IDebugNameData & {
equalsFn?: EqualityComparer<T>;
lazy?: boolean;
},
initialValue: T
): ISettableObservable<T, TChange> {
if (options.lazy) {
return new LazyObservableValue(
new DebugNameData(options.owner, options.debugName, undefined),
initialValue,
options.equalsFn ?? strictEquals,
);
}
return new ObservableValue(
new DebugNameData(options.owner, options.debugName, undefined),
initialValue,
options.equalsFn ?? strictEquals,
);
}

View File

@ -6,7 +6,7 @@
import { strictEquals, EqualityComparer } from 'vs/base/common/equals';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { keepObserved, recomputeInitiallyAndOnChange } from 'vs/base/common/observable';
import { DebugNameData, IDebugNameData, Owner, getFunctionName } from 'vs/base/common/observableInternal/debugName';
import { DebugNameData, Owner, getFunctionName } from 'vs/base/common/observableInternal/debugName';
import type { derivedOpts } from 'vs/base/common/observableInternal/derived';
import { getLogger } from 'vs/base/common/observableInternal/logging';
@ -385,19 +385,6 @@ export function observableValue<T, TChange = void>(nameOrOwner: string | object,
return new ObservableValue(debugNameData, initialValue, strictEquals);
}
export function observableValueOpts<T>(
options: IDebugNameData & {
equalsFn?: EqualityComparer<T>;
},
initialValue: T
): ISettableObservable<T> {
return new ObservableValue(
new DebugNameData(options.owner, options.debugName, undefined),
initialValue,
options.equalsFn ?? strictEquals,
);
}
export class ObservableValue<T, TChange = void>
extends BaseObservable<T, TChange>
implements ISettableObservable<T, TChange> {

View File

@ -113,6 +113,14 @@ function findKey(obj: object, value: object): string | undefined {
const countPerClassName = new Map<string, number>();
const ownerId = new WeakMap<object, string>();
/**
* Don't call this method outside of tests.
*/
export function testingClearObservableNamingCache(): void {
countPerName.clear();
countPerClassName.clear();
}
function formatOwner(owner: object): string {
const id = ownerId.get(owner);
if (id) {

View File

@ -0,0 +1,146 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EqualityComparer } from 'vs/base/common/equals';
import { ISettableObservable, ITransaction } from 'vs/base/common/observable';
import { BaseObservable, IObserver, TransactionImpl } from 'vs/base/common/observableInternal/base';
import { DebugNameData } from 'vs/base/common/observableInternal/debugName';
/**
* Holds off updating observers until the value is actually read.
*/
export class LazyObservableValue<T, TChange = void>
extends BaseObservable<T, TChange>
implements ISettableObservable<T, TChange> {
protected _value: T;
private _isUpToDate = true;
private readonly _deltas: TChange[] = [];
get debugName() {
return this._debugNameData.getDebugName(this) ?? 'LazyObservableValue';
}
constructor(
private readonly _debugNameData: DebugNameData,
initialValue: T,
private readonly _equalityComparator: EqualityComparer<T>,
) {
super();
this._value = initialValue;
}
public override get(): T {
this._update();
return this._value;
}
private _update(): void {
if (this._isUpToDate) {
return;
}
this._isUpToDate = true;
if (this._deltas.length > 0) {
for (const observer of this.observers) {
for (const change of this._deltas) {
observer.handleChange(this, change);
}
}
this._deltas.length = 0;
} else {
for (const observer of this.observers) {
observer.handleChange(this, undefined);
}
}
}
private _updateCounter = 0;
private _beginUpdate(): void {
this._updateCounter++;
if (this._updateCounter === 1) {
for (const observer of this.observers) {
observer.beginUpdate(this);
}
}
}
private _endUpdate(): void {
this._updateCounter--;
if (this._updateCounter === 0) {
this._update();
// End update could change the observer list.
const observers = [...this.observers];
for (const r of observers) {
r.endUpdate(this);
}
}
}
public override addObserver(observer: IObserver): void {
const shouldCallBeginUpdate = !this.observers.has(observer) && this._updateCounter > 0;
super.addObserver(observer);
if (shouldCallBeginUpdate) {
observer.beginUpdate(this);
}
}
public override removeObserver(observer: IObserver): void {
const shouldCallEndUpdate = this.observers.has(observer) && this._updateCounter > 0;
super.removeObserver(observer);
if (shouldCallEndUpdate) {
// Calling end update after removing the observer makes sure endUpdate cannot be called twice here.
observer.endUpdate(this);
}
}
public set(value: T, tx: ITransaction | undefined, change: TChange): void {
if (change === undefined && this._equalityComparator(this._value, value)) {
return;
}
let _tx: TransactionImpl | undefined;
if (!tx) {
tx = _tx = new TransactionImpl(() => { }, () => `Setting ${this.debugName}`);
}
try {
this._isUpToDate = false;
this._setValue(value);
if (change !== undefined) {
this._deltas.push(change);
}
tx.updateObserver({
beginUpdate: () => this._beginUpdate(),
endUpdate: () => this._endUpdate(),
handleChange: (observable, change) => { },
handlePossibleChange: (observable) => { },
}, this);
if (this._updateCounter > 1) {
// We already started begin/end update, so we need to manually call handlePossibleChange
for (const observer of this.observers) {
observer.handlePossibleChange(this);
}
}
} finally {
if (_tx) {
_tx.finish();
}
}
}
override toString(): string {
return `${this.debugName}: ${this._value}`;
}
protected _setValue(newValue: T): void {
this._value = newValue;
}
}

View File

@ -54,9 +54,9 @@ export function observableFromPromise<T>(promise: Promise<T>): IObservable<{ val
export function observableFromEvent<T, TArgs = unknown>(
event: Event<TArgs>,
getValue: (args: TArgs | undefined) => T
getValue: (args: TArgs | undefined) => T,
): IObservable<T> {
return new FromEventObservable(event, getValue);
return new FromEventObservable(event, getValue, () => FromEventObservable.globalTransaction);
}
export class FromEventObservable<TArgs, T> extends BaseObservable<T> {
@ -68,7 +68,8 @@ export class FromEventObservable<TArgs, T> extends BaseObservable<T> {
constructor(
private readonly event: Event<TArgs>,
public readonly _getValue: (args: TArgs | undefined) => T
public readonly _getValue: (args: TArgs | undefined) => T,
private readonly _getTransaction: () => ITransaction | undefined,
) {
super();
}
@ -99,7 +100,7 @@ export class FromEventObservable<TArgs, T> extends BaseObservable<T> {
if (this.hasValue) {
didRunTransaction = true;
subtransaction(
FromEventObservable.globalTransaction,
this._getTransaction(),
(tx) => {
getLogger()?.handleFromEventObservableTriggered(this, { oldValue, newValue, change: undefined, didChange, hadValue: this.hasValue });
@ -229,6 +230,10 @@ class ObservableSignal<TChange> extends BaseObservable<void, TChange> implements
return new DebugNameData(this._owner, this._debugName, undefined).getDebugName(this) ?? 'Observable Signal';
}
public override toString(): string {
return this.debugName;
}
constructor(
private readonly _debugName: string | undefined,
private readonly _owner?: object,

View File

@ -3,11 +3,17 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { autorunOpts, derivedOpts, IObservable, observableFromEvent } from 'vs/base/common/observable';
import { equalsIfDefined, itemsEquals } from 'vs/base/common/equals';
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { IObservable, ITransaction, autorunOpts, derived, derivedOpts, observableFromEvent, observableSignal, observableValue, observableValueOpts } from 'vs/base/common/observable';
import { TransactionImpl } from 'vs/base/common/observableInternal/base';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { Position } from 'vs/editor/common/core/position';
import { IModelDeltaDecoration } from 'vs/editor/common/model';
import { Selection } from 'vs/editor/common/core/selection';
import { ICursorSelectionChangedEvent } from 'vs/editor/common/cursorEvents';
import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model';
import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents';
/**
* Returns a facade for the code editor that provides observables for various states/events.
@ -16,7 +22,7 @@ export function obsCodeEditor(editor: ICodeEditor): ObservableCodeEditor {
return ObservableCodeEditor.get(editor);
}
class ObservableCodeEditor {
class ObservableCodeEditor extends Disposable {
private static _map = new Map<ICodeEditor, ObservableCodeEditor>();
/**
@ -28,21 +34,129 @@ class ObservableCodeEditor {
result = new ObservableCodeEditor(editor);
ObservableCodeEditor._map.set(editor, result);
const d = editor.onDidDispose(() => {
ObservableCodeEditor._map.delete(editor);
d.dispose();
const item = ObservableCodeEditor._map.get(editor);
if (item) {
ObservableCodeEditor._map.delete(editor);
item.dispose();
d.dispose();
}
});
}
return result;
}
private constructor(public readonly editor: ICodeEditor) {
private _updateCounter = 0;
private _currentTransaction: TransactionImpl | undefined = undefined;
private _beginUpdate(): void {
this._updateCounter++;
if (this._updateCounter === 1) {
this._currentTransaction = new TransactionImpl(() => {
/** @description Update editor state */
});
}
}
public readonly model = observableFromEvent(this.editor.onDidChangeModel, () => this.editor.getModel());
public readonly value = observableFromEvent(this.editor.onDidChangeModelContent, () => this.editor.getValue());
public readonly valueIsEmpty = observableFromEvent(this.editor.onDidChangeModelContent, () => this.editor.getModel()?.getValueLength() === 0);
public readonly selections = observableFromEvent(this.editor.onDidChangeCursorSelection, () => this.editor.getSelections());
public readonly cursorPosition = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.selections.read(reader)?.[0]?.getPosition() ?? null);
private _endUpdate(): void {
this._updateCounter--;
if (this._updateCounter === 0) {
const t = this._currentTransaction!;
this._currentTransaction = undefined;
t.finish();
}
}
private constructor(public readonly editor: ICodeEditor) {
super();
this._register(this.editor.onBeginUpdate(() => this._beginUpdate()));
this._register(this.editor.onEndUpdate(() => this._endUpdate()));
this._register(this.editor.onDidChangeModel(() => {
this._beginUpdate();
try {
this._model.set(this.editor.getModel(), this._currentTransaction);
this._forceUpdate();
} finally {
this._endUpdate();
}
}));
this._register(this.editor.onDidType((e) => {
this._beginUpdate();
try {
this._forceUpdate();
this.onDidType.trigger(this._currentTransaction, e);
} finally {
this._endUpdate();
}
}));
this._register(this.editor.onDidChangeModelContent(e => {
this._beginUpdate();
try {
this._versionId.set(this.editor.getModel()?.getVersionId() ?? null, this._currentTransaction, e);
this._forceUpdate();
} finally {
this._endUpdate();
}
}));
this._register(this.editor.onDidChangeCursorSelection(e => {
this._beginUpdate();
try {
this._selections.set(this.editor.getSelections(), this._currentTransaction, e);
this._forceUpdate();
} finally {
this._endUpdate();
}
}));
}
public forceUpdate(): void;
public forceUpdate<T>(cb: (tx: ITransaction) => T): T;
public forceUpdate<T>(cb?: (tx: ITransaction) => T): T {
this._beginUpdate();
try {
this._forceUpdate();
if (!cb) { return undefined as T; }
return cb(this._currentTransaction!);
} finally {
this._endUpdate();
}
}
private _forceUpdate(): void {
this._beginUpdate();
try {
this._model.set(this.editor.getModel(), this._currentTransaction);
this._versionId.set(this.editor.getModel()?.getVersionId() ?? null, this._currentTransaction, undefined);
this._selections.set(this.editor.getSelections(), this._currentTransaction, undefined);
} finally {
this._endUpdate();
}
}
private readonly _model = observableValue(this, this.editor.getModel());
public readonly model: IObservable<ITextModel | null> = this._model;
public readonly isReadonly = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.readOnly));
private readonly _versionId = observableValueOpts<number | null, IModelContentChangedEvent | undefined>({ owner: this, lazy: true }, this.editor.getModel()?.getVersionId() ?? null);
public readonly versionId: IObservable<number | null, IModelContentChangedEvent | undefined> = this._versionId;
private readonly _selections = observableValueOpts<Selection[] | null, ICursorSelectionChangedEvent | undefined>(
{ owner: this, equalsFn: equalsIfDefined(itemsEquals(Selection.selectionsEqual)), lazy: true },
this.editor.getSelections() ?? null
);
public readonly selections: IObservable<Selection[] | null, ICursorSelectionChangedEvent | undefined> = this._selections;
public readonly positions = derivedOpts<readonly Position[] | null>(
{ owner: this, equalsFn: equalsIfDefined(itemsEquals(Position.equals)) },
reader => this.selections.read(reader)?.map(s => s.getStartPosition()) ?? null
);
public readonly isFocused = observableFromEvent(e => {
const d1 = this.editor.onDidFocusEditorWidget(e);
const d2 = this.editor.onDidBlurEditorWidget(e);
@ -54,6 +168,12 @@ class ObservableCodeEditor {
};
}, () => this.editor.hasWidgetFocus());
public readonly value = derived(this, reader => { this.versionId.read(reader); return this.model.read(reader)?.getValue(); });
public readonly valueIsEmpty = derived(this, reader => { this.versionId.read(reader); return this.editor.getModel()?.getValueLength() === 0; });
public readonly cursorPosition = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.selections.read(reader)?.[0]?.getPosition() ?? null);
public readonly onDidType = observableSignal<string>(this);
public setDecorations(decorations: IObservable<IModelDeltaDecoration[]>): IDisposable {
const d = new DisposableStore();
const decorationsCollection = this.editor.createDecorationsCollection();

View File

@ -1648,6 +1648,16 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
this.languageConfigurationService,
this._themeService,
attachedView,
{
batchChanges: (cb) => {
try {
this._beginUpdate();
return cb();
} finally {
this._endUpdate();
}
},
}
);
// Someone might destroy the model from under the editor, so prevent any exceptions by setting a null model

View File

@ -413,7 +413,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
private _assertNotDisposed(): void {
if (this._isDisposed) {
throw new Error('Model is disposed!');
throw new BugIndicatingError('Model is disposed!');
}
}

View File

@ -71,6 +71,7 @@ export class ViewModel extends Disposable implements IViewModel {
private readonly languageConfigurationService: ILanguageConfigurationService,
private readonly _themeService: IThemeService,
private readonly _attachedView: IAttachedView,
private readonly _transactionalTarget: IBatchableTarget,
) {
super();
@ -1102,12 +1103,14 @@ export class ViewModel extends Disposable implements IViewModel {
//#endregion
private _withViewEventsCollector<T>(callback: (eventsCollector: ViewModelEventsCollector) => T): T {
try {
const eventsCollector = this._eventDispatcher.beginEmitViewEvents();
return callback(eventsCollector);
} finally {
this._eventDispatcher.endEmitViewEvents();
}
return this._transactionalTarget.batchChanges(() => {
try {
const eventsCollector = this._eventDispatcher.beginEmitViewEvents();
return callback(eventsCollector);
} finally {
this._eventDispatcher.endEmitViewEvents();
}
});
}
public batchEvents(callback: () => void): void {
@ -1127,6 +1130,13 @@ export class ViewModel extends Disposable implements IViewModel {
}
}
export interface IBatchableTarget {
/**
* Allows the target to apply the changes introduced by the callback in a batch.
*/
batchChanges<T>(cb: () => T): T;
}
class ViewportStart implements IDisposable {
public static create(model: ITextModel): ViewportStart {

View File

@ -7,26 +7,25 @@ import { createStyleSheet2 } from 'vs/base/browser/dom';
import { alert } from 'vs/base/browser/ui/aria/aria';
import { timeout } from 'vs/base/common/async';
import { cancelOnDispose } from 'vs/base/common/cancellation';
import { itemEquals, itemsEquals } from 'vs/base/common/equals';
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { IObservable, ITransaction, autorun, autorunHandleChanges, constObservable, derived, disposableObservableValue, observableFromEvent, observableSignal, observableValue, transaction, waitForState } from 'vs/base/common/observable';
import { ISettableObservable, observableValueOpts } from 'vs/base/common/observableInternal/base';
import { IObservable, ITransaction, autorun, autorunHandleChanges, autorunWithStoreHandleChanges, constObservable, derived, disposableObservableValue, observableFromEvent, observableSignal, observableValue, transaction, waitForState } from 'vs/base/common/observable';
import { ISettableObservable } from 'vs/base/common/observableInternal/base';
import { mapObservableArrayCached } from 'vs/base/common/observableInternal/utils';
import { isUndefined } from 'vs/base/common/types';
import { CoreEditingCommands } from 'vs/editor/browser/coreCommands';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { obsCodeEditor } from 'vs/editor/browser/observableUtilities';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { CursorChangeReason } from 'vs/editor/common/cursorEvents';
import { ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents';
import { inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/browser/commandIds';
import { GhostTextWidget } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextWidget';
import { InlineCompletionContextKeys } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys';
import { InlineCompletionsHintsWidget, InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget';
import { InlineCompletionsModel, VersionIdChangeReason } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel';
import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel';
import { SuggestWidgetAdaptor } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider';
import { localize } from 'vs/nls';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
@ -44,42 +43,47 @@ export class InlineCompletionsController extends Disposable {
return editor.getContribution<InlineCompletionsController>(InlineCompletionsController.ID);
}
public readonly model = this._register(disposableObservableValue<InlineCompletionsModel | undefined>('inlineCompletionModel', undefined));
private readonly _textModelVersionId = observableValue<number, VersionIdChangeReason>(this, -1);
private readonly _positions = observableValueOpts<readonly Position[]>({ owner: this, equalsFn: itemsEquals(itemEquals()) }, [new Position(1, 1)]);
public readonly model = this._register(disposableObservableValue<InlineCompletionsModel | undefined>(this, undefined));
private readonly _editorObs = obsCodeEditor(this.editor);
private readonly _positions = derived(this, reader => this._editorObs.positions.read(reader) ?? [new Position(1, 1)]);
private readonly _suggestWidgetAdaptor = this._register(new SuggestWidgetAdaptor(
this.editor,
() => this.model.get()?.selectedInlineCompletion.get()?.toSingleTextEdit(undefined),
(tx) => this.updateObservables(tx, VersionIdChangeReason.Other),
() => {
this._editorObs.forceUpdate();
return this.model.get()?.selectedInlineCompletion.get()?.toSingleTextEdit(undefined);
},
(item) => {
transaction(tx => {
this._editorObs.forceUpdate(tx => {
/** @description InlineCompletionsController.handleSuggestAccepted */
this.updateObservables(tx, VersionIdChangeReason.Other);
this.model.get()?.handleSuggestAccepted(item);
});
}
));
private readonly _suggestWidgetSelectedItem = observableFromEvent(cb => this._suggestWidgetAdaptor.onDidSelectedItemChange(() => {
this._editorObs.forceUpdate(_tx => cb(undefined));
}), () => this._suggestWidgetAdaptor.selectedItem);
private readonly _enabledInConfig = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).enabled);
private readonly _isScreenReaderEnabled = observableFromEvent(this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized());
private readonly _editorDictationInProgress = observableFromEvent(this._contextKeyService.onDidChangeContext, () => this._contextKeyService.getContext(this.editor.getDomNode()).getValue('editorDictation.inProgress') === true);
private readonly _editorDictationInProgress = observableFromEvent(
this._contextKeyService.onDidChangeContext,
() => this._contextKeyService.getContext(this.editor.getDomNode()).getValue('editorDictation.inProgress') === true
);
private readonly _enabled = derived(this, reader => this._enabledInConfig.read(reader) && (!this._isScreenReaderEnabled.read(reader) || !this._editorDictationInProgress.read(reader)));
private readonly _fontFamily = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).fontFamily);
private readonly _ghostTexts = derived(this, (reader) => {
const model = this.model.read(reader);
return model?.ghostTexts.read(reader) ?? [];
});
private readonly _stablizedGhostTexts = convertItemsToStableObservables(this._ghostTexts, this._store);
private readonly _ghostTextWidgets = mapObservableArrayCached(this, this._stablizedGhostTexts, (ghostText, store) => {
return store.add(this._instantiationService.createInstance(GhostTextWidget, this.editor, {
private readonly _ghostTextWidgets = mapObservableArrayCached(this, this._stablizedGhostTexts, (ghostText, store) =>
store.add(this._instantiationService.createInstance(GhostTextWidget, this.editor, {
ghostText: ghostText,
minReservedLineCount: constObservable(0),
targetTextModel: this.model.map(v => v?.textModel),
}));
}).recomputeInitiallyAndOnChange(this._store);
}))
).recomputeInitiallyAndOnChange(this._store);
private readonly _debounceValue = this._debounceService.for(
this._languageFeaturesService.inlineCompletionsProvider,
@ -88,10 +92,7 @@ export class InlineCompletionsController extends Disposable {
);
private readonly _playAccessibilitySignal = observableSignal(this);
private readonly _isReadonly = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.readOnly));
private readonly _textModel = observableFromEvent(this.editor.onDidChangeModel, () => this.editor.getModel());
private readonly _textModelIfWritable = derived(reader => this._isReadonly.read(reader) ? undefined : this._textModel.read(reader));
private readonly _textModelIfWritable = derived(reader => this._editorObs.isReadonly.read(reader) ? undefined : this._editorObs.model.read(reader));
constructor(
public readonly editor: ICodeEditor,
@ -115,14 +116,12 @@ export class InlineCompletionsController extends Disposable {
transaction(tx => {
/** @description InlineCompletionsController.onDidChangeModel/readonly */
this.model.set(undefined, tx);
this.updateObservables(tx, VersionIdChangeReason.Other);
if (textModel) {
const model = _instantiationService.createInstance(
InlineCompletionsModel,
textModel,
this._suggestWidgetAdaptor.selectedItem,
this._textModelVersionId,
this._suggestWidgetSelectedItem,
this._editorObs.versionId,
this._positions,
this._debounceValue,
observableFromEvent(editor.onDidChangeConfiguration, () => editor.getOption(EditorOption.suggest).preview),
@ -146,28 +145,26 @@ export class InlineCompletionsController extends Disposable {
}`);
}));
const getReason = (e: IModelContentChangedEvent): VersionIdChangeReason => {
if (e.isUndoing) { return VersionIdChangeReason.Undo; }
if (e.isRedoing) { return VersionIdChangeReason.Redo; }
if (this.model.get()?.isAcceptingPartially) { return VersionIdChangeReason.AcceptWord; }
return VersionIdChangeReason.Other;
};
this._register(editor.onDidChangeModelContent((e) => transaction(tx =>
/** @description InlineCompletionsController.onDidChangeModelContent */
this.updateObservables(tx, getReason(e))
)));
this._register(editor.onDidChangeCursorPosition(e => transaction(tx => {
/** @description InlineCompletionsController.onDidChangeCursorPosition */
this.updateObservables(tx, VersionIdChangeReason.Other);
if (e.reason === CursorChangeReason.Explicit || e.source === 'api') {
this.model.get()?.stop(tx);
this._register(autorunWithStoreHandleChanges({
createEmptyChangeSummary: () => ({ shouldStop: false }),
handleChange: (context, changeSummary) => {
if (context.didChange(this._editorObs.selections)) {
const e = context.change;
if (e && (e.reason === CursorChangeReason.Explicit || e.source === 'api')) {
changeSummary.shouldStop = true;
}
}
return changeSummary.shouldStop;
},
}, (reader, changeSummary) => {
this._editorObs.selections.read(reader);
if (changeSummary.shouldStop) {
this.model.get()?.stop(undefined);
}
})));
}));
this._register(editor.onDidType(() => transaction(tx => {
this._register(editor.onDidType(() => this._editorObs.forceUpdate(tx => {
/** @description InlineCompletionsController.onDidType */
this.updateObservables(tx, VersionIdChangeReason.Other);
if (this._enabled.get()) {
this.model.get()?.trigger(tx);
}
@ -183,7 +180,7 @@ export class InlineCompletionsController extends Disposable {
'acceptSelectedSuggestion',
]);
if (commands.has(e.commandId) && editor.hasTextFocus() && this._enabled.get()) {
transaction(tx => {
this._editorObs.forceUpdate(tx => {
/** @description onDidExecuteCommand */
this.model.get()?.trigger(tx);
});
@ -246,7 +243,7 @@ export class InlineCompletionsController extends Disposable {
const lineText = model.textModel.getLineContent(state.primaryGhostText.lineNumber);
await timeout(50, cancelOnDispose(cancellationStore));
await waitForState(this._suggestWidgetAdaptor.selectedItem, isUndefined, () => false, cancelOnDispose(cancellationStore));
await waitForState(this._suggestWidgetSelectedItem, isUndefined, () => false, cancelOnDispose(cancellationStore));
await this._accessibilitySignalService.playSignal(AccessibilitySignal.inlineSuggestion);
@ -257,6 +254,8 @@ export class InlineCompletionsController extends Disposable {
}));
this._register(new InlineCompletionsHintsWidget(this.editor, this.model, this._instantiationService));
// TODO@hediet
this._register(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('accessibility.verbosity.inlineCompletions')) {
this.editor.updateOptions({ inlineCompletionsAccessibilityVerbose: this._configurationService.getValue('accessibility.verbosity.inlineCompletions') });
@ -279,17 +278,6 @@ export class InlineCompletionsController extends Disposable {
hint ? alert(content + ', ' + hint) : alert(content);
}
/**
* Copies over the relevant state from the text model to observables.
* This solves all kind of eventing issues, as we make sure we always operate on the latest state,
* regardless of who calls into us.
*/
private updateObservables(tx: ITransaction, changeReason: VersionIdChangeReason): void {
const newModel = this.editor.getModel();
this._textModelVersionId.set(newModel?.getVersionId() ?? -1, tx, changeReason);
this._positions.set(this.editor.getSelections()?.map(selection => selection.getPosition()) ?? [new Position(1, 1)], tx);
}
public shouldShowHoverAt(range: Range) {
const ghostText = this.model.get()?.primaryGhostText.get();
if (ghostText) {

View File

@ -23,6 +23,7 @@ import { Command, InlineCompletionContext, InlineCompletionTriggerKind, PartialA
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model';
import { IFeatureDebounceInformation } from 'vs/editor/common/services/languageFeatureDebounce';
import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents';
import { GhostText, GhostTextOrReplacement, ghostTextOrReplacementEquals, ghostTextsOrReplacementsEqual } from 'vs/editor/contrib/inlineCompletions/browser/ghostText';
import { InlineCompletionWithUpdatedRange, InlineCompletionsSource } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource';
import { computeGhostText, singleTextEditAugments, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit';
@ -41,7 +42,7 @@ export enum VersionIdChangeReason {
export class InlineCompletionsModel extends Disposable {
private readonly _source = this._register(this._instantiationService.createInstance(InlineCompletionsSource, this.textModel, this.textModelVersionId, this._debounceValue));
private readonly _isActive = observableValue<boolean, InlineCompletionTriggerKind | void>(this, false);
private readonly _isActive = observableValue<boolean>(this, false);
readonly _forceUpdateExplicitlySignal = observableSignal(this);
// We use a semantic id to keep the same inline completion selected even if the provider reorders the completions.
@ -54,7 +55,7 @@ export class InlineCompletionsModel extends Disposable {
constructor(
public readonly textModel: ITextModel,
public readonly selectedSuggestItem: IObservable<SuggestItemInfo | undefined>,
public readonly textModelVersionId: IObservable<number, VersionIdChangeReason>,
public readonly textModelVersionId: IObservable<number | null, IModelContentChangedEvent | undefined>,
private readonly _positions: IObservable<readonly Position[]>,
private readonly _debounceValue: IFeatureDebounceInformation,
private readonly _suggestPreviewEnabled: IObservable<boolean>,
@ -91,6 +92,13 @@ export class InlineCompletionsModel extends Disposable {
VersionIdChangeReason.AcceptWord,
]);
private _getReason(e: IModelContentChangedEvent | undefined): VersionIdChangeReason {
if (e?.isUndoing) { return VersionIdChangeReason.Undo; }
if (e?.isRedoing) { return VersionIdChangeReason.Redo; }
if (this.isAcceptingPartially) { return VersionIdChangeReason.AcceptWord; }
return VersionIdChangeReason.Other;
}
private readonly _fetchInlineCompletionsPromise = derivedHandleChanges({
owner: this,
createEmptyChangeSummary: () => ({
@ -99,7 +107,7 @@ export class InlineCompletionsModel extends Disposable {
}),
handleChange: (ctx, changeSummary) => {
/** @description fetch inline completions */
if (ctx.didChange(this.textModelVersionId) && this._preserveCurrentCompletionReasons.has(ctx.change)) {
if (ctx.didChange(this.textModelVersionId) && this._preserveCurrentCompletionReasons.has(this._getReason(ctx.change))) {
changeSummary.preserveCurrentCompletion = true;
} else if (ctx.didChange(this._forceUpdateExplicitlySignal)) {
changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit;

View File

@ -27,7 +27,7 @@ export class InlineCompletionsSource extends Disposable {
constructor(
private readonly textModel: ITextModel,
private readonly versionId: IObservable<number>,
private readonly versionId: IObservable<number | null>,
private readonly _debounceValue: IFeatureDebounceInformation,
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
@ILanguageConfigurationService private readonly languageConfigurationService: ILanguageConfigurationService,
@ -62,7 +62,7 @@ export class InlineCompletionsSource extends Disposable {
await wait(this._debounceValue.get(this.textModel), source.token);
}
if (source.token.isCancellationRequested || this.textModel.getVersionId() !== request.versionId) {
if (source.token.isCancellationRequested || this._store.isDisposed || this.textModel.getVersionId() !== request.versionId) {
return false;
}
@ -76,7 +76,7 @@ export class InlineCompletionsSource extends Disposable {
this.languageConfigurationService
);
if (source.token.isCancellationRequested || this.textModel.getVersionId() !== request.versionId) {
if (source.token.isCancellationRequested || this._store.isDisposed || this.textModel.getVersionId() !== request.versionId) {
return false;
}
@ -182,7 +182,7 @@ export class UpToDateInlineCompletions implements IDisposable {
private readonly inlineCompletionProviderResult: InlineCompletionProviderResult,
public readonly request: UpdateRequest,
private readonly _textModel: ITextModel,
private readonly _versionId: IObservable<number>,
private readonly _versionId: IObservable<number | null>,
) {
const ids = _textModel.deltaDecorations([], inlineCompletionProviderResult.completions.map(i => ({
range: i.range,
@ -254,7 +254,7 @@ export class InlineCompletionWithUpdatedRange {
public readonly inlineCompletion: InlineCompletionItem,
public readonly decorationId: string,
private readonly _textModel: ITextModel,
private readonly _modelVersion: IObservable<number>,
private readonly _modelVersion: IObservable<number | null>,
) {
}

View File

@ -3,39 +3,36 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { compareBy, numberComparator } from 'vs/base/common/arrays';
import { findFirstMax } from 'vs/base/common/arraysFind';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { SingleTextEdit } from 'vs/editor/common/core/textEdit';
import { CompletionItemInsertTextRule, CompletionItemKind, SelectedSuggestionInfo } from 'vs/editor/common/languages';
import { ITextModel } from 'vs/editor/common/model';
import { singleTextEditAugments, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit';
import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser';
import { SnippetSession } from 'vs/editor/contrib/snippet/browser/snippetSession';
import { CompletionItem } from 'vs/editor/contrib/suggest/browser/suggest';
import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController';
import { IObservable, ITransaction, observableValue, transaction } from 'vs/base/common/observable';
import { SingleTextEdit } from 'vs/editor/common/core/textEdit';
import { ITextModel } from 'vs/editor/common/model';
import { compareBy, numberComparator } from 'vs/base/common/arrays';
import { findFirstMax } from 'vs/base/common/arraysFind';
import { singleTextEditAugments, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit';
export class SuggestWidgetAdaptor extends Disposable {
private isSuggestWidgetVisible: boolean = false;
private isShiftKeyPressed = false;
private _isActive = false;
private _currentSuggestItemInfo: SuggestItemInfo | undefined = undefined;
private readonly _selectedItem = observableValue(this, undefined as SuggestItemInfo | undefined);
public get selectedItem(): IObservable<SuggestItemInfo | undefined> {
return this._selectedItem;
public get selectedItem(): SuggestItemInfo | undefined {
return this._currentSuggestItemInfo;
}
private _onDidSelectedItemChange = this._register(new Emitter<void>());
public readonly onDidSelectedItemChange: Event<void> = this._onDidSelectedItemChange.event;
constructor(
private readonly editor: ICodeEditor,
private readonly suggestControllerPreselector: () => SingleTextEdit | undefined,
private readonly checkModelVersion: (tx: ITransaction) => void,
private readonly onWillAccept: (item: SuggestItemInfo) => void,
) {
super();
@ -59,8 +56,6 @@ export class SuggestWidgetAdaptor extends Disposable {
this._register(suggestController.registerSelector({
priority: 100,
select: (model, pos, suggestItems) => {
transaction(tx => this.checkModelVersion(tx));
const textModel = this.editor.getModel();
if (!textModel) {
// Should not happen
@ -142,11 +137,7 @@ export class SuggestWidgetAdaptor extends Disposable {
this._isActive = newActive;
this._currentSuggestItemInfo = newInlineCompletion;
transaction(tx => {
/** @description Update state from suggest widget */
this.checkModelVersion(tx);
this._selectedItem.set(this._isActive ? this._currentSuggestItemInfo : undefined, tx);
});
this._onDidSelectedItemChange.fire();
}
}

View File

@ -22,6 +22,8 @@ export function testViewModel(text: string[], options: IEditorOptions, callback:
const viewModel = new ViewModel(EDITOR_ID, configuration, model, monospaceLineBreaksComputerFactory, monospaceLineBreaksComputerFactory, null!, testLanguageConfigurationService, new TestThemeService(), {
setVisibleLines(visibleLines, stabilized) {
},
}, {
batchChanges: (cb) => cb(),
});
callback(viewModel, model);

View File

@ -0,0 +1,209 @@
/*---------------------------------------------------------------------------------------------
* 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 { IObservable, derivedHandleChanges } from 'vs/base/common/observable';
import { testingClearObservableNamingCache } from 'vs/base/common/observableInternal/debugName';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { obsCodeEditor } from 'vs/editor/browser/observableUtilities';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl';
import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
suite('CodeEditorWidget', () => {
ensureNoDisposablesAreLeakedInTestSuite();
function withTestFixture(cb: (args: { editor: ICodeEditor; viewModel: ViewModel; log: Log; derived: IObservable<string> }) => void) {
withEditorSetupTestFixture(undefined, cb);
}
function withEditorSetupTestFixture(
preSetupCallback: ((editor: ICodeEditor, disposables: DisposableStore) => void) | undefined,
cb: (args: { editor: ICodeEditor; viewModel: ViewModel; log: Log; derived: IObservable<string> }) => void,
) {
testingClearObservableNamingCache();
withTestCodeEditor('hello world', {}, (editor, viewModel) => {
const disposables = new DisposableStore();
preSetupCallback?.(editor, disposables);
const obsEditor = obsCodeEditor(editor);
const log = new Log();
const derived = derivedHandleChanges({
createEmptyChangeSummary: () => undefined,
handleChange: (context, changeSummary) => {
const formattedChange = JSON.stringify(
context.change,
(key, value) => {
if (value instanceof Range) {
return value.toString();
}
if (value === false || Array.isArray(value) && value.length === 0) { return undefined; }
return value;
}
);
log.log(`handle change ${context.changedObservable.toString()} ${formattedChange}`);
return true;
},
}, reader => {
const versionId = obsEditor.versionId.read(reader);
const selection = obsEditor.selections.read(reader)?.map(s => s.toString()).join(', ');
obsEditor.onDidType.read(reader);
const str = `running derived -> selection: ${selection}, value: ${versionId}`;
log.log(str);
return str;
});
derived.recomputeInitiallyAndOnChange(disposables);
assert.deepStrictEqual(log.getAndClearEntries(), (["running derived -> selection: [1,1 -> 1,1], value: 1"]));
cb({ editor, viewModel, log, derived });
disposables.dispose();
});
}
test('setPosition', () => withTestFixture(({ editor, log }) => {
editor.setPosition(new Position(1, 2));
assert.deepStrictEqual(log.getAndClearEntries(), [
'handle change ObservableCodeEditor._selections: [1,2 -> 1,2] {"selection":"[1,2 -> 1,2]","modelVersionId":1,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"api","reason":0}',
"running derived -> selection: [1,2 -> 1,2], value: 1",
]);
}));
test('keyboard.type', () => withTestFixture(({ editor, log }) => {
editor.trigger('keyboard', 'type', { text: 'abc' });
assert.deepStrictEqual(log.getAndClearEntries(), [
'handle change ObservableCodeEditor.onDidType "abc"',
'handle change ObservableCodeEditor._versionId: 4 {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}',
'handle change ObservableCodeEditor._versionId: 4 {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}',
'handle change ObservableCodeEditor._versionId: 4 {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}',
'handle change ObservableCodeEditor._selections: [1,4 -> 1,4] {"selection":"[1,4 -> 1,4]","modelVersionId":4,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}',
"running derived -> selection: [1,4 -> 1,4], value: 4",
]);
}));
test('keyboard.type and set position', () => withTestFixture(({ editor, log }) => {
editor.trigger('keyboard', 'type', { text: 'abc' });
assert.deepStrictEqual(log.getAndClearEntries(), [
'handle change ObservableCodeEditor.onDidType "abc"',
'handle change ObservableCodeEditor._versionId: 4 {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}',
'handle change ObservableCodeEditor._versionId: 4 {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}',
'handle change ObservableCodeEditor._versionId: 4 {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}',
'handle change ObservableCodeEditor._selections: [1,4 -> 1,4] {"selection":"[1,4 -> 1,4]","modelVersionId":4,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}',
"running derived -> selection: [1,4 -> 1,4], value: 4",
]);
editor.setPosition(new Position(1, 5), 'test');
assert.deepStrictEqual(log.getAndClearEntries(), [
'handle change ObservableCodeEditor._selections: [1,5 -> 1,5] {"selection":"[1,5 -> 1,5]","modelVersionId":4,"oldSelections":["[1,4 -> 1,4]"],"oldModelVersionId":4,"source":"test","reason":0}',
"running derived -> selection: [1,5 -> 1,5], value: 4",
]);
}));
test('listener interaction', () => {
let derived: IObservable<string, unknown>;
let log: Log;
let force = false;
withEditorSetupTestFixture(
(editor, disposables) => {
disposables.add(editor.onDidChangeModelContent(() => {
if (force) {
log.log('>>> before forceUpdate');
obsCodeEditor(editor).forceUpdate();
}
log.log('>>> before get');
derived.get();
log.log('<<< after get');
}));
},
(args) => {
const editor = args.editor;
derived = args.derived;
log = args.log;
editor.trigger("keyboard", "type", { text: "a" });
assert.deepStrictEqual(log.getAndClearEntries(), [
">>> before get",
"<<< after get",
'handle change ObservableCodeEditor.onDidType "a"',
'handle change ObservableCodeEditor._versionId: 2 {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}',
'handle change ObservableCodeEditor._selections: [1,2 -> 1,2] {"selection":"[1,2 -> 1,2]","modelVersionId":2,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}',
"running derived -> selection: [1,2 -> 1,2], value: 2",
]);
editor.executeEdits(undefined, [
{ range: new Range(1, 1, 1, 1), text: "x" },
]);
assert.deepStrictEqual(log.getAndClearEntries(), [
">>> before get",
"<<< after get",
'handle change ObservableCodeEditor._versionId: 3 {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"x","rangeOffset":0}],"eol":"\\n","versionId":3}',
'handle change ObservableCodeEditor._selections: [1,3 -> 1,3] {"selection":"[1,3 -> 1,3]","modelVersionId":3,"oldSelections":["[1,2 -> 1,2]"],"oldModelVersionId":3,"source":"modelChange","reason":2}',
"running derived -> selection: [1,3 -> 1,3], value: 3",
]);
force = true;
editor.trigger("keyboard", "type", { text: "a" });
assert.deepStrictEqual(log.getAndClearEntries(), [
">>> before forceUpdate",
">>> before get",
"handle change ObservableCodeEditor._versionId: 4 undefined",
"running derived -> selection: [1,4 -> 1,4], value: 4",
"<<< after get",
'handle change ObservableCodeEditor.onDidType "a"',
'handle change ObservableCodeEditor._versionId: 4 {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"text":"a","rangeOffset":2}],"eol":"\\n","versionId":4}',
'handle change ObservableCodeEditor._selections: [1,4 -> 1,4] {"selection":"[1,4 -> 1,4]","modelVersionId":4,"oldSelections":["[1,3 -> 1,3]"],"oldModelVersionId":3,"source":"keyboard","reason":0}',
"running derived -> selection: [1,4 -> 1,4], value: 4",
]);
editor.executeEdits(undefined, [
{ range: new Range(1, 1, 1, 1), text: "x" },
]);
assert.deepStrictEqual(log.getAndClearEntries(), [
">>> before forceUpdate",
">>> before get",
"handle change ObservableCodeEditor._versionId: 5 undefined",
"running derived -> selection: [1,5 -> 1,5], value: 5",
"<<< after get",
'handle change ObservableCodeEditor._versionId: 5 {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"x","rangeOffset":0}],"eol":"\\n","versionId":5}',
'handle change ObservableCodeEditor._selections: [1,5 -> 1,5] {"selection":"[1,5 -> 1,5]","modelVersionId":5,"oldSelections":["[1,4 -> 1,4]"],"oldModelVersionId":5,"source":"modelChange","reason":2}',
"running derived -> selection: [1,5 -> 1,5], value: 5",
]);
}
);
});
});
class Log {
private readonly entries: string[] = [];
public log(message: string): void {
this.entries.push(message);
}
public getAndClearEntries(): string[] {
const entries = [...this.entries];
this.entries.length = 0;
return entries;
}
}