diff --git a/internal/lsp/lspwatcher/lspwatcher.go b/internal/lsp/lspwatcher/lspwatcher.go index 6b5fbc6544..0bcf09325b 100644 --- a/internal/lsp/lspwatcher/lspwatcher.go +++ b/internal/lsp/lspwatcher/lspwatcher.go @@ -25,6 +25,7 @@ const throttleWindow = 75 * time.Millisecond type watcherBackend interface { WatchDirectory(dir string, fn fswatch.WatchCallback, opts ...fswatch.WatchOption) (io.Closer, error) + HasFastRecursiveBackend() bool } type defaultWatcherBackend struct { @@ -35,6 +36,10 @@ func (d defaultWatcherBackend) WatchDirectory(dir string, fn fswatch.WatchCallba return d.watcher.WatchDirectory(dir, fn, opts...) } +func (d defaultWatcherBackend) HasFastRecursiveBackend() bool { + return d.watcher.HasFastRecursiveBackend() +} + // Watcher manages a set of file system subscriptions identified by // WatcherID strings (matching the LSP server's project.WatcherID type). // Events are delivered to onChanges in batches as `*lsproto.FileEvent`, @@ -286,7 +291,11 @@ func (w *watch) reconcile(emitSyntheticCreates bool) error { if !w.watchingTarget && w.subscription != nil && w.watchedDirectory == ancestorDirectory { return nil // already watching the correct ancestor } - subscription, err := watcher.backend.WatchDirectory(ancestorDirectory, w.ancestorCallback()) + var options []fswatch.WatchOption + if watcher.backend.HasFastRecursiveBackend() { + options = append(options, fswatch.WithRecursive()) + } + subscription, err := watcher.backend.WatchDirectory(ancestorDirectory, w.ancestorCallback(), options...) if err != nil { return err } diff --git a/internal/lsp/lspwatcher/lspwatcher_test.go b/internal/lsp/lspwatcher/lspwatcher_test.go index 90ad3a010c..6a29c7d1ed 100644 --- a/internal/lsp/lspwatcher/lspwatcher_test.go +++ b/internal/lsp/lspwatcher/lspwatcher_test.go @@ -166,11 +166,12 @@ func TestRootFromGlob(t *testing.T) { } type fakeBackend struct { - mu sync.Mutex - byDir map[string]fswatch.WatchCallback - closed map[string]int - optCount map[string]int - failDirs map[string]error + mu sync.Mutex + byDir map[string]fswatch.WatchCallback + closed map[string]int + optCount map[string]int + failDirs map[string]error + fastRecursive bool } func newFakeBackend() *fakeBackend { @@ -199,6 +200,10 @@ func (f *fakeBackend) WatchDirectory(dir string, fn fswatch.WatchCallback, opts }}, nil } +func (f *fakeBackend) HasFastRecursiveBackend() bool { + return f.fastRecursive +} + // watchedDirs returns the directories currently subscribed, for assertions. func (f *fakeBackend) watchedDirs() []string { f.mu.Lock() @@ -490,6 +495,72 @@ func TestWatcher_MissingDirectoryPromotesOnCreate(t *testing.T) { }, "synthetic create events for target and child") } +func TestWatcher_MissingDirectoryFastRecursiveAncestor(t *testing.T) { + t.Parallel() + + fs := bundled.WrapFS(osvfs.FS()) + backend := newFakeBackend() + backend.fastRecursive = true + var ( + mu sync.Mutex + got []*lsproto.FileEvent + ) + w := newWithBackend(fs, backend, func(changes []*lsproto.FileEvent) { + mu.Lock() + got = append(got, changes...) + mu.Unlock() + }, logging.NewLogger(os.Stderr)) + t.Cleanup(w.Close) + + base := t.TempDir() + baseNorm := tspath.NormalizeSlashes(base) + target := tspath.NormalizeSlashes(filepath.Join(base, "pkg")) + pattern := target + "/*" + kind := lsproto.WatchKindCreate | lsproto.WatchKindChange | lsproto.WatchKindDelete + + if err := w.WatchFiles("id", []*lsproto.FileSystemWatcher{{ + GlobPattern: lsproto.PatternOrRelativePattern{Pattern: &pattern}, + Kind: &kind, + }}); err != nil { + t.Fatal(err) + } + + backend.mu.Lock() + if got := backend.optCount[baseNorm]; got != 1 { + t.Fatalf("fast recursive backend should install recursive ancestor watch, got %d options", got) + } + backend.mu.Unlock() + + if err := os.MkdirAll(filepath.Join(base, "pkg"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(base, "pkg", "index.ts"), []byte("export {}"), 0o644); err != nil { + t.Fatal(err) + } + backend.emit(baseNorm, []fswatch.Event{ + {Kind: fswatch.EventUpdate, Path: filepath.Join(base, "pkg", "index.ts")}, + }, nil) + + waitFor(t, func() bool { return backend.isWatching(target) }, "promotion from descendant event") + + backend.mu.Lock() + if got := backend.optCount[target]; got != 0 { + t.Fatalf("non-recursive target watch should stay non-recursive, got %d options", got) + } + backend.mu.Unlock() + + waitFor(t, func() bool { + mu.Lock() + defer mu.Unlock() + for _, e := range got { + if e.Type == lsproto.FileChangeTypeCreated && strings.HasSuffix(string(e.Uri), "/pkg/index.ts") { + return true + } + } + return false + }, "synthetic create for child") +} + func TestWatcher_MultiLevelDescend(t *testing.T) { t.Parallel()