diff --git a/src/typings/nsfw.d.ts b/src/typings/nsfw.d.ts index 8e0f911eb15b3d8c4eb628ac23e547edd213786b..d7d23f3c2b7ae7e245639ffe1414b3a2e3adcc74 100644 --- a/src/typings/nsfw.d.ts +++ b/src/typings/nsfw.d.ts @@ -5,8 +5,8 @@ declare module 'nsfw' { interface NsfwWatcher { - start(): void; - stop(): void; + start(): any; + stop(): any; } interface NsfwWatchingPromise { diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index e3d19f65eca43ae0fe29be266a871e67a969b99a..de5ef1f63bd565f453fa593b4d7239925a97057b 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -153,7 +153,7 @@ export class FileService implements IFileService { } private setupNsfwWorkspceWatching(): void { - this.toDispose.push(toDisposable(new NsfwWatcherService(this.contextService, this.options.watcherIgnoredPatterns, e => this._onFileChanges.fire(e), this.options.errorLogger, this.options.verboseLogging).startWatching())); + this.toDispose.push(toDisposable(new NsfwWatcherService(this.contextService, this.configurationService, e => this._onFileChanges.fire(e), this.options.errorLogger, this.options.verboseLogging).startWatching())); } public resolveFile(resource: uri, options?: IResolveFileOptions): TPromise { diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts b/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts index e0946b4359ebdc38546b21537f15f962f2ae552c..2fd7d222f6458d355ed01c71c6d912e41db2b14d 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts @@ -8,7 +8,7 @@ import * as path from 'path'; import * as watcher from 'vs/workbench/services/files/node/watcher/common'; import * as nsfw from 'nsfw'; import { IWatcherService, IWatcherRequest } from 'vs/workbench/services/files/node/watcher/nsfw/watcher'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { TPromise, ProgressCallback, TValueCallback } from 'vs/base/common/winjs.base'; import { ThrottledDelayer } from 'vs/base/common/async'; import { FileChangeType } from 'vs/platform/files/common/files'; @@ -17,21 +17,36 @@ nsfwActionToRawChangeType[nsfw.actions.CREATED] = FileChangeType.ADDED; nsfwActionToRawChangeType[nsfw.actions.MODIFIED] = FileChangeType.UPDATED; nsfwActionToRawChangeType[nsfw.actions.DELETED] = FileChangeType.DELETED; +interface IWatcherObjet { + start(): void; + stop(): void; +} interface IPathWatcher { - watcher?: { - start(): void; - stop(): void; - }; + ready: TPromise; + watcher?: IWatcherObjet; + ignored: string[]; } export class NsfwWatcherService implements IWatcherService { private static FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms) private _pathWatchers: { [watchPath: string]: IPathWatcher } = {}; + private _watcherPromise: TPromise; + private _progressCallback: ProgressCallback; + private _verboseLogging: boolean; + + + public initialize(verboseLogging: boolean): TPromise { + this._verboseLogging = verboseLogging; + this._watcherPromise = new TPromise((c, e, p) => { + this._progressCallback = p; + }); + return this._watcherPromise; + } public watch(request: IWatcherRequest): TPromise { - if (request.verboseLogging) { + if (this._verboseLogging) { console.log('request', request); } @@ -39,15 +54,21 @@ export class NsfwWatcherService implements IWatcherService { const fileEventDelayer = new ThrottledDelayer(NsfwWatcherService.FS_EVENT_DELAY); console.log('starting to watch ' + request.basePath); - this._pathWatchers[request.basePath] = {}; + + let readyPromiseCallback: TValueCallback; + this._pathWatchers[request.basePath] = { + ready: new TPromise(c => readyPromiseCallback = c), + ignored: request.ignored + }; const promise = new TPromise((c, e, p) => { nsfw(request.basePath, events => { + console.log('received events for path: ' + request.basePath); for (let i = 0; i < events.length; i++) { const e = events[i]; // Logging - if (request.verboseLogging) { + if (this._verboseLogging) { const logPath = e.action === nsfw.actions.RENAMED ? path.join(e.directory, e.oldFile) + ' -> ' + e.newFile : path.join(e.directory, e.file); console.log(e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]', logPath); } @@ -57,16 +78,17 @@ export class NsfwWatcherService implements IWatcherService { if (e.action === nsfw.actions.RENAMED) { // Rename fires when a file's name changes within a single directory absolutePath = path.join(e.directory, e.oldFile); - if (!this._isPathIgnored(absolutePath, request.ignored)) { + if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.basePath].ignored)) { undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath }); } absolutePath = path.join(e.directory, e.newFile); - if (!this._isPathIgnored(absolutePath, request.ignored)) { + if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.basePath].ignored)) { undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath }); } } else { absolutePath = path.join(e.directory, e.file); - if (!this._isPathIgnored(absolutePath, request.ignored)) { + if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.basePath].ignored)) { + console.log('adding event for path', absolutePath); undeliveredFileEvents.push({ type: nsfwActionToRawChangeType[e.action], path: absolutePath @@ -78,6 +100,7 @@ export class NsfwWatcherService implements IWatcherService { // Delay and send buffer fileEventDelayer.trigger(() => { const events = undeliveredFileEvents; + console.log('sending events!', events); undeliveredFileEvents = []; // Broadcast to clients normalized @@ -85,7 +108,7 @@ export class NsfwWatcherService implements IWatcherService { p(res); // Logging - if (request.verboseLogging) { + if (this._verboseLogging) { res.forEach(r => { console.log(' >> normalized', r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', r.path); }); @@ -96,43 +119,70 @@ export class NsfwWatcherService implements IWatcherService { }).then(watcher => { console.log('watcher ready ' + request.basePath); this._pathWatchers[request.basePath].watcher = watcher; - return watcher.start(); + const startPromise = watcher.start(); + startPromise.then(() => readyPromiseCallback(watcher)); + return startPromise; }); }); return promise; } - public setRoots(roots: string[]): TPromise { + // TODO: This should probably be the only way to watch a folder + public setRoots(roots: IWatcherRequest[]): TPromise { const normalizedRoots = this._normalizeRoots(roots); - const rootsToStartWatching = normalizedRoots.filter(r => !(r in this._pathWatchers)); - const rootsToStopWatching = Object.keys(this._pathWatchers).filter(r => normalizedRoots.indexOf(r) === -1); - // TODO: Don't watch inner folders - // TODO: Move verboseLogging to constructor + // Start watching roots that are not currently being watched + const rootsToStartWatching = normalizedRoots.filter(r => { + return !(r.basePath in this._pathWatchers); + }); + + // Stop watching roots that don't exist in the new roots + const rootsToStopWatching = Object.keys(this._pathWatchers).filter(r => { + return normalizedRoots.every(normalizedRoot => normalizedRoot.basePath !== r); + }); + + // TODO: Support updating roots when only the ignored files change + // This should be just a matter of updating the ignored part + const rootsWithChangedOptions = Object.keys(this._pathWatchers).filter(r => { + return normalizedRoots.some(normalizedRoot => { + if (normalizedRoot.basePath !== r) { + return false; + } + const ignored = this._pathWatchers[r].ignored; + // TODO: Improve comments, refactor + if (normalizedRoot.ignored.length !== ignored.length) { + console.log('ignored changed on root: ' + r); + this._pathWatchers[r].ignored = normalizedRoot.ignored; + return true; + } + // Check deep equality + for (let i = 0; i < ignored.length; i++) { + if (normalizedRoot.ignored[i] !== ignored[i]) { + console.log('ignored changed on root: ' + r); + this._pathWatchers[r].ignored = normalizedRoot.ignored; + return true; + } + } + return false; + }); + }); + // Logging - if (true) { - console.log(`Set watch roots: start: [${rootsToStartWatching.join(',')}], stop: [${rootsToStopWatching.join(',')}]`); + if (this._verboseLogging) { + console.log(`Set watch roots: start: [${rootsToStartWatching.map(r => r.basePath).join(',')}], stop: [${rootsToStopWatching.join(',')}], changed: [${rootsWithChangedOptions.join(', ')}]`); } const promises: TPromise[] = []; - if (rootsToStartWatching.length) { - rootsToStartWatching.forEach(root => { - promises.push(this.watch({ - basePath: root, - ignored: [], - // TODO: Inherit from initial request - verboseLogging: true - })); - }); - } - if (rootsToStopWatching.length) { - rootsToStopWatching.forEach(root => { - this._pathWatchers[root].watcher.stop(); - delete this._pathWatchers[root]; - }); - } + // Stop watching some roots + rootsToStopWatching.forEach(root => { + this._pathWatchers[root].ready.then(watcher => watcher.stop()); + delete this._pathWatchers[root]; + }); + + // Start watching some roots + rootsToStartWatching.forEach(root => promises.push(this.watch(root))); // TODO: Don't watch sub-folders of folders return TPromise.join(promises).then(() => void 0); @@ -142,13 +192,15 @@ export class NsfwWatcherService implements IWatcherService { * Normalizes a set of root paths by removing any folders that are * sub-folders of other roots. */ - protected _normalizeRoots(roots: string[]): string[] { + protected _normalizeRoots(roots: IWatcherRequest[]): IWatcherRequest[] { return roots.filter(r => roots.every(other => { - return !(r.length > other.length && r.indexOf(other) === 0); + return !(r.basePath.length > other.basePath.length && r.basePath.indexOf(other.basePath) === 0); })); } private _isPathIgnored(absolutePath: string, ignored: string[]): boolean { + console.log('is "' + absolutePath + '" ignored? ' + (ignored && ignored.some(ignore => glob.match(ignore, absolutePath)))); + console.log(' ignored: ', ignored); return ignored && ignored.some(ignore => glob.match(ignore, absolutePath)); } } diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts b/src/vs/workbench/services/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts index f7496d0dc55191896cb06fb94a34c7921a41f05f..caca4e25996aa9b04334da9a5f0df20f8ae95fcd 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts +++ b/src/vs/workbench/services/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts @@ -7,9 +7,16 @@ import assert = require('assert'); import { NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService'; +import { IWatcherRequest } from "vs/workbench/services/files/node/watcher/nsfw/watcher"; class TestNsfwWatcherService extends NsfwWatcherService { - public normalizeRoots(roots: string[]): string[] { return this._normalizeRoots(roots); } + public normalizeRoots(roots: string[]): string[] { + // Work with strings as paths to simplify testing + const requests: IWatcherRequest[] = roots.map(r => { + return { basePath: r, ignored: [] }; + }); + return this._normalizeRoots(requests).map(r => r.basePath); + } } suite('NSFW Watcher Service', () => { diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/watcher.ts b/src/vs/workbench/services/files/node/watcher/nsfw/watcher.ts index 2676e0e2b09ac3d351806f13c83bf79080d8689c..97896d21b3e6c75b020cdaa0f5a668cc3c95a845 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/watcher.ts +++ b/src/vs/workbench/services/files/node/watcher/nsfw/watcher.ts @@ -10,11 +10,12 @@ import { TPromise } from 'vs/base/common/winjs.base'; export interface IWatcherRequest { basePath: string; ignored: string[]; - verboseLogging: boolean; + // verboseLogging: boolean; } export interface IWatcherService { - setRoots(roots: string[]): TPromise; + initialize(verboseLogging: boolean): TPromise; + setRoots(roots: IWatcherRequest[]): TPromise; watch(request: IWatcherRequest): TPromise; } diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/watcherIpc.ts b/src/vs/workbench/services/files/node/watcher/nsfw/watcherIpc.ts index 971cdc3594ee6b3a8163fdd28d68b4f92c0a7370..5a523a6e8a7f8c893527b42627072f2218dc4ae1 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/watcherIpc.ts +++ b/src/vs/workbench/services/files/node/watcher/nsfw/watcherIpc.ts @@ -10,6 +10,8 @@ import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { IWatcherRequest, IWatcherService } from './watcher'; export interface IWatcherChannel extends IChannel { + call(command: 'initialize', verboseLogging: boolean): TPromise; + call(command: 'setRoots', request: IWatcherRequest[]): TPromise; call(command: 'watch', request: IWatcherRequest): TPromise; call(command: string, arg: any): TPromise; } @@ -20,6 +22,7 @@ export class WatcherChannel implements IWatcherChannel { call(command: string, arg: any): TPromise { switch (command) { + case 'initialize': return this.service.initialize(arg); case 'setRoots': return this.service.setRoots(arg); case 'watch': return this.service.watch(arg); } @@ -31,7 +34,11 @@ export class WatcherChannelClient implements IWatcherService { constructor(private channel: IWatcherChannel) { } - setRoots(roots: string[]): TPromise { + initialize(verboseLogging: boolean): TPromise { + return this.channel.call('initialize', verboseLogging); + } + + setRoots(roots: IWatcherRequest[]): TPromise { return this.channel.call('setRoots', roots); } diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts b/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts index 454a074383f4ce82daa5d39a930dd799422d3525..9f006d04c6e0fa916992dae2b85420b96d74b821 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts @@ -11,19 +11,20 @@ import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; import uri from 'vs/base/common/uri'; import { toFileChangesEvent, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; import { IWatcherChannel, WatcherChannelClient } from 'vs/workbench/services/files/node/watcher/nsfw/watcherIpc'; -import { FileChangesEvent } from 'vs/platform/files/common/files'; +import { FileChangesEvent, IFilesConfiguration } from 'vs/platform/files/common/files'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { normalize } from "path"; +import { IConfigurationService } from "vs/platform/configuration/common/configuration"; export class FileWatcher { private static MAX_RESTARTS = 5; + private service: WatcherChannelClient; private isDisposed: boolean; private restartCounter: number; constructor( private contextService: IWorkspaceContextService, - private ignored: string[], + private configurationService: IConfigurationService, private onFileChanges: (changes: FileChangesEvent) => void, private errorLogger: (msg: string) => void, private verboseLogging: boolean, @@ -48,13 +49,10 @@ export class FileWatcher { } ); + // Initialize watcher const channel = getNextTickChannel(client.getChannel('watcher')); - const service = new WatcherChannelClient(channel); - - // Start watching - const activeRoots = this.contextService.getWorkspace2().roots; - const basePath: string = normalize(activeRoots[0].fsPath); - service.watch({ basePath, ignored: this.ignored, verboseLogging: this.verboseLogging }).then(null, (err) => { + this.service = new WatcherChannelClient(channel); + this.service.initialize(this.verboseLogging).then(null, (err) => { if (!(err instanceof Error && err.name === 'Canceled' && err.message === 'Canceled')) { return TPromise.wrapError(err); // the service lib uses the promise cancel error to indicate the process died, we do not want to bubble this up } @@ -73,14 +71,11 @@ export class FileWatcher { } } }, this.errorLogger); - if (activeRoots.length > 1) { - service.setRoots(activeRoots.map(r => r.fsPath)); - } - this.contextService.onDidChangeWorkspaceRoots(() => { - const roots = this.contextService.getWorkspace2().roots; - service.setRoots(roots.map(r => r.fsPath)); - }); + // Start watching + this.updateRoots(); + this.contextService.onDidChangeWorkspaceRoots(() => this.updateRoots()); + this.configurationService.onDidUpdateConfiguration(() => this.updateRoots()); return () => { client.dispose(); @@ -88,6 +83,26 @@ export class FileWatcher { }; } + private updateRoots() { + const roots = this.contextService.getWorkspace2().roots; + console.log('updateRoots'); + this.service.setRoots(roots.map(root => { + const configuration = this.configurationService.getConfiguration(undefined, { + resource: root + }); + let ignored: string[] = []; + console.log(' root: ' + root); + if (configuration.files && configuration.files.watcherExclude) { + console.log(' config: ', configuration.files.watcherExclude); + ignored = Object.keys(configuration.files.watcherExclude).filter(k => !!configuration.files.watcherExclude[k]); + } + return { + basePath: root.fsPath, + ignored + }; + })); + } + private onRawFileEvents(events: IRawFileChange[]): void { // Emit through broadcast service