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>
This commit is contained in:
Robert Findley 2022-08-05 18:08:11 -04:00
parent 98aef77998
commit 92d58ea4e7
5 changed files with 77 additions and 49 deletions

View File

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

View File

@ -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")

View File

@ -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 {

View File

@ -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
}

View File

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