all: merge master (92d58ea) into gopls-release-branch.0.9
Also add a replace directive to gopls/go.mod, and re-tidy.
Merge List:
+ 2022-08-08 92d58ea4e internal/lsp/cache: register a file watcher for explicit GOWORK values
+ 2022-08-08 98aef7799 internal/lsp/cache: track explicit go.work files outside the workspace
+ 2022-08-08 fff6d6d39 internal/lsp: update the broken workspace message to mention go.work
+ 2022-08-08 9b6085242 gopls/internal/regtest: move TestMultipleModules_Warning to ./workspace
+ 2022-08-05 06d96ee8f gopls/internal/regtest/bench: add a test for completion following edits
Change-Id: I78f2b3552725d0317ec1dd62196558997f70fe29
diff --git a/gopls/go.mod b/gopls/go.mod
index d68c4a2..b3fc2db 100644
--- a/gopls/go.mod
+++ b/gopls/go.mod
@@ -23,3 +23,5 @@
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
golang.org/x/text v0.3.7 // indirect
)
+
+replace golang.org/x/tools => ../
diff --git a/gopls/go.sum b/gopls/go.sum
index 9696486..ecd3f4d 100644
--- a/gopls/go.sum
+++ b/gopls/go.sum
@@ -41,38 +41,20 @@
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20220722155223-a9213eeb770e h1:7Xs2YCOpMlNqSQSmrrnhlzBXIE/bpMecZplbLePTJvE=
golang.org/x/exp/typeparams v0.0.0-20220722155223-a9213eeb770e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -82,23 +64,11 @@
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
-golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
-golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
-golang.org/x/tools v0.1.11-0.20220523181440-ccb10502d1a5/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
-golang.org/x/tools v0.1.13-0.20220805154628-fad228792ce9 h1:wnZ6J3Vs1RS/EjUI71D435/4VhO7j/XKzGYkQy4Rfp0=
-golang.org/x/tools v0.1.13-0.20220805154628-fad228792ce9/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/vuln v0.0.0-20220725105440-4151a5aca1df h1:BkeW9/QJhcigekDUPS9N9bIb0v7gPKKmLYeczVAqr2s=
golang.org/x/vuln v0.0.0-20220725105440-4151a5aca1df/go.mod h1:UZshlUPxXeGUM9I14UOawXQg6yosDE9cr1vKY/DzgWo=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/gopls/internal/regtest/bench/bench_test.go b/gopls/internal/regtest/bench/bench_test.go
index a3780f0..cfe4db6 100644
--- a/gopls/internal/regtest/bench/bench_test.go
+++ b/gopls/internal/regtest/bench/bench_test.go
@@ -133,7 +133,7 @@
dir := benchmarkDir()
var err error
- sandbox, editor, awaiter, err = connectEditor(dir)
+ sandbox, editor, awaiter, err = connectEditor(dir, fake.EditorConfig{})
if err != nil {
log.Fatalf("connecting editor: %v", err)
}
@@ -154,7 +154,7 @@
// connectEditor connects a fake editor session in the given dir, using the
// given editor config.
-func connectEditor(dir string) (*fake.Sandbox, *fake.Editor, *regtest.Awaiter, error) {
+func connectEditor(dir string, config fake.EditorConfig) (*fake.Sandbox, *fake.Editor, *regtest.Awaiter, error) {
s, err := fake.NewSandbox(&fake.SandboxConfig{
Workdir: dir,
GOPROXY: "https://proxy.golang.org",
@@ -165,7 +165,7 @@
a := regtest.NewAwaiter(s.Workdir)
ts := getServer()
- e, err := fake.NewEditor(s, fake.EditorConfig{}).Connect(context.Background(), ts, a.Hooks())
+ e, err := fake.NewEditor(s, config).Connect(context.Background(), ts, a.Hooks())
if err != nil {
return nil, nil, nil, err
}
diff --git a/gopls/internal/regtest/bench/completion_test.go b/gopls/internal/regtest/bench/completion_test.go
index cdafb08..a8725ce 100644
--- a/gopls/internal/regtest/bench/completion_test.go
+++ b/gopls/internal/regtest/bench/completion_test.go
@@ -18,8 +18,9 @@
type completionBenchOptions struct {
file, locationRegexp string
- // hook to run edits before initial completion
- preCompletionEdits func(*Env)
+ // Hooks to run edits before initial completion
+ setup func(*Env) // run before the benchmark starts
+ beforeCompletion func(*Env) // run before each completion
}
func benchmarkCompletion(options completionBenchOptions, b *testing.B) {
@@ -27,7 +28,11 @@
// Use a new environment for each test, to avoid any existing state from the
// previous session.
- sandbox, editor, awaiter, err := connectEditor(dir)
+ sandbox, editor, awaiter, err := connectEditor(dir, fake.EditorConfig{
+ Settings: map[string]interface{}{
+ "completionBudget": "1m", // arbitrary long completion budget
+ },
+ })
if err != nil {
b.Fatal(err)
}
@@ -45,11 +50,10 @@
Sandbox: sandbox,
Awaiter: awaiter,
}
- env.OpenFile(options.file)
// Run edits required for this completion.
- if options.preCompletionEdits != nil {
- options.preCompletionEdits(env)
+ if options.setup != nil {
+ options.setup(env)
}
// Run a completion to make sure the system is warm.
@@ -70,6 +74,9 @@
// initialization).
b.Run("completion", func(b *testing.B) {
for i := 0; i < b.N; i++ {
+ if options.beforeCompletion != nil {
+ options.beforeCompletion(env)
+ }
env.Completion(options.file, pos)
}
})
@@ -92,7 +99,7 @@
func BenchmarkStructCompletion(b *testing.B) {
file := "internal/lsp/cache/session.go"
- preCompletionEdits := func(env *Env) {
+ setup := func(env *Env) {
env.OpenFile(file)
originalBuffer := env.Editor.BufferText(file)
env.EditBuffer(file, fake.Edit{
@@ -102,17 +109,19 @@
}
benchmarkCompletion(completionBenchOptions{
- file: file,
- locationRegexp: `var testVariable map\[string\]bool = Session{}(\.)`,
- preCompletionEdits: preCompletionEdits,
+ file: file,
+ locationRegexp: `var testVariable map\[string\]bool = Session{}(\.)`,
+ setup: setup,
}, b)
}
// Benchmark import completion in tools codebase.
func BenchmarkImportCompletion(b *testing.B) {
+ const file = "internal/lsp/source/completion/completion.go"
benchmarkCompletion(completionBenchOptions{
- file: "internal/lsp/source/completion/completion.go",
+ file: file,
locationRegexp: `go\/()`,
+ setup: func(env *Env) { env.OpenFile(file) },
}, b)
}
@@ -120,7 +129,7 @@
func BenchmarkSliceCompletion(b *testing.B) {
file := "internal/lsp/cache/session.go"
- preCompletionEdits := func(env *Env) {
+ setup := func(env *Env) {
env.OpenFile(file)
originalBuffer := env.Editor.BufferText(file)
env.EditBuffer(file, fake.Edit{
@@ -130,9 +139,9 @@
}
benchmarkCompletion(completionBenchOptions{
- file: file,
- locationRegexp: `var testVariable \[\]byte (=)`,
- preCompletionEdits: preCompletionEdits,
+ file: file,
+ locationRegexp: `var testVariable \[\]byte (=)`,
+ setup: setup,
}, b)
}
@@ -144,7 +153,7 @@
c.inference.kindMatches(c.)
}
`
- preCompletionEdits := func(env *Env) {
+ setup := func(env *Env) {
env.OpenFile(file)
originalBuffer := env.Editor.BufferText(file)
env.EditBuffer(file, fake.Edit{
@@ -154,8 +163,42 @@
}
benchmarkCompletion(completionBenchOptions{
- file: file,
- locationRegexp: `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`,
- preCompletionEdits: preCompletionEdits,
+ file: file,
+ locationRegexp: `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`,
+ setup: setup,
+ }, b)
+}
+
+// Benchmark completion following an arbitrary edit.
+//
+// Edits force type-checked packages to be invalidated, so we want to measure
+// how long it takes before completion results are available.
+func BenchmarkCompletionFollowingEdit(b *testing.B) {
+ file := "internal/lsp/source/completion/completion2.go"
+ fileContent := `
+package completion
+
+func (c *completer) _() {
+ c.inference.kindMatches(c.)
+ // __MAGIC_STRING_1
+}
+`
+ setup := func(env *Env) {
+ env.CreateBuffer(file, fileContent)
+ }
+
+ n := 1
+ beforeCompletion := func(env *Env) {
+ old := fmt.Sprintf("__MAGIC_STRING_%d", n)
+ new := fmt.Sprintf("__MAGIC_STRING_%d", n+1)
+ n++
+ env.RegexpReplace(file, old, new)
+ }
+
+ benchmarkCompletion(completionBenchOptions{
+ file: file,
+ locationRegexp: `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`,
+ setup: setup,
+ beforeCompletion: beforeCompletion,
}, b)
}
diff --git a/gopls/internal/regtest/bench/iwl_test.go b/gopls/internal/regtest/bench/iwl_test.go
index e262a39..b223e33 100644
--- a/gopls/internal/regtest/bench/iwl_test.go
+++ b/gopls/internal/regtest/bench/iwl_test.go
@@ -8,6 +8,7 @@
"context"
"testing"
+ "golang.org/x/tools/internal/lsp/fake"
. "golang.org/x/tools/internal/lsp/regtest"
)
@@ -19,7 +20,7 @@
ctx := context.Background()
for i := 0; i < b.N; i++ {
- _, editor, awaiter, err := connectEditor(dir)
+ _, editor, awaiter, err := connectEditor(dir, fake.EditorConfig{})
if err != nil {
b.Fatal(err)
}
diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go
index d7246ae..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"),
)
})
}
@@ -1692,57 +1693,6 @@
})
}
-func TestMultipleModules_Warning(t *testing.T) {
- const modules = `
--- a/go.mod --
-module a.com
-
-go 1.12
--- a/a.go --
-package a
--- b/go.mod --
-module b.com
-
-go 1.12
--- b/b.go --
-package b
-`
- for _, go111module := range []string{"on", "auto"} {
- t.Run("GO111MODULE="+go111module, func(t *testing.T) {
- WithOptions(
- Modes(Default),
- EnvVars{"GO111MODULE": go111module},
- ).Run(t, modules, func(t *testing.T, env *Env) {
- env.OpenFile("a/a.go")
- env.OpenFile("b/go.mod")
- env.Await(
- env.DiagnosticAtRegexp("a/a.go", "package a"),
- env.DiagnosticAtRegexp("b/go.mod", "module b.com"),
- OutstandingWork(lsp.WorkspaceLoadFailure, "gopls requires a module at the root of your workspace."),
- )
- })
- })
- }
-
- // Expect no warning if GO111MODULE=auto in a directory in GOPATH.
- t.Run("GOPATH_GO111MODULE_auto", func(t *testing.T) {
- WithOptions(
- Modes(Default),
- EnvVars{"GO111MODULE": "auto"},
- InGOPATH(),
- ).Run(t, modules, func(t *testing.T, env *Env) {
- env.OpenFile("a/a.go")
- env.Await(
- OnceMet(
- env.DoneWithOpen(),
- EmptyDiagnostics("a/a.go"),
- ),
- NoOutstandingWork(),
- )
- })
- })
-}
-
func TestNestedModules(t *testing.T) {
const proxy = `
-- nested.com@v1.0.0/go.mod --
diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go
index e88b98b..fbc41de 100644
--- a/gopls/internal/regtest/workspace/broken_test.go
+++ b/gopls/internal/regtest/workspace/broken_test.go
@@ -16,6 +16,10 @@
// This file holds various tests for UX with respect to broken workspaces.
//
// TODO: consolidate other tests here.
+//
+// TODO: write more tests:
+// - an explicit GOWORK value that doesn't exist
+// - using modules and/or GOWORK inside of GOPATH?
// Test for golang/go#53933
func TestBrokenWorkspace_DuplicateModules(t *testing.T) {
@@ -28,8 +32,6 @@
module example.com/foo
go 1.12
--- example.com/foo@v1.2.3/foo.go --
-package foo
`
const src = `
@@ -167,3 +169,83 @@
env.Await(NoOutstandingDiagnostics())
})
}
+
+func TestMultipleModules_Warning(t *testing.T) {
+ msgForVersion := func(ver int) string {
+ if ver >= 18 {
+ return `gopls was not able to find modules in your workspace.`
+ } else {
+ return `gopls requires a module at the root of your workspace.`
+ }
+ }
+
+ const modules = `
+-- a/go.mod --
+module a.com
+
+go 1.12
+-- a/a.go --
+package a
+-- b/go.mod --
+module b.com
+
+go 1.12
+-- b/b.go --
+package b
+`
+ for _, go111module := range []string{"on", "auto"} {
+ t.Run("GO111MODULE="+go111module, func(t *testing.T) {
+ WithOptions(
+ Modes(Default),
+ EnvVars{"GO111MODULE": go111module},
+ ).Run(t, modules, func(t *testing.T, env *Env) {
+ ver := env.GoVersion()
+ msg := msgForVersion(ver)
+ env.OpenFile("a/a.go")
+ env.OpenFile("b/go.mod")
+ env.Await(
+ env.DiagnosticAtRegexp("a/a.go", "package a"),
+ env.DiagnosticAtRegexp("b/go.mod", "module b.com"),
+ OutstandingWork(lsp.WorkspaceLoadFailure, msg),
+ )
+
+ // Changing the workspace folders to the valid modules should resolve
+ // the workspace error.
+ env.ChangeWorkspaceFolders("a", "b")
+ env.Await(NoOutstandingWork())
+
+ env.ChangeWorkspaceFolders(".")
+
+ // TODO(rfindley): when GO111MODULE=auto, we need to open or change a
+ // file here in order to detect a critical error. This is because gopls
+ // has forgotten about a/a.go, and therefor doesn't hit the heuristic
+ // "all packages are command-line-arguments".
+ //
+ // This is broken, and could be fixed by adjusting the heuristic to
+ // account for the scenario where there are *no* workspace packages, or
+ // (better) trying to get workspace packages for each open file. See
+ // also golang/go#54261.
+ env.OpenFile("b/b.go")
+ env.Await(OutstandingWork(lsp.WorkspaceLoadFailure, msg))
+ })
+ })
+ }
+
+ // Expect no warning if GO111MODULE=auto in a directory in GOPATH.
+ t.Run("GOPATH_GO111MODULE_auto", func(t *testing.T) {
+ WithOptions(
+ Modes(Default),
+ EnvVars{"GO111MODULE": "auto"},
+ InGOPATH(),
+ ).Run(t, modules, func(t *testing.T, env *Env) {
+ env.OpenFile("a/a.go")
+ env.Await(
+ OnceMet(
+ env.DoneWithOpen(),
+ EmptyDiagnostics("a/a.go"),
+ ),
+ NoOutstandingWork(),
+ )
+ })
+ })
+}
diff --git a/gopls/internal/regtest/workspace/fromenv_test.go b/gopls/internal/regtest/workspace/fromenv_test.go
new file mode 100644
index 0000000..8a77867
--- /dev/null
+++ b/gopls/internal/regtest/workspace/fromenv_test.go
@@ -0,0 +1,56 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package workspace
+
+import (
+ "testing"
+
+ . "golang.org/x/tools/internal/lsp/regtest"
+)
+
+// Test that setting go.work via environment variables or settings works.
+func TestUseGoWorkOutsideTheWorkspace(t *testing.T) {
+ const files = `
+-- work/a/go.mod --
+module a.com
+
+go 1.12
+-- work/a/a.go --
+package a
+-- work/b/go.mod --
+module b.com
+
+go 1.12
+-- work/b/b.go --
+package b
+
+func _() {
+ x := 1 // unused
+}
+-- config/go.work --
+go 1.18
+
+use (
+ $SANDBOX_WORKDIR/work/a
+ $SANDBOX_WORKDIR/work/b
+)
+`
+
+ 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")
+ env.Await(
+ OnceMet(
+ env.DoneWithOpen(),
+ env.DiagnosticAtRegexpWithMessage("work/b/b.go", "x := 1", "not used"),
+ ),
+ )
+ })
+}
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index 0952fc6..7f30939 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -283,15 +283,20 @@
// workspaceLayoutErrors returns a diagnostic for every open file, as well as
// an error message if there are no open files.
func (s *snapshot) workspaceLayoutError(ctx context.Context) *source.CriticalError {
+ // TODO(rfindley): do we really not want to show a critical error if the user
+ // has no go.mod files?
if len(s.workspace.getKnownModFiles()) == 0 {
return nil
}
+
+ // TODO(rfindley): both of the checks below should be delegated to the workspace.
if s.view.userGo111Module == off {
return nil
}
if s.workspace.moduleSource != legacyWorkspace {
return nil
}
+
// If the user has one module per view, there is nothing to warn about.
if s.ValidBuildConfiguration() && len(s.workspace.getKnownModFiles()) == 1 {
return nil
@@ -305,10 +310,21 @@
// that the user has opened a directory that contains multiple modules.
// Check for that an warn about it.
if !s.ValidBuildConfiguration() {
- msg := `gopls requires a module at the root of your workspace.
-You can work with multiple modules by opening each one as a workspace folder.
-Improvements to this workflow will be coming soon, and you can learn more here:
+ var msg string
+ if s.view.goversion >= 18 {
+ msg = `gopls was not able to find modules in your workspace.
+When outside of GOPATH, gopls needs to know which modules you are working on.
+You can fix this by opening your workspace to a folder inside a Go module, or
+by using a go.work file to specify multiple modules.
+See the documentation for more information on setting up your workspace:
https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`
+ } else {
+ msg = `gopls requires a module at the root of your workspace.
+You can work with multiple modules by upgrading to Go 1.18 or later, and using
+go workspaces (go.work files).
+See the documentation for more information on setting up your workspace:
+https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`
+ }
return &source.CriticalError{
MainError: fmt.Errorf(msg),
Diagnostics: s.applyCriticalErrorToFiles(ctx, msg, openFiles),
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 984e8c1..d11c06d 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -7,6 +7,7 @@
import (
"context"
"fmt"
+ "os"
"strconv"
"strings"
"sync"
@@ -199,8 +200,14 @@
}
}
+ explicitGowork := os.Getenv("GOWORK")
+ if v, ok := options.Env["GOWORK"]; ok {
+ explicitGowork = v
+ }
+ goworkURI := span.URIFromPath(explicitGowork)
+
// Build the gopls workspace, collecting active modules in the view.
- workspace, err := newWorkspace(ctx, root, s, pathExcludedByFilterFunc(root.Filename(), wsInfo.gomodcache, options), wsInfo.userGo111Module == off, options.ExperimentalWorkspaceModule)
+ workspace, err := newWorkspace(ctx, root, goworkURI, s, pathExcludedByFilterFunc(root.Filename(), wsInfo.gomodcache, options), wsInfo.userGo111Module == off, options.ExperimentalWorkspaceModule)
if err != nil {
return nil, nil, func() {}, err
}
@@ -223,6 +230,7 @@
filesByURI: map[span.URI]*fileBase{},
filesByBase: map[string][]*fileBase{},
rootURI: root,
+ explicitGowork: goworkURI,
workspaceInformation: *wsInfo,
}
v.importsState = &importsState{
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index da2d7b5..0fa670c 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -274,7 +274,24 @@
}
func (s *snapshot) ValidBuildConfiguration() bool {
- return validBuildConfiguration(s.view.rootURI, &s.view.workspaceInformation, s.workspace.getActiveModFiles())
+ // Since we only really understand the `go` command, if the user has a
+ // different GOPACKAGESDRIVER, assume that their configuration is valid.
+ if s.view.hasGopackagesDriver {
+ return true
+ }
+ // Check if the user is working within a module or if we have found
+ // multiple modules in the workspace.
+ if len(s.workspace.getActiveModFiles()) > 0 {
+ return true
+ }
+ // The user may have a multiple directories in their GOPATH.
+ // Check if the workspace is within any of them.
+ for _, gp := range filepath.SplitList(s.view.gopath) {
+ if source.InDir(filepath.Join(gp, "src"), s.view.rootURI.Filename()) {
+ return true
+ }
+ }
+ return false
}
// workspaceMode describes the way in which the snapshot's workspace should
@@ -419,6 +436,12 @@
s.view.optionsMu.Lock()
allowModfileModificationOption := s.view.options.AllowModfileModifications
allowNetworkOption := s.view.options.AllowImplicitNetworkAccess
+
+ // TODO(rfindley): this is very hard to follow, and may not even be doing the
+ // right thing: should inv.Env really trample view.options? Do we ever invoke
+ // this with a non-empty inv.Env?
+ //
+ // We should refactor to make it clearer that the correct env is being used.
inv.Env = append(append(append(os.Environ(), s.view.options.EnvSlice()...), inv.Env...), "GO111MODULE="+s.view.effectiveGo111Module)
inv.BuildFlags = append([]string{}, s.view.options.BuildFlags...)
s.view.optionsMu.Unlock()
@@ -864,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 {
@@ -1358,6 +1385,8 @@
// with the user's workspace layout. Workspace packages that only have the
// ID "command-line-arguments" are usually a symptom of a bad workspace
// configuration.
+ //
+ // TODO(rfindley): re-evaluate this heuristic.
if containsCommandLineArguments(wsPkgs) {
return s.workspaceLayoutError(ctx)
}
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 6150109..b23ed61 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -27,6 +27,7 @@
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/imports"
+ "golang.org/x/tools/internal/lsp/bug"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
@@ -98,6 +99,13 @@
// is just the folder. If we are in module mode, this is the module rootURI.
rootURI span.URI
+ // explicitGowork is, if non-empty, the URI for the explicit go.work file
+ // provided via the users environment.
+ //
+ // TODO(rfindley): this is duplicated in the workspace type. Refactor to
+ // eliminate this duplication.
+ explicitGowork span.URI
+
// workspaceInformation tracks various details about this view's
// environment variables, go version, and use of modules.
workspaceInformation
@@ -469,7 +477,7 @@
// TODO(rstambler): Make sure the go.work/gopls.mod files are always known
// to the view.
for _, src := range []workspaceSource{goWorkWorkspace, goplsModWorkspace} {
- if c.URI == uriForSource(v.rootURI, src) {
+ if c.URI == uriForSource(v.rootURI, v.explicitGowork, src) {
return true
}
}
@@ -813,9 +821,13 @@
}
// The value of GOPACKAGESDRIVER is not returned through the go command.
gopackagesdriver := os.Getenv("GOPACKAGESDRIVER")
+ // TODO(rfindley): this looks wrong, or at least overly defensive. If the
+ // value of GOPACKAGESDRIVER is not returned from the go command... why do we
+ // look it up here?
for _, s := range env {
split := strings.SplitN(s, "=", 2)
if split[0] == "GOPACKAGESDRIVER" {
+ bug.Reportf("found GOPACKAGESDRIVER from the go command") // see note above
gopackagesdriver = split[1]
}
}
@@ -926,27 +938,6 @@
return nil
}
-func validBuildConfiguration(folder span.URI, ws *workspaceInformation, modFiles map[span.URI]struct{}) bool {
- // Since we only really understand the `go` command, if the user has a
- // different GOPACKAGESDRIVER, assume that their configuration is valid.
- if ws.hasGopackagesDriver {
- return true
- }
- // Check if the user is working within a module or if we have found
- // multiple modules in the workspace.
- if len(modFiles) > 0 {
- return true
- }
- // The user may have a multiple directories in their GOPATH.
- // Check if the workspace is within any of them.
- for _, gp := range filepath.SplitList(ws.gopath) {
- if source.InDir(filepath.Join(gp, "src"), folder.Filename()) {
- return true
- }
- }
- return false
-}
-
// getGoEnv gets the view's various GO* values.
func (s *Session) getGoEnv(ctx context.Context, folder string, goversion int, go111module string, configEnv []string) (environmentVariables, map[string]string, error) {
envVars := environmentVariables{}
diff --git a/internal/lsp/cache/workspace.go b/internal/lsp/cache/workspace.go
index 9182cb9..f04fbe8 100644
--- a/internal/lsp/cache/workspace.go
+++ b/internal/lsp/cache/workspace.go
@@ -46,6 +46,19 @@
}
}
+// workspaceCommon holds immutable information about the workspace setup.
+//
+// TODO(rfindley): there is some redundancy here with workspaceInformation.
+// Reconcile these two types.
+type workspaceCommon struct {
+ root span.URI
+ excludePath func(string) bool
+
+ // explicitGowork is, if non-empty, the URI for the explicit go.work file
+ // provided via the user's environment.
+ explicitGowork span.URI
+}
+
// workspace tracks go.mod files in the workspace, along with the
// gopls.mod file, to provide support for multi-module workspaces.
//
@@ -58,8 +71,8 @@
// This type is immutable (or rather, idempotent), so that it may be shared
// across multiple snapshots.
type workspace struct {
- root span.URI
- excludePath func(string) bool
+ workspaceCommon
+
moduleSource workspaceSource
// activeModFiles holds the active go.mod files.
@@ -98,17 +111,21 @@
//
// TODO(rfindley): newWorkspace should perhaps never fail, relying instead on
// the criticalError method to surface problems in the workspace.
-// TODO(rfindley): this function should accept the GOWORK value, if specified
-// by the user.
-func newWorkspace(ctx context.Context, root span.URI, fs source.FileSource, excludePath func(string) bool, go111moduleOff, useWsModule bool) (*workspace, error) {
+func newWorkspace(ctx context.Context, root, explicitGowork span.URI, fs source.FileSource, excludePath func(string) bool, go111moduleOff, useWsModule bool) (*workspace, error) {
ws := &workspace{
- root: root,
- excludePath: excludePath,
+ workspaceCommon: workspaceCommon{
+ root: root,
+ explicitGowork: explicitGowork,
+ excludePath: excludePath,
+ },
}
// The user may have a gopls.mod or go.work file that defines their
// workspace.
- if err := loadExplicitWorkspaceFile(ctx, ws, fs); err == nil {
+ //
+ // TODO(rfindley): if GO111MODULE=off, this looks wrong, though there are
+ // probably other problems.
+ if err := ws.loadExplicitWorkspaceFile(ctx, fs); err == nil {
return ws, nil
}
@@ -140,15 +157,15 @@
// loadExplicitWorkspaceFile loads workspace information from go.work or
// gopls.mod files, setting the active modules, mod file, and module source
// accordingly.
-func loadExplicitWorkspaceFile(ctx context.Context, ws *workspace, fs source.FileSource) error {
+func (ws *workspace) loadExplicitWorkspaceFile(ctx context.Context, fs source.FileSource) error {
for _, src := range []workspaceSource{goWorkWorkspace, goplsModWorkspace} {
- fh, err := fs.GetFile(ctx, uriForSource(ws.root, src))
+ fh, err := fs.GetFile(ctx, uriForSource(ws.root, ws.explicitGowork, src))
if err != nil {
return err
}
contents, err := fh.Read()
if err != nil {
- continue
+ continue // TODO(rfindley): is it correct to proceed here?
}
var file *modfile.File
var activeModFiles map[span.URI]struct{}
@@ -313,15 +330,14 @@
// Clone the workspace. This may be discarded if nothing changed.
changed := false
result := &workspace{
- root: w.root,
- moduleSource: w.moduleSource,
- knownModFiles: make(map[span.URI]struct{}),
- activeModFiles: make(map[span.URI]struct{}),
- workFile: w.workFile,
- mod: w.mod,
- sum: w.sum,
- wsDirs: w.wsDirs,
- excludePath: w.excludePath,
+ workspaceCommon: w.workspaceCommon,
+ moduleSource: w.moduleSource,
+ knownModFiles: make(map[span.URI]struct{}),
+ activeModFiles: make(map[span.URI]struct{}),
+ workFile: w.workFile,
+ mod: w.mod,
+ sum: w.sum,
+ wsDirs: w.wsDirs,
}
for k, v := range w.knownModFiles {
result.knownModFiles[k] = v
@@ -391,7 +407,7 @@
// exists or walk the filesystem if it has been deleted.
// go.work should override the gopls.mod if both exist.
for _, src := range []workspaceSource{goWorkWorkspace, goplsModWorkspace} {
- uri := uriForSource(ws.root, src)
+ uri := uriForSource(ws.root, ws.explicitGowork, src)
// File opens/closes are just no-ops.
change, ok := changes[uri]
if !ok {
@@ -460,12 +476,15 @@
}
// goplsModURI returns the URI for the gopls.mod file contained in root.
-func uriForSource(root span.URI, src workspaceSource) span.URI {
+func uriForSource(root, explicitGowork span.URI, src workspaceSource) span.URI {
var basename string
switch src {
case goplsModWorkspace:
basename = "gopls.mod"
case goWorkWorkspace:
+ if explicitGowork != "" {
+ return explicitGowork
+ }
basename = "go.work"
default:
return ""
diff --git a/internal/lsp/cache/workspace_test.go b/internal/lsp/cache/workspace_test.go
index 871e4bb..f1cd00b 100644
--- a/internal/lsp/cache/workspace_test.go
+++ b/internal/lsp/cache/workspace_test.go
@@ -280,7 +280,7 @@
fs := &osFileSource{}
excludeNothing := func(string) bool { return false }
- w, err := newWorkspace(ctx, root, fs, excludeNothing, false, !test.legacyMode)
+ w, err := newWorkspace(ctx, root, "", fs, excludeNothing, false, !test.legacyMode)
if err != nil {
t.Fatal(err)
}
@@ -325,7 +325,7 @@
fs := &osFileSource{}
excludeNothing := func(string) bool { return false }
- workspace, err := newWorkspace(ctx, root, fs, excludeNothing, false, false)
+ workspace, err := newWorkspace(ctx, root, "", fs, excludeNothing, false, false)
return workspace, cleanup, err
}
diff --git a/internal/lsp/fake/client.go b/internal/lsp/fake/client.go
index 4c5f2a2..bb258f2 100644
--- a/internal/lsp/fake/client.go
+++ b/internal/lsp/fake/client.go
@@ -74,10 +74,11 @@
func (c *Client) Configuration(_ context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) {
results := make([]interface{}, len(p.Items))
for i, item := range p.Items {
- if item.Section != "gopls" {
- continue
+ if item.Section == "gopls" {
+ c.editor.mu.Lock()
+ results[i] = c.editor.settingsLocked()
+ c.editor.mu.Unlock()
}
- results[i] = c.editor.settings()
}
return results, nil
}
diff --git a/internal/lsp/fake/edit.go b/internal/lsp/fake/edit.go
index 8b04c39..579c3a1 100644
--- a/internal/lsp/fake/edit.go
+++ b/internal/lsp/fake/edit.go
@@ -108,6 +108,8 @@
// editContent implements a simplistic, inefficient algorithm for applying text
// edits to our buffer representation. It returns an error if the edit is
// invalid for the current content.
+//
+// TODO(rfindley): this function does not handle non-ascii text correctly.
func editContent(content []string, edits []Edit) ([]string, error) {
newEdits := make([]Edit, len(edits))
copy(newEdits, edits)
diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go
index bc2cb2f..db43260 100644
--- a/internal/lsp/fake/editor.go
+++ b/internal/lsp/fake/editor.go
@@ -121,7 +121,7 @@
//
// It returns the editor, so that it may be called as follows:
//
-// editor, err := NewEditor(s).Connect(ctx, conn)
+// editor, err := NewEditor(s).Connect(ctx, conn, hooks)
func (e *Editor) Connect(ctx context.Context, connector servertest.Connector, hooks ClientHooks) (*Editor, error) {
bgCtx, cancelConn := context.WithCancel(xcontext.Detach(ctx))
conn := connector.Connect(bgCtx)
@@ -135,7 +135,7 @@
protocol.ClientHandler(e.client,
jsonrpc2.MethodNotFound)))
- if err := e.initialize(ctx, e.config.WorkspaceFolders); err != nil {
+ if err := e.initialize(ctx); err != nil {
return nil, err
}
e.sandbox.Workdir.AddWatcher(e.onFileChanges)
@@ -197,11 +197,10 @@
return e.client
}
-// settings builds the settings map for use in LSP settings
-// RPCs.
-func (e *Editor) settings() map[string]interface{} {
- e.mu.Lock()
- defer e.mu.Unlock()
+// settingsLocked builds the settings map for use in LSP settings RPCs.
+//
+// e.mu must be held while calling this function.
+func (e *Editor) settingsLocked() map[string]interface{} {
env := make(map[string]string)
for k, v := range e.defaultEnv {
env[k] = v
@@ -240,26 +239,19 @@
return settings
}
-func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) error {
+func (e *Editor) initialize(ctx context.Context) error {
params := &protocol.ParamInitialize{}
params.ClientInfo.Name = "fakeclient"
params.ClientInfo.Version = "v1.0.0"
-
- if workspaceFolders == nil {
- workspaceFolders = []string{string(e.sandbox.Workdir.RelativeTo)}
- }
- for _, folder := range workspaceFolders {
- params.WorkspaceFolders = append(params.WorkspaceFolders, protocol.WorkspaceFolder{
- URI: string(e.sandbox.Workdir.URI(folder)),
- Name: filepath.Base(folder),
- })
- }
-
+ e.mu.Lock()
+ params.WorkspaceFolders = e.makeWorkspaceFoldersLocked()
+ params.InitializationOptions = e.settingsLocked()
+ e.mu.Unlock()
params.Capabilities.Workspace.Configuration = true
params.Capabilities.Window.WorkDoneProgress = true
+
// TODO: set client capabilities
params.Capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated}
- params.InitializationOptions = e.settings()
params.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true
params.Capabilities.TextDocument.SemanticTokens.Requests.Full = true
@@ -296,6 +288,27 @@
return nil
}
+// makeWorkspaceFoldersLocked creates a slice of workspace folders to use for
+// this editing session, based on the editor configuration.
+//
+// e.mu must be held while calling this function.
+func (e *Editor) makeWorkspaceFoldersLocked() (folders []protocol.WorkspaceFolder) {
+ paths := e.config.WorkspaceFolders
+ if len(paths) == 0 {
+ paths = append(paths, string(e.sandbox.Workdir.RelativeTo))
+ }
+
+ for _, path := range paths {
+ uri := string(e.sandbox.Workdir.URI(path))
+ folders = append(folders, protocol.WorkspaceFolder{
+ URI: uri,
+ Name: filepath.Base(uri),
+ })
+ }
+
+ return folders
+}
+
// onFileChanges is registered to be called by the Workdir on any writes that
// go through the Workdir API. It is called synchronously by the Workdir.
func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) {
@@ -1195,6 +1208,54 @@
return nil
}
+// ChangeWorkspaceFolders sets the new workspace folders, and sends a
+// didChangeWorkspaceFolders notification to the server.
+//
+// The given folders must all be unique.
+func (e *Editor) ChangeWorkspaceFolders(ctx context.Context, folders []string) error {
+ // capture existing folders so that we can compute the change.
+ e.mu.Lock()
+ oldFolders := e.makeWorkspaceFoldersLocked()
+ e.config.WorkspaceFolders = folders
+ newFolders := e.makeWorkspaceFoldersLocked()
+ e.mu.Unlock()
+
+ if e.Server == nil {
+ return nil
+ }
+
+ var params protocol.DidChangeWorkspaceFoldersParams
+
+ // Keep track of old workspace folders that must be removed.
+ toRemove := make(map[protocol.URI]protocol.WorkspaceFolder)
+ for _, folder := range oldFolders {
+ toRemove[folder.URI] = folder
+ }
+
+ // Sanity check: if we see a folder twice the algorithm below doesn't work,
+ // so track seen folders to ensure that we panic in that case.
+ seen := make(map[protocol.URI]protocol.WorkspaceFolder)
+ for _, folder := range newFolders {
+ if _, ok := seen[folder.URI]; ok {
+ panic(fmt.Sprintf("folder %s seen twice", folder.URI))
+ }
+
+ // If this folder already exists, we don't want to remove it.
+ // Otherwise, we need to add it.
+ if _, ok := toRemove[folder.URI]; ok {
+ delete(toRemove, folder.URI)
+ } else {
+ params.Event.Added = append(params.Event.Added, folder)
+ }
+ }
+
+ for _, v := range toRemove {
+ params.Event.Removed = append(params.Event.Removed, v)
+ }
+
+ return e.Server.DidChangeWorkspaceFolders(ctx, ¶ms)
+}
+
// CodeAction executes a codeAction request on the server.
func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) {
if e.Server == nil {
diff --git a/internal/lsp/fake/sandbox.go b/internal/lsp/fake/sandbox.go
index 72b846c..e0e113f 100644
--- a/internal/lsp/fake/sandbox.go
+++ b/internal/lsp/fake/sandbox.go
@@ -23,10 +23,11 @@
// Sandbox holds a collection of temporary resources to use for working with Go
// code in tests.
type Sandbox struct {
- gopath string
- rootdir string
- goproxy string
- Workdir *Workdir
+ gopath string
+ rootdir string
+ goproxy string
+ Workdir *Workdir
+ goCommandRunner gocommand.Runner
}
// SandboxConfig controls the behavior of a test sandbox. The zero value
@@ -229,30 +230,36 @@
return vars
}
-// RunGoCommand executes a go command in the sandbox. If checkForFileChanges is
-// true, the sandbox scans the working directory and emits file change events
-// for any file changes it finds.
-func (sb *Sandbox) RunGoCommand(ctx context.Context, dir, verb string, args []string, checkForFileChanges bool) error {
+// goCommandInvocation returns a new gocommand.Invocation initialized with the
+// sandbox environment variables and working directory.
+func (sb *Sandbox) goCommandInvocation() gocommand.Invocation {
var vars []string
for k, v := range sb.GoEnv() {
vars = append(vars, fmt.Sprintf("%s=%s", k, v))
}
inv := gocommand.Invocation{
- Verb: verb,
- Args: args,
- Env: vars,
+ Env: vars,
}
- // Use the provided directory for the working directory, if available.
// sb.Workdir may be nil if we exited the constructor with errors (we call
// Close to clean up any partial state from the constructor, which calls
// RunGoCommand).
- if dir != "" {
- inv.WorkingDir = sb.Workdir.AbsPath(dir)
- } else if sb.Workdir != nil {
+ if sb.Workdir != nil {
inv.WorkingDir = string(sb.Workdir.RelativeTo)
}
- gocmdRunner := &gocommand.Runner{}
- stdout, stderr, _, err := gocmdRunner.RunRaw(ctx, inv)
+ return inv
+}
+
+// RunGoCommand executes a go command in the sandbox. If checkForFileChanges is
+// true, the sandbox scans the working directory and emits file change events
+// for any file changes it finds.
+func (sb *Sandbox) RunGoCommand(ctx context.Context, dir, verb string, args []string, checkForFileChanges bool) error {
+ inv := sb.goCommandInvocation()
+ inv.Verb = verb
+ inv.Args = args
+ if dir != "" {
+ inv.WorkingDir = sb.Workdir.AbsPath(dir)
+ }
+ stdout, stderr, _, err := sb.goCommandRunner.RunRaw(ctx, inv)
if err != nil {
return fmt.Errorf("go command failed (stdout: %s) (stderr: %s): %v", stdout.String(), stderr.String(), err)
}
@@ -269,6 +276,13 @@
return nil
}
+// GoVersion checks the version of the go command.
+// It returns the X in Go 1.X.
+func (sb *Sandbox) GoVersion(ctx context.Context) (int, error) {
+ inv := sb.goCommandInvocation()
+ return gocommand.GoVersion(ctx, inv, &sb.goCommandRunner)
+}
+
// Close removes all state associated with the sandbox.
func (sb *Sandbox) Close() error {
var goCleanErr error
diff --git a/internal/lsp/progress/progress.go b/internal/lsp/progress/progress.go
index 8b0d1c6..ee63bb7 100644
--- a/internal/lsp/progress/progress.go
+++ b/internal/lsp/progress/progress.go
@@ -118,7 +118,7 @@
},
})
if err != nil {
- event.Error(ctx, "generate progress begin", err)
+ event.Error(ctx, "progress begin", err)
}
return wd
}
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),
}
diff --git a/internal/lsp/regtest/wrappers.go b/internal/lsp/regtest/wrappers.go
index d8c080c..e8f49a6 100644
--- a/internal/lsp/regtest/wrappers.go
+++ b/internal/lsp/regtest/wrappers.go
@@ -277,6 +277,17 @@
}
}
+// GoVersion checks the version of the go command.
+// It returns the X in Go 1.X.
+func (e *Env) GoVersion() int {
+ e.T.Helper()
+ v, err := e.Sandbox.GoVersion(e.Ctx)
+ if err != nil {
+ e.T.Fatal(err)
+ }
+ return v
+}
+
// DumpGoSum prints the correct go.sum contents for dir in txtar format,
// for use in creating regtests.
func (e *Env) DumpGoSum(dir string) {
@@ -433,3 +444,12 @@
e.T.Fatal(err)
}
}
+
+// ChangeWorkspaceFolders updates the editor workspace folders, calling t.Fatal
+// on any error.
+func (e *Env) ChangeWorkspaceFolders(newFolders ...string) {
+ e.T.Helper()
+ if err := e.Editor.ChangeWorkspaceFolders(e.Ctx, newFolders); err != nil {
+ e.T.Fatal(err)
+ }
+}
diff --git a/internal/span/uri.go b/internal/span/uri.go
index 8132665..ce874e7 100644
--- a/internal/span/uri.go
+++ b/internal/span/uri.go
@@ -144,7 +144,9 @@
}
// URIFromPath returns a span URI for the supplied file path.
-// It will always have the file scheme.
+//
+// For empty paths, URIFromPath returns the empty URI "".
+// For non-empty paths, URIFromPath returns a uri with the file:// scheme.
func URIFromPath(path string) URI {
if path == "" {
return ""