@@ -121,16 +121,48 @@ func (w *Watcher) run(delay time.Duration, handler func([]protocol.FileEvent, er
121
121
if ! ok {
122
122
continue
123
123
}
124
+
125
+ // fsnotify does not guarantee clean filepaths.
126
+ event .Name = filepath .Clean (event .Name )
127
+
124
128
// fsnotify.Event should not be handled concurrently, to preserve their
125
129
// original order. For example, if a file is deleted and recreated,
126
130
// concurrent handling could process the events in reverse order.
127
131
//
128
132
// Only reset the timer if a relevant event happened.
129
133
// https://github.com/fsnotify/fsnotify?tab=readme-ov-file#why-do-i-get-many-chmod-events
130
- if e := w .handleEvent (event ); e != nil {
131
- w .addEvent (* e )
132
- timer .Reset (delay )
134
+ e , isDir := w .convertEvent (event )
135
+ if e == (protocol.FileEvent {}) {
136
+ continue
137
+ }
138
+
139
+ if isDir {
140
+ switch e .Type {
141
+ case protocol .Created :
142
+ // Newly created directories are watched asynchronously to prevent
143
+ // a potential deadlock on Windows(see fsnotify/fsnotify#502).
144
+ // Errors are reported internally.
145
+ if done , release := w .addWatchHandle (event .Name ); done != nil {
146
+ go func () {
147
+ w .errs <- w .watchDir (event .Name , done )
148
+
149
+ // Only release after the error is sent.
150
+ release ()
151
+ }()
152
+ }
153
+ case protocol .Deleted :
154
+ // Upon removal, we only need to remove the entries from
155
+ // the map. The [fsnotify.Watcher] removes the watch for
156
+ // us. fsnotify/fsnotify#268
157
+ w .removeWatchHandle (event .Name )
158
+ default :
159
+ // convertEvent enforces that dirs are only Created or Deleted.
160
+ panic ("impossible" )
161
+ }
133
162
}
163
+
164
+ w .addEvent (e )
165
+ timer .Reset (delay )
134
166
}
135
167
}
136
168
}
@@ -171,108 +203,70 @@ func (w *Watcher) WatchDir(path string) error {
171
203
})
172
204
}
173
205
174
- // handleEvent converts an [fsnotify.Event] to the corresponding [protocol.FileEvent]
175
- // and updates the watcher state, returning nil if the event is not relevant.
176
- //
177
- // To avoid blocking, any required watches for new subdirectories are registered
178
- // asynchronously in a separate goroutine.
179
- func (w * Watcher ) handleEvent (event fsnotify.Event ) * protocol.FileEvent {
180
- // fsnotify does not guarantee clean filepaths.
181
- path := filepath .Clean (event .Name )
182
-
183
- var isDir bool
184
- if info , err := os .Stat (path ); err == nil {
206
+ // convertEvent translates an [fsnotify.Event] into a [protocol.FileEvent].
207
+ // It returns the translated event and a boolean indicating if the path was a
208
+ // directory. For directories, the event Type is either Created or Deleted.
209
+ // It returns empty event for events that should be ignored.
210
+ func (w * Watcher ) convertEvent (event fsnotify.Event ) (_ protocol.FileEvent , isDir bool ) {
211
+ // Determine if the event is for a directory.
212
+ if info , err := os .Stat (event .Name ); err == nil {
185
213
isDir = info .IsDir ()
186
214
} else if os .IsNotExist (err ) {
187
- // Upon deletion, the file/dir has been removed. fsnotify
188
- // does not provide information regarding the deleted item.
215
+ // Upon deletion, the file/dir has been removed. fsnotify does not
216
+ // provide information regarding the deleted item.
189
217
// Use watchHandles to determine if the deleted item was a directory.
190
- isDir = w .isWatchedDir (path )
218
+ isDir = w .isWatchedDir (event . Name )
191
219
} else {
192
220
// If statting failed, something is wrong with the file system.
193
221
// Log and move on.
194
222
if w .logger != nil {
195
- w .logger .Error ("failed to stat path, skipping event as its type (file/dir) is unknown" , "path" , path , "err" , err )
223
+ w .logger .Error ("failed to stat path, skipping event as its type (file/dir) is unknown" , "path" , event . Name , "err" , err )
196
224
}
197
- return nil
225
+ return protocol. FileEvent {}, false
198
226
}
199
227
228
+ // Filter out events for directories and files that are not of interest.
200
229
if isDir {
201
- if skipDir (filepath .Base (path )) {
202
- return nil
203
- }
204
-
205
- switch {
206
- case event .Op .Has (fsnotify .Rename ):
207
- // A rename is treated as a deletion of the old path because the
208
- // fsnotify RENAME event doesn't include the new path. A separate
209
- // CREATE event will be sent for the new path if the destination
210
- // directory is watched.
211
- fallthrough
212
- case event .Op .Has (fsnotify .Remove ):
213
- // Upon removal, we only need to remove the entries from the map.
214
- // The [fsnotify.Watcher] removes the watch for us.
215
- // fsnotify/fsnotify#268
216
- w .removeWatchHandle (path )
217
-
218
- // TODO(hxjiang): Directory removal events from some LSP clients may
219
- // not include corresponding removal events for child files and
220
- // subdirectories. Should we do some filtering when adding the dir
221
- // deletion event to the events slice.
222
- return & protocol.FileEvent {
223
- URI : protocol .URIFromPath (path ),
224
- Type : protocol .Deleted ,
225
- }
226
- case event .Op .Has (fsnotify .Create ):
227
- // This watch is added asynchronously to prevent a potential
228
- // deadlock on Windows. See fsnotify/fsnotify#502.
229
- // Error encountered will be sent to internal error channel.
230
- if done , release := w .addWatchHandle (path ); done != nil {
231
- go func () {
232
- w .errs <- w .watchDir (path , done )
233
-
234
- // Only release after the error is sent.
235
- release ()
236
- }()
237
- }
238
-
239
- return & protocol.FileEvent {
240
- URI : protocol .URIFromPath (path ),
241
- Type : protocol .Created ,
242
- }
243
- default :
244
- return nil
230
+ if skipDir (filepath .Base (event .Name )) {
231
+ return protocol.FileEvent {}, true
245
232
}
246
233
} else {
247
- // Only watch files of interest.
248
- switch strings .TrimPrefix (filepath .Ext (path ), "." ) {
234
+ switch strings .TrimPrefix (filepath .Ext (event .Name ), "." ) {
249
235
case "go" , "mod" , "sum" , "work" , "s" :
250
236
default :
251
- return nil
237
+ return protocol. FileEvent {}, false
252
238
}
239
+ }
253
240
254
- var t protocol.FileChangeType
255
- switch {
256
- case event .Op .Has (fsnotify .Rename ):
257
- // A rename is treated as a deletion of the old path because the
258
- // fsnotify RENAME event doesn't include the new path. A separate
259
- // CREATE event will be sent for the new path if the destination
260
- // directory is watched.
261
- fallthrough
262
- case event .Op .Has (fsnotify .Remove ):
263
- t = protocol .Deleted
264
- case event .Op .Has (fsnotify .Create ):
265
- t = protocol .Created
266
- case event .Op .Has (fsnotify .Write ):
267
- t = protocol .Changed
268
- default :
269
- return nil // ignore the rest of the events
270
- }
271
- return & protocol.FileEvent {
272
- URI : protocol .URIFromPath (path ),
273
- Type : t ,
241
+ var t protocol.FileChangeType
242
+ switch {
243
+ case event .Op .Has (fsnotify .Rename ):
244
+ // A rename is treated as a deletion of the old path because the
245
+ // fsnotify RENAME event doesn't include the new path. A separate
246
+ // CREATE event will be sent for the new path if the destination
247
+ // directory is watched.
248
+ fallthrough
249
+ case event .Op .Has (fsnotify .Remove ):
250
+ // TODO(hxjiang): Directory removal events from some LSP clients may
251
+ // not include corresponding removal events for child files and
252
+ // subdirectories. Should we do some filtering when adding the dir
253
+ // deletion event to the events slice.
254
+ t = protocol .Deleted
255
+ case event .Op .Has (fsnotify .Create ):
256
+ t = protocol .Created
257
+ case event .Op .Has (fsnotify .Write ):
258
+ if isDir {
259
+ return protocol.FileEvent {}, isDir // ignore dir write events
274
260
}
261
+ t = protocol .Changed
262
+ default :
263
+ return protocol.FileEvent {}, isDir // ignore the rest of the events
275
264
}
265
+
266
+ return protocol.FileEvent {
267
+ URI : protocol .URIFromPath (event .Name ),
268
+ Type : t ,
269
+ }, isDir
276
270
}
277
271
278
272
// watchDir registers a watch for a directory, retrying with backoff if it fails.
0 commit comments