internal/lsp: explicitly watch all known directories

VS Code's file watching API doesn't send notifications about directory
deletions unless you register for them explicitly (see
https://github.com/microsoft/vscode/issues/109754). Rather than watch
every file in the workspace, keep track of every relevant directory and
register file watchers for it.

This CL moves the snapshot's WorkspaceDirectories function to the
session and changes it to a KnownDirectories function. It returns all of
the directories and subdirectories known the session at a given moment.
Top-level directories are marked as such so that their *.{go,mod,sum}
contents can be watched, while subdirectories are just watched by path
so we can be notified of deletions.

Fixes golang/go#42348

Change-Id: Ic6d02dba55b5de89370522ea5d3cf1d198927997
Reviewed-on: https://go-review.googlesource.com/c/tools/+/271438
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
Trust: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 0a27af3..bcf7232 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -688,3 +688,15 @@
 	}
 	return overlays
 }
+
+func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[string]struct{} {
+	patterns := map[string]struct{}{}
+	for _, view := range s.views {
+		snapshot, release := view.getSnapshot(ctx)
+		for k, v := range snapshot.fileWatchingGlobPatterns(ctx) {
+			patterns[k] = v
+		}
+		release()
+	}
+	return patterns
+}
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index e611d62..7556d77 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -604,19 +604,46 @@
 	return ids
 }
 
-func (s *snapshot) WorkspaceDirectories(ctx context.Context) []span.URI {
-	return s.workspace.dirs(ctx, s)
+func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]struct{} {
+	// Work-around microsoft/vscode#100870 by making sure that we are,
+	// at least, watching the user's entire workspace. This will still be
+	// applied to every folder in the workspace.
+	patterns := map[string]struct{}{
+		"**/*.{go,mod,sum}": {},
+	}
+	dirs := s.workspace.dirs(ctx, s)
+	for _, dir := range dirs {
+		dirName := dir.Filename()
+
+		// If the directory is within the view's folder, we're already watching
+		// it with the pattern above.
+		if source.InDirLex(s.view.folder.Filename(), dirName) {
+			continue
+		}
+		// TODO(rstambler): If microsoft/vscode#3025 is resolved before
+		// microsoft/vscode#101042, we will need a work-around for Windows
+		// drive letter casing.
+		patterns[fmt.Sprintf("%s/**/*.{go,mod,sum}", dirName)] = struct{}{}
+	}
+
+	// Some clients do not send notifications for changes to directories that
+	// contain Go code (golang/go#42348). To handle this, explicitly watch all
+	// of the directories in the workspace. We find them by adding the
+	// directories of every file in the snapshot's workspace directories.
+	var dirNames []string
+	for uri := range s.allKnownSubdirs(ctx) {
+		dirNames = append(dirNames, uri.Filename())
+	}
+	sort.Strings(dirNames)
+	if len(dirNames) > 0 {
+		patterns[fmt.Sprintf("{%s}", strings.Join(dirNames, ","))] = struct{}{}
+	}
+	return patterns
 }
 
 // allKnownSubdirs returns all of the subdirectories within the snapshot's
 // workspace directories. None of the workspace directories are included.
 func (s *snapshot) allKnownSubdirs(ctx context.Context) map[span.URI]struct{} {
-	// Don't return results until the snapshot is loaded, otherwise it may not
-	// yet "know" its files.
-	if err := s.awaitLoaded(ctx); err != nil {
-		return nil
-	}
-
 	dirs := s.workspace.dirs(ctx, s)
 
 	s.mu.Lock()
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index b0099e3..28c3309 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -196,9 +196,9 @@
 			}()
 		}()
 	}
-	dirsToWatch := map[span.URI]struct{}{}
 	// Only one view gets to have a workspace.
 	assignedWorkspace := false
+	var allFoldersWg sync.WaitGroup
 	for _, folder := range folders {
 		uri := span.URIFromURI(folder.URI)
 		// Ignore non-file URIs.
@@ -229,16 +229,14 @@
 		}
 		var swg sync.WaitGroup
 		swg.Add(1)
+		allFoldersWg.Add(1)
 		go func() {
 			defer swg.Done()
+			defer allFoldersWg.Done()
 			snapshot.AwaitInitialized(ctx)
 			work.end("Finished loading packages.")
 		}()
 
-		for _, dir := range snapshot.WorkspaceDirectories(ctx) {
-			dirsToWatch[dir] = struct{}{}
-		}
-
 		// Print each view's environment.
 		buf := &bytes.Buffer{}
 		if err := snapshot.WriteEnv(ctx, buf); err != nil {
@@ -256,13 +254,15 @@
 			wg.Done()
 		}()
 	}
