From 92d58ea4e734e02be542fe27eef6fb1a600e50f4 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 5 Aug 2022 18:08:11 -0400 Subject: [PATCH] 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 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Suzy Mueller --- .../regtest/diagnostics/diagnostics_test.go | 7 +- .../regtest/workspace/fromenv_test.go | 2 + internal/lsp/cache/snapshot.go | 4 + internal/lsp/regtest/env.go | 11 +- internal/lsp/regtest/expectation.go | 102 ++++++++++-------- 5 files changed, 77 insertions(+), 49 deletions(-) diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index b377668e87..209e015e0e 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -1548,7 +1548,7 @@ func Hello() { } -- go.mod -- module mod.com --- main.go -- +-- cmd/main.go -- package main import "mod.com/bob" @@ -1558,11 +1558,12 @@ func main() { } ` 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 1d95160eaa..8a77867cf4 100644 --- a/gopls/internal/regtest/workspace/fromenv_test.go +++ b/gopls/internal/regtest/workspace/fromenv_test.go @@ -41,6 +41,8 @@ use ( 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 6ed6fe5360..0fa670cf20 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -887,6 +887,10 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru 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 502636a850..f8a68b3419 100644 --- a/internal/lsp/regtest/env.go +++ b/internal/lsp/regtest/env.go @@ -85,8 +85,9 @@ type State struct { 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 @@ func (a *Awaiter) onRegistration(_ context.Context, m *protocol.RegistrationPara 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 a0a7d529aa..7867af980b 100644 --- a/internal/lsp/regtest/expectation.go +++ b/internal/lsp/regtest/expectation.go @@ -394,32 +394,66 @@ func NoLogMatching(typ protocol.MessageType, re string) LogExpectation { } } -// 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 @@ func RegistrationMatching(re string) RegistrationExpectation { } 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 @@ func UnregistrationMatching(re string) UnregistrationExpectation { } return Unmet } - return UnregistrationExpectation{ + return SimpleExpectation{ check: check, description: fmt.Sprintf("unregistration matching %q", re), }