diff --git a/src/FileSystemWatcherManager.ts b/src/FileSystemWatcherManager.ts index 4aac5a3..bd0056e 100644 --- a/src/FileSystemWatcherManager.ts +++ b/src/FileSystemWatcherManager.ts @@ -3,16 +3,17 @@ * GPL-3.0-only. See LICENSE.md in the project root for license details. */ -import { Disposable, Uri, WorkspaceFolder, WorkspaceFoldersChangeEvent, window } from 'vscode' -import { FSWatcher, existsSync, watch } from 'fs' +import { Disposable, WorkspaceFolder, WorkspaceFoldersChangeEvent, window } from 'vscode' +import { existsSync, WatchEventType } from 'fs' +import { PathWatcher, WatcherCallback } from './Foundation/PathWatcher' import { join } from 'path' -type CallbackFunction = (event: Uri) => void +type RepoWatcherCallback = (event: WatchEventType, filename: string) => void // https://github.com/Microsoft/vscode/issues/3025 export default class implements Disposable { - private callback: CallbackFunction - private watchers: Map = new Map() as Map + private callback: RepoWatcherCallback + private watchers: Map = new Map() as Map /** * Creates a new watcher. @@ -20,9 +21,9 @@ export default class implements Disposable { * @param repos the repositories to watch * @param callback the callback to run when detecting changes */ - constructor(repos: string[], callback: CallbackFunction) { + constructor(repos: string[], callback: RepoWatcherCallback) { this.callback = callback - repos.forEach((directory) => { this.registerProjectWatcher(directory) }) + repos.forEach((directory) => { this.registerProjectWatchers(directory) }) } /** @@ -33,67 +34,77 @@ export default class implements Disposable { public configure(directoryChanges: WorkspaceFoldersChangeEvent): void { directoryChanges.added.forEach((changedDirectory: WorkspaceFolder) => { const directory = changedDirectory.uri.fsPath - this.registerProjectWatcher(directory) + this.registerProjectWatchers(directory) }) directoryChanges.removed.forEach((changedDirectory: WorkspaceFolder) => { const directory = changedDirectory.uri.fsPath - this.removeProjectWatcher(directory) + this.removeProjectWatchers(directory) }) } /** * Disposes this object. + * @see Disposable.dispose() */ public dispose(): void { for (const path of this.watchers.keys()) { - this.removeProjectWatcher(path) + this.removeProjectWatchers(path) } } /** - * Registers a new project directory watcher. + * Registers the project directory watchers. * * @param projectPath the directory path */ - private registerProjectWatcher(projectPath: string): void { + private registerProjectWatchers(projectPath: string): void { global.dbg(`[FSWatch] Watch ${projectPath} ...`) - if (this.watchers.has(projectPath)) { + const pathToMonitor = join(projectPath, '.git', 'refs') + if (!existsSync(pathToMonitor)) { return } - const pathToMonitor = join(projectPath, '.git', 'refs') + this.registerPathWatcher(pathToMonitor, projectPath) + } - if (!existsSync(pathToMonitor)) { + /** + * Creates a FS Watcher for a directory. + * @param pathToMonitor the path to monitor + * @param projectPath the path to use in the callback + */ + private registerPathWatcher(pathToMonitor: string, projectPath: string) { + const watchers = this.watchers.get(projectPath) + ?? this.watchers.set(projectPath, []).get(projectPath)! // eslint-disable-line @typescript-eslint/no-non-null-assertion + + if (watchers.length && watchers.find((watcher) => watcher.for(pathToMonitor))) { return } try { - const watcher = watch(pathToMonitor, (event: string, filename) => { + // Create the watcher callback which will review if a reaction is needed + // based on the changed (stash) file. + const callback: WatcherCallback = (event, filename) => { if (filename?.includes('stash')) { - this.callback(Uri.file(projectPath)) + this.callback(event, projectPath) } - }) + } - this.watchers.set(projectPath, watcher) + watchers.push(PathWatcher.watch(pathToMonitor, callback)) } catch (error) { + const msg = `Unable to create a stashes monitor for ${pathToMonitor}.` + + ' This may happen on NFS or if the path is a link.' + + ' See the console for details' + console.error(msg) console.error(error) - void window.showErrorMessage(`Unable to a create a stashes monitor for - ${projectPath}. This may happen on NFS or if the path is a link`) + void window.showErrorMessage(msg) } } - /** - * Removes an active project directory watcher. - * - * @param path the directory path - */ - private removeProjectWatcher(path: string): void { - if (this.watchers.has(path)) { - global.dbg(`[FSWatch] Stop watching ${path} ...`) - this.watchers.get(path)?.close() - this.watchers.delete(path) - } + private removeProjectWatchers(path: string): void { + global.dbg(`[FSWatch] Stop watching ${path} ...`) + this.watchers.get(path)?.forEach((watcher) => { watcher.dispose() }) + this.watchers.delete(path) } } diff --git a/src/Foundation/PathWatcher.ts b/src/Foundation/PathWatcher.ts new file mode 100644 index 0000000..df9713f --- /dev/null +++ b/src/Foundation/PathWatcher.ts @@ -0,0 +1,44 @@ +/* + * Copyright (c) Arturo Rodríguez V. + * GPL-3.0-only. See LICENSE.md in the project root for license details. + */ + +import { FSWatcher, WatchListener, watch } from 'fs' + +/** + * The callback used on PathWatcher events. + */ +export type WatcherCallback = WatchListener + +/** + * A class wrapping a FSWatcher adding a path property to identify the watched directory. + */ +export class PathWatcher { + protected constructor( + public path: string, + public watcher: FSWatcher, + ) { } + + /** + * Creates a watcher for the given file. + */ + public static watch(path: string, callback: WatcherCallback) { + return new PathWatcher(path, watch(path, (event, filename) => { + callback(event, filename) + })) + } + + /** + * Indicates if the watcher if for the specified path. + */ + public for(path: string): boolean { + return this.path === path + } + + /** + * Disposes the watcher by removing every listener and closing it. + */ + public dispose(): void { + this.watcher.removeAllListeners().close() + } +} diff --git a/src/StashNode/NodeContainer.ts b/src/StashNode/NodeContainer.ts index f065dd2..8bd2a7c 100644 --- a/src/StashNode/NodeContainer.ts +++ b/src/StashNode/NodeContainer.ts @@ -37,7 +37,7 @@ export default class NodeContainer { * @param eagerLoadStashes indicates if children should be preloaded */ public async getRepositories(eagerLoadStashes: boolean): Promise { - const paths = await this.gitWorkspace.getRepositories(false) + const paths = await this.gitWorkspace.getRepositories() const repositoryNodes: RepositoryNode[] = [] for (const repositoryPath of paths) { diff --git a/src/StashNode/NodeFactory.ts b/src/StashNode/NodeFactory.ts index 140847f..14e799b 100644 --- a/src/StashNode/NodeFactory.ts +++ b/src/StashNode/NodeFactory.ts @@ -21,11 +21,14 @@ export default class NodeFactory { // May be undefined if the directory is not part of the workspace, // this happens on upper directories by negative search depth setting. const wsFolder = workspace.getWorkspaceFolder(Uri.file(path)) + // In a root dir, the received path is contained in the workspace's path. + const isRoot = wsFolder?.uri.fsPath.includes(path) ?? false return new RepositoryNode( dirname(path), basename(path), - wsFolder?.name, + // wsFolder is defined when isRoot is true but TS cannot infer that so add a ? + isRoot ? wsFolder?.name : undefined, ) } diff --git a/src/extension.ts b/src/extension.ts index 0c382b2..3c761ab 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -101,9 +101,9 @@ export async function activate(context: ExtensionContext): Promise { const watcherManager = new FileSystemWatcherManager( repos, - (projectDirectory: Uri) => { - global.dbg(`[watch] Reloading explorer (${projectDirectory.fsPath})...`) - treeProvider.reload('update', projectDirectory) + (event, projectDirectory) => { + global.dbg(`[watch] Reloading explorer (${projectDirectory})...`) + treeProvider.reload('update', Uri.file(projectDirectory)) }, ) global.dbg('[boot] FS Watcher created')