Skip to content

Commit 61f37dc

Browse files
committed
gopls: use new gomodcache index
This CL changes the default for 'importsSource' to 'gopls', so that imports and unimpoted completions will use the GOMODCACHE index. One test had to be changed, and there is a benchmark, with some data. (On unimported completions from the module cache, the new code is more than 10 times faster, and always succeeds where the old code sometimes failed.) Change-Id: Ie5c8001ac1292498b72e5b42b51b4fcb06ab6fa9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/678475 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Alan Donovan <adonovan@google.com>
1 parent fed8cc8 commit 61f37dc

File tree

5 files changed

+179
-18
lines changed

5 files changed

+179
-18
lines changed

gopls/doc/release/v0.19.0.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,15 @@ func f(x int) {
173173
println(fmt.Sprintf("+%d", x))
174174
}
175175
```
176+
177+
## Use index for GOMODCACHE in imports and unimported completions
178+
179+
The default for the option `importsSource` changes from "goimports" to "gopls".
180+
This has the effect of building and maintaining an index to
181+
the packages in GOMODCACHE.
182+
The index is stored in the directory `os.UserCacheDir()/go/imports`.
183+
Users who want the old behavior can change the option back. Users who don't
184+
the module cache used at all for imports or completions
185+
can change the option to
186+
"off". The new code is many times faster than the old when accessing the
187+
module cache.

gopls/internal/golang/completion/unimported.go

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,21 +52,6 @@ func (c *completer) unimported(ctx context.Context, pkgname metadata.PackageName
5252
}
5353
}
5454
// do the stdlib next.
55-
// For now, use the workspace version of stdlib packages
56-
// to get function snippets. CL 665335 will fix this.
57-
var x []metadata.PackageID
58-
for _, mp := range stdpkgs {
59-
if slices.Contains(wsIDs, metadata.PackageID(mp)) {
60-
x = append(x, metadata.PackageID(mp))
61-
}
62-
}
63-
if len(x) > 0 {
64-
items := c.pkgIDmatches(ctx, x, pkgname, prefix)
65-
if c.scoreList(items) {
66-
return
67-
}
68-
}
69-
// just use the stdlib
7055
items := c.stdlibMatches(stdpkgs, pkgname, prefix)
7156
if c.scoreList(items) {
7257
return
@@ -164,7 +149,7 @@ func (c *completer) pkgIDmatches(ctx context.Context, ids []metadata.PackageID,
164149
}
165150
kind = protocol.FunctionCompletion
166151
detail = fmt.Sprintf("func (from %q)", pkg.PkgPath)
167-
case protocol.Variable:
152+
case protocol.Variable, protocol.Struct:
168153
kind = protocol.VariableCompletion
169154
detail = fmt.Sprintf("var (from %q)", pkg.PkgPath)
170155
case protocol.Constant:
@@ -264,6 +249,9 @@ func (c *completer) modcacheMatches(pkg metadata.PackageName, prefix string) ([]
264249
case modindex.Const:
265250
kind = protocol.ConstantCompletion
266251
detail = fmt.Sprintf("const (from %s)", cand.ImportPath)
252+
case modindex.Type: // might be a type alias
253+
kind = protocol.VariableCompletion
254+
detail = fmt.Sprintf("type (from %s)", cand.ImportPath)
267255
default:
268256
continue
269257
}

gopls/internal/settings/default.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func DefaultOptions(overrides ...func(*Options)) *Options {
3939
DynamicWatchedFilesSupported: true,
4040
LineFoldingOnly: false,
4141
HierarchicalDocumentSymbolSupport: true,
42-
ImportsSource: ImportsSourceGoimports,
42+
ImportsSource: ImportsSourceGopls,
4343
},
4444
ServerOptions: ServerOptions{
4545
SupportedCodeActions: map[file.Kind]map[protocol.CodeActionKind]bool{
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package bench
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"go/token"
11+
"os"
12+
"os/exec"
13+
"path/filepath"
14+
"strings"
15+
"testing"
16+
17+
. "golang.org/x/tools/gopls/internal/test/integration"
18+
"golang.org/x/tools/gopls/internal/test/integration/fake"
19+
"golang.org/x/tools/internal/modindex"
20+
)
21+
22+
// experiments show the new code about 15 times faster than the old,
23+
// and the old code sometimes fails to find the completion
24+
func BenchmarkLocalModcache(b *testing.B) {
25+
budgets := []string{"0s", "100ms", "200ms", "500ms", "1s", "5s"}
26+
sources := []string{"gopls", "goimports"}
27+
for _, budget := range budgets {
28+
b.Run(fmt.Sprintf("budget=%s", budget), func(b *testing.B) {
29+
for _, source := range sources {
30+
b.Run(fmt.Sprintf("source=%s", source), func(b *testing.B) {
31+
runModcacheCompletion(b, budget, source)
32+
})
33+
}
34+
})
35+
}
36+
}
37+
38+
func runModcacheCompletion(b *testing.B, budget, source string) {
39+
// First set up the program to be edited
40+
gomod := `
41+
module mod.com
42+
43+
go 1.21
44+
`
45+
pat := `
46+
package main
47+
var _ = %s.%s
48+
`
49+
pkg, name, modcache := findSym(b)
50+
name, _, _ = strings.Cut(name, " ")
51+
mainfile := fmt.Sprintf(pat, pkg, name)
52+
// Second, create the Env and start gopls
53+
dir := getTempDir()
54+
if err := os.Mkdir(dir, 0750); err != nil {
55+
if !os.IsExist(err) {
56+
b.Fatal(err)
57+
}
58+
}
59+
defer os.RemoveAll(dir) // is this right? needed?
60+
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0644); err != nil {
61+
b.Fatal(err)
62+
}
63+
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainfile), 0644); err != nil {
64+
b.Fatal(err)
65+
}
66+
ts, err := newGoplsConnector(nil)
67+
if err != nil {
68+
b.Fatal(err)
69+
}
70+
// PJW: put better EditorConfig here
71+
envvars := map[string]string{
72+
"GOMODCACHE": modcache,
73+
//"GOPATH": sandbox.GOPATH(), // do we need a GOPATH?
74+
}
75+
fc := fake.EditorConfig{
76+
Env: envvars,
77+
Settings: map[string]any{
78+
"completeUnimported": true,
79+
"completionBudget": budget, // "0s", "100ms"
80+
"importsSource": source, // "gopls" or "goimports"
81+
},
82+
}
83+
sandbox, editor, awaiter, err := connectEditor(dir, fc, ts)
84+
if err != nil {
85+
b.Fatal(err)
86+
}
87+
defer sandbox.Close()
88+
defer editor.Close(context.Background())
89+
if err := awaiter.Await(context.Background(), InitialWorkspaceLoad); err != nil {
90+
b.Fatal(err)
91+
}
92+
env := &Env{
93+
TB: b,
94+
Ctx: context.Background(),
95+
Editor: editor,
96+
Sandbox: sandbox,
97+
Awaiter: awaiter,
98+
}
99+
// Check that completion works as expected
100+
env.CreateBuffer("main.go", mainfile)
101+
env.AfterChange()
102+
if false { // warm up? or not?
103+
loc := env.RegexpSearch("main.go", name)
104+
completions := env.Completion(loc)
105+
if len(completions.Items) == 0 {
106+
b.Fatal("no completions")
107+
}
108+
}
109+
110+
// run benchmark
111+
for b.Loop() {
112+
loc := env.RegexpSearch("main.go", name)
113+
env.Completion(loc)
114+
}
115+
}
116+
117+
// find some symbol in the module cache
118+
func findSym(t testing.TB) (pkg, name, gomodcache string) {
119+
initForTest(t)
120+
cmd := exec.Command("go", "env", "GOMODCACHE")
121+
out, err := cmd.Output()
122+
if err != nil {
123+
t.Fatal(err)
124+
}
125+
modcache := strings.TrimSpace(string(out))
126+
ix, err := modindex.ReadIndex(modcache)
127+
if err != nil {
128+
t.Fatal(err)
129+
}
130+
if ix == nil {
131+
t.Fatal("no index")
132+
}
133+
if len(ix.Entries) == 0 {
134+
t.Fatal("no entries")
135+
}
136+
nth := 100 // or something
137+
for _, e := range ix.Entries {
138+
if token.IsExported(e.PkgName) || strings.HasPrefix(e.PkgName, "_") {
139+
continue // weird stuff in module cache
140+
}
141+
142+
for _, nm := range e.Names {
143+
nth--
144+
if nth == 0 {
145+
return e.PkgName, nm, modcache
146+
}
147+
}
148+
}
149+
t.Fatalf("index doesn't have enough usable names, need another %d", nth)
150+
return "", "", modcache
151+
}
152+
153+
// Set IndexDir, avoiding the special case for tests,
154+
func initForTest(t testing.TB) {
155+
dir, err := os.UserCacheDir()
156+
if err != nil {
157+
t.Fatalf("os.UserCacheDir: %v", err)
158+
}
159+
dir = filepath.Join(dir, "go", "imports")
160+
modindex.IndexDir = dir
161+
}

gopls/internal/test/marker/testdata/completion/issue62676.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import "os"
5353

5454
func _() {
5555
// This uses goimports-based completion; TODO: this should insert snippets.
56-
os.Open //@acceptcompletion(re"Open()", "Open", open)
56+
os.Open(${1:}) //@acceptcompletion(re"Open()", "Open", open)
5757
}
5858

5959
func _() {

0 commit comments

Comments
 (0)