gopls/completion: avoid duplicating text in test func completions

If a user types 'func Te' in a test file, gopls suggest a
completion using a snippet 'TestXxx(t *testing.T)' which will
fill in the entire function declaration. Until this CL it would
also suggest the snippet if the user had type 'func Te(t *testing.T)',
and went back to fill in the function name. This CL fixes that
by no longer suggesting the snippet if the user has already typed
a parenthesis.

Fixes: golang/go#57480

Change-Id: I5061a6ba5ca22ecba60de537a41fcc04356dc7ba
Reviewed-on: https://go-review.googlesource.com/c/tools/+/459559
Reviewed-by: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/gopls/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go
index 19713f2..fe80d89 100644
--- a/gopls/internal/lsp/regtest/wrappers.go
+++ b/gopls/internal/lsp/regtest/wrappers.go
@@ -138,7 +138,7 @@
 		pos, err = e.Sandbox.Workdir.RegexpSearch(name, re)
 	}
 	if err != nil {
-		e.T.Fatalf("RegexpSearch: %v, %v", name, err)
+		e.T.Fatalf("RegexpSearch: %v, %v for %q", name, err, re)
 	}
 	return pos
 }
diff --git a/gopls/internal/lsp/source/completion/definition.go b/gopls/internal/lsp/source/completion/definition.go
index cc7b42e..bf14ad4 100644
--- a/gopls/internal/lsp/source/completion/definition.go
+++ b/gopls/internal/lsp/source/completion/definition.go
@@ -18,7 +18,7 @@
 	"golang.org/x/tools/gopls/internal/span"
 )
 
-// some definitions can be completed
+// some function definitions in test files can be completed
 // So far, TestFoo(t *testing.T), TestMain(m *testing.M)
 // BenchmarkFoo(b *testing.B), FuzzFoo(f *testing.F)
 
@@ -28,7 +28,7 @@
 		return nil, nil // not a function at all
 	}
 	if !strings.HasSuffix(fh.URI().Filename(), "_test.go") {
-		return nil, nil
+		return nil, nil // not a test file
 	}
 
 	name := path[0].(*ast.Ident).Name
@@ -44,21 +44,49 @@
 		rng:     span.NewRange(tokFile, start, end),
 	}
 	var ans []CompletionItem
+	var hasParens bool
+	n, ok := path[1].(*ast.FuncDecl)
+	if !ok {
+		return nil, nil // can't happen
+	}
+	if n.Recv != nil {
+		return nil, nil // a method, not a function
+	}
+	t := n.Type.Params
+	if t.Closing != t.Opening {
+		hasParens = true
+	}
 
 	// Always suggest TestMain, if possible
 	if strings.HasPrefix("TestMain", name) {
-		ans = []CompletionItem{defItem("TestMain(m *testing.M)", obj)}
+		if hasParens {
+			ans = append(ans, defItem("TestMain", obj))
+		} else {
+			ans = append(ans, defItem("TestMain(m *testing.M)", obj))
+		}
 	}
 
 	// If a snippet is possible, suggest it
 	if strings.HasPrefix("Test", name) {
-		ans = append(ans, defSnippet("Test", "Xxx", "(t *testing.T)", obj))
+		if hasParens {
+			ans = append(ans, defItem("Test", obj))
+		} else {
+			ans = append(ans, defSnippet("Test", "(t *testing.T)", obj))
+		}
 		return ans, sel
 	} else if strings.HasPrefix("Benchmark", name) {
-		ans = append(ans, defSnippet("Benchmark", "Xxx", "(b *testing.B)", obj))
+		if hasParens {
+			ans = append(ans, defItem("Benchmark", obj))
+		} else {
+			ans = append(ans, defSnippet("Benchmark", "(b *testing.B)", obj))
+		}
 		return ans, sel
 	} else if strings.HasPrefix("Fuzz", name) {
-		ans = append(ans, defSnippet("Fuzz", "Xxx", "(f *testing.F)", obj))
+		if hasParens {
+			ans = append(ans, defItem("Fuzz", obj))
+		} else {
+			ans = append(ans, defSnippet("Fuzz", "(f *testing.F)", obj))
+		}
 		return ans, sel
 	}
 
@@ -73,9 +101,9 @@
 	return ans, sel
 }
 
