mirror of
https://github.com/Microsoft/vscode
synced 2024-10-04 02:14:06 +00:00
Improves observable logging experience (#215782)
This commit is contained in:
parent
937c8c0c94
commit
8dcd7945af
|
@ -76,7 +76,7 @@ export function autorunWithStoreHandleChanges<TChangeSummary>(
|
|||
{
|
||||
owner: options.owner,
|
||||
debugName: options.debugName,
|
||||
debugReferenceFn: options.debugReferenceFn,
|
||||
debugReferenceFn: options.debugReferenceFn ?? fn,
|
||||
createEmptyChangeSummary: options.createEmptyChangeSummary,
|
||||
handleChange: options.handleChange,
|
||||
},
|
||||
|
|
|
@ -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, Owner, getFunctionName } from 'vs/base/common/observableInternal/debugName';
|
||||
import { DebugNameData, DebugOwner, getFunctionName } from 'vs/base/common/observableInternal/debugName';
|
||||
import type { derivedOpts } from 'vs/base/common/observableInternal/derived';
|
||||
import { getLogger } from 'vs/base/common/observableInternal/logging';
|
||||
|
||||
|
@ -201,9 +201,9 @@ export abstract class ConvenientObservable<T, TChange> implements IObservable<T,
|
|||
|
||||
/** @sealed */
|
||||
public map<TNew>(fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
|
||||
public map<TNew>(owner: Owner, fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
|
||||
public map<TNew>(fnOrOwner: Owner | ((value: T, reader: IReader) => TNew), fnOrUndefined?: (value: T, reader: IReader) => TNew): IObservable<TNew> {
|
||||
const owner = fnOrUndefined === undefined ? undefined : fnOrOwner as Owner;
|
||||
public map<TNew>(owner: DebugOwner, fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
|
||||
public map<TNew>(fnOrOwner: DebugOwner | ((value: T, reader: IReader) => TNew), fnOrUndefined?: (value: T, reader: IReader) => TNew): IObservable<TNew> {
|
||||
const owner = fnOrUndefined === undefined ? undefined : fnOrOwner as DebugOwner;
|
||||
const fn = fnOrUndefined === undefined ? fnOrOwner as (value: T, reader: IReader) => TNew : fnOrUndefined;
|
||||
|
||||
return _derived(
|
||||
|
|
|
@ -8,7 +8,7 @@ export interface IDebugNameData {
|
|||
* The owner object of an observable.
|
||||
* Used for debugging only, such as computing a name for the observable by iterating over the fields of the owner.
|
||||
*/
|
||||
readonly owner?: Owner | undefined;
|
||||
readonly owner?: DebugOwner | undefined;
|
||||
|
||||
/**
|
||||
* A string or function that returns a string that represents the name of the observable.
|
||||
|
@ -25,7 +25,7 @@ export interface IDebugNameData {
|
|||
|
||||
export class DebugNameData {
|
||||
constructor(
|
||||
public readonly owner: Owner | undefined,
|
||||
public readonly owner: DebugOwner | undefined,
|
||||
public readonly debugNameSource: DebugNameSource | undefined,
|
||||
public readonly referenceFn: Function | undefined,
|
||||
) { }
|
||||
|
@ -36,10 +36,10 @@ export class DebugNameData {
|
|||
}
|
||||
|
||||
/**
|
||||
* The owner object of an observable.
|
||||
* The owning object of an observable.
|
||||
* Is only used for debugging purposes, such as computing a name for the observable by iterating over the fields of the owner.
|
||||
*/
|
||||
export type Owner = object | undefined;
|
||||
export type DebugOwner = object | undefined;
|
||||
export type DebugNameSource = string | (() => string | undefined);
|
||||
|
||||
const countPerName = new Map<string, number>();
|
||||
|
|
|
@ -7,7 +7,7 @@ import { assertFn } from 'vs/base/common/assert';
|
|||
import { EqualityComparer, strictEquals } from 'vs/base/common/equals';
|
||||
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { BaseObservable, IChangeContext, IObservable, IObserver, IReader, ISettableObservable, ITransaction, _setDerivedOpts, } from 'vs/base/common/observableInternal/base';
|
||||
import { DebugNameData, IDebugNameData, Owner } from 'vs/base/common/observableInternal/debugName';
|
||||
import { DebugNameData, IDebugNameData, DebugOwner } from 'vs/base/common/observableInternal/debugName';
|
||||
import { getLogger } from 'vs/base/common/observableInternal/logging';
|
||||
|
||||
/**
|
||||
|
@ -17,8 +17,8 @@ import { getLogger } from 'vs/base/common/observableInternal/logging';
|
|||
* {@link computeFn} should start with a JS Doc using `@description` to name the derived.
|
||||
*/
|
||||
export function derived<T>(computeFn: (reader: IReader) => T): IObservable<T>;
|
||||
export function derived<T>(owner: Owner, computeFn: (reader: IReader) => T): IObservable<T>;
|
||||
export function derived<T>(computeFnOrOwner: ((reader: IReader) => T) | Owner, computeFn?: ((reader: IReader) => T) | undefined): IObservable<T> {
|
||||
export function derived<T>(owner: DebugOwner, computeFn: (reader: IReader) => T): IObservable<T>;
|
||||
export function derived<T>(computeFnOrOwner: ((reader: IReader) => T) | DebugOwner, computeFn?: ((reader: IReader) => T) | undefined): IObservable<T> {
|
||||
if (computeFn !== undefined) {
|
||||
return new Derived(
|
||||
new DebugNameData(computeFnOrOwner, undefined, computeFn),
|
||||
|
@ -39,7 +39,7 @@ export function derived<T>(computeFnOrOwner: ((reader: IReader) => T) | Owner, c
|
|||
);
|
||||
}
|
||||
|
||||
export function derivedWithSetter<T>(owner: Owner | undefined, computeFn: (reader: IReader) => T, setter: (value: T, transaction: ITransaction | undefined) => void): ISettableObservable<T> {
|
||||
export function derivedWithSetter<T>(owner: DebugOwner | undefined, computeFn: (reader: IReader) => T, setter: (value: T, transaction: ITransaction | undefined) => void): ISettableObservable<T> {
|
||||
return new DerivedWithSetter(
|
||||
new DebugNameData(owner, undefined, computeFn),
|
||||
computeFn,
|
||||
|
@ -105,7 +105,7 @@ export function derivedWithStore<T>(computeFn: (reader: IReader, store: Disposab
|
|||
export function derivedWithStore<T>(owner: object, computeFn: (reader: IReader, store: DisposableStore) => T): IObservable<T>;
|
||||
export function derivedWithStore<T>(computeFnOrOwner: ((reader: IReader, store: DisposableStore) => T) | object, computeFnOrUndefined?: ((reader: IReader, store: DisposableStore) => T)): IObservable<T> {
|
||||
let computeFn: (reader: IReader, store: DisposableStore) => T;
|
||||
let owner: Owner;
|
||||
let owner: DebugOwner;
|
||||
if (computeFnOrUndefined === undefined) {
|
||||
computeFn = computeFnOrOwner as any;
|
||||
owner = undefined;
|
||||
|
@ -128,10 +128,10 @@ export function derivedWithStore<T>(computeFnOrOwner: ((reader: IReader, store:
|
|||
}
|
||||
|
||||
export function derivedDisposable<T extends IDisposable | undefined>(computeFn: (reader: IReader) => T): IObservable<T>;
|
||||
export function derivedDisposable<T extends IDisposable | undefined>(owner: Owner, computeFn: (reader: IReader) => T): IObservable<T>;
|
||||
export function derivedDisposable<T extends IDisposable | undefined>(computeFnOrOwner: ((reader: IReader) => T) | Owner, computeFnOrUndefined?: ((reader: IReader) => T)): IObservable<T> {
|
||||
export function derivedDisposable<T extends IDisposable | undefined>(owner: DebugOwner, computeFn: (reader: IReader) => T): IObservable<T>;
|
||||
export function derivedDisposable<T extends IDisposable | undefined>(computeFnOrOwner: ((reader: IReader) => T) | DebugOwner, computeFnOrUndefined?: ((reader: IReader) => T)): IObservable<T> {
|
||||
let computeFn: (reader: IReader) => T;
|
||||
let owner: Owner;
|
||||
let owner: DebugOwner;
|
||||
if (computeFnOrUndefined === undefined) {
|
||||
computeFn = computeFnOrOwner as any;
|
||||
owner = undefined;
|
||||
|
|
|
@ -6,7 +6,7 @@ import { autorun } from 'vs/base/common/observableInternal/autorun';
|
|||
import { IObservable, IReader, observableValue, transaction } from './base';
|
||||
import { Derived, derived } from 'vs/base/common/observableInternal/derived';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { DebugNameData, Owner } from 'vs/base/common/observableInternal/debugName';
|
||||
import { DebugNameData, DebugOwner } from 'vs/base/common/observableInternal/debugName';
|
||||
import { strictEquals } from 'vs/base/common/equals';
|
||||
import { CancellationError } from 'vs/base/common/errors';
|
||||
|
||||
|
@ -179,7 +179,7 @@ export function derivedWithCancellationToken<T>(computeFn: (reader: IReader, can
|
|||
export function derivedWithCancellationToken<T>(owner: object, computeFn: (reader: IReader, cancellationToken: CancellationToken) => T): IObservable<T>;
|
||||
export function derivedWithCancellationToken<T>(computeFnOrOwner: ((reader: IReader, cancellationToken: CancellationToken) => T) | object, computeFnOrUndefined?: ((reader: IReader, cancellationToken: CancellationToken) => T)): IObservable<T> {
|
||||
let computeFn: (reader: IReader, store: CancellationToken) => T;
|
||||
let owner: Owner;
|
||||
let owner: DebugOwner;
|
||||
if (computeFnOrUndefined === undefined) {
|
||||
computeFn = computeFnOrOwner as any;
|
||||
owner = undefined;
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { autorun } from 'vs/base/common/observableInternal/autorun';
|
||||
import { autorun, autorunOpts } from 'vs/base/common/observableInternal/autorun';
|
||||
import { BaseObservable, ConvenientObservable, IObservable, IObserver, IReader, ITransaction, _setKeepObserved, _setRecomputeInitiallyAndOnChange, observableValue, subtransaction, transaction } from 'vs/base/common/observableInternal/base';
|
||||
import { DebugNameData, Owner, getFunctionName } from 'vs/base/common/observableInternal/debugName';
|
||||
import { DebugNameData, IDebugNameData, DebugOwner, getDebugName, } from 'vs/base/common/observableInternal/debugName';
|
||||
import { derived, derivedOpts } from 'vs/base/common/observableInternal/derived';
|
||||
import { getLogger } from 'vs/base/common/observableInternal/logging';
|
||||
import { IValueWithChangeEvent } from '../event';
|
||||
|
@ -54,21 +54,49 @@ export function observableFromPromise<T>(promise: Promise<T>): IObservable<{ val
|
|||
return observable;
|
||||
}
|
||||
|
||||
|
||||
export function observableFromEvent<T, TArgs = unknown>(
|
||||
owner: DebugOwner,
|
||||
event: Event<TArgs>,
|
||||
getValue: (args: TArgs | undefined) => T,
|
||||
): IObservable<T>;
|
||||
export function observableFromEvent<T, TArgs = unknown>(
|
||||
event: Event<TArgs>,
|
||||
getValue: (args: TArgs | undefined) => T,
|
||||
): IObservable<T> {
|
||||
return new FromEventObservable(event, getValue, () => FromEventObservable.globalTransaction, strictEquals);
|
||||
): IObservable<T>;
|
||||
export function observableFromEvent(...args:
|
||||
[owner: DebugOwner, event: Event<any>, getValue: (args: any | undefined) => any]
|
||||
| [event: Event<any>, getValue: (args: any | undefined) => any]
|
||||
): IObservable<any> {
|
||||
let owner;
|
||||
let event;
|
||||
let getValue;
|
||||
if (args.length === 3) {
|
||||
[owner, event, getValue] = args;
|
||||
} else {
|
||||
[event, getValue] = args;
|
||||
}
|
||||
return new FromEventObservable(
|
||||
new DebugNameData(owner, undefined, getValue),
|
||||
event,
|
||||
getValue,
|
||||
() => FromEventObservable.globalTransaction,
|
||||
strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
export function observableFromEventOpts<T, TArgs = unknown>(
|
||||
options: {
|
||||
options: IDebugNameData & {
|
||||
equalsFn?: EqualityComparer<T>;
|
||||
},
|
||||
event: Event<TArgs>,
|
||||
getValue: (args: TArgs | undefined) => T,
|
||||
): IObservable<T> {
|
||||
return new FromEventObservable(event, getValue, () => FromEventObservable.globalTransaction, options.equalsFn ?? strictEquals);
|
||||
return new FromEventObservable(
|
||||
new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? getValue),
|
||||
event,
|
||||
getValue, () => FromEventObservable.globalTransaction, options.equalsFn ?? strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
export class FromEventObservable<TArgs, T> extends BaseObservable<T> {
|
||||
|
@ -79,6 +107,7 @@ export class FromEventObservable<TArgs, T> extends BaseObservable<T> {
|
|||
private subscription: IDisposable | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly _debugNameData: DebugNameData,
|
||||
private readonly event: Event<TArgs>,
|
||||
public readonly _getValue: (args: TArgs | undefined) => T,
|
||||
private readonly _getTransaction: () => ITransaction | undefined,
|
||||
|
@ -88,7 +117,7 @@ export class FromEventObservable<TArgs, T> extends BaseObservable<T> {
|
|||
}
|
||||
|
||||
private getDebugName(): string | undefined {
|
||||
return getFunctionName(this._getValue);
|
||||
return this._debugNameData.getDebugName(this);
|
||||
}
|
||||
|
||||
public get debugName(): string {
|
||||
|
@ -424,9 +453,9 @@ export class KeepAliveObserver implements IObserver {
|
|||
}
|
||||
}
|
||||
|
||||
export function derivedObservableWithCache<T>(owner: Owner, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T> {
|
||||
export function derivedObservableWithCache<T>(owner: DebugOwner, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T> {
|
||||
let lastValue: T | undefined = undefined;
|
||||
const observable = derived(owner, reader => {
|
||||
const observable = derivedOpts({ owner, debugReferenceFn: computeFn }, reader => {
|
||||
lastValue = computeFn(reader, lastValue);
|
||||
return lastValue;
|
||||
});
|
||||
|
@ -457,7 +486,7 @@ export function derivedObservableWithWritableCache<T>(owner: object, computeFn:
|
|||
/**
|
||||
* When the items array changes, referential equal items are not mapped again.
|
||||
*/
|
||||
export function mapObservableArrayCached<TIn, TOut, TKey = TIn>(owner: Owner, items: IObservable<readonly TIn[]>, map: (input: TIn, store: DisposableStore) => TOut, keySelector?: (input: TIn) => TKey): IObservable<readonly TOut[]> {
|
||||
export function mapObservableArrayCached<TIn, TOut, TKey = TIn>(owner: DebugOwner, items: IObservable<readonly TIn[]>, map: (input: TIn, store: DisposableStore) => TOut, keySelector?: (input: TIn) => TKey): IObservable<readonly TOut[]> {
|
||||
let m = new ArrayMap(map, keySelector);
|
||||
const self = derivedOpts({
|
||||
debugReferenceFn: map,
|
||||
|
@ -533,11 +562,11 @@ export class ValueWithChangeEventFromObservable<T> implements IValueWithChangeEv
|
|||
}
|
||||
}
|
||||
|
||||
export function observableFromValueWithChangeEvent<T>(_owner: Owner, value: IValueWithChangeEvent<T>): IObservable<T> {
|
||||
export function observableFromValueWithChangeEvent<T>(owner: DebugOwner, value: IValueWithChangeEvent<T>): IObservable<T> {
|
||||
if (value instanceof ValueWithChangeEventFromObservable) {
|
||||
return value.observable;
|
||||
}
|
||||
return observableFromEvent(value.onDidChange, () => value.value);
|
||||
return observableFromEvent(owner, value.onDidChange, () => value.value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -546,7 +575,7 @@ export function observableFromValueWithChangeEvent<T>(_owner: Owner, value: IVal
|
|||
* When observed and any of the observables change, it has the value of the last changed observable.
|
||||
* If multiple observables change in the same transaction, the last observable wins.
|
||||
*/
|
||||
export function latestChangedValue<T extends IObservable<any>[]>(...observables: T): IObservable<ReturnType<T[number]['get']>> {
|
||||
export function latestChangedValue<T extends IObservable<any>[]>(owner: DebugOwner, observables: T): IObservable<ReturnType<T[number]['get']>> {
|
||||
if (observables.length === 0) {
|
||||
throw new BugIndicatingError();
|
||||
}
|
||||
|
@ -554,10 +583,10 @@ export function latestChangedValue<T extends IObservable<any>[]>(...observables:
|
|||
let hasLastChangedValue = false;
|
||||
let lastChangedValue: any = undefined;
|
||||
|
||||
return observableFromEvent<any, void>(cb => {
|
||||
const result = observableFromEvent<any, void>(owner, cb => {
|
||||
const store = new DisposableStore();
|
||||
for (const o of observables) {
|
||||
store.add(autorun(reader => {
|
||||
store.add(autorunOpts({ debugName: () => getDebugName(result, new DebugNameData(owner, undefined, undefined)) + '.updateLastChangedValue' }, reader => {
|
||||
hasLastChangedValue = true;
|
||||
lastChangedValue = o.read(reader);
|
||||
cb();
|
||||
|
@ -577,4 +606,5 @@ export function latestChangedValue<T extends IObservable<any>[]>(...observables:
|
|||
return observables[observables.length - 1].get();
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -140,7 +140,7 @@ export class ObservableCodeEditor extends Disposable {
|
|||
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));
|
||||
public readonly isReadonly = observableFromEvent(this, 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;
|
||||
|
@ -157,7 +157,7 @@ export class ObservableCodeEditor extends Disposable {
|
|||
reader => this.selections.read(reader)?.map(s => s.getStartPosition()) ?? null
|
||||
);
|
||||
|
||||
public readonly isFocused = observableFromEvent(e => {
|
||||
public readonly isFocused = observableFromEvent(this, e => {
|
||||
const d1 = this.editor.onDidFocusEditorWidget(e);
|
||||
const d2 = this.editor.onDidBlurEditorWidget(e);
|
||||
return {
|
||||
|
@ -175,7 +175,7 @@ export class ObservableCodeEditor extends Disposable {
|
|||
public readonly onDidType = observableSignal<string>(this);
|
||||
|
||||
public getOption<T extends EditorOption>(id: T): IObservable<FindComputedEditorOptionValueById<T>> {
|
||||
return observableFromEvent(cb => this.editor.onDidChangeConfiguration(e => {
|
||||
return observableFromEvent(this, cb => this.editor.onDidChangeConfiguration(e => {
|
||||
if (e.hasChanged(id)) { cb(undefined); }
|
||||
}), () => this.editor.getOption(id));
|
||||
}
|
||||
|
|
|
@ -27,15 +27,15 @@ export class DiffEditorEditors extends Disposable {
|
|||
private readonly _onDidContentSizeChange = this._register(new Emitter<IContentSizeChangedEvent>());
|
||||
public get onDidContentSizeChange() { return this._onDidContentSizeChange.event; }
|
||||
|
||||
public readonly modifiedScrollTop = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollTop */ this.modified.getScrollTop());
|
||||
public readonly modifiedScrollHeight = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollHeight */ this.modified.getScrollHeight());
|
||||
public readonly modifiedScrollTop = observableFromEvent(this, this.modified.onDidScrollChange, () => /** @description modified.getScrollTop */ this.modified.getScrollTop());
|
||||
public readonly modifiedScrollHeight = observableFromEvent(this, this.modified.onDidScrollChange, () => /** @description modified.getScrollHeight */ this.modified.getScrollHeight());
|
||||
|
||||
public readonly modifiedModel = observableCodeEditor(this.modified).model;
|
||||
|
||||
public readonly modifiedSelections = observableFromEvent(this.modified.onDidChangeCursorSelection, () => this.modified.getSelections() ?? []);
|
||||
public readonly modifiedSelections = observableFromEvent(this, this.modified.onDidChangeCursorSelection, () => this.modified.getSelections() ?? []);
|
||||
public readonly modifiedCursor = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.modifiedSelections.read(reader)[0]?.getPosition() ?? new Position(1, 1));
|
||||
|
||||
public readonly originalCursor = observableFromEvent(this.original.onDidChangeCursorPosition, () => this.original.getPosition() ?? new Position(1, 1));
|
||||
public readonly originalCursor = observableFromEvent(this, this.original.onDidChangeCursorPosition, () => this.original.getPosition() ?? new Position(1, 1));
|
||||
|
||||
public readonly isOriginalFocused = observableCodeEditor(this.original).isFocused;
|
||||
public readonly isModifiedFocused = observableCodeEditor(this.modified).isFocused;
|
||||
|
|
|
@ -81,7 +81,7 @@ export class DiffEditorViewZones extends Disposable {
|
|||
}));
|
||||
|
||||
const originalModelTokenizationCompleted = this._diffModel.map(m =>
|
||||
m ? observableFromEvent(m.model.original.onDidChangeTokens, () => m.model.original.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Completed) : undefined
|
||||
m ? observableFromEvent(this, m.model.original.onDidChangeTokens, () => m.model.original.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Completed) : undefined
|
||||
).map((m, reader) => m?.read(reader));
|
||||
|
||||
const alignments = derived<ILineRangeAlignment[] | null>((reader) => {
|
||||
|
|
|
@ -16,7 +16,7 @@ export class DiffEditorOptions {
|
|||
|
||||
private readonly _diffEditorWidth = observableValue<number>(this, 0);
|
||||
|
||||
private readonly _screenReaderMode = observableFromEvent(this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized());
|
||||
private readonly _screenReaderMode = observableFromEvent(this, this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized());
|
||||
|
||||
constructor(
|
||||
options: Readonly<IDiffEditorOptions>,
|
||||
|
|
|
@ -37,7 +37,7 @@ const width = 35;
|
|||
|
||||
export class DiffEditorGutter extends Disposable {
|
||||
private readonly _menu = this._register(this._menuService.createMenu(MenuId.DiffEditorHunkToolbar, this._contextKeyService));
|
||||
private readonly _actions = observableFromEvent(this._menu.onDidChange, () => this._menu.getActions());
|
||||
private readonly _actions = observableFromEvent(this, this._menu.onDidChange, () => this._menu.getActions());
|
||||
private readonly _hasActions = this._actions.map(a => a.length > 0);
|
||||
private readonly _showSash = derived(this, reader => this._options.renderSideBySide.read(reader) && this._hasActions.read(reader));
|
||||
|
||||
|
|
|
@ -26,8 +26,8 @@ export class MovedBlocksLinesFeature extends Disposable {
|
|||
public static readonly movedCodeBlockPadding = 4;
|
||||
|
||||
private readonly _element: SVGElement;
|
||||
private readonly _originalScrollTop = observableFromEvent(this._editors.original.onDidScrollChange, () => this._editors.original.getScrollTop());
|
||||
private readonly _modifiedScrollTop = observableFromEvent(this._editors.modified.onDidScrollChange, () => this._editors.modified.getScrollTop());
|
||||
private readonly _originalScrollTop = observableFromEvent(this, this._editors.original.onDidScrollChange, () => this._editors.original.getScrollTop());
|
||||
private readonly _modifiedScrollTop = observableFromEvent(this, this._editors.modified.onDidScrollChange, () => this._editors.modified.getScrollTop());
|
||||
private readonly _viewZonesChanged = observableSignalFromEvent('onDidChangeViewZones', this._editors.modified.onDidChangeViewZones);
|
||||
|
||||
public readonly width = observableValue(this, 0);
|
||||
|
|
|
@ -11,12 +11,12 @@ import { LineRange } from 'vs/editor/common/core/lineRange';
|
|||
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
|
||||
|
||||
export class EditorGutter<T extends IGutterItemInfo = IGutterItemInfo> extends Disposable {
|
||||
private readonly scrollTop = observableFromEvent(
|
||||
private readonly scrollTop = observableFromEvent(this,
|
||||
this._editor.onDidScrollChange,
|
||||
(e) => /** @description editor.onDidScrollChange */ this._editor.getScrollTop()
|
||||
);
|
||||
private readonly isScrollTopZero = this.scrollTop.map((scrollTop) => /** @description isScrollTopZero */ scrollTop === 0);
|
||||
private readonly modelAttached = observableFromEvent(
|
||||
private readonly modelAttached = observableFromEvent(this,
|
||||
this._editor.onDidChangeModel,
|
||||
(e) => /** @description editor.onDidChangeModel */ this._editor.hasModel()
|
||||
);
|
||||
|
|
|
@ -73,8 +73,8 @@ export class MultiDiffEditorWidgetImpl extends Disposable {
|
|||
return template;
|
||||
}));
|
||||
|
||||
public readonly scrollTop = observableFromEvent(this._scrollableElement.onScroll, () => /** @description scrollTop */ this._scrollableElement.getScrollPosition().scrollTop);
|
||||
public readonly scrollLeft = observableFromEvent(this._scrollableElement.onScroll, () => /** @description scrollLeft */ this._scrollableElement.getScrollPosition().scrollLeft);
|
||||
public readonly scrollTop = observableFromEvent(this, this._scrollableElement.onScroll, () => /** @description scrollTop */ this._scrollableElement.getScrollPosition().scrollTop);
|
||||
public readonly scrollLeft = observableFromEvent(this, this._scrollableElement.onScroll, () => /** @description scrollLeft */ this._scrollableElement.getScrollPosition().scrollLeft);
|
||||
|
||||
private readonly _viewItemsInfo = derivedWithStore<{ items: readonly VirtualizedViewItem[]; getItem: (viewModel: DocumentDiffItemViewModel) => VirtualizedViewItem }>(this,
|
||||
(reader, store) => {
|
||||
|
|
|
@ -34,7 +34,7 @@ export interface IGhostTextWidgetModel {
|
|||
|
||||
export class GhostTextWidget extends Disposable {
|
||||
private readonly isDisposed = observableValue(this, false);
|
||||
private readonly currentTextModel = observableFromEvent(this.editor.onDidChangeModel, () => /** @description editor.model */ this.editor.getModel());
|
||||
private readonly currentTextModel = observableFromEvent(this, this.editor.onDidChangeModel, () => /** @description editor.model */ this.editor.getModel());
|
||||
|
||||
constructor(
|
||||
private readonly editor: ICodeEditor,
|
||||
|
|
|
@ -59,14 +59,14 @@ export class InlineCompletionsController extends Disposable {
|
|||
})
|
||||
));
|
||||
|
||||
private readonly _suggestWidgetSelectedItem = observableFromEvent(cb => this._suggestWidgetAdaptor.onDidSelectedItemChange(() => {
|
||||
private readonly _suggestWidgetSelectedItem = observableFromEvent(this, 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(
|
||||
private readonly _enabledInConfig = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).enabled);
|
||||
private readonly _isScreenReaderEnabled = observableFromEvent(this, this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized());
|
||||
private readonly _editorDictationInProgress = observableFromEvent(this,
|
||||
this._contextKeyService.onDidChangeContext,
|
||||
() => this._contextKeyService.getContext(this.editor.getDomNode()).getValue('editorDictation.inProgress') === true
|
||||
);
|
||||
|
@ -114,7 +114,7 @@ export class InlineCompletionsController extends Disposable {
|
|||
|
||||
private readonly _playAccessibilitySignal = observableSignal(this);
|
||||
|
||||
private readonly _fontFamily = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).fontFamily);
|
||||
private readonly _fontFamily = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).fontFamily);
|
||||
|
||||
constructor(
|
||||
public readonly editor: ICodeEditor,
|
||||
|
|
|
@ -36,7 +36,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
|||
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
|
||||
|
||||
export class InlineCompletionsHintsWidget extends Disposable {
|
||||
private readonly alwaysShowToolbar = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).showToolbar === 'always');
|
||||
private readonly alwaysShowToolbar = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).showToolbar === 'always');
|
||||
|
||||
private sessionPosition: Position | undefined = undefined;
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ export interface IGhostTextWidgetModel {
|
|||
|
||||
export class GhostTextWidget extends Disposable {
|
||||
private readonly isDisposed = observableValue(this, false);
|
||||
private readonly currentTextModel = observableFromEvent(this.editor.onDidChangeModel, () => /** @description editor.model */ this.editor.getModel());
|
||||
private readonly currentTextModel = observableFromEvent(this, this.editor.onDidChangeModel, () => /** @description editor.model */ this.editor.getModel());
|
||||
|
||||
constructor(
|
||||
private readonly editor: ICodeEditor,
|
||||
|
|
|
@ -52,9 +52,9 @@ export class InlineEditController extends Disposable {
|
|||
private _jumpBackPosition: Position | undefined;
|
||||
private _isAccepting: ISettableObservable<boolean> = observableValue(this, false);
|
||||
|
||||
private readonly _enabled = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).enabled);
|
||||
private readonly _fontFamily = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).fontFamily);
|
||||
private readonly _backgroundColoring = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).backgroundColoring);
|
||||
private readonly _enabled = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).enabled);
|
||||
private readonly _fontFamily = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).fontFamily);
|
||||
private readonly _backgroundColoring = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).backgroundColoring);
|
||||
|
||||
|
||||
constructor(
|
||||
|
@ -84,7 +84,7 @@ export class InlineEditController extends Disposable {
|
|||
}));
|
||||
|
||||
//Check if the cursor is at the ghost text
|
||||
const cursorPosition = observableFromEvent(editor.onDidChangeCursorPosition, () => editor.getPosition());
|
||||
const cursorPosition = observableFromEvent(this, editor.onDidChangeCursorPosition, () => editor.getPosition());
|
||||
this._register(autorun(reader => {
|
||||
/** @description InlineEditController.cursorPositionChanged model */
|
||||
if (!this._enabled.read(reader)) {
|
||||
|
|
|
@ -27,7 +27,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
|||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
export class InlineEditHintsWidget extends Disposable {
|
||||
private readonly alwaysShowToolbar = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).showToolbar === 'always');
|
||||
private readonly alwaysShowToolbar = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).showToolbar === 'always');
|
||||
|
||||
private sessionPosition: Position | undefined = undefined;
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ export interface IAccessbilitySignalOptions {
|
|||
export class AccessibilitySignalService extends Disposable implements IAccessibilitySignalService {
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly sounds: Map<string, HTMLAudioElement> = new Map();
|
||||
private readonly screenReaderAttached = observableFromEvent(
|
||||
private readonly screenReaderAttached = observableFromEvent(this,
|
||||
this.accessibilityService.onDidChangeScreenReaderOptimized,
|
||||
() => /** @description accessibilityService.onDidChangeScreenReaderOptimized */ this.accessibilityService.isScreenReaderOptimized()
|
||||
);
|
||||
|
|
|
@ -4,13 +4,14 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { autorunOpts, IObservable, IReader, observableFromEvent } from 'vs/base/common/observable';
|
||||
import { autorunOpts, IObservable, IReader } from 'vs/base/common/observable';
|
||||
import { observableFromEventOpts } from 'vs/base/common/observableInternal/utils';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ContextKeyValue, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ContextKeyValue, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
/** Creates an observable update when a configuration key updates. */
|
||||
export function observableConfigValue<T>(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable<T> {
|
||||
return observableFromEvent(
|
||||
return observableFromEventOpts({ debugName: () => `Configuration Key "${key}"`, },
|
||||
(handleChange) => configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration(key)) {
|
||||
handleChange(e);
|
||||
|
|
|
@ -19,7 +19,7 @@ export class AccessibilitySignalLineDebuggerContribution
|
|||
) {
|
||||
super();
|
||||
|
||||
const isEnabled = observableFromEvent(
|
||||
const isEnabled = observableFromEvent(this,
|
||||
accessibilitySignalService.onSoundEnabledChanged(AccessibilitySignal.onDebugBreak),
|
||||
() => accessibilitySignalService.isSoundEnabled(AccessibilitySignal.onDebugBreak)
|
||||
);
|
||||
|
|
|
@ -35,7 +35,7 @@ export class EditorTextPropertySignalsContribution extends Disposable implements
|
|||
.some(signal => observableFromValueWithChangeEvent(this, this._accessibilitySignalService.getEnabledState(signal, false)).read(reader))
|
||||
);
|
||||
|
||||
private readonly _activeEditorObservable = observableFromEvent(
|
||||
private readonly _activeEditorObservable = observableFromEvent(this,
|
||||
this._editorService.onDidActiveEditorChange,
|
||||
(_) => {
|
||||
const activeTextEditorControl = this._editorService.activeTextEditorControl;
|
||||
|
|
|
@ -33,7 +33,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont
|
|||
const isEmbeddedDiffEditor = this._diffEditor instanceof EmbeddedDiffEditorWidget;
|
||||
|
||||
if (!isEmbeddedDiffEditor) {
|
||||
const computationResult = observableFromEvent(e => this._diffEditor.onDidUpdateDiff(e), () => /** @description diffEditor.diffComputationResult */ this._diffEditor.getDiffComputationResult());
|
||||
const computationResult = observableFromEvent(this, e => this._diffEditor.onDidUpdateDiff(e), () => /** @description diffEditor.diffComputationResult */ this._diffEditor.getDiffComputationResult());
|
||||
const onlyWhiteSpaceChange = computationResult.map(r => r && !r.identical && r.changes2.length === 0);
|
||||
|
||||
this._register(autorunWithStore((reader, store) => {
|
||||
|
|
|
@ -126,7 +126,7 @@ export class TempFileMergeEditorModeFactory implements IMergeEditorInputModelFac
|
|||
|
||||
class TempFileMergeEditorInputModel extends EditorModel implements IMergeEditorInputModel {
|
||||
private readonly savedAltVersionId = observableValue(this, this.model.resultTextModel.getAlternativeVersionId());
|
||||
private readonly altVersionId = observableFromEvent(
|
||||
private readonly altVersionId = observableFromEvent(this,
|
||||
e => this.model.resultTextModel.onDidChangeContent(e),
|
||||
() =>
|
||||
/** @description getAlternativeVersionId */ this.model.resultTextModel.getAlternativeVersionId()
|
||||
|
@ -340,7 +340,7 @@ export class WorkspaceMergeEditorModeFactory implements IMergeEditorInputModelFa
|
|||
}
|
||||
|
||||
class WorkspaceMergeEditorInputModel extends EditorModel implements IMergeEditorInputModel {
|
||||
public readonly isDirty = observableFromEvent(
|
||||
public readonly isDirty = observableFromEvent(this,
|
||||
Event.any(this.resultTextFileModel.onDidChangeDirty, this.resultTextFileModel.onDidSaveError),
|
||||
() => /** @description isDirty */ this.resultTextFileModel.isDirty()
|
||||
);
|
||||
|
|
|
@ -10,12 +10,12 @@ import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditor
|
|||
import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange';
|
||||
|
||||
export class EditorGutter<T extends IGutterItemInfo = IGutterItemInfo> extends Disposable {
|
||||
private readonly scrollTop = observableFromEvent(
|
||||
private readonly scrollTop = observableFromEvent(this,
|
||||
this._editor.onDidScrollChange,
|
||||
(e) => /** @description editor.onDidScrollChange */ this._editor.getScrollTop()
|
||||
);
|
||||
private readonly isScrollTopZero = this.scrollTop.map((scrollTop) => /** @description isScrollTopZero */ scrollTop === 0);
|
||||
private readonly modelAttached = observableFromEvent(
|
||||
private readonly modelAttached = observableFromEvent(this,
|
||||
this._editor.onDidChangeModel,
|
||||
(e) => /** @description editor.onDidChangeModel */ this._editor.hasModel()
|
||||
);
|
||||
|
|
|
@ -79,17 +79,17 @@ export abstract class CodeEditorView extends Disposable {
|
|||
this.editor.updateOptions(newOptions);
|
||||
}
|
||||
|
||||
public readonly isFocused = observableFromEvent(
|
||||
public readonly isFocused = observableFromEvent(this,
|
||||
Event.any(this.editor.onDidBlurEditorWidget, this.editor.onDidFocusEditorWidget),
|
||||
() => /** @description editor.hasWidgetFocus */ this.editor.hasWidgetFocus()
|
||||
);
|
||||
|
||||
public readonly cursorPosition = observableFromEvent(
|
||||
public readonly cursorPosition = observableFromEvent(this,
|
||||
this.editor.onDidChangeCursorPosition,
|
||||
() => /** @description editor.getPosition */ this.editor.getPosition()
|
||||
);
|
||||
|
||||
public readonly selection = observableFromEvent(
|
||||
public readonly selection = observableFromEvent(this,
|
||||
this.editor.onDidChangeCursorSelection,
|
||||
() => /** @description editor.getSelections */ this.editor.getSelections()
|
||||
);
|
||||
|
|
|
@ -61,11 +61,11 @@ export class ScmMultiDiffSourceResolver implements IMultiDiffSourceResolver {
|
|||
|
||||
async resolveDiffSource(uri: URI): Promise<IResolvedMultiDiffSource> {
|
||||
const { repositoryUri, groupId } = ScmMultiDiffSourceResolver.parseUri(uri)!;
|
||||
const repository = await waitForState(observableFromEvent(
|
||||
const repository = await waitForState(observableFromEvent(this,
|
||||
this._scmService.onDidAddRepository,
|
||||
() => [...this._scmService.repositories].find(r => r.provider.rootUri?.toString() === repositoryUri.toString()))
|
||||
);
|
||||
const group = await waitForState(observableFromEvent(
|
||||
const group = await waitForState(observableFromEvent(this,
|
||||
repository.provider.onDidChangeResourceGroups,
|
||||
() => repository.provider.groups.find(g => g.id === groupId)
|
||||
));
|
||||
|
|
|
@ -46,7 +46,7 @@ export class NotebookAccessibilityProvider extends Disposable implements IListAc
|
|||
|
||||
getAriaLabel(element: CellViewModel) {
|
||||
const event = Event.filter(this.onDidAriaLabelChange, e => e === element);
|
||||
return observableFromEvent(event, () => {
|
||||
return observableFromEvent(this, event, () => {
|
||||
const viewModel = this.viewModel();
|
||||
if (!viewModel) {
|
||||
return '';
|
||||
|
|
|
@ -33,17 +33,17 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/
|
|||
export class SCMActiveRepositoryController extends Disposable implements IWorkbenchContribution {
|
||||
private readonly _countBadgeConfig = observableConfigValue<'all' | 'focused' | 'off'>('scm.countBadge', 'all', this.configurationService);
|
||||
|
||||
private readonly _repositories = observableFromEvent(
|
||||
private readonly _repositories = observableFromEvent(this,
|
||||
Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository),
|
||||
() => this.scmService.repositories);
|
||||
|
||||
private readonly _focusedRepository = observableFromEventOpts<ISCMRepository | undefined>(
|
||||
{ equalsFn: () => false },
|
||||
{ owner: this, equalsFn: () => false },
|
||||
this.scmViewService.onDidFocusRepository,
|
||||
() => this.scmViewService.focusedRepository);
|
||||
|
||||
private readonly _activeEditor = observableFromEventOpts(
|
||||
{ equalsFn: () => false },
|
||||
{ owner: this, equalsFn: () => false },
|
||||
this.editorService.onDidActiveEditorChange,
|
||||
() => this.editorService.activeEditor);
|
||||
|
||||
|
@ -71,9 +71,9 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe
|
|||
* The focused repository takes precedence over the active editor repository when the observable
|
||||
* values are updated in the same transaction (or during the initial read of the observable value).
|
||||
*/
|
||||
private readonly _activeRepository = latestChangedValue(this._activeEditorRepository, this._focusedRepository);
|
||||
private readonly _activeRepository = latestChangedValue(this, [this._activeEditorRepository, this._focusedRepository]);
|
||||
|
||||
private readonly _countBadgeRepositories = derived(reader => {
|
||||
private readonly _countBadgeRepositories = derived(this, reader => {
|
||||
switch (this._countBadgeConfig.read(reader)) {
|
||||
case 'all': {
|
||||
const repositories = this._repositories.read(reader);
|
||||
|
@ -90,7 +90,7 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe
|
|||
}
|
||||
});
|
||||
|
||||
private readonly _countBadge = derived(reader => {
|
||||
private readonly _countBadge = derived(this, reader => {
|
||||
let total = 0;
|
||||
|
||||
for (const repository of this._countBadgeRepositories.read(reader)) {
|
||||
|
@ -171,7 +171,7 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe
|
|||
}
|
||||
|
||||
private _getRepositoryResourceCount(repository: ISCMRepository): IObservable<number> {
|
||||
return observableFromEvent(repository.provider.onDidChangeResources, () => getRepositoryResourceCount(repository.provider));
|
||||
return observableFromEvent(this, repository.provider.onDidChangeResources, () => /** @description repositoryResourceCount */ getRepositoryResourceCount(repository.provider));
|
||||
}
|
||||
|
||||
private _updateActivityCountBadge(count: number, store: DisposableStore): void {
|
||||
|
|
|
@ -81,8 +81,8 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri
|
|||
|
||||
this.summaryWidget = new Lazy(() => this._register(instantiationService.createInstance(CoverageToolbarWidget, this.editor)));
|
||||
|
||||
const modelObs = observableFromEvent(editor.onDidChangeModel, () => editor.getModel());
|
||||
const configObs = observableFromEvent(editor.onDidChangeConfiguration, i => i);
|
||||
const modelObs = observableFromEvent(this, editor.onDidChangeModel, () => editor.getModel());
|
||||
const configObs = observableFromEvent(this, editor.onDidChangeConfiguration, i => i);
|
||||
|
||||
const fileCoverage = derived(reader => {
|
||||
const report = coverage.selected.read(reader);
|
||||
|
|
|
@ -821,12 +821,12 @@ class TestingExplorerViewModel extends Disposable {
|
|||
this.tree.rerender();
|
||||
}));
|
||||
|
||||
const allOpenEditorInputs = observableFromEvent(
|
||||
const allOpenEditorInputs = observableFromEvent(this,
|
||||
editorService.onDidEditorsChange,
|
||||
() => new Set(editorGroupsService.groups.flatMap(g => g.editors).map(e => e.resource).filter(isDefined)),
|
||||
);
|
||||
|
||||
const activeResource = observableFromEvent(editorService.onDidActiveEditorChange, () => {
|
||||
const activeResource = observableFromEvent(this, editorService.onDidActiveEditorChange, () => {
|
||||
if (editorService.activeEditor instanceof DiffEditorInput) {
|
||||
return editorService.activeEditor.primary.resource;
|
||||
} else {
|
||||
|
|
Loading…
Reference in a new issue