@@ -30,9 +30,9 @@ type Watcher struct {
3030
3131 mu sync.Mutex // guards all fields below
3232
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
3636
3737 // events is the current batch of unsent file events, which will be sent
3838 // when the timer expires.
@@ -51,10 +51,10 @@ func New(delay time.Duration, logger *slog.Logger, handler func([]protocol.FileE
5151 return nil , err
5252 }
5353 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 {}),
5858 }
5959
6060 w .wg .Add (1 )
@@ -134,6 +134,7 @@ func (w *Watcher) WatchDir(path string) error {
134134 if skipDir (d .Name ()) {
135135 return filepath .SkipDir
136136 }
137+ w .addKnownDir (path )
137138 if err := w .watchDir (path ); err != nil {
138139 // TODO(hxjiang): retry on watch failures.
139140 return filepath .SkipDir
@@ -143,9 +144,11 @@ func (w *Watcher) WatchDir(path string) error {
143144 })
144145}
145146
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.
149152func (w * Watcher ) handleEvent (event fsnotify.Event ) * protocol.FileEvent {
150153 // fsnotify does not guarantee clean filepaths.
151154 path := filepath .Clean (event .Name )
@@ -157,7 +160,7 @@ func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
157160 // Upon deletion, the file/dir has been removed. fsnotify
158161 // does not provide information regarding the deleted item.
159162 // Use the watchedDirs to determine whether it's a dir.
160- isDir = w .isDir (path )
163+ isDir = w .isKnownDir (path )
161164 } else {
162165 // If statting failed, something is wrong with the file system.
163166 // Log and move on.
@@ -180,7 +183,10 @@ func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
180183 // directory is watched.
181184 fallthrough
182185 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 )
184190
185191 // TODO(hxjiang): Directory removal events from some LSP clients may
186192 // not include corresponding removal events for child files and
@@ -191,8 +197,13 @@ func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
191197 Type : protocol .Deleted ,
192198 }
193199 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).
194205 // TODO(hxjiang): retry on watch failure.
195- _ = w .watchDir (path )
206+ go w .watchDir (path )
196207
197208 return & protocol.FileEvent {
198209 URI : protocol .URIFromPath (path ),
@@ -233,35 +244,32 @@ func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
233244 }
234245}
235246
247+ // watchDir register the watch for the input dir. This function may be blocking
248+ // because of the issue fsnotify/fsnotify#502.
236249func (w * Watcher ) watchDir (path string ) error {
237- w .mu .Lock ()
238- defer w .mu .Unlock ()
239-
240250 // Dir with broken symbolic link can not be watched.
241251 // TODO(hxjiang): is it possible the files/dirs are
242252 // 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 )
248254}
249255
250- func (w * Watcher ) unwatchDir (path string ) {
256+ func (w * Watcher ) addKnownDir (path string ) {
251257 w .mu .Lock ()
252258 defer w .mu .Unlock ()
259+ w .knownDirs [path ] = true
260+ }
253261
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 )
258266}
259267
260- func (w * Watcher ) isDir (path string ) bool {
268+ func (w * Watcher ) isKnownDir (path string ) bool {
261269 w .mu .Lock ()
262270 defer w .mu .Unlock ()
263271
264- _ , isDir := w .watchedDirs [path ]
272+ _ , isDir := w .knownDirs [path ]
265273 return isDir
266274}
267275
@@ -298,6 +306,7 @@ func (w *Watcher) sendEvents(handler func([]protocol.FileEvent, error)) {
298306// and returns any final error.
299307func (w * Watcher ) Close () error {
300308 err := w .watcher .Close ()
309+
301310 // Wait for the go routine to finish. So all the channels will be closed and
302311 // all go routine will be terminated.
303312 close (w .stop )
0 commit comments