@@ -30,9 +30,9 @@ type Watcher struct {
30
30
31
31
mu sync.Mutex // guards all fields below
32
32
33
- // watchedDirs keeps track of which directories are being watched by the
34
- // watcher, explicitly added via [fsnotify.Watcher.Add] .
35
- watchedDirs map [string ]bool
33
+ // knownDirs tracks all known directories to help distinguish between file
34
+ // and directory deletion events .
35
+ knownDirs map [string ]bool
36
36
37
37
// events is the current batch of unsent file events, which will be sent
38
38
// when the timer expires.
@@ -51,10 +51,10 @@ func New(delay time.Duration, logger *slog.Logger, handler func([]protocol.FileE
51
51
return nil , err
52
52
}
53
53
w := & Watcher {
54
- logger : logger ,
55
- watcher : watcher ,
56
- watchedDirs : make (map [string ]bool ),
57
- stop : make (chan struct {}),
54
+ logger : logger ,
55
+ watcher : watcher ,
56
+ knownDirs : make (map [string ]bool ),
57
+ stop : make (chan struct {}),
58
58
}
59
59
60
60
w .wg .Add (1 )
@@ -134,6 +134,7 @@ func (w *Watcher) WatchDir(path string) error {
134
134
if skipDir (d .Name ()) {
135
135
return filepath .SkipDir
136
136
}
137
+ w .addKnownDir (path )
137
138
if err := w .watchDir (path ); err != nil {
138
139
// TODO(hxjiang): retry on watch failures.
139
140
return filepath .SkipDir
@@ -143,9 +144,11 @@ func (w *Watcher) WatchDir(path string) error {
143
144
})
144
145
}
145
146
146
- // handleEvent converts a single [fsnotify.Event] to the corresponding
147
- // [protocol.FileEvent] and updates the watcher state.
148
- // Returns nil if the input event is not relevant.
147
+ // handleEvent converts an [fsnotify.Event] to the corresponding [protocol.FileEvent]
148
+ // and updates the watcher state, returning nil if the event is not relevant.
149
+ //
150
+ // To avoid blocking, any required watches for new subdirectories are registered
151
+ // asynchronously in a separate goroutine.
149
152
func (w * Watcher ) handleEvent (event fsnotify.Event ) * protocol.FileEvent {
150
153
// fsnotify does not guarantee clean filepaths.
151
154
path := filepath .Clean (event .Name )
@@ -157,7 +160,7 @@ func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
157
160
// Upon deletion, the file/dir has been removed. fsnotify
158
161
// does not provide information regarding the deleted item.
159
162
// Use the watchedDirs to determine whether it's a dir.
160
- isDir = w .isDir (path )
163
+ isDir = w .isKnownDir (path )
161
164
} else {
162
165
// If statting failed, something is wrong with the file system.
163
166
// Log and move on.
@@ -180,7 +183,10 @@ func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
180
183
// directory is watched.
181
184
fallthrough
182
185
case event .Op .Has (fsnotify .Remove ):
183
- w .unwatchDir (path )
186
+ // Upon removal, we only need to remove the entries from the map.
187
+ // The [fsnotify.Watcher] remove the watch for us.
188
+ // fsnotify/fsnotify#268
189
+ w .removeKnownDir (path )
184
190
185
191
// TODO(hxjiang): Directory removal events from some LSP clients may
186
192
// not include corresponding removal events for child files and
@@ -191,8 +197,13 @@ func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
191
197
Type : protocol .Deleted ,
192
198
}
193
199
case event .Op .Has (fsnotify .Create ):
200
+ w .addKnownDir (path )
201
+
202
+ // This watch is added asynchronously to prevent a potential deadlock
203
+ // on Windows. The fsnotify library can block when registering a watch
204
+ // if its event channel is full (see fsnotify/fsnotify#502).
194
205
// TODO(hxjiang): retry on watch failure.
195
- _ = w .watchDir (path )
206
+ go w .watchDir (path )
196
207
197
208
return & protocol.FileEvent {
198
209
URI : protocol .URIFromPath (path ),
@@ -233,35 +244,32 @@ func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
233
244
}
234
245
}
235
246
247
+ // watchDir register the watch for the input dir. This function may be blocking
248
+ // because of the issue fsnotify/fsnotify#502.
236
249
func (w * Watcher ) watchDir (path string ) error {
237
- w .mu .Lock ()
238
- defer w .mu .Unlock ()
239
-
240
250
// Dir with broken symbolic link can not be watched.
241
251
// TODO(hxjiang): is it possible the files/dirs are
242
252
// created before the watch is successfully registered.
243
- if err := w .watcher .Add (path ); err != nil {
244
- return err
245
- }
246
- w .watchedDirs [path ] = true
247
- return nil
253
+ return w .watcher .Add (path )
248
254
}
249
255
250
- func (w * Watcher ) unwatchDir (path string ) {
256
+ func (w * Watcher ) addKnownDir (path string ) {
251
257
w .mu .Lock ()
252
258
defer w .mu .Unlock ()
259
+ w .knownDirs [path ] = true
260
+ }
253
261
254
- // Upon removal, we only need to remove the entries from the map.
255
- // The [fsnotify.Watcher] remove the watch for us.
256
- // fsnotify/fsnotify#268
257
- delete (w .watchedDirs , path )
262
+ func ( w * Watcher ) removeKnownDir ( path string ) {
263
+ w . mu . Lock ()
264
+ defer w . mu . Unlock ()
265
+ delete (w .knownDirs , path )
258
266
}
259
267
260
- func (w * Watcher ) isDir (path string ) bool {
268
+ func (w * Watcher ) isKnownDir (path string ) bool {
261
269
w .mu .Lock ()
262
270
defer w .mu .Unlock ()
263
271
264
- _ , isDir := w .watchedDirs [path ]
272
+ _ , isDir := w .knownDirs [path ]
265
273
return isDir
266
274
}
267
275
@@ -298,6 +306,7 @@ func (w *Watcher) sendEvents(handler func([]protocol.FileEvent, error)) {
298
306
// and returns any final error.
299
307
func (w * Watcher ) Close () error {
300
308
err := w .watcher .Close ()
309
+
301
310
// Wait for the go routine to finish. So all the channels will be closed and
302
311
// all go routine will be terminated.
303
312
close (w .stop )
0 commit comments