+
 	// Register for file watching notifications, if they are supported.
-	s.watchedDirectoriesMu.Lock()
-	err := s.registerWatchedDirectoriesLocked(ctx, dirsToWatch)
-	s.watchedDirectoriesMu.Unlock()
-	if err != nil {
-		return err
+	// Wait for all snapshots to be initialized first, since all files might
+	// not yet be known to the snapshots.
+	allFoldersWg.Wait()
+	if err := s.updateWatchedDirectories(ctx); err != nil {
+		event.Error(ctx, "failed to register for file watching notifications", err)
 	}
+
 	if len(viewErrors) > 0 {
 		errMsg := fmt.Sprintf("Error loading workspace folders (expected %v, got %v)\n", len(folders), len(s.session.Views())-originalViews)
 		for uri, err := range viewErrors {
@@ -280,34 +280,14 @@
 // with the previously registered set of directories. If the set of directories
 // has changed, we unregister and re-register for file watching notifications.
 // updatedSnapshots is the set of snapshots that have been updated.
-func (s *Server) updateWatchedDirectories(ctx context.Context, updatedSnapshots map[source.View]source.Snapshot) error {
-	dirsToWatch := map[span.URI]struct{}{}
-	seenViews := map[source.View]struct{}{}
+func (s *Server) updateWatchedDirectories(ctx context.Context) error {
+	patterns := s.session.FileWatchingGlobPatterns(ctx)
 
-	// Collect all of the workspace directories from the updated snapshots.
-	for _, snapshot := range updatedSnapshots {
-		seenViews[snapshot.View()] = struct{}{}
-		for _, dir := range snapshot.WorkspaceDirectories(ctx) {
-			dirsToWatch[dir] = struct{}{}
-		}
-	}
-	// Not all views were necessarily updated, so check the remaining views.
-	for _, view := range s.session.Views() {
-		if _, ok := seenViews[view]; ok {
-			continue
-		}
-		snapshot, release := view.Snapshot(ctx)
-		for _, dir := range snapshot.WorkspaceDirectories(ctx) {
-			dirsToWatch[dir] = struct{}{}
-		}
-		release()
-	}
-
-	s.watchedDirectoriesMu.Lock()
-	defer s.watchedDirectoriesMu.Unlock()
+	s.watchedGlobPatternsMu.Lock()
+	defer s.watchedGlobPatternsMu.Unlock()
 
 	// Nothing to do if the set of workspace directories is unchanged.
-	if equalURISet(s.watchedDirectories, dirsToWatch) {
+	if equalURISet(s.watchedGlobPatterns, patterns) {
 		return nil
 	}
 
@@ -316,11 +296,11 @@
 	// period where no files are being watched. Still, if a user makes on-disk
 	// changes before these updates are complete, we may miss them for the new
 	// directories.
-	if s.watchRegistrationCount > 0 {
-		prevID := s.watchRegistrationCount - 1
-		if err := s.registerWatchedDirectoriesLocked(ctx, dirsToWatch); err != nil {
-			return err
-		}
+	prevID := s.watchRegistrationCount - 1
+	if err := s.registerWatchedDirectoriesLocked(ctx, patterns); err != nil {
+		return err
+	}
+	if prevID >= 0 {
 		return s.client.UnregisterCapability(ctx, &protocol.UnregistrationParams{
 			Unregisterations: []protocol.Unregistration{{
 				ID:     watchedFilesCapabilityID(prevID),
@@ -331,11 +311,11 @@
 	return nil
 }
 
-func watchedFilesCapabilityID(id uint64) string {
+func watchedFilesCapabilityID(id int) string {
 	return fmt.Sprintf("workspace/didChangeWatchedFiles-%d", id)
 }
 
-func equalURISet(m1, m2 map[span.URI]struct{}) bool {
+func equalURISet(m1, m2 map[string]struct{}) bool {
 	if len(m1) != len(m2) {
 		return false
 	}
@@ -350,44 +330,21 @@
 
 // registerWatchedDirectoriesLocked sends the workspace/didChangeWatchedFiles
 // registrations to the client and updates s.watchedDirectories.
-func (s *Server) registerWatchedDirectoriesLocked(ctx context.Context, dirs map[span.URI]struct{}) error {
+func (s *Server) registerWatchedDirectoriesLocked(ctx context.Context, patterns map[string]struct{}) error {
 	if !s.session.Options().DynamicWatchedFilesSupported {
 		return nil
 	}
-	for k := range s.watchedDirectories {
-		delete(s.watchedDirectories, k)
+	for k := range s.watchedGlobPatterns {
+		delete(s.watchedGlobPatterns, k)
 	}
-	// Work-around microsoft/vscode#100870 by making sure that we are,
-	// at least, watching the user's entire workspace. This will still be
-	// applied to every folder in the workspace.
-	watchers := []protocol.FileSystemWatcher{{
-		GlobPattern: "**/*.{go,mod,sum}",
-		Kind:        float64(protocol.WatchChange + protocol.WatchDelete + protocol.WatchCreate),
-	}}
-	for dir := range dirs {
-		filename := dir.Filename()
-
-		// If the directory is within a workspace folder, we're already
-		// watching it via the relative path above.
-		var matched bool
-		for _, view := range s.session.Views() {
-			if source.InDir(view.Folder().Filename(), filename) {
-				matched = true
-				break
-			}
-		}
-		if matched {
-			continue
-		}
-
-		// If microsoft/vscode#100870 is resolved before
-		// microsoft/vscode#104387, we will need a work-around for Windows
-		// drive letter casing.
+	var watchers []protocol.FileSystemWatcher
+	for pattern := range patterns {
 		watchers = append(watchers, protocol.FileSystemWatcher{
-			GlobPattern: fmt.Sprintf("%s/**/*.{go,mod,sum}", filename),
+			GlobPattern: pattern,
 			Kind:        float64(protocol.WatchChange + protocol.WatchDelete + protocol.WatchCreate),
 		})
 	}
+
 	if err := s.client.RegisterCapability(ctx, &protocol.RegistrationParams{
 		Registrations: []protocol.Registration{{
 			ID:     watchedFilesCapabilityID(s.watchRegistrationCount),
@@ -401,8 +358,8 @@
 	}
 	s.watchRegistrationCount++
 
-	for dir := range dirs {
-		s.watchedDirectories[dir] = struct{}{}
+	for k, v := range patterns {
+		s.watchedGlobPatterns[k] = v
 	}
 	return nil
 }
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 6df2d2d..0f74ff3 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -25,7 +25,7 @@
 	return &Server{
 		diagnostics:           map[span.URI]*fileReports{},
 		gcOptimizationDetails: make(map[span.URI]struct{}),
-		watchedDirectories:    make(map[span.URI]struct{}),
+		watchedGlobPatterns:   make(map[string]struct{}),
 		changedFiles:          make(map[span.URI]struct{}),
 		session:               session,
 		client:                client,
@@ -79,12 +79,12 @@
 	// set of folders to build views for when we are ready
 	pendingFolders []protocol.WorkspaceFolder
 
-	// watchedDirectories is the set of directories that we have requested that
+	// watchedGlobPatterns is the set of glob patterns that we have requested
 	// the client watch on disk. It will be updated as the set of directories
 	// that the server should watch changes.
-	watchedDirectoriesMu   sync.Mutex
-	watchedDirectories     map[span.URI]struct{}
-	watchRegistrationCount uint64
+	watchedGlobPatternsMu  sync.Mutex
+	watchedGlobPatterns    map[string]struct{}
+	watchRegistrationCount int
 
 	diagnosticsMu sync.Mutex
 	diagnostics   map[span.URI]*fileReports
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 43157f7..cc9511d 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -142,10 +142,6 @@
 
 	// WorkspacePackages returns the snapshot's top-level packages.
 	WorkspacePackages(ctx context.Context) ([]Package, error)
-
-	// WorkspaceDirectories returns any directory known by the view. For views
-	// within a module, this is the module root and any replace targets.
-	WorkspaceDirectories(ctx context.Context) []span.URI
 }
 
 // PackageFilter sets how a package is filtered out from a set of packages
@@ -301,6 +297,11 @@
 
 	// SetOptions sets the options of this session to new values.
 	SetOptions(*Options)
+
+	// FileWatchingGlobPatterns returns glob patterns to watch every directory
+	// known by the view. For views within a module, this is the module root,
+	// any directory in the module root, and any replace targets.
+	FileWatchingGlobPatterns(ctx context.Context) map[string]struct{}
 }
 
 // Overlay is the type for a file held in memory on a session.
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
index 7281c90..d08d25f 100644
--- a/internal/lsp/text_synchronization.go
+++ b/internal/lsp/text_synchronization.go
@@ -239,10 +239,7 @@
 	// After any file modifications, we need to update our watched files,
 	// in case something changed. Compute the new set of directories to watch,
 	// and if it differs from the current set, send updated registrations.
-	if err := s.updateWatchedDirectories(ctx, snapshots); err != nil {
-		return err
-	}
-	return nil
+	return s.updateWatchedDirectories(ctx)
 }
 
 // DiagnosticWorkTitle returns the title of the diagnostic work resulting from a