internal/lsp/cache: register a file watcher for explicit GOWORK values

When the go.work file is set by the GOWORK environment variable, we must
create an additional file watching pattern.

Fixes golang/go#53631

Change-Id: I2d78c5a9ee8a71551d5274db7eb4e6c623d8db74
Reviewed-on: https://go-review.googlesource.com/c/tools/+/421501
gopls-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go
index b377668..209e015 100644
--- a/gopls/internal/regtest/diagnostics/diagnostics_test.go
+++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go
@@ -1548,7 +1548,7 @@
 }
 -- go.mod --
 module mod.com
--- main.go --
+-- cmd/main.go --
 package main
 
 import "mod.com/bob"
@@ -1558,11 +1558,12 @@
 }
 `
 	Run(t, mod, func(t *testing.T, env *Env) {
+		env.Await(FileWatchMatching("bob"))
 		env.RemoveWorkspaceFile("bob")
 		env.Await(
-			env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`),
+			env.DiagnosticAtRegexp("cmd/main.go", `"mod.com/bob"`),
 			EmptyDiagnostics("bob/bob.go"),
-			RegistrationMatching("didChangeWatchedFiles"),
+			NoFileWatchMatching("bob"),
 		)
 	})
 }
diff --git a/gopls/internal/regtest/workspace/fromenv_test.go b/gopls/internal/regtest/workspace/fromenv_test.go
index 1d95160..8a77867 100644
--- a/gopls/internal/regtest/workspace/fromenv_test.go
+++ b/gopls/internal/regtest/workspace/fromenv_test.go
@@ -41,6 +41,8 @@
 	WithOptions(
 		EnvVars{"GOWORK": "$SANDBOX_WORKDIR/config/go.work"},
 	).Run(t, files, func(t *testing.T, env *Env) {
+		// When we have an explicit GOWORK set, we should get a file watch request.
+		env.Await(FileWatchMatching(`config.go\.work`))
 		// Even though work/b is not open, we should get its diagnostics as it is
 		// included in the workspace.
 		env.OpenFile("work/a/a.go")
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 6ed6fe5..0fa670c 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -887,6 +887,10 @@
 		fmt.Sprintf("**/*.{%s}", extensions): {},
 	}
 
+	if s.view.explicitGowork != "" {
+		patterns[s.view.explicitGowork.Filename()] = struct{}{}
+	}
+
 	// Add a pattern for each Go module in the workspace that is not within the view.
 	dirs := s.workspace.dirs(ctx, s)
 	for _, dir := range dirs {
diff --git a/internal/lsp/regtest/env.go b/internal/lsp/regtest/env.go
index 502636a..f8a68b3 100644
--- a/internal/lsp/regtest/env.go
+++ b/internal/lsp/regtest/env.go
@@ -85,8 +85,9 @@
 	showMessage        []*protocol.ShowMessageParams
 	showMessageRequest []*protocol.ShowMessageRequestParams
 
-	registrations   []*protocol.RegistrationParams
-	unregistrations []*protocol.UnregistrationParams
+	registrations          []*protocol.RegistrationParams
+	registeredCapabilities map[string]protocol.Registration
+	unregistrations        []*protocol.UnregistrationParams
 
 	// outstandingWork is a map of token->work summary. All tokens are assumed to
 	// be string, though the spec allows for numeric tokens as well.  When work
@@ -226,6 +227,12 @@
 	defer a.mu.Unlock()
 
 	a.state.registrations = append(a.state.registrations, m)
+	if a.state.registeredCapabilities == nil {
+		a.state.registeredCapabilities = make(map[string]protocol.Registration)
+	}
+	for _, reg := range m.Registrations {
+		a.state.registeredCapabilities[reg.Method] = reg
+	}
 	a.checkConditionsLocked()
 	return nil
 }
diff --git a/internal/lsp/regtest/expectation.go b/internal/lsp/regtest/expectation.go
index a0a7d52..7867af9 100644
--- a/internal/lsp/regtest/expectation.go
+++ b/internal/lsp/regtest/expectation.go
@@ -394,32 +394,66 @@
 	}
 }
 
