diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index d859f958b2e..cab1972b2e0 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -188,8 +188,14 @@ export interface IStat { type: FileType2; } +export interface IWatchOptions { + recursive?: boolean; + exclude?: string[]; +} + export interface IFileSystemProviderBase { - onDidChange: Event; + onDidChangeFile: Event; + watch(resource: URI, opts: IWatchOptions): IDisposable; stat(resource: URI): TPromise; rename(from: URI, to: URI, opts: { flags: FileOpenFlags }): TPromise; mkdir(resource: URI): TPromise; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 3d49702be68..6bf0f87445f 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -232,12 +232,21 @@ declare module 'vscode' { // todo@joh add open/close calls? export interface FileSystemProvider2 { - _version: 6; + _version: 7; /** - * An event to signal that a resource has been created, changed, or deleted. + * An event to signal that a resource has been created, changed, or deleted. This + * event should fire for resources that are being [watched](#FileSystemProvider2.watch) + * by clients of this provider. */ - readonly onDidChange: Event; + readonly onDidChangeFile: Event; + + /** + * Subscribe to events in the file or folder denoted by `uri`. + * @param uri + * @param options + */ + watch(uri: Uri, options: { recursive?: boolean; excludes?: string[] }): Disposable; /** * Retrieve metadata about a file. Must throw an [`ENOENT`](#FileError.ENOENT)-error diff --git a/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts b/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts index d48c4b7ba43..0a9f3485f76 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts @@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { FileOpenFlags, IFileChange, IFileService, IFileSystemProviderBase, ISimpleReadWriteProvider, IStat } from 'vs/platform/files/common/files'; +import { FileOpenFlags, IFileChange, IFileService, IFileSystemProviderBase, ISimpleReadWriteProvider, IStat, IWatchOptions } from 'vs/platform/files/common/files'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { ExtHostContext, ExtHostFileSystemShape, IExtHostContext, IFileChangeDto, MainContext, MainThreadFileSystemShape } from '../node/extHost.protocol'; @@ -50,7 +50,7 @@ class RemoteFileSystemProvider implements ISimpleReadWriteProvider, IFileSystemP private readonly _onDidChange = new Emitter(); private readonly _registrations: IDisposable[]; - readonly onDidChange: Event = this._onDidChange.event; + readonly onDidChangeFile: Event = this._onDidChange.event; constructor( fileService: IFileService, @@ -66,6 +66,16 @@ class RemoteFileSystemProvider implements ISimpleReadWriteProvider, IFileSystemP this._onDidChange.dispose(); } + watch(resource: URI, opts: IWatchOptions) { + const session = Math.random(); + this._proxy.$watch(this._handle, session, resource, opts); + return { + dispose: () => { + this._proxy.$unwatch(this._handle, session); + } + }; + } + $onFileSystemChange(changes: IFileChangeDto[]): void { this._onDidChange.fire(changes.map(RemoteFileSystemProvider._createFileChange)); } diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 37f274df23a..bec0acdb06e 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -42,7 +42,7 @@ import { ITreeItem } from 'vs/workbench/common/views'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { IDisposable } from 'vs/base/common/lifecycle'; import { SerializedError } from 'vs/base/common/errors'; -import { IStat, FileChangeType, FileOpenFlags } from 'vs/platform/files/common/files'; +import { IStat, FileChangeType, FileOpenFlags, IWatchOptions } from 'vs/platform/files/common/files'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { CommentRule, CharacterPair, EnterAction } from 'vs/editor/common/modes/languageConfiguration'; import { ISingleEditOperation } from 'vs/editor/common/model'; @@ -569,15 +569,14 @@ export interface ExtHostWorkspaceShape { export interface ExtHostFileSystemShape { $stat(handle: number, resource: UriComponents): TPromise; - $readFile(handle: number, resource: UriComponents, flags: FileOpenFlags): TPromise; $writeFile(handle: number, resource: UriComponents, base64Encoded: string, flags: FileOpenFlags): TPromise; - $rename(handle: number, resource: UriComponents, target: UriComponents, flags: FileOpenFlags): TPromise; $mkdir(handle: number, resource: UriComponents): TPromise; $readdir(handle: number, resource: UriComponents): TPromise<[string, IStat][]>; - $delete(handle: number, resource: UriComponents): TPromise; + $watch(handle: number, session: number, resource: UriComponents, opts: IWatchOptions): void; + $unwatch(handle: number, session: number): void; } export interface ExtHostSearchShape { diff --git a/src/vs/workbench/api/node/extHostFileSystem.ts b/src/vs/workbench/api/node/extHostFileSystem.ts index 43612f7ab34..935c24b8365 100644 --- a/src/vs/workbench/api/node/extHostFileSystem.ts +++ b/src/vs/workbench/api/node/extHostFileSystem.ts @@ -58,18 +58,24 @@ class FsLinkProvider implements vscode.DocumentLinkProvider { class FileSystemProviderShim implements vscode.FileSystemProvider2 { - _version: 6; + _version: 7 = 7; - onDidChange: vscode.Event; + onDidChangeFile: vscode.Event; constructor(private readonly _delegate: vscode.FileSystemProvider) { if (!this._delegate.onDidChange) { - this.onDidChange = Event.None; + this.onDidChangeFile = Event.None; } else { - this.onDidChange = mapEvent(this._delegate.onDidChange, old => old.map(FileSystemProviderShim._modernizeFileChange)); + this.onDidChangeFile = mapEvent(this._delegate.onDidChange, old => old.map(FileSystemProviderShim._modernizeFileChange)); } } + watch(uri: vscode.Uri, options: {}): vscode.Disposable { + // does nothing because in the old API there was no notion of + // watch and provider decide what file events to generate... + return { dispose() { } }; + } + stat(resource: vscode.Uri): Thenable { return this._delegate.stat(resource).then(stat => FileSystemProviderShim._modernizeFileStat(stat)); } @@ -157,6 +163,7 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { private readonly _proxy: MainThreadFileSystemShape; private readonly _fsProvider = new Map(); private readonly _linkProvider = new FsLinkProvider(); + private readonly _watches = new Map(); private _handlePool: number = 0; @@ -170,12 +177,12 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { } registerFileSystemProvider(scheme: string, provider: vscode.FileSystemProvider, newProvider: vscode.FileSystemProvider2) { - if (newProvider && newProvider._version === 6) { + if (newProvider && newProvider._version === 7) { return this._doRegisterFileSystemProvider(scheme, newProvider); } else if (provider) { return this._doRegisterFileSystemProvider(scheme, new FileSystemProviderShim(provider)); } else { - throw new Error('IGNORED both provider'); + throw new Error('FAILED to register file system provider, the new provider does not meet the version-constraint and there is no fallback, old provider'); } } @@ -185,8 +192,8 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { this._fsProvider.set(handle, provider); this._proxy.$registerFileSystemProvider(handle, scheme); let reg: IDisposable; - if (provider.onDidChange) { - reg = provider.onDidChange(event => { + if (provider.onDidChangeFile) { + reg = provider.onDidChangeFile(event => { let newEvent = event.map(e => { let { uri: resource, type } = e; let newType: files.FileChangeType; @@ -243,4 +250,17 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { $mkdir(handle: number, resource: UriComponents): TPromise { return asWinJsPromise(token => this._fsProvider.get(handle).createDirectory(URI.revive(resource), token)); } + $watch(handle: number, session: number, resource: UriComponents, opts: files.IWatchOptions): void { + asWinJsPromise(token => { + let subscription = this._fsProvider.get(handle).watch(URI.revive(resource), opts); + this._watches.set(session, subscription); + }); + } + $unwatch(handle: number, session: number): void { + let subscription = this._watches.get(session); + if (subscription) { + subscription.dispose(); + this._watches.delete(session); + } + } } diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index df15aa6abad..b6619cd0664 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -93,7 +93,7 @@ export class FileService implements IFileService { protected readonly _onFileChanges: Emitter; protected readonly _onAfterOperation: Emitter; - private toDispose: IDisposable[]; + protected toDispose: IDisposable[]; private activeWorkspaceFileChangeWatcher: IDisposable; private activeFileChangesWatchers: ResourceMap; diff --git a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts index 7728937a544..418cade8906 100644 --- a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts +++ b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts @@ -4,25 +4,25 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; -import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; -import { IContent, IStreamContent, IFileStat, IResolveContentOptions, IUpdateContentOptions, IResolveFileOptions, IResolveFileResult, FileOperationEvent, FileOperation, IFileSystemProvider, IStat, FileType2, FileChangesEvent, ICreateFileOptions, FileOperationError, FileOperationResult, ITextSnapshot, StringSnapshot, FileOpenFlags, FileError } from 'vs/platform/files/common/files'; -import { TPromise } from 'vs/base/common/winjs.base'; import { posix } from 'path'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { isFalsyOrEmpty, distinct, flatten } from 'vs/base/common/arrays'; -import { Schemas } from 'vs/base/common/network'; -import { toDecodeStream, IDecodeStreamOptions, decodeStream } from 'vs/base/node/encoding'; +import { distinct, flatten, isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/map'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { IStorageService } from 'vs/platform/storage/common/storage'; +import { Schemas } from 'vs/base/common/network'; +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IDecodeStreamOptions, decodeStream, toDecodeStream } from 'vs/base/node/encoding'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { FileChangesEvent, FileError, FileOpenFlags, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileType2, IContent, ICreateFileOptions, IFileStat, IFileSystemProvider, IFilesConfiguration, IResolveContentOptions, IResolveFileOptions, IResolveFileResult, IStat, IStreamContent, ITextSnapshot, IUpdateContentOptions, StringSnapshot } 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'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; import { createReadableOfProvider, createReadableOfSnapshot, createWritableOfProvider } from 'vs/workbench/services/files/electron-browser/streams'; function toIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], recurse?: (tuple: [URI, IStat]) => boolean): TPromise { @@ -72,6 +72,77 @@ export function toDeepIFileStat(provider: IFileSystemProvider, tuple: [URI, ISta }); } +class WorkspaceWatchLogic { + + private _disposables: IDisposable[] = []; + private _watches = new Map(); + + constructor( + private _fileService: RemoteFileService, + @IConfigurationService private _configurationService: IConfigurationService, + @IWorkspaceContextService private _contextService: IWorkspaceContextService, + ) { + this._refresh(); + + this._disposables.push(this._contextService.onDidChangeWorkspaceFolders(e => { + for (const removed of e.removed) { + this._unwatchWorkspace(removed.uri); + } + for (const added of e.added) { + this._watchWorkspace(added.uri); + } + })); + this._disposables.push(this._contextService.onDidChangeWorkbenchState(e => { + this._refresh(); + })); + this._disposables.push(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('files.watcherExclude')) { + this._refresh(); + } + })); + } + + dispose(): void { + this._unwatchWorkspaces(); + this._disposables = dispose(this._disposables); + } + + private _refresh(): void { + this._unwatchWorkspaces(); + for (const folder of this._contextService.getWorkspace().folders) { + if (folder.uri.scheme !== Schemas.file) { + this._watchWorkspace(folder.uri); + } + } + } + + private _watchWorkspace(resource: URI) { + let exclude: string[] = []; + let config = this._configurationService.getValue({ resource }); + if (config.files && config.files.watcherExclude) { + for (const key in config.files.watcherExclude) { + if (config.files.watcherExclude[key] === true) { + exclude.push(key); + } + } + } + this._watches.set(resource.toString(), resource); + this._fileService.watchFileChanges(resource, { recursive: true, exclude }); + } + + private _unwatchWorkspace(resource: URI) { + if (this._watches.has(resource.toString())) { + this._fileService.unwatchFileChanges(resource); + this._watches.delete(resource.toString()); + } + } + + private _unwatchWorkspaces() { + this._watches.forEach(uri => this._fileService.unwatchFileChanges(uri)); + this._watches.clear(); + } +} + export class RemoteFileService extends FileService { private readonly _provider = new Map(); @@ -98,6 +169,7 @@ export class RemoteFileService extends FileService { ); this._supportedSchemes = JSON.parse(this._storageService.get('remote_schemes', undefined, '[]')); + this.toDispose.push(new WorkspaceWatchLogic(this, configurationService, contextService)); } registerProvider(authority: string, provider: IFileSystemProvider): IDisposable { @@ -109,7 +181,7 @@ export class RemoteFileService extends FileService { this._storageService.store('remote_schemes', JSON.stringify(distinct(this._supportedSchemes))); this._provider.set(authority, provider); - const reg = provider.onDidChange(changes => { + const reg = provider.onDidChangeFile(changes => { // forward change events this._onFileChanges.fire(new FileChangesEvent(changes)); }); @@ -480,15 +552,38 @@ export class RemoteFileService extends FileService { }); } - // TODO@Joh - file watching on demand! - public watchFileChanges(resource: URI): void { + private _activeWatches = new Map, count: number }>(); + + public watchFileChanges(resource: URI, opts: { recursive?: boolean, exclude?: string[] } = {}): void { if (resource.scheme === Schemas.file) { - super.watchFileChanges(resource); + return super.watchFileChanges(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() { } }; + }) + }); } + public unwatchFileChanges(resource: URI): void { if (resource.scheme === Schemas.file) { - super.unwatchFileChanges(resource); + return super.unwatchFileChanges(resource); + } + let entry = this._activeWatches.get(resource.toString()); + if (entry && --entry.count === 0) { + entry.unwatch.then(dispose); + this._activeWatches.delete(resource.toString()); } } }