UNPKG

@pkerschbaum/code-oss-file-service

Version:

VS Code ([microsoft/vscode](https://github.com/microsoft/vscode)) includes a rich "`FileService`" and "`DiskFileSystemProvider`" abstraction built on top of Node.js core modules (`fs`, `path`) and Electron's `shell` module. This package allows to use that

413 lines (340 loc) 10.7 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { once } from 'vs/base/common/functional'; import { Iterable } from 'vs/base/common/iterator'; /** * Enables logging of potentially leaked disposables. * * A disposable is considered leaked if it is not disposed or not registered as the child of * another disposable. This tracking is very simple an only works for classes that either * extend Disposable or use a DisposableStore. This means there are a lot of false positives. */ const TRACK_DISPOSABLES = false; let disposableTracker: IDisposableTracker | null = null; export interface IDisposableTracker { /** * Is called on construction of a disposable. */ trackDisposable(disposable: IDisposable): void; /** * Is called when a disposable is registered as child of another disposable (e.g. {@link DisposableStore}). * If parent is `null`, the disposable is removed from its former parent. */ setParent(child: IDisposable, parent: IDisposable | null): void; /** * Is called after a disposable is disposed. */ markAsDisposed(disposable: IDisposable): void; /** * Indicates that the given object is a singleton which does not need to be disposed. */ markAsSingleton(disposable: IDisposable): void; } export function setDisposableTracker(tracker: IDisposableTracker | null): void { disposableTracker = tracker; } if (TRACK_DISPOSABLES) { const __is_disposable_tracked__ = '__is_disposable_tracked__'; setDisposableTracker(new class implements IDisposableTracker { trackDisposable(x: IDisposable): void { const stack = new Error('Potentially leaked disposable').stack!; setTimeout(() => { if (!(x as any)[__is_disposable_tracked__]) { console.log(stack); } }, 3000); } setParent(child: IDisposable, parent: IDisposable | null): void { if (child && child !== Disposable.None) { try { (child as any)[__is_disposable_tracked__] = true; } catch { // noop } } } markAsDisposed(disposable: IDisposable): void { if (disposable && disposable !== Disposable.None) { try { (disposable as any)[__is_disposable_tracked__] = true; } catch { // noop } } } markAsSingleton(disposable: IDisposable): void { } }); } function trackDisposable<T extends IDisposable>(x: T): T { disposableTracker?.trackDisposable(x); return x; } function markAsDisposed(disposable: IDisposable): void { disposableTracker?.markAsDisposed(disposable); } function setParentOfDisposable(child: IDisposable, parent: IDisposable | null): void { disposableTracker?.setParent(child, parent); } function setParentOfDisposables(children: IDisposable[], parent: IDisposable | null): void { if (!disposableTracker) { return; } for (const child of children) { disposableTracker.setParent(child, parent); } } /** * Indicates that the given object is a singleton which does not need to be disposed. */ export function markAsSingleton<T extends IDisposable>(singleton: T): T { disposableTracker?.markAsSingleton(singleton); return singleton; } export class MultiDisposeError extends Error { constructor( public readonly errors: any[] ) { super(`Encountered errors while disposing of store. Errors: [${errors.join(', ')}]`); } } export interface IDisposable { dispose(): void; } export function isDisposable<E extends object>(thing: E): thing is E & IDisposable { return typeof (<IDisposable>thing).dispose === 'function' && (<IDisposable>thing).dispose.length === 0; } export function dispose<T extends IDisposable>(disposable: T): T; export function dispose<T extends IDisposable>(disposable: T | undefined): T | undefined; export function dispose<T extends IDisposable, A extends IterableIterator<T> = IterableIterator<T>>(disposables: IterableIterator<T>): A; export function dispose<T extends IDisposable>(disposables: Array<T>): Array<T>; export function dispose<T extends IDisposable>(disposables: ReadonlyArray<T>): ReadonlyArray<T>; export function dispose<T extends IDisposable>(arg: T | IterableIterator<T> | undefined): any { if (Iterable.is(arg)) { let errors: any[] = []; for (const d of arg) { if (d) { try { d.dispose(); } catch (e) { errors.push(e); } } } if (errors.length === 1) { throw errors[0]; } else if (errors.length > 1) { throw new MultiDisposeError(errors); } return Array.isArray(arg) ? [] : arg; } else if (arg) { arg.dispose(); return arg; } } export function combinedDisposable(...disposables: IDisposable[]): IDisposable { const parent = toDisposable(() => dispose(disposables)); setParentOfDisposables(disposables, parent); return parent; } export function toDisposable(fn: () => void): IDisposable { const self = trackDisposable({ dispose: once(() => { markAsDisposed(self); fn(); }) }); return self; } export class DisposableStore implements IDisposable { static DISABLE_DISPOSED_WARNING = false; private _toDispose = new Set<IDisposable>(); private _isDisposed = false; constructor() { trackDisposable(this); } /** * Dispose of all registered disposables and mark this object as disposed. * * Any future disposables added to this object will be disposed of on `add`. */ public dispose(): void { if (this._isDisposed) { return; } markAsDisposed(this); this._isDisposed = true; this.clear(); } /** * Returns `true` if this object has been disposed */ public get isDisposed(): boolean { return this._isDisposed; } /** * Dispose of all registered disposables but do not mark this object as disposed. */ public clear(): void { try { dispose(this._toDispose.values()); } finally { this._toDispose.clear(); } } public add<T extends IDisposable>(o: T): T { if (!o) { return o; } if ((o as unknown as DisposableStore) === this) { throw new Error('Cannot register a disposable on itself!'); } setParentOfDisposable(o, this); if (this._isDisposed) { if (!DisposableStore.DISABLE_DISPOSED_WARNING) { console.warn(new Error('Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!').stack); } } else { this._toDispose.add(o); } return o; } } export abstract class Disposable implements IDisposable { static readonly None = Object.freeze<IDisposable>({ dispose() { } }); private readonly _store = new DisposableStore(); constructor() { trackDisposable(this); setParentOfDisposable(this._store, this); } public dispose(): void { markAsDisposed(this); this._store.dispose(); } protected _register<T extends IDisposable>(o: T): T { if ((o as unknown as Disposable) === this) { throw new Error('Cannot register a disposable on itself!'); } return this._store.add(o); } } /** * Manages the lifecycle of a disposable value that may be changed. * * This ensures that when the disposable value is changed, the previously held disposable is disposed of. You can * also register a `MutableDisposable` on a `Disposable` to ensure it is automatically cleaned up. */ export class MutableDisposable<T extends IDisposable> implements IDisposable { private _value?: T; private _isDisposed = false; constructor() { trackDisposable(this); } get value(): T | undefined { return this._isDisposed ? undefined : this._value; } set value(value: T | undefined) { if (this._isDisposed || value === this._value) { return; } this._value?.dispose(); if (value) { setParentOfDisposable(value, this); } this._value = value; } clear() { this.value = undefined; } dispose(): void { this._isDisposed = true; markAsDisposed(this); this._value?.dispose(); this._value = undefined; } /** * Clears the value, but does not dispose it. * The old value is returned. */ clearAndLeak(): T | undefined { const oldValue = this._value; this._value = undefined; if (oldValue) { setParentOfDisposable(oldValue, null); } return oldValue; } } export class RefCountedDisposable { private _counter: number = 1; constructor( private readonly _disposable: IDisposable, ) { } acquire() { this._counter++; return this; } release() { if (--this._counter === 0) { this._disposable.dispose(); } return this; } } export interface IReference<T> extends IDisposable { readonly object: T; } export abstract class ReferenceCollection<T> { private readonly references: Map<string, { readonly object: T; counter: number; }> = new Map(); acquire(key: string, ...args: any[]): IReference<T> { let reference = this.references.get(key); if (!reference) { reference = { counter: 0, object: this.createReferencedObject(key, ...args) }; this.references.set(key, reference); } const { object } = reference; const dispose = once(() => { if (--reference!.counter === 0) { this.destroyReferencedObject(key, reference!.object); this.references.delete(key); } }); reference.counter++; return { object, dispose }; } protected abstract createReferencedObject(key: string, ...args: any[]): T; protected abstract destroyReferencedObject(key: string, object: T): void; } /** * Unwraps a reference collection of promised values. Makes sure * references are disposed whenever promises get rejected. */ export class AsyncReferenceCollection<T> { constructor(private referenceCollection: ReferenceCollection<Promise<T>>) { } async acquire(key: string, ...args: any[]): Promise<IReference<T>> { const ref = this.referenceCollection.acquire(key, ...args); try { const object = await ref.object; return { object, dispose: () => ref.dispose() }; } catch (error) { ref.dispose(); throw error; } } } export class ImmortalReference<T> implements IReference<T> { constructor(public object: T) { } dispose(): void { /* noop */ } } export function disposeOnReturn(fn: (store: DisposableStore) => void): void { const store = new DisposableStore(); try { fn(store); } finally { store.dispose(); } }