+// defMatches returns text for defItem, never for defSnippet
 func defMatches(name, pat string, path []ast.Node, arg string) string {
-	idx := strings.Index(name, pat)
-	if idx < 0 {
+	if !strings.HasPrefix(name, pat) {
 		return ""
 	}
 	c, _ := utf8.DecodeRuneInString(name[len(pat):])
@@ -88,25 +116,27 @@
 		return ""
 	}
 	fp := fd.Type.Params
-	if fp != nil && len(fp.List) > 0 {
-		// signature already there, minimal suggestion
-		return name
+	if len(fp.List) > 0 {
+		// signature already there, nothing to suggest
+		return ""
+	}
+	if fp.Opening != fp.Closing {
+		// nothing: completion works on words, not easy to insert arg
+		return ""
 	}
 	// suggesting signature too
 	return name + arg
 }
 
-func defSnippet(prefix, placeholder, suffix string, obj types.Object) CompletionItem {
+func defSnippet(prefix, suffix string, obj types.Object) CompletionItem {
 	var sn snippet.Builder
 	sn.WriteText(prefix)
-	if placeholder != "" {
-		sn.WritePlaceholder(func(b *snippet.Builder) { b.WriteText(placeholder) })
-	}
-	sn.WriteText(suffix + " {\n")
+	sn.WritePlaceholder(func(b *snippet.Builder) { b.WriteText("Xxx") })
+	sn.WriteText(suffix + " {\n\t")
 	sn.WriteFinalTabstop()
 	sn.WriteText("\n}")
 	return CompletionItem{
-		Label:         prefix + placeholder + suffix,
+		Label:         prefix + "Xxx" + suffix,
 		Detail:        "tab, type the rest of the name, then tab",
 		Kind:          protocol.FunctionCompletion,
 		Depth:         0,
@@ -123,7 +153,7 @@
 		Kind:          protocol.FunctionCompletion,
 		Depth:         0,
 		Score:         9, // prefer the snippets when available
-		Documentation: "complete the parameter",
+		Documentation: "complete the function name",
 		obj:           obj,
 	}
 }
diff --git a/gopls/internal/regtest/completion/completion18_test.go b/gopls/internal/regtest/completion/completion18_test.go
index 7c53252..936436b 100644
--- a/gopls/internal/regtest/completion/completion18_test.go
+++ b/gopls/internal/regtest/completion/completion18_test.go
@@ -44,7 +44,7 @@
 			pos := env.RegexpSearch("main.go", tst.pat)
 			pos.Column += len(tst.pat)
 			completions := env.Completion("main.go", pos)
-			result := compareCompletionResults(tst.want, completions.Items)
+			result := compareCompletionLabels(tst.want, completions.Items)
 			if result != "" {
 				t.Errorf("%s: wanted %v", result, tst.want)
 				for i, g := range completions.Items {
@@ -111,7 +111,7 @@
 			pos := env.RegexpSearch(test.file, test.pat)
 			pos.Column += test.offset // character user just typed? will type?
 			completions := env.Completion(test.file, pos)
-			result := compareCompletionResults(test.want, completions.Items)
+			result := compareCompletionLabels(test.want, completions.Items)
 			if result != "" {
 				t.Errorf("pat %q %q", test.pat, result)
 				for i, it := range completions.Items {
diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go
index 08a1421..5f65593 100644
--- a/gopls/internal/regtest/completion/completion_test.go
+++ b/gopls/internal/regtest/completion/completion_test.go
@@ -207,7 +207,7 @@
 					}
 				}
 
-				diff := compareCompletionResults(tc.want, completions.Items)
+				diff := compareCompletionLabels(tc.want, completions.Items)
 				if diff != "" {
 					t.Error(diff)
 				}
@@ -234,14 +234,14 @@
 			Column: 10,
 		})
 
-		diff := compareCompletionResults(want, completions.Items)
+		diff := compareCompletionLabels(want, completions.Items)
 		if diff != "" {
 			t.Fatal(diff)
 		}
 	})
 }
 
