From 11d42b6e37048185cba0aa1ecf437f4aa1f556b4 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 1 Apr 2019 12:29:09 +0200 Subject: [PATCH] files2 - scaffold watch() that returns a disposable --- .../configuration/node/configuration.ts | 5 +- src/vs/platform/files/common/files.ts | 13 +--- .../browser/nodeless.simpleservices.ts | 4 +- .../browser/editors/fileEditorTracker.ts | 24 +++---- .../snippets/browser/snippetsService.ts | 22 +++---- .../configuration/node/configuration.ts | 27 ++++---- .../services/files/node/fileService.ts | 13 ++-- .../services/files/node/remoteFileService.ts | 51 +++------------ .../services/files2/common/fileService2.ts | 63 ++++++++++++++++--- .../files2/test/browser/fileService2.test.ts | 50 ++++++++++++++- .../themes/browser/workbenchThemeService.ts | 22 +++---- .../workbench/test/workbenchTestServices.ts | 10 +-- 12 files changed, 170 insertions(+), 134 deletions(-) diff --git a/src/vs/platform/configuration/node/configuration.ts b/src/vs/platform/configuration/node/configuration.ts index 8fe9ab3af47..605d6812d1b 100644 --- a/src/vs/platform/configuration/node/configuration.ts +++ b/src/vs/platform/configuration/node/configuration.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { onUnexpectedError } from 'vs/base/common/errors'; import { ConfigurationModelParser, ConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; import { ConfigWatcher } from 'vs/base/node/config'; @@ -70,8 +70,7 @@ export class FileServiceBasedUserConfiguration extends Disposable { this._register(fileService.onFileChanges(e => this.handleFileEvents(e))); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50)); - this.fileService.watch(this.configurationResource); - this._register(toDisposable(() => this.fileService.unwatch(this.configurationResource))); + this._register(this.fileService.watch(this.configurationResource)); } initialize(): Promise { diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index dbcdbd44031..5bf160442d6 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -164,14 +164,9 @@ export interface IFileService { del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise; /** - * Allows to start a watcher that reports file change events on the provided resource. + * Allows to start a watcher that reports file/folder change events on the provided resource. */ - watch(resource: URI): void; - - /** - * Allows to stop a watcher on the provided resource or absolute fs path. - */ - unwatch(resource: URI): void; + watch(resource: URI, opts?: IWatchOptions): IDisposable; /** * Frees up any resources occupied by this service. @@ -1135,8 +1130,4 @@ export interface ILegacyFileService { updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): Promise; createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise; - - watch(resource: URI): void; - - unwatch(resource: URI): void; } \ No newline at end of file diff --git a/src/vs/workbench/browser/nodeless.simpleservices.ts b/src/vs/workbench/browser/nodeless.simpleservices.ts index ec18782e100..46339fb6d8a 100644 --- a/src/vs/workbench/browser/nodeless.simpleservices.ts +++ b/src/vs/workbench/browser/nodeless.simpleservices.ts @@ -802,9 +802,7 @@ export class SimpleRemoteFileService implements IFileService { del(_resource: URI, _options?: { useTrash?: boolean, recursive?: boolean }): Promise { return Promise.resolve(); } - watch(_resource: URI): void { } - - unwatch(_resource: URI): void { } + watch(_resource: URI): IDisposable { return Disposable.None; } getWriteEncoding(_resource: URI): IResourceEncoding { return { encoding: 'utf8', hasBOM: false }; } diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts index 9aadc757a24..12dee821985 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts @@ -12,7 +12,7 @@ import { ITextFileService, ITextFileEditorModel } from 'vs/workbench/services/te import { FileOperationEvent, FileOperation, IFileService, FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { distinct, coalesce } from 'vs/base/common/arrays'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -31,10 +31,9 @@ import { withNullAsUndefined } from 'vs/base/common/types'; export class FileEditorTracker extends Disposable implements IWorkbenchContribution { - protected closeOnFileDelete: boolean; - - private modelLoadQueue: ResourceQueue; - private activeOutOfWorkspaceWatchers: ResourceMap; + private closeOnFileDelete: boolean; + private modelLoadQueue = new ResourceQueue(); + private activeOutOfWorkspaceWatchers = new Map(); constructor( @IEditorService private readonly editorService: IEditorService, @@ -49,9 +48,6 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut ) { super(); - this.modelLoadQueue = new ResourceQueue(); - this.activeOutOfWorkspaceWatchers = new ResourceMap(); - this.onConfigurationUpdated(configurationService.getValue()); this.registerListeners(); @@ -350,9 +346,9 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut }); // Handle no longer visible out of workspace resources - this.activeOutOfWorkspaceWatchers.forEach(resource => { + this.activeOutOfWorkspaceWatchers.forEach((disposable, resource) => { if (!visibleOutOfWorkspacePaths.get(resource)) { - this.fileService.unwatch(resource); + dispose(disposable); this.activeOutOfWorkspaceWatchers.delete(resource); } }); @@ -360,8 +356,8 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut // Handle newly visible out of workspace resources visibleOutOfWorkspacePaths.forEach(resource => { if (!this.activeOutOfWorkspaceWatchers.get(resource)) { - this.fileService.watch(resource); - this.activeOutOfWorkspaceWatchers.set(resource, resource); + const disposable = this.fileService.watch(resource); + this.activeOutOfWorkspaceWatchers.set(resource, disposable); } }); } @@ -369,8 +365,8 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut dispose(): void { super.dispose(); - // Dispose watchers if any - this.activeOutOfWorkspaceWatchers.forEach(resource => this.fileService.unwatch(resource)); + // Dispose remaining watchers if any + this.activeOutOfWorkspaceWatchers.forEach(disposable => dispose(disposable)); this.activeOutOfWorkspaceWatchers.clear(); } } diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts index 607b97ce40e..b659d7a0b75 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts @@ -114,20 +114,16 @@ namespace snippetExt { } function watch(service: IFileService, resource: URI, callback: (type: FileChangeType, resource: URI) => any): IDisposable { - let listener = service.onFileChanges(e => { - for (const change of e.changes) { - if (resources.isEqualOrParent(change.resource, resource)) { - callback(change.type, change.resource); + return combinedDisposable([ + service.watch(resource), + service.onFileChanges(e => { + for (const change of e.changes) { + if (resources.isEqualOrParent(change.resource, resource)) { + callback(change.type, change.resource); + } } - } - }); - service.watch(resource); - return { - dispose() { - listener.dispose(); - service.unwatch(resource); - } - }; + }) + ]); } class SnippetsService implements ISnippetsService { diff --git a/src/vs/workbench/services/configuration/node/configuration.ts b/src/vs/workbench/services/configuration/node/configuration.ts index a8cb65f81f8..1b0fa56d23e 100644 --- a/src/vs/workbench/services/configuration/node/configuration.ts +++ b/src/vs/workbench/services/configuration/node/configuration.ts @@ -10,7 +10,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import * as pfs from 'vs/base/node/pfs'; import * as errors from 'vs/base/common/errors'; import * as collections from 'vs/base/common/collections'; -import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { RunOnceScheduler, Delayer } from 'vs/base/common/async'; import { FileChangeType, FileChangesEvent, IContent, IFileService } from 'vs/platform/files/common/files'; import { ConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; @@ -355,6 +355,8 @@ class NodeBasedWorkspaceConfiguration extends AbstractWorkspaceConfiguration { class FileServiceBasedWorkspaceConfiguration extends AbstractWorkspaceConfiguration { private workspaceConfig: URI | null = null; + private workspaceConfigWatcher: IDisposable; + private readonly reloadConfigurationScheduler: RunOnceScheduler; constructor(private fileService: IFileService, from?: IWorkspaceConfiguration) { @@ -362,27 +364,22 @@ class FileServiceBasedWorkspaceConfiguration extends AbstractWorkspaceConfigurat this.workspaceConfig = from && from.workspaceIdentifier ? from.workspaceIdentifier.configPath : null; this._register(fileService.onFileChanges(e => this.handleWorkspaceFileEvents(e))); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50)); - this.watchWorkspaceConfigurationFile(); - this._register(toDisposable(() => this.unWatchWorkspaceConfigurtionFile())); + this.workspaceConfigWatcher = this.watchWorkspaceConfigurationFile(); } - private watchWorkspaceConfigurationFile(): void { + private watchWorkspaceConfigurationFile(): IDisposable { if (this.workspaceConfig) { - this.fileService.watch(this.workspaceConfig); + return this.fileService.watch(this.workspaceConfig); } - } - private unWatchWorkspaceConfigurtionFile(): void { - if (this.workspaceConfig) { - this.fileService.unwatch(this.workspaceConfig); - } + return Disposable.None; } protected loadWorkspaceConfigurationContents(workspaceIdentifier: IWorkspaceIdentifier): Promise { if (!(this.workspaceConfig && resources.isEqual(this.workspaceConfig, workspaceIdentifier.configPath))) { - this.unWatchWorkspaceConfigurtionFile(); + this.workspaceConfigWatcher = dispose(this.workspaceConfigWatcher); this.workspaceConfig = workspaceIdentifier.configPath; - this.watchWorkspaceConfigurationFile(); + this.workspaceConfigWatcher = this.watchWorkspaceConfigurationFile(); } return this.fileService.resolveContent(this.workspaceConfig) .then(content => content.value, e => { @@ -406,6 +403,12 @@ class FileServiceBasedWorkspaceConfiguration extends AbstractWorkspaceConfigurat } } } + + dispose(): void { + super.dispose(); + + this.workspaceConfigWatcher = dispose(this.workspaceConfigWatcher); + } } class CachedWorkspaceConfiguration extends Disposable implements IWorkspaceConfiguration { diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 65057c649c7..001e616d795 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -1002,7 +1002,7 @@ export class FileService extends Disposable implements ILegacyFileService, IFile }); } - watch(resource: uri): void { + watch(resource: uri): IDisposable { assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource for watching: ${resource}`); // Check for existing watcher first @@ -1010,7 +1010,7 @@ export class FileService extends Disposable implements ILegacyFileService, IFile if (entry) { entry.count += 1; - return; + return Disposable.None; } // Create or get watcher for provided path @@ -1066,6 +1066,8 @@ export class FileService extends Disposable implements ILegacyFileService, IFile count: 1, unwatch: () => watcherDisposable.dispose() }); + + return watcherDisposable; } private onRawFileChange(event: IRawFileChange): void { @@ -1119,13 +1121,6 @@ export class FileService extends Disposable implements ILegacyFileService, IFile this.activeFileChangesWatchers.clear(); } - - - - - - - // Tests only resolve(resource: uri, options?: IResolveFileOptions): Promise; diff --git a/src/vs/workbench/services/files/node/remoteFileService.ts b/src/vs/workbench/services/files/node/remoteFileService.ts index 27b0a0cd855..74c187b0916 100644 --- a/src/vs/workbench/services/files/node/remoteFileService.ts +++ b/src/vs/workbench/services/files/node/remoteFileService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; @@ -12,7 +12,7 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/res import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileWriteOptions, FileSystemProviderCapabilities, IContent, ICreateFileOptions, IFileSystemProvider, IFilesConfiguration, IResolveContentOptions, IStreamContent, ITextSnapshot, IUpdateContentOptions, StringSnapshot, IWatchOptions, ILegacyFileService, IFileService, toFileOperationResult, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileWriteOptions, FileSystemProviderCapabilities, IContent, ICreateFileOptions, IFileSystemProvider, IFilesConfiguration, IResolveContentOptions, IStreamContent, ITextSnapshot, IUpdateContentOptions, StringSnapshot, ILegacyFileService, IFileService, toFileOperationResult, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -23,10 +23,10 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; class WorkspaceWatchLogic extends Disposable { - private _watches = new Map(); + private _watches = new Map(); constructor( - private _fileService: RemoteFileService, + private _fileService: IFileService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, ) { @@ -76,19 +76,19 @@ class WorkspaceWatchLogic extends Disposable { } } } - this._watches.set(resource.toString(), resource); - this._fileService.watch(resource, { recursive: true, excludes }); + const disposable = this._fileService.watch(resource, { recursive: true, excludes }); + this._watches.set(resource.toString(), disposable); } private _unwatchWorkspace(resource: URI) { if (this._watches.has(resource.toString())) { - this._fileService.unwatch(resource); + dispose(this._watches.get(resource.toString())); this._watches.delete(resource.toString()); } } private _unwatchWorkspaces() { - this._watches.forEach(uri => this._fileService.unwatch(uri)); + this._watches.forEach(disposable => dispose(disposable)); this._watches.clear(); } } @@ -309,41 +309,6 @@ export class RemoteFileService extends FileService { content.value.on('end', () => resolve(result)); }); } - - private _activeWatches = new Map, count: number }>(); - - watch(resource: URI, opts: IWatchOptions = { recursive: false, excludes: [] }): void { - if (resource.scheme === Schemas.file) { - return super.watch(resource); - } - - const key = resource.toString(); - const entry = this._activeWatches.get(key); - if (entry) { - entry.count += 1; - return; - } - - this._activeWatches.set(key, { - count: 1, - unwatch: this._withProvider(resource).then(provider => { - return provider.watch(resource, opts); - }, _err => { - return { dispose() { } }; - }) - }); - } - - unwatch(resource: URI): void { - if (resource.scheme === Schemas.file) { - return super.unwatch(resource); - } - let entry = this._activeWatches.get(resource.toString()); - if (entry && --entry.count === 0) { - entry.unwatch.then(dispose); - this._activeWatches.delete(resource.toString()); - } - } } registerSingleton(ILegacyFileService, RemoteFileService); \ No newline at end of file diff --git a/src/vs/workbench/services/files2/common/fileService2.ts b/src/vs/workbench/services/files2/common/fileService2.ts index 0b91eac8d4b..7398133c01c 100644 --- a/src/vs/workbench/services/files2/common/fileService2.ts +++ b/src/vs/workbench/services/files2/common/fileService2.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; -import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ITextSnapshot, IUpdateContentOptions, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata } from 'vs/platform/files/common/files'; +import { Disposable, IDisposable, toDisposable, combinedDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ITextSnapshot, IUpdateContentOptions, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; @@ -20,6 +20,8 @@ export class FileService2 extends Disposable implements IFileService { //#region TODO@Ben HACKS private _legacy: IFileService | null; + private joinOnLegacy: Promise; + private joinOnImplResolve: (service: IFileService) => void; setLegacyService(legacy: IFileService): void { this._legacy = this._register(legacy); @@ -38,9 +40,6 @@ export class FileService2 extends Disposable implements IFileService { _serviceBrand: ServiceIdentifier; - private joinOnLegacy: Promise; - private joinOnImplResolve: (service: IFileService) => void; - constructor(@ILogService private logService: ILogService) { super(); @@ -586,12 +585,58 @@ export class FileService2 extends Disposable implements IFileService { private _onFileChanges: Emitter = this._register(new Emitter()); get onFileChanges(): Event { return this._onFileChanges.event; } - watch(resource: URI): void { - this.joinOnLegacy.then(legacy => legacy.watch(resource)); + private activeWatches = new Map(); + + watch(resource: URI, options: IWatchOptions = { recursive: false, excludes: [] }): IDisposable { + let watchDisposable: IDisposable = Disposable.None; + + // Watch and wire in disposable which is async + this.doWatch(resource, options).then(disposable => watchDisposable = disposable); + + return toDisposable(() => watchDisposable.dispose()); } - unwatch(resource: URI): void { - this.joinOnLegacy.then(legacy => legacy.unwatch(resource)); + async doWatch(resource: URI, options: IWatchOptions): Promise { + const provider = await this.withProvider(resource); + const key = this.toWatchKey(provider, resource, options); + + // Only start watching if we are the first for the given key + const entry = this.activeWatches.get(key) || { count: 0, disposable: provider.watch(resource, options) }; + if (!this.activeWatches.has(key)) { + this.activeWatches.set(key, entry); + } + + // Increment usage counter + entry.count += 1; + + return toDisposable(() => { + + // Unref + entry.count--; + + // Dispose only when last user is reached + if (entry.count === 0) { + dispose(entry.disposable); + this.activeWatches.delete(key); + } + }); + } + + private toWatchKey(provider: IFileSystemProvider, resource: URI, options: IWatchOptions): string { + const isPathCaseSensitive = !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); + + return [ + isPathCaseSensitive ? resource.toString() : resource.toString().toLowerCase(), // lowercase path is the provider is case insensitive + String(options.recursive), // use recursive: true | false as part of the key + options.excludes.join() // use excludes as part of the key + ].join(); + } + + dispose(): void { + super.dispose(); + + this.activeWatches.forEach(watcher => dispose(watcher.disposable)); + this.activeWatches.clear(); } //#endregion diff --git a/src/vs/workbench/services/files2/test/browser/fileService2.test.ts b/src/vs/workbench/services/files2/test/browser/fileService2.test.ts index 30a7a82d686..5208536c7ba 100644 --- a/src/vs/workbench/services/files2/test/browser/fileService2.test.ts +++ b/src/vs/workbench/services/files2/test/browser/fileService2.test.ts @@ -7,9 +7,10 @@ import * as assert from 'assert'; import { FileService2 } from 'vs/workbench/services/files2/common/fileService2'; import { URI } from 'vs/base/common/uri'; import { IFileSystemProviderRegistrationEvent, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { NullFileSystemProvider } from 'vs/workbench/test/workbenchTestServices'; import { NullLogService } from 'vs/platform/log/common/log'; +import { timeout } from 'vs/base/common/async'; suite('File Service 2', () => { @@ -61,4 +62,51 @@ suite('File Service 2', () => { assert.equal(registrations[1].scheme, 'test'); assert.equal(registrations[1].added, false); }); + + test('watch', async () => { + const service = new FileService2(new NullLogService()); + + let disposeCounter = 0; + service.registerProvider('test', new NullFileSystemProvider(() => { + return toDisposable(() => { + disposeCounter++; + }); + })); + await service.activateProvider('test'); + + const resource1 = URI.parse('test://foo/bar1'); + const watcher1Disposable = service.watch(resource1); + + await timeout(0); // service.watch() is async + assert.equal(disposeCounter, 0); + watcher1Disposable.dispose(); + assert.equal(disposeCounter, 1); + + disposeCounter = 0; + const resource2 = URI.parse('test://foo/bar2'); + const watcher2Disposable1 = service.watch(resource2); + const watcher2Disposable2 = service.watch(resource2); + const watcher2Disposable3 = service.watch(resource2); + + await timeout(0); // service.watch() is async + assert.equal(disposeCounter, 0); + watcher2Disposable1.dispose(); + assert.equal(disposeCounter, 0); + watcher2Disposable2.dispose(); + assert.equal(disposeCounter, 0); + watcher2Disposable3.dispose(); + assert.equal(disposeCounter, 1); + + disposeCounter = 0; + const resource3 = URI.parse('test://foo/bar3'); + const watcher3Disposable1 = service.watch(resource3); + const watcher3Disposable2 = service.watch(resource3, { recursive: true, excludes: [] }); + + await timeout(0); // service.watch() is async + assert.equal(disposeCounter, 0); + watcher3Disposable1.dispose(); + assert.equal(disposeCounter, 1); + watcher3Disposable2.dispose(); + assert.equal(disposeCounter, 2); + }); }); \ No newline at end of file diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index eeed8a58d7d..5e64858ee43 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -17,7 +17,7 @@ import { ColorThemeData } from './colorThemeData'; import { ITheme, Extensions as ThemingExtensions, IThemingRegistry } from 'vs/platform/theme/common/themeService'; import { Event, Emitter } from 'vs/base/common/event'; import { registerFileIconThemeSchemas } from 'vs/workbench/services/themes/common/fileIconThemeSchema'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ColorThemeStore } from 'vs/workbench/services/themes/browser/colorThemeStore'; import { FileIconThemeStore } from 'vs/workbench/services/themes/common/fileIconThemeStore'; import { FileIconThemeData } from 'vs/workbench/services/themes/common/fileIconThemeData'; @@ -75,11 +75,13 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { private container: HTMLElement; private readonly onColorThemeChange: Emitter; private watchedColorThemeLocation: URI | undefined; + private watchedColorThemeDisposable: IDisposable; private iconThemeStore: FileIconThemeStore; private currentIconTheme: FileIconThemeData; private readonly onFileIconThemeChange: Emitter; private watchedIconThemeLocation: URI | undefined; + private watchedIconThemeDisposable: IDisposable; private themingParticipantChangeListener: IDisposable; @@ -393,13 +395,12 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } if (this.fileService && !resources.isEqual(newTheme.location, this.watchedColorThemeLocation)) { - if (this.watchedColorThemeLocation) { - this.fileService.unwatch(this.watchedColorThemeLocation); - this.watchedColorThemeLocation = undefined; - } + this.watchedColorThemeDisposable = dispose(this.watchedColorThemeDisposable); + this.watchedColorThemeLocation = undefined; + if (newTheme.location && (newTheme.watch || !!this.environmentService.extensionDevelopmentLocationURI)) { this.watchedColorThemeLocation = newTheme.location; - this.fileService.watch(this.watchedColorThemeLocation); + this.watchedColorThemeDisposable = this.fileService.watch(newTheme.location); } } @@ -511,13 +512,12 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } if (this.fileService && !resources.isEqual(iconThemeData.location, this.watchedIconThemeLocation)) { - if (this.watchedIconThemeLocation) { - this.fileService.unwatch(this.watchedIconThemeLocation); - this.watchedIconThemeLocation = undefined; - } + this.watchedIconThemeDisposable = dispose(this.watchedIconThemeDisposable); + this.watchedIconThemeLocation = undefined; + if (iconThemeData.location && (iconThemeData.watch || !!this.environmentService.extensionDevelopmentLocationURI)) { this.watchedIconThemeLocation = iconThemeData.location; - this.fileService.watch(this.watchedIconThemeLocation); + this.watchedIconThemeDisposable = this.fileService.watch(iconThemeData.location); } } diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 2e8e1d28be6..25dfde26b4c 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -1032,10 +1032,8 @@ export class TestFileService implements IFileService { return Promise.resolve(); } - watch(_resource: URI): void { - } - - unwatch(_resource: URI): void { + watch(_resource: URI): IDisposable { + return Disposable.None; } getWriteEncoding(_resource: URI): IResourceEncoding { @@ -1582,7 +1580,9 @@ export class NullFileSystemProvider implements IFileSystemProvider { onDidChangeCapabilities: Event = Event.None; onDidChangeFile: Event = Event.None; - watch(resource: URI, opts: IWatchOptions): IDisposable { return Disposable.None; } + constructor(private disposableFactory: () => IDisposable = () => Disposable.None) { } + + watch(resource: URI, opts: IWatchOptions): IDisposable { return this.disposableFactory(); } stat(resource: URI): Promise { return Promise.resolve(undefined!); } mkdir(resource: URI): Promise { return Promise.resolve(undefined!); } readdir(resource: URI): Promise<[string, FileType][]> { return Promise.resolve(undefined!); } -- GitLab