internal/lsp: watch all files in the module and replace target

Previously, our file watching only considered the root directory of the
view, which may not include the entire module or its replaced
dependencies. Now we expand our watching to include the whole module.

As part of testing this, I noticed that VS Code's file watcher actually
only sends updates for files in the workspace, even if we request
notifications for all files in the module. I filed an issue to ask about
this: https://github.com/microsoft/vscode-languageserver-node/issues/641.

Change-Id: I9499d31aff273f69e9c117511e7985ff58b7fdc4
Reviewed-on: https://go-review.googlesource.com/c/tools/+/239198
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index f78dbcd..ee79f74 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -519,6 +519,36 @@
 	return strings.ToLower(filepath.Base(filename))
 }
 
+func (v *View) WorkspaceDirectories(ctx context.Context) ([]string, error) {
+	// If the view does not have a go.mod file, only the root directory
+	// is known. In GOPATH mode, we should really watch the entire GOPATH,
+	// but that's probably too expensive.
+	// TODO(rstambler): Figure out a better approach in the future.
+	if v.modURI == "" {
+		return []string{v.folder.Filename()}, nil
+	}
+	// Anything inside of the module root is known.
+	dirs := []string{filepath.Dir(v.modURI.Filename())}
+
+	// Keep track of any directories mentioned in replace targets.
+	fh, err := v.session.GetFile(ctx, v.modURI)
+	if err != nil {
+		return nil, err
+	}
+	pmh, err := v.Snapshot().ParseModHandle(ctx, fh)
+	if err != nil {
+		return nil, err
+	}
+	parsed, _, _, err := pmh.Parse(ctx)
+	if err != nil {
+		return nil, err
+	}
+	for _, replace := range parsed.Replace {
+		dirs = append(dirs, replace.New.Path)
+	}
+	return dirs, nil
+}
+
 func (v *View) relevantChange(c source.FileModification) bool {
 	// If the file is known to the view, the change is relevant.
 	known := v.knownFile(c.URI)
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index c2051ce..16cee52 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -152,25 +152,6 @@
 		)
 	}
 
-	if options.DynamicWatchedFilesSupported {
-		registrations = append(registrations, protocol.Registration{
-			ID:     "workspace/didChangeWatchedFiles",
-			Method: "workspace/didChangeWatchedFiles",
-			RegisterOptions: protocol.DidChangeWatchedFilesRegistrationOptions{
-				Watchers: []protocol.FileSystemWatcher{{
-					GlobPattern: "**/*.go",
-					Kind:        float64(protocol.WatchChange + protocol.WatchDelete + protocol.WatchCreate),
-				}},
-			},
-		})
-	}
-
-	if len(registrations) > 0 {
-		s.client.RegisterCapability(ctx, &protocol.RegistrationParams{
-			Registrations: registrations,
-		})
-	}
-
 	// TODO: this event logging may be unnecessary. The version info is included in the initialize response.
 	buf := &bytes.Buffer{}
 	debug.PrintVersionInfo(ctx, buf, true, debug.PlainText)
@@ -179,6 +160,31 @@
 	s.addFolders(ctx, s.pendingFolders)
 	s.pendingFolders = nil
 
+	if options.DynamicWatchedFilesSupported {
+		for _, view := range s.session.Views() {
+			dirs, err := view.WorkspaceDirectories(ctx)
+			if err != nil {
+				return err
+			}
+			for _, dir := range dirs {
+				registrations = append(registrations, protocol.Registration{
+					ID:     "workspace/didChangeWatchedFiles",
+					Method: "workspace/didChangeWatchedFiles",
+					RegisterOptions: protocol.DidChangeWatchedFilesRegistrationOptions{
+						Watchers: []protocol.FileSystemWatcher{{
+							GlobPattern: fmt.Sprintf("%s/**.go", dir),
+							Kind:        float64(protocol.WatchChange + protocol.WatchDelete + protocol.WatchCreate),
+						}},
+					},
+				})
+			}
+		}
+		if len(registrations) > 0 {
+			s.client.RegisterCapability(ctx, &protocol.RegistrationParams{
+				Registrations: registrations,
+			})
+		}
+	}
 	return nil
 }
 
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 1854bc8..89bed4b 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -177,6 +177,10 @@
 	// IgnoredFile reports if a file would be ignored by a `go list` of the whole
 	// workspace.
 	IgnoredFile(uri span.URI) bool
+
+	// 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) ([]string, error)
 }
 
 type BuiltinPackage interface {