Skip to content

Commit 9ffa3ad

Browse files
committed
internal/lsp: Provide completions for test function definitions
In test files, function definitions starting with Test, Bench, or Fuzz can be completed almost automatically. For the snippets the user hits tab, completes the name, hits tab again, and the function is defined, except (of course) for its body. Otherwise a completion that fills in the signature is proposed. Where appropriate, 'TestMain(m *testing.M)' is also offered as a completion. Fixes golang/go#46896 and golang/go#51089 Change-Id: I46c05af0ead79c1d82ca40b2c605045e06e1a35d Reviewed-on: https://go-review.googlesource.com/c/tools/+/385974 Run-TryBot: Peter Weinberger <[email protected]> Trust: Peter Weinberger <[email protected]> TryBot-Result: Gopher Robot <[email protected]> gopls-CI: kokoro <[email protected]> Reviewed-by: Hyang-Ah Hana Kim <[email protected]>
1 parent b7525f4 commit 9ffa3ad

File tree

3 files changed

+188
-1
lines changed

3 files changed

+188
-1
lines changed

gopls/internal/regtest/completion/completion_test.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ func compareCompletionResults(want []string, gotItems []protocol.CompletionItem)
256256

257257
for i, v := range got {
258258
if v != want[i] {
259-
return fmt.Sprintf("completion results are not the same: got %v, want %v", got, want)
259+
return fmt.Sprintf("%d completion result not the same: got %q, want %q", i, v, want[i])
260260
}
261261
}
262262

@@ -546,3 +546,56 @@ func main() {
546546
}
547547
})
548548
}
549+
550+
func TestDefinition(t *testing.T) {
551+
stuff := `
552+
-- go.mod --
553+
module mod.com
554+
555+
go 1.18
556+
-- a_test.go --
557+
package foo
558+
func T()
559+
func TestG()
560+
func TestM()
561+
func TestMi()
562+
func Ben()
563+
func Fuz()
564+
func Testx()
565+
func TestMe(t *testing.T)
566+
func BenchmarkFoo()
567+
`
568+
// All those parentheses are needed for the completion code to see
569+
// later lines as being definitions
570+
tests := []struct {
571+
pat string
572+
want []string
573+
}{
574+
{"T", []string{"TestXxx(t *testing.T)", "TestMain(m *testing.M)"}},
575+
{"TestM", []string{"TestMain(m *testing.M)", "TestM(t *testing.T)"}},
576+
{"TestMi", []string{"TestMi(t *testing.T)"}},
577+
{"TestG", []string{"TestG(t *testing.T)"}},
578+
{"B", []string{"BenchmarkXxx(b *testing.B)"}},
579+
{"BenchmarkFoo", []string{"BenchmarkFoo(b *testing.B)"}},
580+
{"F", []string{"FuzzXxx(f *testing.F)"}},
581+
{"Testx", nil},
582+
{"TestMe", []string{"TestMe"}},
583+
}
584+
fname := "a_test.go"
585+
Run(t, stuff, func(t *testing.T, env *Env) {
586+
env.OpenFile(fname)
587+
env.Await(env.DoneWithOpen())
588+
for _, tst := range tests {
589+
pos := env.RegexpSearch(fname, tst.pat)
590+
pos.Column += len(tst.pat)
591+
completions := env.Completion(fname, pos)
592+
result := compareCompletionResults(tst.want, completions.Items)
593+
if result != "" {
594+
t.Errorf("%s failed: %s:%q", tst.pat, result, tst.want)
595+
for i, it := range completions.Items {
596+
t.Errorf("%d got %q %q", i, it.Label, it.Detail)
597+
}
598+
}
599+
}
600+
})
601+
}

