Skip to content

Commit a95309e

Browse files
committed
Refctor the filesystem watcher
1 parent 5af3264 commit a95309e

File tree

5 files changed

+94
-39
lines changed

5 files changed

+94
-39
lines changed

src/FileSystemWatcherManager.ts

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,31 @@
33
* GPL-3.0-only. See LICENSE.md in the project root for license details.
44
*/
55

6-
import { Disposable, Uri, WorkspaceFolder, WorkspaceFoldersChangeEvent, window } from 'vscode'
7-
import { FSWatcher, existsSync, watch } from 'fs'
6+
import { Disposable, WorkspaceFolder, WorkspaceFoldersChangeEvent, window } from 'vscode'
7+
import { existsSync, WatchEventType } from 'fs'
8+
import { PathWatcher, WatcherCallback } from './Foundation/PathWatcher'
89
import { join } from 'path'
910

10-
type CallbackFunction = (event: Uri) => void
11+
type RepoWatcherCallback = (event: WatchEventType, filename: string) => void
1112

1213
// https://github.com/Microsoft/vscode/issues/3025
1314
export default class implements Disposable {
14-
private callback: CallbackFunction
15-
private watchers: Map<string, FSWatcher> = new Map() as Map<string, FSWatcher>
15+
private callback: RepoWatcherCallback
16+
private watchers: Map<string, PathWatcher[]> = new Map() as Map<string, PathWatcher[]>
1617

1718
/**
1819
* Creates a new watcher.
1920
*
20-
* @param repositories the open repositories when starting the extension
21-
* @param callback the callback to run when identifying changes
21+
* @param repositories the repositories to watch
22+
* @param callback the callback to run when identifying changes
23+
* @param withSubmodules Monitor submodules too
2224
*/
23-
constructor(repositories: Promise<string[]>, callback: CallbackFunction) {
25+
constructor(repositories: Promise<string[]>, callback: RepoWatcherCallback) {
2426
this.callback = callback
2527

2628
void repositories.then((directories) => {
2729
directories.forEach((directory) => {
28-
this.registerProjectWatcher(directory)
30+
this.registerProjectWatchers(directory)
2931
})
3032
})
3133
}
@@ -38,67 +40,77 @@ export default class implements Disposable {
3840
public configure(directoryChanges: WorkspaceFoldersChangeEvent): void {
3941
directoryChanges.added.forEach((changedDirectory: WorkspaceFolder) => {
4042
const directory = changedDirectory.uri.fsPath
41-
this.registerProjectWatcher(directory)
43+
this.registerProjectWatchers(directory)
4244
})
4345

4446
directoryChanges.removed.forEach((changedDirectory: WorkspaceFolder) => {
4547
const directory = changedDirectory.uri.fsPath
46-
this.removeProjectWatcher(directory)
48+
this.removeProjectWatchers(directory)
4749
})
4850
}
4951

5052
/**
5153
* Disposes this object.
54+
* @see Disposable.dispose()
5255
*/
5356
public dispose(): void {
5457
for (const path of this.watchers.keys()) {
55-
this.removeProjectWatcher(path)
58+
this.removeProjectWatchers(path)
5659
}
5760
}
5861

5962
/**
60-
* Registers a new project directory watcher.
63+
* Registers the project directory watchers.
6164
*
6265
* @param projectPath the directory path
6366
*/
64-
private registerProjectWatcher(projectPath: string): void {
67+
private registerProjectWatchers(projectPath: string): void {
6568
global.dbg(`[FSWatch] Watch ${projectPath} ...`)
66-
if (this.watchers.has(projectPath)) {
69+
const pathToMonitor = join(projectPath, '.git', 'refs')
70+
if (!existsSync(pathToMonitor)) {
6771
return
6872
}
6973

70-
const pathToMonitor = join(projectPath, '.git', 'refs')
74+
this.registerPathWatcher(pathToMonitor, projectPath)
75+
}
7176

72-
if (!existsSync(pathToMonitor)) {
77+
/**
78+
* Creates a FS Watcher for a directory.
79+
* @param pathToMonitor the path to monitor
80+
* @param projectPath the path to use in the callback
81+
*/
82+
private registerPathWatcher(pathToMonitor: string, projectPath: string) {
83+
const watchers = this.watchers.get(projectPath)
84+
?? this.watchers.set(projectPath, []).get(projectPath)! // eslint-disable-line @typescript-eslint/no-non-null-assertion
85+
86+
if (watchers.length && watchers.find((watcher) => watcher.for(pathToMonitor))) {
7387
return
7488
}
7589

7690
try {
77-
const watcher = watch(pathToMonitor, (event: string, filename) => {
91+
// Create the watcher callback which will review if a reaction is needed
92+
// based on the changed (stash) file.
93+
const callback: WatcherCallback = (event, filename) => {
7894
if (filename?.includes('stash')) {
79-
this.callback(Uri.file(projectPath))
95+
this.callback(event, projectPath)
8096
}
81-
})
97+
}
8298

83-
this.watchers.set(projectPath, watcher)
99+
watchers.push(PathWatcher.watch(pathToMonitor, callback))
84100
}
85101
catch (error) {
102+
const msg = `Unable to create a stashes monitor for ${pathToMonitor}.`
103+
+ ' This may happen on NFS or if the path is a link.'
104+
+ ' See the console for details'
105+
console.error(msg)
86106
console.error(error)
87-
void window.showErrorMessage(`Unable to a create a stashes monitor for
88-
${projectPath}. This may happen on NFS or if the path is a link`)
107+
void window.showErrorMessage(msg)
89108
}
90109
}
91110

92-
/**
93-
* Removes an active project directory watcher.
94-
*
95-
* @param path the directory path
96-
*/
97-
private removeProjectWatcher(path: string): void {
98-
if (this.watchers.has(path)) {
99-
global.dbg(`[FSWatch] Stop watching ${path} ...`)
100-
this.watchers.get(path)?.close()
101-
this.watchers.delete(path)
102-
}
111+
private removeProjectWatchers(path: string): void {
112+
global.dbg(`[FSWatch] Stop watching ${path} ...`)
113+
this.watchers.get(path)?.forEach((watcher) => { watcher.dispose() })
114+
this.watchers.delete(path)
103115
}
104116
}

src/Foundation/PathWatcher.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) Arturo Rodríguez V.
3+
* GPL-3.0-only. See LICENSE.md in the project root for license details.
4+
*/
5+
6+
import { FSWatcher, WatchListener, watch } from 'fs'
7+
8+
/**
9+
* The callback used on PathWatcher events.
10+
*/
11+
export type WatcherCallback = WatchListener<string>
12+
13+
/**
14+
* A class wrapping a FSWatcher adding a path property to identify the watched directory.
15+
*/
16+
export class PathWatcher {
17+
protected constructor(
18+
public path: string,
19+
public watcher: FSWatcher,
20+
) { }
21+
22+
/**
23+
* Creates a watcher for the given file.
24+
*/
25+
public static watch(path: string, callback: WatcherCallback) {
26+
return new PathWatcher(path, watch(path, (event, filename) => {
27+
callback(event, filename)
28+
}))
29+
}
30+
31+
/**
32+
* Indicates if the watcher if for the specified path.
33+
*/
34+
public for(path: string): boolean {
35+
return this.path === path
36+
}
37+
38+
/**
39+
* Disposes the watcher by removing every listener and closing it.
40+
*/
41+
public dispose(): void {
42+
this.watcher.removeAllListeners().close()
43+
}
44+
}

src/StashNode/NodeContainer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export default class NodeContainer {
3737
* @param eagerLoadStashes indicates if children should be preloaded
3838
*/
3939
public async getRepositories(eagerLoadStashes: boolean): Promise<RepositoryNode[]> {
40-
const paths = await this.gitWorkspace.getRepositories(false)
40+
const paths = await this.gitWorkspace.getRepositories()
4141

4242
const repositoryNodes: RepositoryNode[] = []
4343
for (const repositoryPath of paths) {

src/StashNode/NodeFactory.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ export default class NodeFactory {
2323
const wsFolder = workspace.getWorkspaceFolder(Uri.file(path))
2424
// In a root dir, the received path is contained in the workspace's path.
2525
const isRoot = wsFolder?.uri.fsPath.includes(path) ?? false
26-
console.log(`createRepositoryNode() ${path}`, wsFolder?.uri)
2726

2827
return new RepositoryNode(
2928
dirname(path),

src/extension.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@ export function activate(context: ExtensionContext): void {
8787

8888
const watcherManager = new FileSystemWatcherManager(
8989
repos,
90-
(projectDirectory: Uri) => {
91-
global.dbg(`[Watch] Reloading explorer (${projectDirectory.fsPath})...`)
92-
treeProvider.reload('update', projectDirectory)
90+
(event, projectDirectory) => {
91+
global.dbg(`[Watch] Reloading explorer (${projectDirectory})...`)
92+
treeProvider.reload('update', Uri.file(projectDirectory))
9393
},
9494
)
9595
global.dbg('[boot] FS Watcher created')

0 commit comments

Comments
 (0)