src/vs/platform/files/common/diskFileSystemProvider.ts
256 LOC · 122 covered · 134 uncovered · 17 ranges · 58 concepts · 3 introducers · 32 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
diskFileSystemProvider.ts ×21
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { insert } from '../../../base/common/arrays.js';
import { ThrottledDelayer } from '../../../base/common/async.js';
import { onUnexpectedError } from '../../../base/common/errors.js';
import { Emitter } from '../../../base/common/event.js';
import { removeTrailingPathSeparator } from '../../../base/common/extpath.js';
import { Disposable, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
import { normalize } from '../../../base/common/path.js';
import { URI } from '../../../base/common/uri.js';
import { IFileChange, IFileSystemProvider, IWatchOptions } from './files.js';
import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, ILogMessage, INonRecursiveWatchRequest, IRecursiveWatcherOptions, isRecursiveWatchRequest, IUniversalWatchRequest, reviveFileChanges } from './watcher.js';
import { ILogService, LogLevel } from '../../log/common/log.js';
export interface IDiskFileSystemProviderOptions {
watcher?: {
/**
* Extra options for the recursive file watching.
*/
recursive?: IRecursiveWatcherOptions;
/**
* Forces all file watch requests to run through a
* single universal file watcher, both recursive
* and non-recursively.
*
* Enabling this option might cause some overhead,
* specifically the universal file watcher will run
* in a separate process given its complexity. Only
* enable it when you understand the consequences.
*/
forceUniversal?: boolean;
};
}
export abstract class AbstractDiskFileSystemProvider extends Disposable implements
Pick<IFileSystemProvider, 'watch'>,
Pick<IFileSystemProvider, 'onDidChangeFile'>,
Pick<IFileSystemProvider, 'onDidWatchError'> {
constructor(
private readonly options?: IDiskFileSystemProviderOptions
) {
super();
}
protected readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile = this._onDidChangeFile.event;
protected readonly _onDidWatchError = this._register(new Emitter<string>());
readonly onDidWatchError = this._onDidWatchError.event;
watch(resource: URI, opts: IWatchOptions): IDisposable {
if (opts.recursive || this.options?.watcher?.forceUniversal) {
return this.watchUniversal(resource, opts);
}
return this.watchNonRecursive(resource, opts);
}
private getRefreshWatchersDelay(count: number): number {
if (count > 200) {
// If there are many requests to refresh, start to throttle
// the refresh to reduce pressure. We see potentially thousands
// of requests coming in on startup repeatedly so we take it easy.
return 500;
}
// By default, use a short delay to keep watchers updating fast but still
// with a delay so that we can efficiently deduplicate requests or reuse
// existing watchers.
return 0;
}
//#region File Watching (universal)
private universalWatcher: AbstractUniversalWatcherClient | undefined;
private readonly universalWatchRequests: IUniversalWatchRequest[] = [];
private readonly universalWatchRequestDelayer = this._register(new ThrottledDelayer<void>(this.getRefreshWatchersDelay(this.universalWatchRequests.length)));
private watchUniversal(resource: URI, opts: IWatchOptions): IDisposable {
const request = this.toWatchRequest(resource, opts);
const remove = insert(this.universalWatchRequests, request);
// Trigger update
this.refreshUniversalWatchers();
return toDisposable(() => {
// Remove from list of paths to watch universally
remove();
// Trigger update
this.refreshUniversalWatchers();
});
}
private toWatchRequest(resource: URI, opts: IWatchOptions): IUniversalWatchRequest {
const request: IUniversalWatchRequest = {
path: this.toWatchPath(resource),
excludes: opts.excludes,
includes: opts.includes,
recursive: opts.recursive,
filter: opts.filter,
correlationId: opts.correlationId
};
if (isRecursiveWatchRequest(request)) {
// Adjust for polling
const usePolling = this.options?.watcher?.recursive?.usePolling;
if (usePolling === true) {
request.pollingInterval = this.options?.watcher?.recursive?.pollingInterval ?? 5000;
} else if (Array.isArray(usePolling)) {
if (usePolling.includes(request.path)) {
request.pollingInterval = this.options?.watcher?.recursive?.pollingInterval ?? 5000;
}
}
}
return request;
}
private refreshUniversalWatchers(): void {
this.universalWatchRequestDelayer.trigger(() => {
return this.doRefreshUniversalWatchers();
}, this.getRefreshWatchersDelay(this.universalWatchRequests.length)).catch(error => onUnexpectedError(error));
}
private doRefreshUniversalWatchers(): Promise<void> {
// Create watcher if this is the first time
if (!this.universalWatcher) {
this.universalWatcher = this._register(this.createUniversalWatcher(
changes => this._onDidChangeFile.fire(reviveFileChanges(changes)),
msg => this.onWatcherLogMessage(msg),
this.logService.getLevel() === LogLevel.Trace
));
// Apply log levels dynamically
this._register(this.logService.onDidChangeLogLevel(() => {
this.universalWatcher?.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace);
}));
}
// Ask to watch the provided paths
return this.universalWatcher.watch(this.universalWatchRequests);
}
protected abstract createUniversalWatcher(
onChange: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean
): AbstractUniversalWatcherClient;
//#endregion
//#region File Watching (non-recursive)
private nonRecursiveWatcher: AbstractNonRecursiveWatcherClient | undefined;
private readonly nonRecursiveWatchRequests: INonRecursiveWatchRequest[] = [];
private readonly nonRecursiveWatchRequestDelayer = this._register(new ThrottledDelayer<void>(this.getRefreshWatchersDelay(this.nonRecursiveWatchRequests.length)));
private watchNonRecursive(resource: URI, opts: IWatchOptions): IDisposable {
// Add to list of paths to watch non-recursively
const request: INonRecursiveWatchRequest = {
path: this.toWatchPath(resource),
excludes: opts.excludes,
includes: opts.includes,
recursive: false,
filter: opts.filter,
correlationId: opts.correlationId
};
const remove = insert(this.nonRecursiveWatchRequests, request);
// Trigger update
this.refreshNonRecursiveWatchers();
return toDisposable(() => {
// Remove from list of paths to watch non-recursively
remove();
// Trigger update
this.refreshNonRecursiveWatchers();
});
}
private refreshNonRecursiveWatchers(): void {
this.nonRecursiveWatchRequestDelayer.trigger(() => {
return this.doRefreshNonRecursiveWatchers();
}, this.getRefreshWatchersDelay(this.nonRecursiveWatchRequests.length)).catch(error => onUnexpectedError(error));
}
private doRefreshNonRecursiveWatchers(): Promise<void> {
// Create watcher if this is the first time
if (!this.nonRecursiveWatcher) {
this.nonRecursiveWatcher = this._register(this.createNonRecursiveWatcher(
changes => this._onDidChangeFile.fire(reviveFileChanges(changes)),
msg => this.onWatcherLogMessage(msg),
this.logService.getLevel() === LogLevel.Trace
));
// Apply log levels dynamically
this._register(this.logService.onDidChangeLogLevel(() => {
this.nonRecursiveWatcher?.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace);
}));
}
// Ask to watch the provided paths
return this.nonRecursiveWatcher.watch(this.nonRecursiveWatchRequests);
}
protected abstract createNonRecursiveWatcher(
onChange: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean
): AbstractNonRecursiveWatcherClient;
//#endregion
private onWatcherLogMessage(msg: ILogMessage): void {
if (msg.type === 'error') {
this._onDidWatchError.fire(msg.message);
}
this.logWatcherMessage(msg);
}
protected logWatcherMessage(msg: ILogMessage): void {
this.logService[msg.type](msg.message);
}
protected toFilePath(resource: URI): string {
}
private toWatchPath(resource: URI): string {
const filePath = this.toFilePath(resource);
// Ensure to have any trailing path separators removed, otherwise
// we may believe the path is not "real" and will convert every
// event back to this form, which is not warranted.
// See also https://github.com/microsoft/vscode/issues/210517
return removeTrailingPathSeparator(filePath);
}