diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 8ce413f6e70..2887429e470 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -6,7 +6,7 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { CancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableMap, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { extUri as defaultExtUri, IExtUri } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { setTimeout0 } from 'vs/base/common/platform'; @@ -490,9 +490,34 @@ export function timeout(millis: number, token?: CancellationToken): CancelablePr }); } -export function disposableTimeout(handler: () => void, timeout = 0): IDisposable { - const timer = setTimeout(handler, timeout); - return toDisposable(() => clearTimeout(timer)); +/** + * Creates a timeout that can be disposed using its returned value. + * @param handler The timeout handler. + * @param timeout An optional timeout in milliseconds. + * @param store An optional {@link DisposableStore} that will have the timeout disposable managed automatically. + * + * @example + * const store = new DisposableStore; + * // Call the timeout after 1000ms at which point it will be automatically + * // evicted from the store. + * const timeoutDisposable = disposableTimeout(() => {}, 1000, store); + * + * if (foo) { + * // Cancel the timeout and evict it from store. + * timeoutDisposable.dispose(); + * } + */ +export function disposableTimeout(handler: () => void, timeout = 0, store?: DisposableStore): IDisposable { + const timer = setTimeout(() => { + handler(); + disposable.dispose(); + }, timeout); + const disposable = toDisposable(() => { + clearTimeout(timer); + store?.deleteAndLeak(disposable); + }); + store?.add(disposable); + return disposable; } /** diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index 0afa0bbd79b..55687fc6934 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -430,6 +430,28 @@ export class DisposableStore implements IDisposable { return o; } + + /** + * Deletes a disposable from store and disposes of it. This will not throw or warn and proceed to dispose the + * disposable even when the disposable is not part in the store. + */ + public delete(o: T): void { + if (!o) { + return; + } + if ((o as unknown as DisposableStore) === this) { + throw new Error('Cannot dispose a disposable on itself!'); + } + this._toDispose.delete(o); + o.dispose(); + } + + public deleteAndLeak(o: T): void { + if (!o) { + return; + } + this._toDispose.delete(o); + } } /** diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 7bfeb8cb546..e6ae569ad9d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -17,7 +17,7 @@ import { ErrorNoTelemetry, onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ISeparator, template } from 'vs/base/common/labels'; -import { Disposable, DisposableMap, IDisposable, MutableDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, MutableDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import { OS, OperatingSystem, isMacintosh, isWindows } from 'vs/base/common/platform'; @@ -87,7 +87,6 @@ import { importAMDNodeModule } from 'vs/amdX'; import { ISimpleSelectedSuggestion } from 'vs/workbench/services/suggest/browser/simpleSuggestWidget'; import type { IMarker, Terminal as XTermTerminal } from 'xterm'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; -import { generateUuid } from 'vs/base/common/uuid'; const enum Constants { /** @@ -122,52 +121,11 @@ const shellIntegrationSupportedShellTypes = [ WindowsShellType.PowerShell ]; -/** - * A wrapper around a {@link DisposableMap} that allows tracking transient disposables and then - * freeing their memory when they are no longer needed. This can be thought of as an alternative to - * a `IDisposable[]` for short lived disposables that allows declaring, initializing and registering - * the disposable in a single line. - * - * Some other benefits: - * - * - Disposable tracking is integrated thanks to using {@link DisposableMap} internally. - * - This is lighter weight and more concise than several {@link MutableDisposable}s. - * - * @example - * class Foo { - * private _transientDisposables = this._register(new TransientDisposableStore()); - * - * constructor() { - * // Cancel the callback if Foo is disposed - * this._transientDisposables.addTimeout(() => { ... }, 1000); - * } - * } - */ -class TransientDisposableStore implements IDisposable { - private _map = new DisposableMap(); - - addTimeout(handler: () => void, timeout = 0) { - const idx = generateUuid(); - this._map.set(idx, disposableTimeout(() => { - handler(); - this._map.deleteAndDispose(idx); - }, timeout)); - } - - // More functions can be added as needed - - dispose() { - this._map.dispose(); - } -} - export class TerminalInstance extends Disposable implements ITerminalInstance { private static _lastKnownCanvasDimensions: ICanvasDimensions | undefined; private static _lastKnownGridDimensions: IGridDimensions | undefined; private static _instanceIdCounter = 1; - private readonly _transientDisposables = this._register(new TransientDisposableStore()); - private readonly _scopedInstantiationService: IInstantiationService; private readonly _processManager: ITerminalProcessManager; @@ -801,7 +759,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._lineDataEventAddon = lineDataEventAddon; // Delay the creation of the bell listener to avoid showing the bell when the terminal // starts up or reconnects - this._transientDisposables.addTimeout(() => { + disposableTimeout(() => { this._register(xterm.raw.onBell(() => { if (this._configHelper.config.enableBell) { this.statusList.add({ @@ -813,7 +771,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._audioCueService.playSound(AudioCue.terminalBell.sound.getSound()); } })); - }, 1000); + }, 1000, this._store); this._register(xterm.raw.onSelectionChange(async () => this._onSelectionChange())); this._register(xterm.raw.buffer.onBufferChange(() => this._refreshAltBufferContextKey()));