@@ -17,54 +17,55 @@ import (
17
17
"golang.org/x/tools/gopls/internal/protocol"
18
18
)
19
19
20
- // FileWatcher collects events from a [fsnotify.Watcher] and converts them into
21
- // batched LSP [protocol.FileEvent]s. Events are debounced and sent to the
22
- // event channel after a configurable period of no new relevant activity.
23
- type FileWatcher struct {
20
+ // Watcher collects events from a [fsnotify.Watcher] and converts them into
21
+ // batched LSP [protocol.FileEvent]s.
22
+ type Watcher struct {
24
23
logger * slog.Logger
25
24
26
- closed chan struct {}
25
+ stop chan struct {} // closed by Close to terminate run loop
27
26
28
- wg sync.WaitGroup
27
+ wg sync.WaitGroup // counts number of active run goroutines (max 1)
29
28
30
- mu sync.Mutex
29
+ watcher * fsnotify.Watcher
30
+
31
+ mu sync.Mutex // guards all fields below
31
32
32
33
// watchedDirs keeps track of which directories are being watched by the
33
34
// watcher, explicitly added via [fsnotify.Watcher.Add].
34
35
watchedDirs map [string ]bool
35
- watcher * fsnotify.Watcher
36
36
37
- // events is the current batch of unsent [protocol.FileEvent]s , which will
38
- // be sent when the timer expires.
37
+ // events is the current batch of unsent file events , which will be sent
38
+ // when the timer expires.
39
39
events []protocol.FileEvent
40
40
}
41
41
42
- // New creates a new FileWatcher and starts its event-handling loop. The
43
- // [FileWatcher.Close] should be called to cleanup.
44
- func New (delay time.Duration , logger * slog.Logger ) (* FileWatcher , <- chan []protocol.FileEvent , <- chan error , error ) {
42
+ // New creates a new file watcher and starts its event-handling loop. The
43
+ // [Watcher.Close] method must be called to clean up resources.
44
+ //
45
+ // The provided handler is called sequentially with either a batch of file
46
+ // events or an error. Events and errors may be interleaved. The watcher blocks
47
+ // until the handler returns, so the handler should be fast and non-blocking.
48
+ func New (delay time.Duration , logger * slog.Logger , handler func ([]protocol.FileEvent , error )) (* Watcher , error ) {
45
49
watcher , err := fsnotify .NewWatcher ()
46
50
if err != nil {
47
- return nil , nil , nil , err
51
+ return nil , err
48
52
}
49
- w := & FileWatcher {
53
+ w := & Watcher {
50
54
logger : logger ,
51
55
watcher : watcher ,
52
56
watchedDirs : make (map [string ]bool ),
53
- closed : make (chan struct {}),
57
+ stop : make (chan struct {}),
54
58
}
55
59
56
- eventsChan := make (chan []protocol.FileEvent )
57
- errorChan := make (chan error )
58
-
59
60
w .wg .Add (1 )
60
- go w .run (eventsChan , errorChan , delay )
61
+ go w .run (delay , handler )
61
62
62
- return w , eventsChan , errorChan , nil
63
+ return w , nil
63
64
}
64
65
65
66
// run is the main event-handling loop for the watcher. It should be run in a
66
67
// separate goroutine.
67
- func (w * FileWatcher ) run (events chan <- []protocol.FileEvent , errs chan <- error , delay time. Duration ) {
68
+ func (w * Watcher ) run (delay time. Duration , handler func ( []protocol.FileEvent , error ) ) {
68
69
defer w .wg .Done ()
69
70
70
71
// timer is used to debounce events.
@@ -73,20 +74,11 @@ func (w *FileWatcher) run(events chan<- []protocol.FileEvent, errs chan<- error,
73
74
74
75
for {
75
76
select {
76
- case <- w .closed :
77
- // File watcher should not send the remaining events to the receiver
78
- // because the client may not listening to the channel, could
79
- // result in blocking forever.
80
- //
81
- // Once close signal received, ErrorChan and EventsChan will be
82
- // closed. Exit the go routine to ensure no more value will be sent
83
- // through those channels.
84
- close (errs )
85
- close (events )
77
+ case <- w .stop :
86
78
return
87
79
88
80
case <- timer .C :
89
- w .sendEvents (events )
81
+ w .sendEvents (handler )
90
82
timer .Reset (delay )
91
83
92
84
case err , ok := <- w .watcher .Errors :
@@ -96,18 +88,20 @@ func (w *FileWatcher) run(events chan<- []protocol.FileEvent, errs chan<- error,
96
88
if ! ok {
97
89
continue
98
90
}
99
- errs <- err
91
+ if err != nil {
92
+ handler (nil , err )
93
+ }
100
94
101
95
case event , ok := <- w .watcher .Events :
102
96
if ! ok {
103
97
continue
104
98
}
105
- // FileWatcher should not handle the fsnotify.Event concurrently,
99
+ // file watcher should not handle the fsnotify.Event concurrently,
106
100
// the original order should be preserved. E.g. if a file get
107
101
// deleted and recreated, running concurrently may result it in
108
102
// reverse order.
109
103
//
110
- // Only reset the timer if an relevant event happened.
104
+ // Only reset the timer if a relevant event happened.
111
105
// https://github.com/fsnotify/fsnotify?tab=readme-ov-file#why-do-i-get-many-chmod-events
112
106
if e := w .handleEvent (event ); e != nil {
113
107
w .addEvent (* e )
@@ -134,7 +128,7 @@ func skipDir(dirName string) bool {
134
128
135
129
// WatchDir walks through the directory and all its subdirectories, adding
136
130
// them to the watcher.
137
- func (w * FileWatcher ) WatchDir (path string ) error {
131
+ func (w * Watcher ) WatchDir (path string ) error {
138
132
return filepath .WalkDir (filepath .Clean (path ), func (path string , d fs.DirEntry , err error ) error {
139
133
if d .IsDir () {
140
134
if skipDir (d .Name ()) {
@@ -150,29 +144,27 @@ func (w *FileWatcher) WatchDir(path string) error {
150
144
}
151
145
152
146
// handleEvent converts a single [fsnotify.Event] to the corresponding
153
- // [protocol.FileEvent].
147
+ // [protocol.FileEvent] and updates the watcher state .
154
148
// Returns nil if the input event is not relevant.
155
- func (w * FileWatcher ) handleEvent (event fsnotify.Event ) * protocol.FileEvent {
149
+ func (w * Watcher ) handleEvent (event fsnotify.Event ) * protocol.FileEvent {
156
150
// fsnotify does not guarantee clean filepaths.
157
151
path := filepath .Clean (event .Name )
158
152
159
153
var isDir bool
160
154
if info , err := os .Stat (path ); err == nil {
161
155
isDir = info .IsDir ()
156
+ } else if os .IsNotExist (err ) {
157
+ // Upon deletion, the file/dir has been removed. fsnotify
158
+ // does not provide information regarding the deleted item.
159
+ // Use the watchedDirs to determine whether it's a dir.
160
+ isDir = w .isDir (path )
162
161
} else {
163
- if os .IsNotExist (err ) {
164
- // Upon deletion, the file/dir has been removed. fsnotify
165
- // does not provide information regarding the deleted item.
166
- // Use the watchedDirs to determine whether it's a dir.
167
- isDir = w .isDir (path )
168
- } else {
169
- // If statting failed, something is wrong with the file system.
170
- // Log and move on.
171
- if w .logger != nil {
172
- w .logger .Error ("failed to stat path, skipping event as its type (file/dir) is unknown" , "path" , path , "err" , err )
173
- }
174
- return nil
162
+ // If statting failed, something is wrong with the file system.
163
+ // Log and move on.
164
+ if w .logger != nil {
165
+ w .logger .Error ("failed to stat path, skipping event as its type (file/dir) is unknown" , "path" , path , "err" , err )
175
166
}
167
+ return nil
176
168
}
177
169
178
170
if isDir {
@@ -210,9 +202,9 @@ func (w *FileWatcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
210
202
return nil
211
203
}
212
204
} else {
213
- // Only watch *.{go,mod,sum,work}
205
+ // Only watch files of interest.
214
206
switch strings .TrimPrefix (filepath .Ext (path ), "." ) {
215
- case "go" , "mod" , "sum" , "work" :
207
+ case "go" , "mod" , "sum" , "work" , "s" :
216
208
default :
217
209
return nil
218
210
}
@@ -241,7 +233,7 @@ func (w *FileWatcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
241
233
}
242
234
}
243
235
244
- func (w * FileWatcher ) watchDir (path string ) error {
236
+ func (w * Watcher ) watchDir (path string ) error {
245
237
w .mu .Lock ()
246
238
defer w .mu .Unlock ()
247
239
@@ -255,30 +247,29 @@ func (w *FileWatcher) watchDir(path string) error {
255
247
return nil
256
248
}
257
249
258
- func (w * FileWatcher ) unwatchDir (path string ) {
250
+ func (w * Watcher ) unwatchDir (path string ) {
259
251
w .mu .Lock ()
260
252
defer w .mu .Unlock ()
261
253
262
- // Upon removal, we only need to remove the entries from the map
263
- // [fileWatcher.watchedDirPath].
254
+ // Upon removal, we only need to remove the entries from the map.
264
255
// The [fsnotify.Watcher] remove the watch for us.
265
256
// fsnotify/fsnotify#268
266
257
delete (w .watchedDirs , path )
267
258
}
268
259
269
- func (w * FileWatcher ) isDir (path string ) bool {
260
+ func (w * Watcher ) isDir (path string ) bool {
270
261
w .mu .Lock ()
271
262
defer w .mu .Unlock ()
272
263
273
264
_ , isDir := w .watchedDirs [path ]
274
265
return isDir
275
266
}
276
267
277
- func (w * FileWatcher ) addEvent (event protocol.FileEvent ) {
268
+ func (w * Watcher ) addEvent (event protocol.FileEvent ) {
278
269
w .mu .Lock ()
279
270
defer w .mu .Unlock ()
280
271
281
- // Some architectures emit duplicate change events in close
272
+ // Some systems emit duplicate change events in close
282
273
// succession upon file modification. While the current
283
274
// deduplication is naive and only handles immediate duplicates,
284
275
// a more robust solution is needed.
@@ -292,27 +283,24 @@ func (w *FileWatcher) addEvent(event protocol.FileEvent) {
292
283
}
293
284
}
294
285
295
- func (w * FileWatcher ) sendEvents (eventsChan chan <- []protocol.FileEvent ) {
296
- w .mu .Lock () // Guard the w.events read and write. Not w.EventChan.
297
- defer w .mu .Unlock ()
286
+ func (w * Watcher ) sendEvents (handler func ([]protocol.FileEvent , error )) {
287
+ w .mu .Lock ()
288
+ events := w .events
289
+ w .events = nil
290
+ w .mu .Unlock ()
298
291
299
- if len (w .events ) != 0 {
300
- eventsChan <- w .events
301
- w .events = make ([]protocol.FileEvent , 0 )
292
+ if len (events ) != 0 {
293
+ handler (events , nil )
302
294
}
303
295
}
304
296
305
297
// Close shuts down the watcher, waits for the internal goroutine to terminate,
306
298
// and returns any final error.
307
- func (w * FileWatcher ) Close () error {
308
- w .mu .Lock ()
309
-
299
+ func (w * Watcher ) Close () error {
310
300
err := w .watcher .Close ()
311
301
// Wait for the go routine to finish. So all the channels will be closed and
312
302
// all go routine will be terminated.
313
- close (w .closed )
314
-
315
- w .mu .Unlock ()
303
+ close (w .stop )
316
304
317
305
w .wg .Wait ()
318
306
0 commit comments