Revert "internal/lsp: explicitly watch all known directories"

This reverts commit 3c3a81204b10d844dbacd6ca8ea5423628e00967.

Reason for revert: OpenBSD trybots are failing

Change-Id: I39f52ef07a667ed1457d2ae53863af15134eb78e
Reviewed-on: https://go-review.googlesource.com/c/tools/+/270798
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Trust: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index cf74534..36bfc50 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -624,15 +624,3 @@
 	}
 	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 648fdbe..33266d7 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -604,77 +604,8 @@
 	return ids
 }
 
-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.InDir(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{}{}
-	}
-
-	// 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 patterns
-	}
-
-	// 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.
-	s.mu.Lock()
-	defer s.mu.Unlock()
-	seen := make(map[span.URI]struct{})
-	for uri := range s.files {
-		dir := filepath.Dir(uri.Filename())
-		var matched bool
-		for _, wsDir := range dirs {
-			// Note: InDir handles symlinks, but InDirLex does not--it's too
-			// expensive to call InDir on every file in the snapshot.
-			if source.InDirLex(wsDir.Filename(), dir) {
-				matched = true
-				break
-			}
-		}
-		// Don't watch any directory outside of the workspace directories.
-		if !matched {
-			continue
-		}
-		for {
-			if dir == "" {
-				break
-			}
-			uri := span.URIFromPath(dir)
-			if _, ok := seen[uri]; ok {
-				break
-			}
-			seen[uri] = struct{}{}
-			dir = filepath.Dir(dir)
-		}
-	}
-	var dirNames []string
-	for uri := range seen {
-		dirNames = append(dirNames, uri.Filename())
-	}
-	sort.Strings(dirNames)
-	if len(dirNames) > 0 {
-		patterns[fmt.Sprintf("{%s}", strings.Join(dirNames, ","))] = struct{}{}
-	}
-	return patterns
+func (s *snapshot) WorkspaceDirectories(ctx context.Context) []span.URI {
+	return s.workspace.dirs(ctx, s)
 }
 
 func (s *snapshot) WorkspacePackages(ctx context.Context) ([]source.Package, error) {
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index bde908d..896e629 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -481,10 +481,6 @@
 }
 
 func (v *View) Snapshot(ctx context.Context) (source.Snapshot, func()) {
-	return v.getSnapshot(ctx)
-}
-
-func (v *View) getSnapshot(ctx context.Context) (*snapshot, func()) {
 	v.snapshotMu.Lock()
 	defer v.snapshotMu.Unlock()
 	return v.snapshot, v.snapshot.generation.Acquire(ctx)
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index 60079b4..558767b 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -196,6 +196,7 @@
 			}()
 		}()
 	}
+	dirsToWatch := map[span.URI]struct{}{}
 	// Only one view gets to have a workspace.
 	assignedWorkspace := false
 	for _, folder := range folders {
@@ -234,6 +235,10 @@
 			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 {
@@ -251,12 +256,13 @@
 			wg.Done()
 		}()
 	}
-
 	// Register for file watching notifications, if they are supported.