-func compareCompletionResults(want []string, gotItems []protocol.CompletionItem) string {
+func compareCompletionLabels(want []string, gotItems []protocol.CompletionItem) string {
 	if len(gotItems) != len(want) {
 		return fmt.Sprintf("got %v completion(s), want %v", len(gotItems), len(want))
 	}
@@ -391,7 +391,7 @@
 	Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("foo.go")
 		completions := env.Completion("foo.go", env.RegexpSearch("foo.go", `if s\.()`))
-		diff := compareCompletionResults([]string{"i"}, completions.Items)
+		diff := compareCompletionLabels([]string{"i"}, completions.Items)
 		if diff != "" {
 			t.Fatal(diff)
 		}
@@ -451,7 +451,7 @@
 		}
 		for _, tt := range tests {
 			completions := env.Completion("main.go", env.RegexpSearch("main.go", tt.re))
-			diff := compareCompletionResults(tt.want, completions.Items)
+			diff := compareCompletionLabels(tt.want, completions.Items)
 			if diff != "" {
 				t.Errorf("%s: %s", tt.re, diff)
 			}
@@ -486,7 +486,7 @@
 		pos := env.RegexpSearch("prog.go", "if fooF")
 		pos.Column += len("if fooF")
 		completions := env.Completion("prog.go", pos)
-		diff := compareCompletionResults([]string{"fooFunc"}, completions.Items)
+		diff := compareCompletionLabels([]string{"fooFunc"}, completions.Items)
 		if diff != "" {
 			t.Error(diff)
 		}
@@ -496,7 +496,7 @@
 		pos = env.RegexpSearch("prog.go", "= badP")
 		pos.Column += len("= badP")
 		completions = env.Completion("prog.go", pos)
-		diff = compareCompletionResults([]string{"badPi"}, completions.Items)
+		diff = compareCompletionLabels([]string{"badPi"}, completions.Items)
 		if diff != "" {
 			t.Error(diff)
 		}
@@ -545,6 +545,7 @@
 }
 
 func TestDefinition(t *testing.T) {
+	testenv.NeedsGo1Point(t, 17) // in go1.16, The FieldList in func x is not empty
 	stuff := `
 -- go.mod --
 module mod.com
@@ -552,43 +553,42 @@
 go 1.18
 -- a_test.go --
 package foo
-func T()
-func TestG()
-func TestM()
-func TestMi()
-func Ben()
-func Fuz()
-func Testx()
-func TestMe(t *testing.T)
-func BenchmarkFoo()
 `
-	// All those parentheses are needed for the completion code to see
-	// later lines as being definitions
 	tests := []struct {
-		pat  string
-		want []string
+		line string   // the sole line in the buffer after the package statement
+		pat  string   // the pattern to search for
+		want []string // expected competions
 	}{
-		{"T", []string{"TestXxx(t *testing.T)", "TestMain(m *testing.M)"}},
-		{"TestM", []string{"TestMain(m *testing.M)", "TestM(t *testing.T)"}},
-		{"TestMi", []string{"TestMi(t *testing.T)"}},
-		{"TestG", []string{"TestG(t *testing.T)"}},
-		{"B", []string{"BenchmarkXxx(b *testing.B)"}},
-		{"BenchmarkFoo", []string{"BenchmarkFoo(b *testing.B)"}},
-		{"F", []string{"FuzzXxx(f *testing.F)"}},
-		{"Testx", nil},
-		{"TestMe", []string{"TestMe"}},
+		{"func T", "T", []string{"TestXxx(t *testing.T)", "TestMain(m *testing.M)"}},
+		{"func T()", "T", []string{"TestMain", "Test"}},
+		{"func TestM", "TestM", []string{"TestMain(m *testing.M)", "TestM(t *testing.T)"}},
+		{"func TestM()", "TestM", []string{"TestMain"}},
+		{"func TestMi", "TestMi", []string{"TestMi(t *testing.T)"}},
+		{"func TestMi()", "TestMi", nil},
+		{"func TestG", "TestG", []string{"TestG(t *testing.T)"}},
+		{"func TestG(", "TestG", nil},
+		{"func Ben", "B", []string{"BenchmarkXxx(b *testing.B)"}},
+		{"func Ben(", "Ben", []string{"Benchmark"}},
+		{"func BenchmarkFoo", "BenchmarkFoo", []string{"BenchmarkFoo(b *testing.B)"}},
+		{"func BenchmarkFoo(", "BenchmarkFoo", nil},
+		{"func Fuz", "F", []string{"FuzzXxx(f *testing.F)"}},
+		{"func Fuz(", "Fuz", []string{"Fuzz"}},
+		{"func Testx", "Testx", nil},
+		{"func TestMe(t *testing.T)", "TestMe", nil},
+		{"func Te(t *testing.T)", "Te", []string{"TestMain", "Test"}},
 	}
 	fname := "a_test.go"
 	Run(t, stuff, func(t *testing.T, env *Env) {
 		env.OpenFile(fname)
 		env.Await(env.DoneWithOpen())
 		for _, tst := range tests {
+			env.SetBufferContent(fname, "package foo\n"+tst.line)
 			pos := env.RegexpSearch(fname, tst.pat)
 			pos.Column += len(tst.pat)
 			completions := env.Completion(fname, pos)
-			result := compareCompletionResults(tst.want, completions.Items)
+			result := compareCompletionLabels(tst.want, completions.Items)
 			if result != "" {
-				t.Errorf("%s failed: %s:%q", tst.pat, result, tst.want)
+				t.Errorf("\npat:%q line:%q failed: %s:%q", tst.pat, tst.line, result, tst.want)
 				for i, it := range completions.Items {
 					t.Errorf("%d got %q %q", i, it.Label, it.Detail)
 				}
@@ -651,7 +651,7 @@
 package foo_test
 
 func Benchmark${1:Xxx}(b *testing.B) {
-$0
+	$0
 \}
 `,
 		},
@@ -675,7 +675,7 @@
 			env.AcceptCompletion("foo_test.go", pos, completions.Items[0])
 			env.Await(env.DoneWithChange())
 			if buf := env.BufferText("foo_test.go"); buf != tst.after {
-				t.Errorf("incorrect completion: got %q, want %q", buf, tst.after)
+				t.Errorf("%s:incorrect completion: got %q, want %q", tst.name, buf, tst.after)
 			}
 		}
 	})
@@ -718,7 +718,7 @@
 		}
 		for _, tt := range tests {
 			completions := env.Completion("go.work", env.RegexpSearch("go.work", tt.re))
-			diff := compareCompletionResults(tt.want, completions.Items)
+			diff := compareCompletionLabels(tt.want, completions.Items)
 			if diff != "" {
 				t.Errorf("%s: %s", tt.re, diff)
 			}