Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion internal/lsp/lspwatcher/lspwatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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`,
Expand Down Expand Up @@ -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
}
Expand Down
81 changes: 76 additions & 5 deletions internal/lsp/lspwatcher/lspwatcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()

Expand Down