internal/lsp/source/completion/completion.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,13 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan
485485
qual := types.RelativeTo(pkg.GetTypes())
486486
objStr = types.ObjectString(obj, qual)
487487
}
488+
ans, sel := definition(path, obj, snapshot.FileSet(), pgf.Mapper, fh)
489+
if ans != nil {
490+
sort.Slice(ans, func(i, j int) bool {
491+
return ans[i].Score > ans[j].Score
492+
})
493+
return ans, sel, nil
494+
}
488495
return nil, nil, ErrIsDefinition{objStr: objStr}
489496
}
490497
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright 2022 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 completion
6+
7+
import (
8+
"go/ast"
9+
"go/token"
10+
"go/types"
11+
"strings"
12+
"unicode"
13+
"unicode/utf8"
14+
15+
"golang.org/x/tools/internal/lsp/protocol"
16+
"golang.org/x/tools/internal/lsp/snippet"
17+
"golang.org/x/tools/internal/lsp/source"
18+
)
19+
20+
// some definitions can be completed
21+
// So far, TestFoo(t *testing.T), TestMain(m *testing.M)
22+
// BenchmarkFoo(b *testing.B), FuzzFoo(f *testing.F)
23+
24+
// path[0] is known to be *ast.Ident
25+
func definition(path []ast.Node, obj types.Object, fset *token.FileSet, mapper *protocol.ColumnMapper, fh source.FileHandle) ([]CompletionItem, *Selection) {
26+
if _, ok := obj.(*types.Func); !ok {
27+
return nil, nil // not a function at all
28+
}
29+
if !strings.HasSuffix(fh.URI().Filename(), "_test.go") {
30+
return nil, nil
31+
}
32+
33+
name := path[0].(*ast.Ident).Name
34+
if len(name) == 0 {
35+
// can't happen
36+
return nil, nil
37+
}
38+
pos := path[0].Pos()
39+
sel := &Selection{
40+
content: "",
41+
cursor: pos,
42+
MappedRange: source.NewMappedRange(fset, mapper, pos, pos),
43+
}
44+
var ans []CompletionItem
45+
46+
// Always suggest TestMain, if possible
47+
if strings.HasPrefix("TestMain", name) {
48+
ans = []CompletionItem{defItem("TestMain(m *testing.M)", obj)}
49+
}
50+
51+
// If a snippet is possible, suggest it
52+
if strings.HasPrefix("Test", name) {
53+
ans = append(ans, defSnippet("Test", "Xxx", "(t *testing.T)", obj))
54+
return ans, sel
55+
} else if strings.HasPrefix("Benchmark", name) {
56+
ans = append(ans, defSnippet("Benchmark", "Xxx", "(b *testing.B)", obj))
57+
return ans, sel
58+
} else if strings.HasPrefix("Fuzz", name) {
59+
ans = append(ans, defSnippet("Fuzz", "Xxx", "(f *testing.F)", obj))
60+
return ans, sel
61+
}
62+
63+
// Fill in the argument for what the user has already typed
64+
if got := defMatches(name, "Test", path, "(t *testing.T)"); got != "" {
65+
ans = append(ans, defItem(got, obj))
66+
} else if got := defMatches(name, "Benchmark", path, "(b *testing.B)"); got != "" {
67+
ans = append(ans, defItem(got, obj))
68+
} else if got := defMatches(name, "Fuzz", path, "(f *testing.F)"); got != "" {
69+
ans = append(ans, defItem(got, obj))
70+
}
71+
return ans, sel
72+
}
73+
74+
func defMatches(name, pat string, path []ast.Node, arg string) string {
75+
idx := strings.Index(name, pat)
76+
if idx < 0 {
77+
return ""
78+
}
79+
c, _ := utf8.DecodeRuneInString(name[len(pat):])
80+
if unicode.IsLower(c) {
81+
return ""
82+
}
83+
fd, ok := path[1].(*ast.FuncDecl)
84+
if !ok {
85+
// we don't know what's going on
86+
return ""
87+
}
88+
fp := fd.Type.Params
89+
if fp != nil && len(fp.List) > 0 {
90+
// signature already there, minimal suggestion
91+
return name
92+
}
93+
// suggesting signature too
94+
return name + arg
95+
}
96+
97+
func defSnippet(prefix, placeholder, suffix string, obj types.Object) CompletionItem {
98+
var sn snippet.Builder
99+
sn.WriteText(prefix)
100+
if placeholder != "" {
101+
sn.WritePlaceholder(func(b *snippet.Builder) { b.WriteText(placeholder) })
102+
}
103+
sn.WriteText(suffix + " {\n")
104+
sn.WriteFinalTabstop()
105+
sn.WriteText("\n}")
106+
return CompletionItem{
107+
Label: prefix + placeholder + suffix,
108+
Detail: "tab, type the rest of the name, then tab",
109+
Kind: protocol.FunctionCompletion,
110+
Depth: 0,
111+
Score: 10,
112+
snippet: &sn,
113+
Documentation: prefix + " test function",
114+
obj: obj,
115+
}
116+
}
117+
func defItem(val string, obj types.Object) CompletionItem {
118+
return CompletionItem{
119+
Label: val,
120+
InsertText: val,
121+
Kind: protocol.FunctionCompletion,
122+
Depth: 0,
123+
Score: 9, // prefer the snippets when available
124+
Documentation: "complete the parameter",
125+
obj: obj,
126+
}
127+
}

0 commit comments

Comments
 (0)