blob: 78de61a0293cf5a28c0e6b261d3a0ff1b48a6fc5 [file] [log] [blame]
// Copyright 2020 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 completion
import (
"fmt"
"sort"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"golang.org/x/telemetry/counter"
"golang.org/x/telemetry/counter/countertest"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/server"
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/test/integration/fake"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/internal/testenv"
)
func TestMain(m *testing.M) {
bug.PanicOnBugs = true
Main(m)
}
const proxy = `
-- example.com@v1.2.3/go.mod --
module example.com
go 1.12
-- example.com@v1.2.3/blah/blah.go --
package blah
const Name = "Blah"
-- random.org@v1.2.3/go.mod --
module random.org
go 1.12
-- random.org@v1.2.3/blah/blah.go --
package hello
const Name = "Hello"
`
func TestPackageCompletion(t *testing.T) {
const files = `
-- go.mod --
module mod.com
go 1.12
-- fruits/apple.go --
package apple
fun apple() int {
return 0
}
-- fruits/testfile.go --
// this is a comment
/*
this is a multiline comment
*/
import "fmt"
func test() {}
-- fruits/testfile2.go --
package
-- fruits/testfile3.go --
pac
-- 123f_r.u~its-123/testfile.go --
package
-- .invalid-dir@-name/testfile.go --
package
`
var (
testfile4 = ""
testfile5 = "/*a comment*/ "
testfile6 = "/*a comment*/\n"
)
for _, tc := range []struct {
name string
filename string
content *string
triggerRegexp string
want []string
editRegexp string
}{
{
name: "package completion at valid position",
filename: "fruits/testfile.go",
triggerRegexp: "\n()",
want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"},
editRegexp: "\n()",
},
{
name: "package completion in a comment",
filename: "fruits/testfile.go",
triggerRegexp: "th(i)s",
want: nil,
},
{
name: "package completion in a multiline comment",
filename: "fruits/testfile.go",
triggerRegexp: `\/\*\n()`,
want: nil,
},
{
name: "package completion at invalid position",
filename: "fruits/testfile.go",
triggerRegexp: "import \"fmt\"\n()",
want: nil,
},
{
name: "package completion after keyword 'package'",
filename: "fruits/testfile2.go",
triggerRegexp: "package()",
want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"},
editRegexp: "package\n",
},
{
name: "package completion with 'pac' prefix",
filename: "fruits/testfile3.go",
triggerRegexp: "pac()",
want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"},
editRegexp: "pac",
},
{
name: "package completion for empty file",
filename: "fruits/testfile4.go",
triggerRegexp: "^$",
content: &testfile4,
want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"},
editRegexp: "^$",
},
{
name: "package completion without terminal newline",
filename: "fruits/testfile5.go",
triggerRegexp: `\*\/ ()`,
content: &testfile5,
want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"},
editRegexp: `\*\/ ()`,
},
{
name: "package completion on terminal newline",
filename: "fruits/testfile6.go",
triggerRegexp: `\*\/\n()`,
content: &testfile6,
want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"},
editRegexp: `\*\/\n()`,
},
// Issue golang/go#44680
{
name: "package completion for dir name with punctuation",
filename: "123f_r.u~its-123/testfile.go",
triggerRegexp: "package()",
want: []string{"package fruits123", "package fruits123_test", "package main"},
editRegexp: "package\n",
},
{
name: "package completion for invalid dir name",
filename: ".invalid-dir@-name/testfile.go",
triggerRegexp: "package()",
want: []string{"package main"},
editRegexp: "package\n",
},
} {
t.Run(tc.name, func(t *testing.T) {
Run(t, files, func(t *testing.T, env *Env) {
if tc.content != nil {
env.WriteWorkspaceFile(tc.filename, *tc.content)
env.Await(env.DoneWithChangeWatchedFiles())
}
env.OpenFile(tc.filename)
completions := env.Completion(env.RegexpSearch(tc.filename, tc.triggerRegexp))
// Check that the completion item suggestions are in the range
// of the file. {Start,End}.Line are zero-based.
lineCount := len(strings.Split(env.BufferText(tc.filename), "\n"))
for _, item := range completions.Items {
if start := int(item.TextEdit.Range.Start.Line); start > lineCount {
t.Fatalf("unexpected text edit range start line number: got %d, want <= %d", start, lineCount)
}
if end := int(item.TextEdit.Range.End.Line); end > lineCount {
t.Fatalf("unexpected text edit range end line number: got %d, want <= %d", end, lineCount)
}
}
if tc.want != nil {
expectedLoc := env.RegexpSearch(tc.filename, tc.editRegexp)
for _, item := range completions.Items {
gotRng := item.TextEdit.Range
if expectedLoc.Range != gotRng {
t.Errorf("unexpected completion range for completion item %s: got %v, want %v",
item.Label, gotRng, expectedLoc.Range)
}
}
}
diff := compareCompletionLabels(tc.want, completions.Items)
if diff != "" {
t.Error(diff)
}
})
})
}
}
func TestPackageNameCompletion(t *testing.T) {
const files = `
-- go.mod --
module mod.com
go 1.12
-- math/add.go --
package ma
`
want := []string{"ma", "ma_test", "main", "math", "math_test"}
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("math/add.go")
completions := env.Completion(env.RegexpSearch("math/add.go", "package ma()"))
diff := compareCompletionLabels(want, completions.Items)
if diff != "" {
t.Fatal(diff)
}
})
}
// TODO(rfindley): audit/clean up call sites for this helper, to ensure
// consistent test errors.
func compareCompletionLabels(want []string, gotItems []protocol.CompletionItem) string {
var got []string
for _, item := range gotItems {
got = append(got, item.Label)
if item.Label != item.InsertText && item.TextEdit == nil {
// Label should be the same as InsertText, if InsertText is to be used
return fmt.Sprintf("label not the same as InsertText %#v", item)
}
}
if len(got) == 0 && len(want) == 0 {
return "" // treat nil and the empty slice as equivalent
}
if diff := cmp.Diff(want, got); diff != "" {
return fmt.Sprintf("completion item mismatch (-want +got):\n%s", diff)
}
return ""
}
func TestUnimportedCompletion(t *testing.T) {
const mod = `
-- go.mod --
module mod.com
go 1.14
require example.com v1.2.3
-- go.sum --
example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY=
example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo=
-- main.go --
package main
func main() {
_ = blah
}
-- main2.go --
package main
import "example.com/blah"
func _() {
_ = blah.Hello
}
`
WithOptions(
ProxyFiles(proxy),
).Run(t, mod, func(t *testing.T, env *Env) {
// Make sure the dependency is in the module cache and accessible for
// unimported completions, and then remove it before proceeding.
env.RemoveWorkspaceFile("main2.go")
env.RunGoCommand("mod", "tidy")
env.Await(env.DoneWithChangeWatchedFiles())
// Trigger unimported completions for the example.com/blah package.
env.OpenFile("main.go")
env.Await(env.DoneWithOpen())
loc := env.RegexpSearch("main.go", "ah")
completions := env.Completion(loc)
if len(completions.Items) == 0 {
t.Fatalf("no completion items")
}
env.AcceptCompletion(loc, completions.Items[0]) // adds blah import to main.go
env.Await(env.DoneWithChange())
// Trigger completions once again for the blah.<> selector.
env.RegexpReplace("main.go", "_ = blah", "_ = blah.")
env.Await(env.DoneWithChange())
loc = env.RegexpSearch("main.go", "\n}")
completions = env.Completion(loc)
if len(completions.Items) != 1 {
t.Fatalf("expected 1 completion item, got %v", len(completions.Items))
}
item := completions.Items[0]
if item.Label != "Name" {
t.Fatalf("expected completion item blah.Name, got %v", item.Label)
}
env.AcceptCompletion(loc, item)
// Await the diagnostics to add example.com/blah to the go.mod file.
env.AfterChange(
Diagnostics(env.AtRegexp("main.go", `"example.com/blah"`)),
)
})
}
// Test that completions still work with an undownloaded module, golang/go#43333.
func TestUndownloadedModule(t *testing.T) {
// mod.com depends on example.com, but only in a file that's hidden by a
// build tag, so the IWL won't download example.com. That will cause errors
// in the go list -m call performed by the imports package.
const files = `
-- go.mod --
module mod.com
go 1.14
require example.com v1.2.3
-- go.sum --
example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY=
example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo=
-- useblah.go --
// +build hidden
package pkg
import "example.com/blah"
var _ = blah.Name
-- mainmod/mainmod.go --
package mainmod
const Name = "mainmod"
`
WithOptions(ProxyFiles(proxy)).Run(t, files, func(t *testing.T, env *Env) {
env.CreateBuffer("import.go", "package pkg\nvar _ = mainmod.Name\n")
env.SaveBuffer("import.go")
content := env.ReadWorkspaceFile("import.go")
if !strings.Contains(content, `import "mod.com/mainmod`) {
t.Errorf("expected import of mod.com/mainmod in %q", content)
}
})
}
// Test that we can doctor the source code enough so the file is
// parseable and completion works as expected.
func TestSourceFixup(t *testing.T) {
const files = `
-- go.mod --
module mod.com
go 1.12
-- foo.go --
package foo
func _() {
var s S
if s.
}
type S struct {
i int
}
`
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("foo.go")
completions := env.Completion(env.RegexpSearch("foo.go", `if s\.()`))
diff := compareCompletionLabels([]string{"i"}, completions.Items)
if diff != "" {
t.Fatal(diff)
}
})
}
func TestCompletion_Issue45510(t *testing.T) {
const files = `
-- go.mod --
module mod.com
go 1.12
-- main.go --
package main
func _() {
type a *a
var aaaa1, aaaa2 a
var _ a = aaaa
type b a
var bbbb1, bbbb2 b
var _ b = bbbb
}
type (
c *d
d *e
e **c
)
func _() {
var (
xxxxc c
xxxxd d
xxxxe e
)
var _ c = xxxx
var _ d = xxxx
var _ e = xxxx
}
`
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
tests := []struct {
re string
want []string
}{
{`var _ a = aaaa()`, []string{"aaaa1", "aaaa2"}},
{`var _ b = bbbb()`, []string{"bbbb1", "bbbb2"}},
{`var _ c = xxxx()`, []string{"xxxxc", "xxxxd", "xxxxe"}},
{`var _ d = xxxx()`, []string{"xxxxc", "xxxxd", "xxxxe"}},
{`var _ e = xxxx()`, []string{"xxxxc", "xxxxd", "xxxxe"}},
}
for _, tt := range tests {
completions := env.Completion(env.RegexpSearch("main.go", tt.re))
diff := compareCompletionLabels(tt.want, completions.Items)
if diff != "" {
t.Errorf("%s: %s", tt.re, diff)
}
}
})
}
func TestCompletionDeprecation(t *testing.T) {
const files = `
-- go.mod --
module test.com
go 1.16
-- prog.go --
package waste
// Deprecated, use newFoof
func fooFunc() bool {
return false
}
// Deprecated
const badPi = 3.14
func doit() {
if fooF
panic()
x := badP
}
`
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("prog.go")
loc := env.RegexpSearch("prog.go", "if fooF")
loc.Range.Start.Character += uint32(protocol.UTF16Len([]byte("if fooF")))
completions := env.Completion(loc)
diff := compareCompletionLabels([]string{"fooFunc"}, completions.Items)
if diff != "" {
t.Error(diff)
}
if completions.Items[0].Tags == nil {
t.Errorf("expected Tags to show deprecation %#v", completions.Items[0].Tags)
}
loc = env.RegexpSearch("prog.go", "= badP")
loc.Range.Start.Character += uint32(protocol.UTF16Len([]byte("= badP")))
completions = env.Completion(loc)
diff = compareCompletionLabels([]string{"badPi"}, completions.Items)
if diff != "" {
t.Error(diff)
}
if completions.Items[0].Tags == nil {
t.Errorf("expected Tags to show deprecation %#v", completions.Items[0].Tags)
}
})
}
func TestUnimportedCompletion_VSCodeIssue1489(t *testing.T) {
const src = `
-- go.mod --
module mod.com
go 1.14
-- main.go --
package main
import "fmt"
func main() {
fmt.Println("a")
math.Sqr
}
`
WithOptions(
WindowsLineEndings(),
Settings{"ui.completion.usePlaceholders": true},
).Run(t, src, func(t *testing.T, env *Env) {
// Trigger unimported completions for the mod.com package.
env.OpenFile("main.go")
env.Await(env.DoneWithOpen())
loc := env.RegexpSearch("main.go", "Sqr()")
completions := env.Completion(loc)
if len(completions.Items) == 0 {
t.Fatalf("no completion items")
}
env.AcceptCompletion(loc, completions.Items[0])
env.Await(env.DoneWithChange())
got := env.BufferText("main.go")
want := "package main\r\n\r\nimport (\r\n\t\"fmt\"\r\n\t\"math\"\r\n)\r\n\r\nfunc main() {\r\n\tfmt.Println(\"a\")\r\n\tmath.Sqrt(${1:x float64})\r\n}\r\n"
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("unimported completion (-want +got):\n%s", diff)
}
})
}
func TestUnimportedCompletionHasPlaceholders60269(t *testing.T) {
// We can't express this as a marker test because it doesn't support AcceptCompletion.
const src = `
-- go.mod --
module example.com
go 1.12
-- a/a.go --
package a
var _ = b.F
-- b/b.go --
package b
func F0(a, b int, c float64) {}
func F1(int, chan *string) {}
func F2[K, V any](map[K]V, chan V) {} // missing type parameters was issue #60959
func F3[K comparable, V any](map[K]V, chan V) {}
`
WithOptions(
WindowsLineEndings(),
Settings{"ui.completion.usePlaceholders": true},
).Run(t, src, func(t *testing.T, env *Env) {
env.OpenFile("a/a.go")
env.Await(env.DoneWithOpen())
// The table lists the expected completions of b.F as they appear in Items.
const common = "package a\r\n\r\nimport \"example.com/b\"\r\n\r\nvar _ = "
for i, want := range []string{
common + "b.F0(${1:a int}, ${2:b int}, ${3:c float64})\r\n",
common + "b.F1(${1:_ int}, ${2:_ chan *string})\r\n",
common + "b.F2[${1:K any}, ${2:V any}](${3:_ map[K]V}, ${4:_ chan V})\r\n",
common + "b.F3[${1:K comparable}, ${2:V any}](${3:_ map[K]V}, ${4:_ chan V})\r\n",
} {
loc := env.RegexpSearch("a/a.go", "b.F()")
completions := env.Completion(loc)
if len(completions.Items) == 0 {
t.Fatalf("no completion items")
}
saved := env.BufferText("a/a.go")
env.AcceptCompletion(loc, completions.Items[i])
env.Await(env.DoneWithChange())
got := env.BufferText("a/a.go")
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("%d: unimported completion (-want +got):\n%s", i, diff)
}
env.SetBufferContent("a/a.go", saved) // restore
}
})
}
func TestPackageMemberCompletionAfterSyntaxError(t *testing.T) {
// This test documents the current broken behavior due to golang/go#58833.
const src = `
-- go.mod --
module mod.com
go 1.14
-- main.go --
package main
import "math"
func main() {
math.Sqrt(,0)
math.Ldex
}
`
Run(t, src, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
env.Await(env.DoneWithOpen())
loc := env.RegexpSearch("main.go", "Ldex()")
completions := env.Completion(loc)
if len(completions.Items) == 0 {
t.Fatalf("no completion items")
}
env.AcceptCompletion(loc, completions.Items[0])
env.Await(env.DoneWithChange())
got := env.BufferText("main.go")
// The completion of math.Ldex after the syntax error on the
// previous line is not "math.Ldexp" but "math.Ldexmath.Abs".
// (In VSCode, "Abs" wrongly appears in the completion menu.)
// This is a consequence of poor error recovery in the parser
// causing "math.Ldex" to become a BadExpr.
want := "package main\n\nimport \"math\"\n\nfunc main() {\n\tmath.Sqrt(,0)\n\tmath.Ldexmath.Abs(${1:})\n}\n"
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("unimported completion (-want +got):\n%s", diff)
}
})
}
func TestCompleteAllFields(t *testing.T) {
// This test verifies that completion results always include all struct fields.
// See golang/go#53992.
const src = `
-- go.mod --
module mod.com
go 1.18
-- p/p.go --
package p
import (
"fmt"
. "net/http"
. "runtime"
. "go/types"
. "go/parser"
. "go/ast"
)
type S struct {
a, b, c, d, e, f, g, h, i, j, k, l, m int
n, o, p, q, r, s, t, u, v, w, x, y, z int
}
func _() {
var s S
fmt.Println(s.)
}
`
WithOptions(Settings{
"completionBudget": "1ns", // must be non-zero as 0 => infinity
}).Run(t, src, func(t *testing.T, env *Env) {
wantFields := make(map[string]bool)
for c := 'a'; c <= 'z'; c++ {
wantFields[string(c)] = true
}
env.OpenFile("p/p.go")
// Make an arbitrary edit to ensure we're not hitting the cache.
env.EditBuffer("p/p.go", fake.NewEdit(0, 0, 0, 0, fmt.Sprintf("// current time: %v\n", time.Now())))
loc := env.RegexpSearch("p/p.go", `s\.()`)
completions := env.Completion(loc)
gotFields := make(map[string]bool)
for _, item := range completions.Items {
if item.Kind == protocol.FieldCompletion {
gotFields[item.Label] = true
}
}
if diff := cmp.Diff(wantFields, gotFields); diff != "" {
t.Errorf("Completion(...) returned mismatching fields (-want +got):\n%s", diff)
}
})
}
func TestDefinition(t *testing.T) {
files := `
-- go.mod --
module mod.com
go 1.18
-- a_test.go --
package foo
`
tests := []struct {
line string // the sole line in the buffer after the package statement
pat string // the pattern to search for
want []string // expected completions
}{
{"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, files, func(t *testing.T, env *Env) {
env.OpenFile(fname)
env.Await(env.DoneWithOpen())
for _, test := range tests {
env.SetBufferContent(fname, "package foo\n"+test.line)
loc := env.RegexpSearch(fname, test.pat)
loc.Range.Start.Character += uint32(protocol.UTF16Len([]byte(test.pat)))
completions := env.Completion(loc)
if diff := compareCompletionLabels(test.want, completions.Items); diff != "" {
t.Error(diff)
}
}
})
}
// Test that completing a definition replaces source text when applied, golang/go#56852.
func TestDefinitionReplaceRange(t *testing.T) {
const mod = `
-- go.mod --
module mod.com
go 1.17
`
tests := []struct {
name string
before, after string
}{
{
name: "func TestMa",
before: `
package foo_test
func TestMa
`,
after: `
package foo_test
func TestMain(m *testing.M)
`,
},
{
name: "func TestSome",
before: `
package foo_test
func TestSome
`,
after: `
package foo_test
func TestSome(t *testing.T)
`,
},
{
name: "func Bench",
before: `
package foo_test
func Bench
`,
// Note: Snippet with escaped }.
after: `
package foo_test
func Benchmark${1:Xxx}(b *testing.B) {
$0
\}
`,
},
}
Run(t, mod, func(t *testing.T, env *Env) {
env.CreateBuffer("foo_test.go", "")
for _, tst := range tests {
tst.before = strings.Trim(tst.before, "\n")
tst.after = strings.Trim(tst.after, "\n")
env.SetBufferContent("foo_test.go", tst.before)
loc := env.RegexpSearch("foo_test.go", tst.name)
loc.Range.Start.Character = uint32(protocol.UTF16Len([]byte(tst.name)))
completions := env.Completion(loc)
if len(completions.Items) == 0 {
t.Fatalf("no completion items")
}
env.AcceptCompletion(loc, completions.Items[0])
env.Await(env.DoneWithChange())
if buf := env.BufferText("foo_test.go"); buf != tst.after {
t.Errorf("%s:incorrect completion: got %q, want %q", tst.name, buf, tst.after)
}
}
})
}
func TestGoWorkCompletion(t *testing.T) {
const files = `
-- go.work --
go 1.18
use ./a
use ./a/ba
use ./a/b/
use ./dir/foo
use ./dir/foobar/
use ./missing/
-- a/go.mod --
-- go.mod --
-- a/bar/go.mod --
-- a/b/c/d/e/f/go.mod --
-- dir/bar --
-- dir/foobar/go.mod --
`
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("go.work")
tests := []struct {
re string
want []string
}{
{`use ()\.`, []string{".", "./a", "./a/bar", "./dir/foobar"}},
{`use \.()`, []string{"", "/a", "/a/bar", "/dir/foobar"}},
{`use \./()`, []string{"a", "a/bar", "dir/foobar"}},
{`use ./a()`, []string{"", "/b/c/d/e/f", "/bar"}},
{`use ./a/b()`, []string{"/c/d/e/f", "ar"}},
{`use ./a/b/()`, []string{`c/d/e/f`}},
{`use ./a/ba()`, []string{"r"}},
{`use ./dir/foo()`, []string{"bar"}},
{`use ./dir/foobar/()`, []string{}},
{`use ./missing/()`, []string{}},
}
for _, tt := range tests {
completions := env.Completion(env.RegexpSearch("go.work", tt.re))
diff := compareCompletionLabels(tt.want, completions.Items)
if diff != "" {
t.Errorf("%s: %s", tt.re, diff)
}
}
})
}
func TestBuiltinCompletion(t *testing.T) {
const files = `
-- go.mod --
module mod.com
go 1.18
-- a.go --
package a
func _() {
// here
}
`
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("a.go")
result := env.Completion(env.RegexpSearch("a.go", `// here`))
builtins := []string{
"any", "append", "bool", "byte", "cap", "close",
"comparable", "complex", "complex128", "complex64", "copy", "delete",
"error", "false", "float32", "float64", "imag", "int", "int16", "int32",
"int64", "int8", "len", "make", "new", "panic", "print", "println", "real",
"recover", "rune", "string", "true", "uint", "uint16", "uint32", "uint64",
"uint8", "uintptr", "nil",
}
if testenv.Go1Point() >= 21 {
builtins = append(builtins, "clear", "max", "min")
}
sort.Strings(builtins)
var got []string
for _, item := range result.Items {
// TODO(rfindley): for flexibility, ignore zero while it is being
// implemented. Remove this if/when zero lands.
if item.Label != "zero" {
got = append(got, item.Label)
}
}
sort.Strings(got)
if diff := cmp.Diff(builtins, got); diff != "" {
t.Errorf("Completion: unexpected mismatch (-want +got):\n%s", diff)
}
})
}
func TestOverlayCompletion(t *testing.T) {
const files = `
-- go.mod --
module foo.test
go 1.18
-- foo/foo.go --
package foo
type Foo struct{}
`
Run(t, files, func(t *testing.T, env *Env) {
env.CreateBuffer("nodisk/nodisk.go", `
package nodisk
import (
"foo.test/foo"
)
func _() {
foo.Foo()
}
`)
list := env.Completion(env.RegexpSearch("nodisk/nodisk.go", "foo.(Foo)"))
want := []string{"Foo"}
var got []string
for _, item := range list.Items {
got = append(got, item.Label)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("Completion: unexpected mismatch (-want +got):\n%s", diff)
}
})
}
// Fix for golang/go#60062: unimported completion included "golang.org/toolchain" results.
func TestToolchainCompletions(t *testing.T) {
const files = `
-- go.mod --
module foo.test/foo
go 1.21
-- foo.go --
package foo
func _() {
os.Open
}
func _() {
strings
}
`
const proxy = `
-- golang.org/toolchain@v0.0.1-go1.21.1.linux-amd64/go.mod --
module golang.org/toolchain
-- golang.org/toolchain@v0.0.1-go1.21.1.linux-amd64/src/os/os.go --
package os
func Open() {}
-- golang.org/toolchain@v0.0.1-go1.21.1.linux-amd64/src/strings/strings.go --
package strings
func Join() {}
`
WithOptions(
ProxyFiles(proxy),
).Run(t, files, func(t *testing.T, env *Env) {
env.RunGoCommand("mod", "download", "golang.org/toolchain@v0.0.1-go1.21.1.linux-amd64")
env.OpenFile("foo.go")
for _, pattern := range []string{"os.Open()", "string()"} {
loc := env.RegexpSearch("foo.go", pattern)
res := env.Completion(loc)
for _, item := range res.Items {
if strings.Contains(item.Detail, "golang.org/toolchain") {
t.Errorf("Completion(...) returned toolchain item %#v", item)
}
}
}
})
}
// show that the efficacy counters get exercised. Fortuntely a small program
// exercises them all
func TestCounters(t *testing.T) {
const files = `
-- go.mod --
module foo
go 1.21
-- x.go --
package foo
func main() {
}
`
WithOptions(
Modes(Default),
).Run(t, files, func(t *testing.T, env *Env) {
cts := func() map[*counter.Counter]uint64 {
ans := make(map[*counter.Counter]uint64)
for _, c := range server.CompletionCounters {
ans[c], _ = countertest.ReadCounter(c)
}
return ans
}
before := cts()
env.OpenFile("x.go")
env.Await(env.DoneWithOpen())
saved := env.BufferText("x.go")
lines := strings.Split(saved, "\n")
// make sure the unused counter is exercised
loc := env.RegexpSearch("x.go", "main")
loc.Range.End = loc.Range.Start
env.Completion(loc) // ignore the proposed completions
env.RegexpReplace("x.go", "main", "Main") // completions are unused
env.SetBufferContent("x.go", saved) // restore x.go
// used:no
// all the action is after 4 characters on line 2 (counting from 0)
for i := 2; i < len(lines); i++ {
l := lines[i]
loc.Range.Start.Line = uint32(i)
for j := 4; j < len(l); j++ {
loc.Range.Start.Character = uint32(j)
loc.Range.End = loc.Range.Start
res := env.Completion(loc)
if len(res.Items) > 0 {
r := res.Items[0]
env.AcceptCompletion(loc, r)
env.SetBufferContent("x.go", saved)
}
}
}
after := cts()
for c := range after {
if after[c] <= before[c] {
t.Errorf("%s did not increase", c.Name())
}
}
})
}