-	if err := s.updateWatchedDirectories(ctx); err != nil {
-		event.Error(ctx, "failed to register for file watching notifications", err)
+	s.watchedDirectoriesMu.Lock()
+	err := s.registerWatchedDirectoriesLocked(ctx, dirsToWatch)
+	s.watchedDirectoriesMu.Unlock()
+	if err != nil {
+		return 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 {
@@ -274,14 +280,34 @@
 // 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) error {
-	patterns := s.session.FileWatchingGlobPatterns(ctx)
+func (s *Server) updateWatchedDirectories(ctx context.Context, updatedSnapshots map[source.View]source.Snapshot) error {
+	dirsToWatch := map[span.URI]struct{}{}
+	seenViews := map[source.View]struct{}{}
 
-	s.watchedGlobPatternsMu.Lock()
-	defer s.watchedGlobPatternsMu.Unlock()
+	// 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()
 
 	// Nothing to do if the set of workspace directories is unchanged.
-	if equalURISet(s.watchedGlobPatterns, patterns) {
+	if equalURISet(s.watchedDirectories, dirsToWatch) {
 		return nil
 	}
 
@@ -290,11 +316,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.
-	prevID := s.watchRegistrationCount - 1
-	if err := s.registerWatchedDirectoriesLocked(ctx, patterns); err != nil {
-		return err
-	}
-	if prevID > 0 {
+	if s.watchRegistrationCount > 0 {
+		prevID := s.watchRegistrationCount - 1
+		if err := s.registerWatchedDirectoriesLocked(ctx, dirsToWatch); err != nil {
+			return err
+		}
 		return s.client.UnregisterCapability(ctx, &protocol.UnregistrationParams{
 			Unregisterations: []protocol.Unregistration{{
 				ID:     watchedFilesCapabilityID(prevID),
@@ -309,7 +335,7 @@
 	return fmt.Sprintf("workspace/didChangeWatchedFiles-%d", id)
 }
 
-func equalURISet(m1, m2 map[string]struct{}) bool {
+func equalURISet(m1, m2 map[span.URI]struct{}) bool {
 	if len(m1) != len(m2) {
 		return false
 	}
@@ -324,21 +350,44 @@
 
 // registerWatchedDirectoriesLocked sends the workspace/didChangeWatchedFiles
 // registrations to the client and updates s.watchedDirectories.
-func (s *Server) registerWatchedDirectoriesLocked(ctx context.Context, patterns map[string]struct{}) error {
+func (s *Server) registerWatchedDirectoriesLocked(ctx context.Context, dirs map[span.URI]struct{}) error {
 	if !s.session.Options().DynamicWatchedFilesSupported {
 		return nil
 	}
-	for k := range s.watchedGlobPatterns {
-		delete(s.watchedGlobPatterns, k)
+	for k := range s.watchedDirectories {
+		delete(s.watchedDirectories, k)
 	}
-	var watchers []protocol.FileSystemWatcher
-	for pattern := range patterns {
+	// 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.
 		watchers = append(watchers, protocol.FileSystemWatcher{
-			GlobPattern: pattern,
+			GlobPattern: fmt.Sprintf("%s/**/*.{go,mod,sum}", filename),
 			Kind:        float64(protocol.WatchChange + protocol.WatchDelete + protocol.WatchCreate),
 		})
 	}
-
 	if err := s.client.RegisterCapability(ctx, &protocol.RegistrationParams{
 		Registrations: []protocol.Registration{{
 			ID:     watchedFilesCapabilityID(s.watchRegistrationCount),
@@ -352,8 +401,8 @@
 	}
 	s.watchRegistrationCount++
 
-	for k, v := range patterns {
-		s.watchedGlobPatterns[k] = v
+	for dir := range dirs {
+		s.watchedDirectories[dir] = struct{}{}
 	}
 	return nil
 }
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index d4a6f0a..905a408 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -25,7 +25,7 @@
 	return &Server{
 		delivered:             make(map[span.URI]sentDiagnostics),
 		gcOptimizationDetails: make(map[span.URI]struct{}),
-		watchedGlobPatterns:   make(map[string]struct{}),
+		watchedDirectories:    make(map[span.URI]struct{}),
 		changedFiles:          make(map[span.URI]struct{}),
 		session:               session,
 		client:                client,
@@ -79,11 +79,11 @@
 	// set of folders to build views for when we are ready
 	pendingFolders []protocol.WorkspaceFolder
 
-	// watchedGlobPatterns is the set of glob patterns that we have requested
+	// watchedDirectories is the set of directories that we have requested that
 	// the client watch on disk. It will be updated as the set of directories
 	// that the server should watch changes.
-	watchedGlobPatternsMu  sync.Mutex
-	watchedGlobPatterns    map[string]struct{}
+	watchedDirectoriesMu   sync.Mutex
+	watchedDirectories     map[span.URI]struct{}
 	watchRegistrationCount uint64
 
 	// delivered is a cache of the diagnostics that the server has sent.
diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go
index 1fffda5..72b074a 100644
--- a/internal/lsp/source/util.go
+++ b/internal/lsp/source/util.go
@@ -447,25 +447,25 @@
 //
 // Copied and slightly adjusted from go/src/cmd/go/internal/search/search.go.
 func InDir(dir, path string) bool {
-	if InDirLex(dir, path) {
+	if rel := inDirLex(path, dir); rel != "" {
 		return true
 	}
 	xpath, err := filepath.EvalSymlinks(path)
 	if err != nil || xpath == path {
 		xpath = ""
 	} else {
-		if InDirLex(dir, xpath) {
+		if rel := inDirLex(xpath, dir); rel != "" {
 			return true
 		}
 	}
 
 	xdir, err := filepath.EvalSymlinks(dir)
 	if err == nil && xdir != dir {
-		if InDirLex(xdir, path) {
+		if rel := inDirLex(path, xdir); rel != "" {
 			return true
 		}
 		if xpath != "" {
-			if InDirLex(xdir, xpath) {
+			if rel := inDirLex(xpath, xdir); rel != "" {
 				return true
 			}
 		}
@@ -473,40 +473,43 @@
 	return false
 }
 
-// InDirLex is like inDir but only checks the lexical form of the file names.
-// It does not consider symbolic links.
-//
 // Copied from go/src/cmd/go/internal/search/search.go.
-func InDirLex(dir, path string) bool {
+//
+// inDirLex is like inDir but only checks the lexical form of the file names.
+// It does not consider symbolic links.
+// TODO(rsc): This is a copy of str.HasFilePathPrefix, modified to
+// return the suffix. Most uses of str.HasFilePathPrefix should probably
+// be calling InDir instead.
+func inDirLex(path, dir string) string {
 	pv := strings.ToUpper(filepath.VolumeName(path))
 	dv := strings.ToUpper(filepath.VolumeName(dir))
 	path = path[len(pv):]
 	dir = dir[len(dv):]
 	switch {
 	default:
-		return false
+		return ""
 	case pv != dv:
-		return false
+		return ""
 	case len(path) == len(dir):
 		if path == dir {
-			return true
+			return "."
 		}
-		return false
+		return ""
 	case dir == "":
-		return path != ""
+		return path
 	case len(path) > len(dir):
 		if dir[len(dir)-1] == filepath.Separator {
 			if path[:len(dir)] == dir {
-				return path[len(dir):] != ""
+				return path[len(dir):]
 			}
-			return false
+			return ""
 		}
 		if path[len(dir)] == filepath.Separator && path[:len(dir)] == dir {
 			if len(path) == len(dir)+1 {
-				return true
+				return "."
 			}
-			return path[len(dir)+1:] != ""
+			return path[len(dir)+1:]
 		}
-		return false
+		return ""
 	}
 }
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 3ba82a2..fa307df 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -142,6 +142,10 @@
 
 	// 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
@@ -297,11 +301,6 @@
 
 	// 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 8839e88..fc35ff5 100644
--- a/internal/lsp/text_synchronization.go
+++ b/internal/lsp/text_synchronization.go
@@ -250,7 +250,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); err != nil {
+	if err := s.updateWatchedDirectories(ctx, snapshots); err != nil {
 		return err
 	}
 	return nil