mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-28 06:31:58 +00:00
improves obsCodeEditor helper (#214215)
* improves obsCodeEditor helper
This commit is contained in:
parent
0fba8ea5f4
commit
3bb57eb6ef
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
31
src/vs/base/common/observableInternal/api.ts
Normal file
31
src/vs/base/common/observableInternal/api.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
@ -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> {
|
||||
|
||||
@ -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) {
|
||||
|
||||
146
src/vs/base/common/observableInternal/lazyObservableValue.ts
Normal file
146
src/vs/base/common/observableInternal/lazyObservableValue.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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!');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
209
src/vs/editor/test/browser/widget/observableCodeEditor.test.ts
Normal file
209
src/vs/editor/test/browser/widget/observableCodeEditor.test.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user