-// RegistrationExpectation is an expectation on the capability registrations
-// received by the editor from gopls.
-type RegistrationExpectation struct {
-	check       func([]*protocol.RegistrationParams) Verdict
-	description string
+// FileWatchMatching expects that a file registration matches re.
+func FileWatchMatching(re string) SimpleExpectation {
+	return SimpleExpectation{
+		check:       checkFileWatch(re, Met, Unmet),
+		description: fmt.Sprintf("file watch matching %q", re),
+	}
 }
 
-// Check implements the Expectation interface.
-func (e RegistrationExpectation) Check(s State) Verdict {
-	return e.check(s.registrations)
+// NoFileWatchMatching expects that no file registration matches re.
+func NoFileWatchMatching(re string) SimpleExpectation {
+	return SimpleExpectation{
+		check:       checkFileWatch(re, Unmet, Met),
+		description: fmt.Sprintf("no file watch matching %q", re),
+	}
 }
 
-// Description implements the Expectation interface.
-func (e RegistrationExpectation) Description() string {
-	return e.description
+func checkFileWatch(re string, onMatch, onNoMatch Verdict) func(State) Verdict {
+	rec := regexp.MustCompile(re)
+	return func(s State) Verdict {
+		r := s.registeredCapabilities["workspace/didChangeWatchedFiles"]
+		watchers := jsonProperty(r.RegisterOptions, "watchers").([]interface{})
+		for _, watcher := range watchers {
+			pattern := jsonProperty(watcher, "globPattern").(string)
+			if rec.MatchString(pattern) {
+				return onMatch
+			}
+		}
+		return onNoMatch
+	}
+}
+
+// jsonProperty extracts a value from a path of JSON property names, assuming
+// the default encoding/json unmarshaling to the empty interface (i.e.: that
+// JSON objects are unmarshalled as map[string]interface{})
+//
+// For example, if obj is unmarshalled from the following json:
+//
+//	{
+//		"foo": { "bar": 3 }
+//	}
+//
+// Then jsonProperty(obj, "foo", "bar") will be 3.
+func jsonProperty(obj interface{}, path ...string) interface{} {
+	if len(path) == 0 || obj == nil {
+		return obj
+	}
+	m := obj.(map[string]interface{})
+	return jsonProperty(m[path[0]], path[1:]...)
 }
 
 // RegistrationMatching asserts that the client has received a capability
 // registration matching the given regexp.
-func RegistrationMatching(re string) RegistrationExpectation {
-	rec, err := regexp.Compile(re)
-	if err != nil {
-		panic(err)
-	}
-	check := func(params []*protocol.RegistrationParams) Verdict {
-		for _, p := range params {
+//
+// TODO(rfindley): remove this once TestWatchReplaceTargets has been revisited.
+//
+// Deprecated: use (No)FileWatchMatching
+func RegistrationMatching(re string) SimpleExpectation {
+	rec := regexp.MustCompile(re)
+	check := func(s State) Verdict {
+		for _, p := range s.registrations {
 			for _, r := range p.Registrations {
 				if rec.Match([]byte(r.Method)) {
 					return Met
@@ -428,38 +462,18 @@
 		}
 		return Unmet
 	}
-	return RegistrationExpectation{
+	return SimpleExpectation{
 		check:       check,
 		description: fmt.Sprintf("registration matching %q", re),
 	}
 }
 
-// UnregistrationExpectation is an expectation on the capability
-// unregistrations received by the editor from gopls.
-type UnregistrationExpectation struct {
-	check       func([]*protocol.UnregistrationParams) Verdict
-	description string
-}
-
-// Check implements the Expectation interface.
-func (e UnregistrationExpectation) Check(s State) Verdict {
-	return e.check(s.unregistrations)
-}
-
-// Description implements the Expectation interface.
-func (e UnregistrationExpectation) Description() string {
-	return e.description
-}
-
 // UnregistrationMatching asserts that the client has received an
 // unregistration whose ID matches the given regexp.
-func UnregistrationMatching(re string) UnregistrationExpectation {
-	rec, err := regexp.Compile(re)
-	if err != nil {
-		panic(err)
-	}
-	check := func(params []*protocol.UnregistrationParams) Verdict {
-		for _, p := range params {
+func UnregistrationMatching(re string) SimpleExpectation {
+	rec := regexp.MustCompile(re)
+	check := func(s State) Verdict {
+		for _, p := range s.unregistrations {
 			for _, r := range p.Unregisterations {
 				if rec.Match([]byte(r.Method)) {
 					return Met
@@ -468,7 +482,7 @@
 		}
 		return Unmet
 	}
-	return UnregistrationExpectation{
+	return SimpleExpectation{
 		check:       check,
 		description: fmt.Sprintf("unregistration matching %q", re),
 	}