Iterates on audio cues.

This commit is contained in:
Henning Dieterichs 2022-02-11 12:20:27 +01:00
parent 6e5373e758
commit e041149fd1
No known key found for this signature in database
GPG key ID: 771381EFFDB9EC06
3 changed files with 739 additions and 147 deletions

View file

@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter } from 'vs/base/common/event';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { firstNonWhitespaceIndex } from 'vs/base/common/strings';
@ -37,6 +38,9 @@ export class GhostTextController extends Disposable {
return this.activeController.value?.model;
}
private readonly activeModelDidChangeEmitter = this._register(new Emitter<void>());
public readonly onActiveModelDidChange = this.activeModelDidChangeEmitter.event;
constructor(
public readonly editor: ICodeEditor,
@IInstantiationService private readonly instantiationService: IInstantiationService
@ -71,6 +75,7 @@ export class GhostTextController extends Disposable {
this.editor
)
: undefined;
this.activeModelDidChangeEmitter.fire();
}
public shouldShowHoverAt(hoverRange: Range): boolean {

View file

@ -5,9 +5,9 @@
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IDebugService } from 'vs/workbench/contrib/debug/common/debug';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { Emitter, Event } from 'vs/base/common/event';
import { Event } from 'vs/base/common/event';
import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { raceTimeout } from 'vs/base/common/async';
@ -15,17 +15,18 @@ import { FileAccess } from 'vs/base/common/network';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers';
import { FoldingController } from 'vs/editor/contrib/folding/browser/folding';
import { FoldingModel } from 'vs/editor/contrib/folding/browser/foldingModel';
import { URI } from 'vs/base/common/uri';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { autorunDelta, constObservable, debouncedObservable, fromEvent, fromPromise, IObservable, LazyDerived, wasEventTriggeredRecently } from 'vs/workbench/contrib/audioCues/browser/observable';
import { ITextModel } from 'vs/editor/common/model';
import { GhostTextController } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextController';
export class AudioCueContribution extends DisposableStore implements IWorkbenchContribution {
export class AudioCueContribution extends Disposable implements IWorkbenchContribution {
private audioCuesEnabled = false;
private readonly store = this.add(new DisposableStore());
private readonly store = this._register(new DisposableStore());
constructor(
@IEditorService private readonly editorService: IEditorService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
@ -35,8 +36,8 @@ export class AudioCueContribution extends DisposableStore implements IWorkbenchC
this.updateAudioCuesEnabled();
});
this.add(
_configurationService.onDidChangeConfiguration((e) => {
this._register(
configurationService.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('audioCues.enabled')) {
this.updateAudioCuesEnabled();
}
@ -47,7 +48,7 @@ export class AudioCueContribution extends DisposableStore implements IWorkbenchC
}
private getAudioCuesEnabled(): boolean {
const value = this._configurationService.getValue<'auto' | 'on' | 'off'>('audioCues.enabled');
const value = this.configurationService.getValue<'auto' | 'on' | 'off'>('audioCues.enabled');
if (value === 'on') {
return true;
} else if (value === 'auto') {
@ -81,96 +82,90 @@ export class AudioCueContribution extends DisposableStore implements IWorkbenchC
? activeTextEditorControl
: undefined;
if (editor) {
this.handleCurrentEditor(editor, store);
if (editor && editor.hasModel()) {
this.handleCurrentEditor(editor, editor.getModel(), store);
}
}
)
);
}
private handleCurrentEditor(editor: ICodeEditor, store: DisposableStore): void {
private handleCurrentEditor(editor: ICodeEditor, editorModel: ITextModel, store: DisposableStore): void {
const features: Feature[] = [
this.instantiationService.createInstance(ErrorFeature),
this.instantiationService.createInstance(FoldedAreaFeature),
this.instantiationService.createInstance(BreakpointFeature),
this.instantiationService.createInstance(InlineCompletionFeature),
];
const featuresPerEditor = new Map(
features.map((feature) => [
feature,
feature.createForEditor(editor, editor.getModel()!.uri),
])
const observableFeatureStates = features.map((feature) =>
feature.getObservableState(editor, editorModel)
);
interface State {
lineNumber: number;
featureStates: Map<Feature, boolean>;
}
const curLineNumber = fromEvent(
editor.onDidChangeCursorPosition,
() => editor.getPosition()?.lineNumber
);
const debouncedLineNumber = debouncedObservable(curLineNumber, 100, store);
const computeNewState = (): State | undefined => {
if (!editor.hasModel()) {
const lineNumberWithObservableFeatures = debouncedLineNumber.map(
(lineNumber) => lineNumber === undefined ? undefined : {
lineNumber,
featureStatesForLine: observableFeatureStates.map(
(featureResult) =>
// This caches the feature state for the active line
new LazyDerived(
(reader) => featureResult.read(reader).isActive(lineNumber),
'isActiveForLine'
)
),
}
);
const isTyping = wasEventTriggeredRecently(
editorModel.onDidChangeContent.bind(editorModel),
1000,
store
);
const featureStatesBeforeTyping = isTyping.map((isTyping) =>
!isTyping
? undefined
: lineNumberWithObservableFeatures
.get()
?.featureStatesForLine?.map((featureState, idx) =>
features[idx].debounceWhileTyping ? featureState.get() : undefined
)
);
const state = new LazyDerived(reader => {
const lineInfo = lineNumberWithObservableFeatures.read(reader);
if (lineInfo === undefined) {
return undefined;
}
const position = editor.getPosition();
const lineNumber = position.lineNumber;
const featureStates = new Map(
features.map((feature) => [
feature,
featuresPerEditor.get(feature)!.isActive(lineNumber),
])
);
return {
lineNumber,
featureStates
lineNumber: lineInfo.lineNumber,
featureStates: new Map(
lineInfo.featureStatesForLine.map((featureState, idx) => [
features[idx],
featureStatesBeforeTyping.read(reader)?.at(idx) ??
featureState.read(reader),
])
),
};
};
}, 'state');
let lastState: State | undefined;
const updateState = () => {
const newState = computeNewState();
for (const feature of features) {
if (
newState &&
newState.featureStates.get(feature) &&
(!lastState?.featureStates?.get(feature) ||
newState.lineNumber !== lastState.lineNumber)
) {
this.playSound(feature.audioCueFilename);
store.add(
autorunDelta(state, ({ lastValue, newValue }) => {
for (const feature of features) {
if (
newValue?.featureStates.get(feature) &&
(!lastValue?.featureStates?.get(feature) ||
newValue.lineNumber !== lastValue.lineNumber)
) {
this.playSound(feature.audioCueFilename);
}
}
}
lastState = newState;
};
for (const feature of featuresPerEditor.values()) {
if (feature.onChange) {
store.add(feature.onChange(updateState));
}
}
{
let lastLineNumber = -1;
store.add(
editor.onDidChangeCursorPosition(() => {
const position = editor.getPosition();
if (!position) {
return;
}
const lineNumber = position.lineNumber;
if (lineNumber === lastLineNumber) {
return;
}
lastLineNumber = lineNumber;
updateState();
})
);
}
updateState();
})
);
}
private async playSound(fileName: string) {
@ -197,82 +192,69 @@ export class AudioCueContribution extends DisposableStore implements IWorkbenchC
interface Feature {
audioCueFilename: string;
createForEditor(
debounceWhileTyping?: boolean;
getObservableState(
editor: ICodeEditor,
uri: URI
): FeatureResult;
model: ITextModel
): IObservable<FeatureState>;
}
interface FeatureResult {
interface FeatureState {
isActive(lineNumber: number): boolean;
onChange?: Event<void>;
}
class ErrorFeature implements Feature {
public readonly audioCueFilename = 'error';
public readonly debounceWhileTyping = true;
constructor(@IMarkerService private readonly markerService: IMarkerService) { }
createForEditor(
editor: ICodeEditor,
uri: URI
): FeatureResult {
return {
isActive: (lineNumber) => {
const hasMarker = this.markerService
.read({ resource: uri })
.some(
(m) =>
m.severity === MarkerSeverity.Error &&
m.startLineNumber <= lineNumber &&
lineNumber <= m.endLineNumber
);
return hasMarker;
},
onChange: Event.map(
Event.filter(
this.markerService.onMarkerChanged,
(changedUris) => {
const curUri = editor.getModel()?.uri?.toString();
return (
!!curUri && changedUris.some((u) => u.toString() === curUri)
);
}
),
(x) => undefined
getObservableState(editor: ICodeEditor, model: ITextModel): IObservable<FeatureState> {
return fromEvent(
Event.filter(this.markerService.onMarkerChanged, (changedUris) =>
changedUris.some((u) => u.toString() === model.uri.toString())
),
};
() => ({
isActive: (lineNumber) => {
const hasMarker = this.markerService
.read({ resource: model.uri })
.some(
(m) =>
m.severity === MarkerSeverity.Error &&
m.startLineNumber <= lineNumber &&
lineNumber <= m.endLineNumber
);
return hasMarker;
},
})
);
}
}
class FoldedAreaFeature implements Feature {
public readonly audioCueFilename = 'foldedAreas';
createForEditor(
editor: ICodeEditor,
uri: URI
): FeatureResult {
const emitter = new Emitter<void>();
let foldingModel: FoldingModel | null = null;
editor
.getContribution<FoldingController>(FoldingController.ID)
?.getFoldingModel()
?.then((newFoldingModel) => {
foldingModel = newFoldingModel;
emitter.fire();
getObservableState(editor: ICodeEditor, model: ITextModel): IObservable<FeatureState> {
const foldingController = FoldingController.get(editor);
if (!foldingController) {
return constObservable({
isActive: () => false,
});
}
return {
isActive: lineNumber => {
const regionAtLine = foldingModel?.getRegionAtLine(lineNumber);
const foldingModel = fromPromise(
foldingController.getFoldingModel() ?? Promise.resolve(undefined)
);
return foldingModel.map((v) => ({
isActive: (lineNumber) => {
const regionAtLine = v.value?.getRegionAtLine(lineNumber);
const hasFolding = !regionAtLine
? false
: regionAtLine.isCollapsed &&
regionAtLine.startLineNumber === lineNumber;
return hasFolding;
},
onChange: emitter.event,
};
}));
}
}
@ -281,22 +263,52 @@ class BreakpointFeature implements Feature {
constructor(@IDebugService private readonly debugService: IDebugService) { }
createForEditor(
editor: ICodeEditor,
uri: URI
): FeatureResult {
return {
isActive: (lineNumber) => {
const breakpoints = this.debugService
.getModel()
.getBreakpoints({ uri, lineNumber });
const hasBreakpoints = breakpoints.length > 0;
return hasBreakpoints;
},
onChange: Event.map(
this.debugService.getModel().onDidChangeBreakpoints,
() => undefined
),
};
getObservableState(editor: ICodeEditor, model: ITextModel): IObservable<FeatureState> {
return fromEvent(
this.debugService.getModel().onDidChangeBreakpoints,
() => ({
isActive: (lineNumber) => {
const breakpoints = this.debugService
.getModel()
.getBreakpoints({ uri: model.uri, lineNumber });
const hasBreakpoints = breakpoints.length > 0;
return hasBreakpoints;
},
})
);
}
}
class InlineCompletionFeature implements Feature {
public readonly audioCueFilename = 'break';
getObservableState(editor: ICodeEditor, model: ITextModel): IObservable<FeatureState> {
const ghostTextController = GhostTextController.get(editor);
if (!ghostTextController) {
return constObservable({
isActive: () => false,
});
}
const activeGhostText = fromEvent(
ghostTextController.onActiveModelDidChange,
() => ghostTextController.activeModel
).map((activeModel) => (
activeModel
? fromEvent(
activeModel.inlineCompletionsModel.onDidChange,
() => activeModel.inlineCompletionsModel.ghostText
)
: undefined
));
return new LazyDerived(reader => {
const ghostText = activeGhostText.read(reader)?.read(reader);
return {
isActive(lineNumber) {
return ghostText?.lineNumber === lineNumber;
}
};
}, 'ghostText');
}
}

View file

@ -0,0 +1,575 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
export interface IObservable<T> {
/**
* Reads the current value.
*
* This causes a recomputation if needed.
* Calling this method forces changes to propagate to observers during update operations.
* Must not be called from {@link IObserver.handleChange}.
*/
get(): T;
/**
* Registers an observer.
*
* Calls {@link IObserver.handleChange} immediately after a change is noticed.
* Might happen while someone calls {@link IObservable.get} or {@link IObservable.read}.
*/
subscribe(observer: IObserver): void;
unsubscribe(observer: IObserver): void;
/**
* Calls {@link IObservable.get} and then {@link IReader.handleBeforeReadObservable}.
*/
read(reader: IReader): T;
map<TNew>(fn: (value: T) => TNew): IObservable<TNew>;
}
export interface IReader {
/**
* Reports an observable that was read.
*
* Is called by `Observable.read`.
*/
handleBeforeReadObservable<T>(observable: IObservable<T>): void;
}
export interface IObserver {
/**
* Indicates that an update operation is about to begin.
*
* During an update, invariants might not hold for subscribed observables and
* change events might be delayed.
* However, all changes must be reported before all update operations are over.
*/
beginUpdate<T>(observable: IObservable<T>): void;
/**
* Is called by a subscribed observable immediately after it notices a change.
*
* When {@link IObservable.get} returns and no change has been reported,
* there has been no change for that observable.
*
* Implementations must not call into other observables!
* The change should be processed when {@link IObserver.endUpdate} is called.
*/
handleChange<T>(observable: IObservable<T>): void;
/**
* Indicates that an update operation has completed.
*/
endUpdate<T>(observable: IObservable<T>): void;
}
export interface ISettable<T> {
set(value: T, transaction: ITransaction | undefined): void;
}
export interface ITransaction {
/**
* Calls `Observer.beginUpdate` immediately
* and `Observer.endUpdate` when the transaction is complete.
*/
updateObserver(
observer: IObserver,
observable: IObservable<any>
): void;
}
// === Base ===
export abstract class ConvenientObservable<T> implements IObservable<T> {
public abstract get(): T;
public abstract subscribe(observer: IObserver): void;
public abstract unsubscribe(observer: IObserver): void;
public read(reader: IReader): T {
reader.handleBeforeReadObservable(this);
return this.get();
}
public map<TNew>(fn: (value: T) => TNew): IObservable<TNew> {
return new LazyDerived((reader) => fn(this.read(reader)), '(mapped)');
}
}
export abstract class BaseObservable<T> extends ConvenientObservable<T> {
protected readonly observers = new Set<IObserver>();
public subscribe(observer: IObserver): void {
const len = this.observers.size;
this.observers.add(observer);
if (len === 0) {
this.onFirstObserverSubscribed();
}
}
public unsubscribe(observer: IObserver): void {
const deleted = this.observers.delete(observer);
if (deleted && this.observers.size === 0) {
this.onLastObserverUnsubscribed();
}
}
protected onFirstObserverSubscribed(): void { }
protected onLastObserverUnsubscribed(): void { }
}
export function transaction(fn: (tx: ITransaction) => void) {
const tx = new TransactionImpl();
try {
fn(tx);
} finally {
tx.finish();
}
}
class TransactionImpl implements ITransaction {
private readonly finishActions = new Array<() => void>();
public updateObserver(
observer: IObserver,
observable: IObservable<any>
): void {
this.finishActions.push(function () {
observer.endUpdate(observable);
});
observer.beginUpdate(observable);
}
public finish(): void {
for (const action of this.finishActions) {
action();
}
}
}
export class ObservableValue<T>
extends BaseObservable<T>
implements ISettable<T>
{
private value: T;
constructor(initialValue: T, public readonly name: string) {
super();
this.value = initialValue;
}
public get(): T {
return this.value;
}
public set(value: T, tx: ITransaction | undefined): void {
if (this.value === value) {
return;
}
if (!tx) {
transaction((tx) => {
this.set(value, tx);
});
return;
}
this.value = value;
for (const observer of this.observers) {
tx.updateObserver(observer, this);
observer.handleChange(this);
}
}
}
export function constObservable<T>(value: T): IObservable<T> {
return new ConstObservable(value);
}
class ConstObservable<T> extends ConvenientObservable<T> {
constructor(private readonly value: T) {
super();
}
public get(): T {
return this.value;
}
public subscribe(observer: IObserver): void {
// NO OP
}
public unsubscribe(observer: IObserver): void {
// NO OP
}
}
// == autorun ==
export function autorun(
fn: (reader: IReader) => void,
name: string
): IDisposable {
return new AutorunObserver(fn, name);
}
export class AutorunObserver implements IObserver, IReader, IDisposable {
public needsToRun = true;
private updateCount = 0;
/**
* The actual dependencies.
*/
private _dependencies = new Set<IObservable<any>>();
public get dependencies() {
return this._dependencies;
}
/**
* Dependencies that have to be removed when {@link runFn} ran through.
*/
private staleDependencies = new Set<IObservable<any>>();
constructor(
private readonly runFn: (reader: IReader) => void,
public readonly name: string
) {
this.runIfNeeded();
}
public handleBeforeReadObservable<T>(observable: IObservable<T>) {
this._dependencies.add(observable);
if (!this.staleDependencies.delete(observable)) {
observable.subscribe(this);
}
}
public handleChange() {
this.needsToRun = true;
if (this.updateCount === 0) {
this.runIfNeeded();
}
}
public beginUpdate() {
this.updateCount++;
}
public endUpdate() {
this.updateCount--;
if (this.updateCount === 0) {
this.runIfNeeded();
}
}
private runIfNeeded(): void {
if (!this.needsToRun) {
return;
}
// Assert: this.staleDependencies is an empty set.
const emptySet = this.staleDependencies;
this.staleDependencies = this._dependencies;
this._dependencies = emptySet;
this.needsToRun = false;
try {
this.runFn(this);
} finally {
// We don't want our observed observables to think that they are (not even temporarily) not being observed.
// Thus, we only unsubscribe from observables that are definitely not read anymore.
for (const o of this.staleDependencies) {
o.unsubscribe(this);
}
this.staleDependencies.clear();
}
}
public dispose() {
for (const o of this._dependencies) {
o.unsubscribe(this);
}
this._dependencies.clear();
}
}
export function autorunDelta<T>(
observable: IObservable<T>,
handler: (args: { lastValue: T | undefined; newValue: T }) => void
): IDisposable {
let _lastValue: T | undefined;
return autorun((reader) => {
const newValue = observable.read(reader);
const lastValue = _lastValue;
_lastValue = newValue;
handler({ lastValue, newValue });
}, '');
}
// == Lazy Derived ==
export class LazyDerived<T> extends ConvenientObservable<T> {
private readonly observer: LazyDerivedObserver<T>;
constructor(computeFn: (reader: IReader) => T, name: string) {
super();
this.observer = new LazyDerivedObserver(computeFn, name);
}
public subscribe(observer: IObserver): void {
this.observer.subscribe(observer);
}
public unsubscribe(observer: IObserver): void {
this.observer.unsubscribe(observer);
}
public get(): T {
return this.observer.get();
}
}
/**
* @internal
*/
class LazyDerivedObserver<T>
extends BaseObservable<T>
implements IReader, IObserver {
private hadValue = false;
private hasValue = false;
private value: T | undefined = undefined;
private updateCount = 0;
private _dependencies = new Set<IObservable<any>>();
public get dependencies(): ReadonlySet<IObservable<any>> {
return this._dependencies;
}
/**
* Dependencies that have to be removed when {@link runFn} ran through.
*/
private staleDependencies = new Set<IObservable<any>>();
constructor(
private readonly computeFn: (reader: IReader) => T,
public readonly name: string
) {
super();
}
protected override onLastObserverUnsubscribed(): void {
/**
* We are not tracking changes anymore, thus we have to assume
* that our cache is invalid.
*/
this.hasValue = false;
this.hadValue = false;
this.value = undefined;
for (const d of this._dependencies) {
d.unsubscribe(this);
}
this._dependencies.clear();
}
public handleBeforeReadObservable<T>(observable: IObservable<T>) {
this._dependencies.add(observable);
if (!this.staleDependencies.delete(observable)) {
observable.subscribe(this);
}
}
public handleChange() {
if (this.hasValue) {
this.hadValue = true;
this.hasValue = false;
}
// Not in transaction: Recompute & inform observers immediately
if (this.updateCount === 0 && this.observers.size > 0) {
this.get();
}
// Otherwise, recompute in `endUpdate` or on demand.
}
public beginUpdate() {
if (this.updateCount === 0) {
for (const r of this.observers) {
r.beginUpdate(this);
}
}
this.updateCount++;
}
public endUpdate() {
this.updateCount--;
if (this.updateCount === 0) {
if (this.observers.size > 0) {
// Propagate invalidation
this.get();
}
for (const r of this.observers) {
r.endUpdate(this);
}
}
}
public get(): T {
if (this.observers.size === 0) {
// Cache is not valid and don't refresh the cache.
// Observables should not be read in non-reactive contexts.
return this.computeFn(this);
}
if (this.updateCount > 0 && this.hasValue) {
// Refresh dependencies
for (const d of this._dependencies) {
// Maybe `.get()` triggers `handleChange`?
d.get();
if (!this.hasValue) {
// The other dependencies will refresh on demand
break;
}
}
}
if (!this.hasValue) {
const emptySet = this.staleDependencies;
this.staleDependencies = this._dependencies;
this._dependencies = emptySet;
const oldValue = this.value;
try {
this.value = this.computeFn(this);
} finally {
// We don't want our observed observables to think that they are (not even temporarily) not being observed.
// Thus, we only unsubscribe from observables that are definitely not read anymore.
for (const o of this.staleDependencies) {
o.unsubscribe(this);
}
this.staleDependencies.clear();
}
this.hasValue = true;
if (this.hadValue && oldValue !== this.value) {
//
for (const r of this.observers) {
r.handleChange(this);
}
}
}
return this.value!;
}
}
export function fromPromise<T>(promise: Promise<T>): IObservable<{ value?: T }> {
const observable = new ObservableValue<{ value?: T }>({}, 'promiseValue');
promise.then((value) => {
observable.set({ value }, undefined);
});
return observable;
}
export function fromEvent<TArgs, T>(
event: Event<TArgs>,
getValue: (args: TArgs | undefined) => T
): IObservable<T> {
return new FromEventObservable(event, getValue);
}
class FromEventObservable<TArgs, T> extends BaseObservable<T> {
private value: T | undefined;
private hasValue = false;
private subscription: IDisposable | undefined;
constructor(
private readonly event: Event<TArgs>,
private readonly getValue: (args: TArgs | undefined) => T
) {
super();
}
protected override onFirstObserverSubscribed(): void {
this.subscription = this.event(this.handleEvent);
}
private readonly handleEvent = (args: TArgs | undefined) => {
const newValue = this.getValue(args);
if (this.value !== newValue) {
this.value = newValue;
if (this.hasValue) {
transaction(tx => {
for (const o of this.observers) {
tx.updateObserver(o, this);
o.handleChange(this);
}
});
}
this.hasValue = true;
}
};
protected override onLastObserverUnsubscribed(): void {
this.subscription!.dispose();
this.subscription = undefined;
this.hasValue = false;
this.value = undefined;
}
public get(): T {
if (this.subscription) {
if (!this.hasValue) {
this.handleEvent(undefined);
}
return this.value!;
} else {
// no cache, as there are no subscribers to clean it up
return this.getValue(undefined);
}
}
}
export function debouncedObservable<T>(observable: IObservable<T>, debounceMs: number, disposableStore: DisposableStore): IObservable<T> {
const debouncedObservable = new ObservableValue(observable.get(), 'debounced');
let timeout: NodeJS.Timeout | undefined;
disposableStore.add(autorun(reader => {
const value = observable.read(reader);
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
transaction(tx => {
debouncedObservable.set(value, tx);
});
}, debounceMs);
}, 'debounce'));
return debouncedObservable;
}
export function wasEventTriggeredRecently(event: Event<any>, timeoutMs: number, disposableStore: DisposableStore): IObservable<boolean> {
const observable = new ObservableValue(false, 'triggeredRecently');
let timeout: NodeJS.Timeout | undefined;
disposableStore.add(event(() => {
observable.set(true, undefined);
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
observable.set(false, undefined);
}, timeoutMs);
}));
return observable;
}