Skip to content

Commit

Permalink
first cut of explict watch, #47475
Browse files Browse the repository at this point in the history
  • Loading branch information
jrieken committed Apr 18, 2018
1 parent c72b553 commit 034b377
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 36 deletions.
8 changes: 7 additions & 1 deletion src/vs/platform/files/common/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,14 @@ export interface IStat {
type: FileType2;
}

export interface IWatchOptions {
recursive?: boolean;
exclude?: string[];
}

export interface IFileSystemProviderBase {
onDidChange: Event<IFileChange[]>;
onDidChangeFile: Event<IFileChange[]>;
watch(resource: URI, opts: IWatchOptions): IDisposable;
stat(resource: URI): TPromise<IStat>;
rename(from: URI, to: URI, opts: { flags: FileOpenFlags }): TPromise<IStat>;
mkdir(resource: URI): TPromise<IStat>;
Expand Down
15 changes: 12 additions & 3 deletions src/vs/vscode.proposed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<FileChange2[]>;
readonly onDidChangeFile: Event<FileChange2[]>;

/**
* 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
Expand Down
14 changes: 12 additions & 2 deletions src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -50,7 +50,7 @@ class RemoteFileSystemProvider implements ISimpleReadWriteProvider, IFileSystemP
private readonly _onDidChange = new Emitter<IFileChange[]>();
private readonly _registrations: IDisposable[];

readonly onDidChange: Event<IFileChange[]> = this._onDidChange.event;
readonly onDidChangeFile: Event<IFileChange[]> = this._onDidChange.event;

constructor(
fileService: IFileService,
Expand All @@ -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));
}
Expand Down
7 changes: 3 additions & 4 deletions src/vs/workbench/api/node/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -569,15 +569,14 @@ export interface ExtHostWorkspaceShape {

export interface ExtHostFileSystemShape {
$stat(handle: number, resource: UriComponents): TPromise<IStat>;

$readFile(handle: number, resource: UriComponents, flags: FileOpenFlags): TPromise<string>;
$writeFile(handle: number, resource: UriComponents, base64Encoded: string, flags: FileOpenFlags): TPromise<void>;

$rename(handle: number, resource: UriComponents, target: UriComponents, flags: FileOpenFlags): TPromise<IStat>;
$mkdir(handle: number, resource: UriComponents): TPromise<IStat>;
$readdir(handle: number, resource: UriComponents): TPromise<[string, IStat][]>;

$delete(handle: number, resource: UriComponents): TPromise<void>;
$watch(handle: number, session: number, resource: UriComponents, opts: IWatchOptions): void;
$unwatch(handle: number, session: number): void;
}

export interface ExtHostSearchShape {
Expand Down
36 changes: 28 additions & 8 deletions src/vs/workbench/api/node/extHostFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,24 @@ class FsLinkProvider implements vscode.DocumentLinkProvider {

class FileSystemProviderShim implements vscode.FileSystemProvider2 {

_version: 6;
_version: 7 = 7;

onDidChange: vscode.Event<vscode.FileChange2[]>;
onDidChangeFile: vscode.Event<vscode.FileChange2[]>;

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<vscode.FileStat2> {
return this._delegate.stat(resource).then(stat => FileSystemProviderShim._modernizeFileStat(stat));
}
Expand Down Expand Up @@ -157,6 +163,7 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape {
private readonly _proxy: MainThreadFileSystemShape;
private readonly _fsProvider = new Map<number, vscode.FileSystemProvider2>();
private readonly _linkProvider = new FsLinkProvider();
private readonly _watches = new Map<number, IDisposable>();

private _handlePool: number = 0;

Expand All @@ -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');
}
}

Expand All @@ -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;
Expand Down Expand Up @@ -243,4 +250,17 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape {
$mkdir(handle: number, resource: UriComponents): TPromise<files.IStat, any> {
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export class FileService implements IFileService {
protected readonly _onFileChanges: Emitter<FileChangesEvent>;
protected readonly _onAfterOperation: Emitter<FileOperationEvent>;

private toDispose: IDisposable[];
protected toDispose: IDisposable[];

private activeWorkspaceFileChangeWatcher: IDisposable;
private activeFileChangesWatchers: ResourceMap<fs.FSWatcher>;
Expand Down
129 changes: 112 additions & 17 deletions src/vs/workbench/services/files/electron-browser/remoteFileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { 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 { localize } from 'vs/nls';
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 { 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 { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { localize } from 'vs/nls';
import { INotificationService } from 'vs/platform/notification/common/notification';
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<IFileStat> {
Expand Down Expand Up @@ -72,6 +72,77 @@ export function toDeepIFileStat(provider: IFileSystemProvider, tuple: [URI, ISta
});
}

class WorkspaceWatchLogic {

private _disposables: IDisposable[] = [];
private _watches = new Map<string, URI>();

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<IFilesConfiguration>({ 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<string, IFileSystemProvider>();
Expand All @@ -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 {
Expand All @@ -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));
});
Expand Down Expand Up @@ -480,15 +552,38 @@ export class RemoteFileService extends FileService {
});
}

// TODO@Joh - file watching on demand!
public watchFileChanges(resource: URI): void {
private _activeWatches = new Map<string, { unwatch: Thenable<IDisposable>, 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());
}
}
}

0 comments on commit 034b377

Please sign in to comment.