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),
}