internal/lsp: reorganize completion tests
Our completion tests check for a lot of different behaviors. It may be
easier to develop if we have separate tests for things like deep
completion and completion snippets.
Change-Id: I7f4b0c0e52670f2a6c00247199933fd1ffa0096f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/196021
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/internal/lsp/cmd/test/cmdtest.go b/internal/lsp/cmd/test/cmdtest.go
index 3352323..d31dfe4 100644
--- a/internal/lsp/cmd/test/cmdtest.go
+++ b/internal/lsp/cmd/test/cmdtest.go
@@ -33,7 +33,27 @@
}
}
-func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests.CompletionSnippets, items tests.CompletionItems) {
+func (r *runner) Completion(t *testing.T, data tests.Completions, items tests.CompletionItems) {
+ //TODO: add command line completions tests when it works
+}
+
+func (r *runner) CompletionSnippets(t *testing.T, data tests.CompletionSnippets, items tests.CompletionItems) {
+ //TODO: add command line completions tests when it works
+}
+
+func (r *runner) UnimportedCompletions(t *testing.T, data tests.UnimportedCompletions, items tests.CompletionItems) {
+ //TODO: add command line completions tests when it works
+}
+
+func (r *runner) DeepCompletions(t *testing.T, data tests.DeepCompletions, items tests.CompletionItems) {
+ //TODO: add command line completions tests when it works
+}
+
+func (r *runner) FuzzyCompletions(t *testing.T, data tests.FuzzyCompletions, items tests.CompletionItems) {
+ //TODO: add command line completions tests when it works
+}
+
+func (r *runner) RankCompletions(t *testing.T, data tests.RankCompletions, items tests.CompletionItems) {
//TODO: add command line completions tests when it works
}
diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go
index d1a98f7..c176be4 100644
--- a/internal/lsp/completion.go
+++ b/internal/lsp/completion.go
@@ -47,11 +47,11 @@
// When using deep completions/fuzzy matching, report results as incomplete so
// client fetches updated completions after every key stroke.
IsIncomplete: options.Completion.Deep,
- Items: s.toProtocolCompletionItems(candidates, rng, options),
+ Items: toProtocolCompletionItems(candidates, rng, options),
}, nil
}
-func (s *Server) toProtocolCompletionItems(candidates []source.CompletionItem, rng protocol.Range, options source.Options) []protocol.CompletionItem {
+func toProtocolCompletionItems(candidates []source.CompletionItem, rng protocol.Range, options source.Options) []protocol.CompletionItem {
var (
items = make([]protocol.CompletionItem, 0, len(candidates))
numDeepCompletionsSeen int
diff --git a/internal/lsp/completion_test.go b/internal/lsp/completion_test.go
new file mode 100644
index 0000000..be42932
--- /dev/null
+++ b/internal/lsp/completion_test.go
@@ -0,0 +1,151 @@
+package lsp
+
+import (
+ "strings"
+ "testing"
+ "time"
+
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/lsp/source"
+ "golang.org/x/tools/internal/lsp/tests"
+ "golang.org/x/tools/internal/span"
+)
+
+func (r *runner) Completion(t *testing.T, data tests.Completions, items tests.CompletionItems) {
+ for src, test := range data {
+ got := r.callCompletion(t, src, source.CompletionOptions{
+ Deep: false,
+ FuzzyMatching: false,
+ Documentation: true,
+ })
+ if !strings.Contains(string(src.URI()), "builtins") {
+ got = tests.FilterBuiltins(got)
+ }
+ want := expected(t, test, items)
+ if diff := tests.DiffCompletionItems(want, got); diff != "" {
+ t.Errorf("%s: %s", src, diff)
+ }
+ }
+}
+
+func (r *runner) CompletionSnippets(t *testing.T, data tests.CompletionSnippets, items tests.CompletionItems) {
+ for _, placeholders := range []bool{true, false} {
+ for src, expected := range data {
+ list := r.callCompletion(t, src, source.CompletionOptions{
+ Placeholders: placeholders,
+ Deep: true,
+ Budget: 5 * time.Second,
+ FuzzyMatching: true,
+ })
+ got := tests.FindItem(list, *items[expected.CompletionItem])
+ want := expected.PlainSnippet
+ if placeholders {
+ want = expected.PlaceholderSnippet
+ }
+ if diff := tests.DiffSnippets(want, got); diff != "" {
+ t.Errorf("%s: %v", src, diff)
+ }
+ }
+ }
+}
+
+func (r *runner) UnimportedCompletions(t *testing.T, data tests.UnimportedCompletions, items tests.CompletionItems) {
+ for src, test := range data {
+ got := r.callCompletion(t, src, source.CompletionOptions{
+ Unimported: true,
+ })
+ if !strings.Contains(string(src.URI()), "builtins") {
+ got = tests.FilterBuiltins(got)
+ }
+ want := expected(t, test, items)
+ if diff := tests.DiffCompletionItems(want, got); diff != "" {
+ t.Errorf("%s: %s", src, diff)
+ }
+ }
+}
+
+func (r *runner) DeepCompletions(t *testing.T, data tests.DeepCompletions, items tests.CompletionItems) {
+ for src, test := range data {
+ got := r.callCompletion(t, src, source.CompletionOptions{
+ Deep: true,
+ Budget: 5 * time.Second,
+ Documentation: true,
+ })
+ if !strings.Contains(string(src.URI()), "builtins") {
+ got = tests.FilterBuiltins(got)
+ }
+ want := expected(t, test, items)
+ if msg := tests.DiffCompletionItems(want, got); msg != "" {
+ t.Errorf("%s: %s", src, msg)
+ }
+ }
+}
+
+func (r *runner) FuzzyCompletions(t *testing.T, data tests.FuzzyCompletions, items tests.CompletionItems) {
+ for src, test := range data {
+ got := r.callCompletion(t, src, source.CompletionOptions{
+ FuzzyMatching: true,
+ Deep: true,
+ Budget: 5 * time.Second,
+ })
+ if !strings.Contains(string(src.URI()), "builtins") {
+ got = tests.FilterBuiltins(got)
+ }
+ want := expected(t, test, items)
+ if msg := tests.DiffCompletionItems(want, got); msg != "" {
+ t.Errorf("%s: %s", src, msg)
+ }
+ }
+}
+
+func (r *runner) RankCompletions(t *testing.T, data tests.RankCompletions, items tests.CompletionItems) {
+ for src, test := range data {
+ got := r.callCompletion(t, src, source.CompletionOptions{
+ FuzzyMatching: true,
+ Deep: true,
+ Budget: 5 * time.Second,
+ })
+ want := expected(t, test, items)
+ if msg := tests.CheckCompletionOrder(want, got); msg != "" {
+ t.Errorf("%s: %s", src, msg)
+ }
+ }
+}
+
+func expected(t *testing.T, test tests.Completion, items tests.CompletionItems) []protocol.CompletionItem {
+ t.Helper()
+
+ var want []protocol.CompletionItem
+ for _, pos := range test.CompletionItems {
+ item := items[pos]
+ want = append(want, tests.ToProtocolCompletionItem(*item))
+ }
+ return want
+}
+func (r *runner) callCompletion(t *testing.T, src span.Span, options source.CompletionOptions) []protocol.CompletionItem {
+ t.Helper()
+
+ view := r.server.session.ViewOf(src.URI())
+ original := view.Options()
+ modified := original
+ modified.InsertTextFormat = protocol.SnippetTextFormat
+ modified.Completion = options
+ view.SetOptions(modified)
+ defer view.SetOptions(original)
+
+ list, err := r.server.Completion(r.ctx, &protocol.CompletionParams{
+ TextDocumentPositionParams: protocol.TextDocumentPositionParams{
+ TextDocument: protocol.TextDocumentIdentifier{
+ URI: protocol.NewURI(src.URI()),
+ },
+ Position: protocol.Position{
+ Line: float64(src.Start().Line() - 1),
+ Character: float64(src.Start().Column() - 1),
+ },
+ },
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ return list.Items
+}
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index ba56315..c133bac 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -15,7 +15,6 @@
"sort"
"strings"
"testing"
- "time"
"golang.org/x/tools/go/packages/packagestest"
"golang.org/x/tools/internal/lsp/cache"
@@ -51,18 +50,7 @@
cache := cache.New()
session := cache.NewSession(ctx)
- options := session.Options()
- options.SupportedCodeActions = map[source.FileKind]map[protocol.CodeActionKind]bool{
- source.Go: {
- protocol.SourceOrganizeImports: true,
- protocol.QuickFix: true,
- },
- source.Mod: {},
- source.Sum: {},
- }
- options.HoverKind = source.SynopsisDocumentation
- // Crank this up so tests don't flake.
- options.Completion.Budget = 5 * time.Second
+ options := tests.DefaultOptions()
session.SetOptions(options)
options.Env = data.Config.Env
session.NewView(ctx, viewName, span.FileURI(data.Config.Dir), options)
@@ -111,218 +99,6 @@
}
}
-func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests.CompletionSnippets, items tests.CompletionItems) {
- for src, test := range data {
- view := r.server.session.ViewOf(src.URI())
- original := view.Options()
- modified := original
-
- // Set this as a default.
- modified.Completion.Documentation = true
-
- var want []source.CompletionItem
- for _, pos := range test.CompletionItems {
- want = append(want, *items[pos])
- }
-
- modified.Completion.Deep = strings.Contains(string(src.URI()), "deepcomplete")
- modified.Completion.FuzzyMatching = strings.Contains(string(src.URI()), "fuzzymatch")
- modified.Completion.Unimported = strings.Contains(string(src.URI()), "unimported")
- view.SetOptions(modified)
-
- list := r.runCompletion(t, src)
-
- wantBuiltins := strings.Contains(string(src.URI()), "builtins")
- var got []protocol.CompletionItem
- for _, item := range list.Items {
- if !wantBuiltins && isBuiltin(item) {
- continue
- }
- got = append(got, item)
- }
-
- switch test.Type {
- case tests.CompletionFull:
- if diff := diffCompletionItems(want, got); diff != "" {
- t.Errorf("%s: %s", src, diff)
- }
- case tests.CompletionPartial:
- if msg := checkCompletionOrder(want, got); msg != "" {
- t.Errorf("%s: %s", src, msg)
- }
- }
- view.SetOptions(original)
- }
-
- for _, usePlaceholders := range []bool{true, false} {
-
- for src, want := range snippets {
- view := r.server.session.ViewOf(src.URI())
- original := view.Options()
- modified := original
-
- modified.InsertTextFormat = protocol.SnippetTextFormat
- modified.Completion.Deep = strings.Contains(string(src.URI()), "deepcomplete")
- modified.Completion.FuzzyMatching = strings.Contains(string(src.URI()), "fuzzymatch")
- modified.Completion.Unimported = strings.Contains(string(src.URI()), "unimported")
- modified.Completion.Placeholders = usePlaceholders
- view.SetOptions(modified)
-
- list := r.runCompletion(t, src)
-
- wantItem := items[want.CompletionItem]
- var got *protocol.CompletionItem
- for _, item := range list.Items {
- if item.Label == wantItem.Label {
- got = &item
- break
- }
- }
- var expected string
- if usePlaceholders {
- expected = want.PlaceholderSnippet
- } else {
- expected = want.PlainSnippet
- }
-
- if expected == "" {
- if got != nil {
- t.Fatalf("%s:%d: expected no snippet but got %q", src.URI(), src.Start().Line(), got.TextEdit.NewText)
- }
- } else {
- if got == nil {
- t.Fatalf("%s:%d: couldn't find completion matching %q", src.URI(), src.Start().Line(), wantItem.Label)
- }
-
- if expected != got.TextEdit.NewText {
- t.Errorf("%s: expected snippet %q, got %q", src, expected, got.TextEdit.NewText)
- }
- }
- view.SetOptions(original)
- }
- }
-}
-
-func (r *runner) runCompletion(t *testing.T, src span.Span) *protocol.CompletionList {
- t.Helper()
- list, err := r.server.Completion(r.ctx, &protocol.CompletionParams{
- TextDocumentPositionParams: protocol.TextDocumentPositionParams{
- TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(src.URI()),
- },
- Position: protocol.Position{
- Line: float64(src.Start().Line() - 1),
- Character: float64(src.Start().Column() - 1),
- },
- },
- })
- if err != nil {
- t.Fatal(err)
- }
- return list
-}
-
-func isBuiltin(item protocol.CompletionItem) bool {
- // If a type has no detail, it is a builtin type.
- if item.Detail == "" && item.Kind == protocol.TypeParameterCompletion {
- return true
- }
- // Remaining builtin constants, variables, interfaces, and functions.
- trimmed := item.Label
- if i := strings.Index(trimmed, "("); i >= 0 {
- trimmed = trimmed[:i]
- }
- switch trimmed {
- case "append", "cap", "close", "complex", "copy", "delete",
- "error", "false", "imag", "iota", "len", "make", "new",
- "nil", "panic", "print", "println", "real", "recover", "true":
- return true
- }
- return false
-}
-
-// diffCompletionItems prints the diff between expected and actual completion
-// test results.
-func diffCompletionItems(want []source.CompletionItem, got []protocol.CompletionItem) string {
- if len(got) != len(want) {
- return summarizeCompletionItems(-1, want, got, "different lengths got %v want %v", len(got), len(want))
- }
- for i, w := range want {
- g := got[i]
- if w.Label != g.Label {
- return summarizeCompletionItems(i, want, got, "incorrect Label got %v want %v", g.Label, w.Label)
- }
- if w.Detail != g.Detail {
- return summarizeCompletionItems(i, want, got, "incorrect Detail got %v want %v", g.Detail, w.Detail)
- }
- if w.Documentation != "" && !strings.HasPrefix(w.Documentation, "@") {
- if w.Documentation != g.Documentation {
- return summarizeCompletionItems(i, want, got, "incorrect Documentation got %v want %v", g.Documentation, w.Documentation)
- }
- }
- if wkind := toProtocolCompletionItemKind(w.Kind); wkind != g.Kind {
- return summarizeCompletionItems(i, want, got, "incorrect Kind got %v want %v", g.Kind, wkind)
- }
- }
- return ""
-}
-
-func checkCompletionOrder(want []source.CompletionItem, got []protocol.CompletionItem) string {
- var (
- matchedIdxs []int
- lastGotIdx int
- inOrder = true
- )
- for _, w := range want {
- var found bool
- for i, g := range got {
- if w.Label == g.Label && w.Detail == g.Detail && toProtocolCompletionItemKind(w.Kind) == g.Kind {
- matchedIdxs = append(matchedIdxs, i)
- found = true
- if i < lastGotIdx {
- inOrder = false
- }
- lastGotIdx = i
- break
- }
- }
- if !found {
- return summarizeCompletionItems(-1, []source.CompletionItem{w}, got, "didn't find expected completion")
- }
- }
-
- sort.Ints(matchedIdxs)
- matched := make([]protocol.CompletionItem, 0, len(matchedIdxs))
- for _, idx := range matchedIdxs {
- matched = append(matched, got[idx])
- }
-
- if !inOrder {
- return summarizeCompletionItems(-1, want, matched, "completions out of order")
- }
-
- return ""
-}
-
-func summarizeCompletionItems(i int, want []source.CompletionItem, got []protocol.CompletionItem, reason string, args ...interface{}) string {
- msg := &bytes.Buffer{}
- fmt.Fprint(msg, "completion failed")
- if i >= 0 {
- fmt.Fprintf(msg, " at %d", i)
- }
- fmt.Fprint(msg, " because of ")
- fmt.Fprintf(msg, reason, args...)
- fmt.Fprint(msg, ":\nexpected:\n")
- for _, d := range want {
- fmt.Fprintf(msg, " %v\n", d)
- }
- fmt.Fprintf(msg, "got:\n")
- for _, d := range got {
- fmt.Fprintf(msg, " %v\n", d)
- }
- return msg.String()
-}
-
func (r *runner) FoldingRange(t *testing.T, data tests.FoldingRanges) {
for _, spn := range data {
uri := spn.URI()
@@ -909,7 +685,7 @@
return ""
}
-func summarizeSymbols(t *testing.T, i int, want []protocol.DocumentSymbol, got []protocol.DocumentSymbol, reason string, args ...interface{}) string {
+func summarizeSymbols(t *testing.T, i int, want, got []protocol.DocumentSymbol, reason string, args ...interface{}) string {
msg := &bytes.Buffer{}
fmt.Fprint(msg, "document symbols failed")
if i >= 0 {
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index 7e9af00..66ef61e 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -49,7 +49,7 @@
cache := cache.New()
session := cache.NewSession(ctx)
- options := session.Options()
+ options := tests.DefaultOptions()
options.Env = data.Config.Env
r := &runner{
view: session.NewView(ctx, "source_test", span.FileURI(data.Config.Dir), options),
@@ -86,245 +86,194 @@
}
}
-func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests.CompletionSnippets, items tests.CompletionItems) {
- ctx := r.ctx
+func (r *runner) Completion(t *testing.T, data tests.Completions, items tests.CompletionItems) {
for src, test := range data {
- var want []source.CompletionItem
+ var want []protocol.CompletionItem
for _, pos := range test.CompletionItems {
- want = append(want, *items[pos])
+ want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
}
- f, err := r.view.GetFile(ctx, src.URI())
- if err != nil {
- t.Fatalf("failed for %v: %v", src, err)
- }
- deepComplete := strings.Contains(string(src.URI()), "deepcomplete")
- fuzzyMatch := strings.Contains(string(src.URI()), "fuzzymatch")
- unimported := strings.Contains(string(src.URI()), "unimported")
- list, surrounding, err := source.Completion(ctx, r.view, f.(source.GoFile), protocol.Position{
- Line: float64(src.Start().Line() - 1),
- Character: float64(src.Start().Column() - 1),
- }, source.CompletionOptions{
+ prefix, list := r.callCompletion(t, src, source.CompletionOptions{
Documentation: true,
- Deep: deepComplete,
- FuzzyMatching: fuzzyMatch,
- Unimported: unimported,
- // Crank this up so tests don't flake.
- Budget: 5 * time.Second,
+ FuzzyMatching: true,
})
- if err != nil {
- t.Fatalf("failed for %v: %v", src, err)
+ if !strings.Contains(string(src.URI()), "builtins") {
+ list = tests.FilterBuiltins(list)
}
- var (
- prefix string
- fuzzyMatcher *fuzzy.Matcher
- )
- if surrounding != nil {
- prefix = strings.ToLower(surrounding.Prefix())
- if deepComplete && prefix != "" {
- fuzzyMatcher = fuzzy.NewMatcher(surrounding.Prefix(), fuzzy.Symbol)
- }
- }
- wantBuiltins := strings.Contains(string(src.URI()), "builtins")
- var got []source.CompletionItem
+ var got []protocol.CompletionItem
for _, item := range list {
- if !wantBuiltins && isBuiltin(item) {
+ if !strings.HasPrefix(strings.ToLower(item.Label), prefix) {
continue
}
-
- // If deep completion is enabled, we need to use the fuzzy matcher to match
- // the code's behavior.
- if deepComplete {
- if fuzzyMatcher != nil && fuzzyMatcher.Score(item.Label) < 0 {
- continue
- }
- } else {
- // We let the client do fuzzy matching, so we return all possible candidates.
- // To simplify testing, filter results with prefixes that don't match exactly.
- if !strings.HasPrefix(strings.ToLower(item.Label), prefix) {
- continue
- }
- }
got = append(got, item)
}
- switch test.Type {
- case tests.CompletionFull:
- if diff := diffCompletionItems(want, got); diff != "" {
+ if diff := tests.DiffCompletionItems(want, got); diff != "" {
+ t.Errorf("%s: %s", src, diff)
+ }
+ }
+}
+
+func (r *runner) CompletionSnippets(t *testing.T, data tests.CompletionSnippets, items tests.CompletionItems) {
+ for _, placeholders := range []bool{true, false} {
+ for src, expected := range data {
+ _, list := r.callCompletion(t, src, source.CompletionOptions{
+ Placeholders: placeholders,
+ Deep: true,
+ Budget: 5 * time.Second,
+ })
+ got := tests.FindItem(list, *items[expected.CompletionItem])
+ want := expected.PlainSnippet
+ if placeholders {
+ want = expected.PlaceholderSnippet
+ }
+ if diff := tests.DiffSnippets(want, got); diff != "" {
t.Errorf("%s: %s", src, diff)
}
- case tests.CompletionPartial:
- if msg := checkCompletionOrder(want, got); msg != "" {
- t.Errorf("%s: %s", src, msg)
- }
- }
- }
- for _, usePlaceholders := range []bool{true, false} {
- for src, want := range snippets {
- f, err := r.view.GetFile(ctx, src.URI())
- if err != nil {
- t.Fatalf("failed for %v: %v", src, err)
- }
-
- list, _, err := source.Completion(ctx, r.view, f.(source.GoFile), protocol.Position{
- Line: float64(src.Start().Line() - 1),
- Character: float64(src.Start().Column() - 1),
- }, source.CompletionOptions{
- Documentation: true,
- Deep: strings.Contains(string(src.URI()), "deepcomplete"),
- FuzzyMatching: strings.Contains(string(src.URI()), "fuzzymatch"),
- Placeholders: usePlaceholders,
- // Crank this up so tests don't flake.
- Budget: 5 * time.Second,
- })
- if err != nil {
- t.Fatalf("failed for %v: %v", src, err)
- }
- wantItem := items[want.CompletionItem]
- var got *source.CompletionItem
- for _, item := range list {
- if item.Label == wantItem.Label {
- got = &item
- break
- }
- }
- expected := want.PlainSnippet
- if usePlaceholders {
- expected = want.PlaceholderSnippet
- }
- if expected == "" {
- if got != nil {
- t.Fatalf("%s:%d: expected no matching snippet", src.URI(), src.Start().Line())
- }
- } else {
- if got == nil {
- t.Fatalf("%s:%d: couldn't find completion matching %q", src.URI(), src.Start().Line(), wantItem.Label)
- }
- actual := got.Snippet()
- if expected != actual {
- t.Errorf("%s: expected placeholder snippet %q, got %q", src, expected, actual)
- }
- }
}
}
}
-func isBuiltin(item source.CompletionItem) bool {
- // If a type has no detail, it is a builtin type.
- if item.Detail == "" && item.Kind == source.TypeCompletionItem {
- return true
+func (r *runner) UnimportedCompletions(t *testing.T, data tests.UnimportedCompletions, items tests.CompletionItems) {
+ for src, test := range data {
+ var want []protocol.CompletionItem
+ for _, pos := range test.CompletionItems {
+ want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
+ }
+ _, got := r.callCompletion(t, src, source.CompletionOptions{
+ Unimported: true,
+ })
+ if !strings.Contains(string(src.URI()), "builtins") {
+ got = tests.FilterBuiltins(got)
+ }
+ if diff := tests.DiffCompletionItems(want, got); diff != "" {
+ t.Errorf("%s: %s", src, diff)
+ }
}
- // Remaining builtin constants, variables, interfaces, and functions.
- trimmed := item.Label
- if i := strings.Index(trimmed, "("); i >= 0 {
- trimmed = trimmed[:i]
- }
- switch trimmed {
- case "append", "cap", "close", "complex", "copy", "delete",
- "error", "false", "imag", "iota", "len", "make", "new",
- "nil", "panic", "print", "println", "real", "recover", "true":
- return true
- }
- return false
}
-// diffCompletionItems prints the diff between expected and actual completion
-// test results.
-func diffCompletionItems(want []source.CompletionItem, got []source.CompletionItem) string {
- sort.SliceStable(got, func(i, j int) bool {
- return got[i].Score > got[j].Score
- })
-
- // duplicate the lsp/completion logic to limit deep candidates to keep expected
- // list short
- var idx, seenDeepCompletions int
- for _, item := range got {
- if item.Depth > 0 {
- if seenDeepCompletions >= 3 {
+func (r *runner) DeepCompletions(t *testing.T, data tests.DeepCompletions, items tests.CompletionItems) {
+ for src, test := range data {
+ var want []protocol.CompletionItem
+ for _, pos := range test.CompletionItems {
+ want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
+ }
+ prefix, list := r.callCompletion(t, src, source.CompletionOptions{
+ Deep: true,
+ Budget: 5 * time.Second,
+ Documentation: true,
+ })
+ if !strings.Contains(string(src.URI()), "builtins") {
+ list = tests.FilterBuiltins(list)
+ }
+ fuzzyMatcher := fuzzy.NewMatcher(prefix, fuzzy.Symbol)
+ var got []protocol.CompletionItem
+ for _, item := range list {
+ if fuzzyMatcher.Score(item.Label) < 0 {
continue
}
- seenDeepCompletions++
+ got = append(got, item)
}
- got[idx] = item
- idx++
- }
- got = got[:idx]
-
- if len(got) != len(want) {
- return summarizeCompletionItems(-1, want, got, "different lengths got %v want %v", len(got), len(want))
- }
- for i, w := range want {
- g := got[i]
- if w.Label != g.Label {
- return summarizeCompletionItems(i, want, got, "incorrect Label got %v want %v", g.Label, w.Label)
- }
- if w.Detail != g.Detail {
- return summarizeCompletionItems(i, want, got, "incorrect Detail got %v want %v", g.Detail, w.Detail)
- }
- if w.Documentation != "" && !strings.HasPrefix(w.Documentation, "@") {
- if w.Documentation != g.Documentation {
- return summarizeCompletionItems(i, want, got, "incorrect Documentation got %v want %v", g.Documentation, w.Documentation)
- }
- }
- if w.Kind != g.Kind {
- return summarizeCompletionItems(i, want, got, "incorrect Kind got %v want %v", g.Kind, w.Kind)
+ if msg := tests.DiffCompletionItems(want, got); msg != "" {
+ t.Errorf("%s: %s", src, msg)
}
}
- return ""
}
-func checkCompletionOrder(want []source.CompletionItem, got []source.CompletionItem) string {
- var (
- matchedIdxs []int
- lastGotIdx int
- inOrder = true
- )
- for _, w := range want {
- var found bool
- for i, g := range got {
- if w.Label == g.Label && w.Detail == g.Detail && w.Kind == g.Kind {
- matchedIdxs = append(matchedIdxs, i)
- found = true
- if i < lastGotIdx {
- inOrder = false
- }
- lastGotIdx = i
- break
+func (r *runner) FuzzyCompletions(t *testing.T, data tests.FuzzyCompletions, items tests.CompletionItems) {
+ for src, test := range data {
+ var want []protocol.CompletionItem
+ for _, pos := range test.CompletionItems {
+ want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
+ }
+ prefix, list := r.callCompletion(t, src, source.CompletionOptions{
+ FuzzyMatching: true,
+ Deep: true,
+ Budget: 5 * time.Second,
+ })
+ if !strings.Contains(string(src.URI()), "builtins") {
+ list = tests.FilterBuiltins(list)
+ }
+ var fuzzyMatcher *fuzzy.Matcher
+ if prefix != "" {
+ fuzzyMatcher = fuzzy.NewMatcher(prefix, fuzzy.Symbol)
+ }
+ var got []protocol.CompletionItem
+ for _, item := range list {
+ if fuzzyMatcher != nil && fuzzyMatcher.Score(item.Label) < 0 {
+ continue
}
+ got = append(got, item)
}
- if !found {
- return summarizeCompletionItems(-1, []source.CompletionItem{w}, got, "didn't find expected completion")
+ if msg := tests.DiffCompletionItems(want, got); msg != "" {
+ t.Errorf("%s: %s", src, msg)
}
}
-
- sort.Ints(matchedIdxs)
- matched := make([]source.CompletionItem, 0, len(matchedIdxs))
- for _, idx := range matchedIdxs {
- matched = append(matched, got[idx])
- }
-
- if !inOrder {
- return summarizeCompletionItems(-1, want, matched, "completions out of order")
- }
-
- return ""
}
-func summarizeCompletionItems(i int, want []source.CompletionItem, got []source.CompletionItem, reason string, args ...interface{}) string {
- msg := &bytes.Buffer{}
- fmt.Fprint(msg, "completion failed")
- if i >= 0 {
- fmt.Fprintf(msg, " at %d", i)
+func (r *runner) RankCompletions(t *testing.T, data tests.RankCompletions, items tests.CompletionItems) {
+ for src, test := range data {
+ var want []protocol.CompletionItem
+ for _, pos := range test.CompletionItems {
+ want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
+ }
+ prefix, list := r.callCompletion(t, src, source.CompletionOptions{
+ FuzzyMatching: true,
+ Deep: true,
+ Budget: 5 * time.Second,
+ })
+ if !strings.Contains(string(src.URI()), "builtins") {
+ list = tests.FilterBuiltins(list)
+ }
+ fuzzyMatcher := fuzzy.NewMatcher(prefix, fuzzy.Symbol)
+ var got []protocol.CompletionItem
+ for _, item := range list {
+ if fuzzyMatcher.Score(item.Label) < 0 {
+ continue
+ }
+ got = append(got, item)
+ }
+ if msg := tests.CheckCompletionOrder(want, got); msg != "" {
+ t.Errorf("%s: %s", src, msg)
+ }
}
- fmt.Fprint(msg, " because of ")
- fmt.Fprintf(msg, reason, args...)
- fmt.Fprint(msg, ":\nexpected:\n")
- for _, d := range want {
- fmt.Fprintf(msg, " %v\n", d)
+}
+
+func (r *runner) callCompletion(t *testing.T, src span.Span, options source.CompletionOptions) (string, []protocol.CompletionItem) {
+ f, err := r.view.GetFile(r.ctx, src.URI())
+ if err != nil {
+ t.Fatal(err)
}
- fmt.Fprintf(msg, "got:\n")
- for _, d := range got {
- fmt.Fprintf(msg, " %v\n", d)
+ list, surrounding, err := source.Completion(r.ctx, r.view, f.(source.GoFile), protocol.Position{
+ Line: float64(src.Start().Line() - 1),
+ Character: float64(src.Start().Column() - 1),
+ }, options)
+ if err != nil {
+ t.Fatalf("failed for %v: %v", src, err)
}
- return msg.String()
+ var prefix string
+ if surrounding != nil {
+ prefix = strings.ToLower(surrounding.Prefix())
+ }
+ // TODO(rstambler): In testing this out, I noticed that scores are equal,
+ // even when they shouldn't be. This needs more investigation.
+ sort.SliceStable(list, func(i, j int) bool {
+ return list[i].Score > list[j].Score
+ })
+ var numDeepCompletionsSeen int
+ var items []source.CompletionItem
+ // Apply deep completion filtering.
+ for _, item := range list {
+ if item.Depth > 0 {
+ if !options.Deep {
+ continue
+ }
+ if numDeepCompletionsSeen >= source.MaxDeepCompletions {
+ continue
+ }
+ numDeepCompletionsSeen++
+ }
+ items = append(items, item)
+ }
+ return prefix, tests.ToProtocolCompletionItems(items)
}
func (r *runner) FoldingRange(t *testing.T, data tests.FoldingRanges) {
diff --git a/internal/lsp/testdata/deepcomplete/deep_complete.go b/internal/lsp/testdata/deep/deep.go
similarity index 82%
rename from internal/lsp/testdata/deepcomplete/deep_complete.go
rename to internal/lsp/testdata/deep/deep.go
index 61b8cd1..f6bcbd7 100644
--- a/internal/lsp/testdata/deepcomplete/deep_complete.go
+++ b/internal/lsp/testdata/deep/deep.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package deepcomplete
+package deep
import "context"
@@ -18,7 +18,7 @@
func _() {
var a deepA //@item(deepAVar, "a", "deepA", "var")
a.b //@item(deepABField, "a.b", "deepB", "field")
- wantsDeepB(a) //@complete(")", deepABField, deepAVar)
+ wantsDeepB(a) //@deep(")", deepABField, deepAVar)
deepA{a} //@snippet("}", deepABField, "a.b", "a.b")
}
@@ -29,7 +29,7 @@
context.Background() //@item(ctxBackground, "context.Background", "func() context.Context", "func", "Background returns a non-nil, empty Context.")
context.TODO() //@item(ctxTODO, "context.TODO", "func() context.Context", "func", "TODO returns a non-nil, empty Context.")
- wantsContext(c) //@completePartial(")", ctxBackground, ctxTODO)
+ wantsContext(c) //@rank(")", ctxBackground, ctxTODO)
}
func _() {
@@ -39,7 +39,7 @@
}
var circle deepCircle //@item(deepCircle, "circle", "deepCircle", "var")
circle.deepCircle //@item(deepCircleField, "circle.deepCircle", "*deepCircle", "field", "deepCircle is circular.")
- var _ deepCircle = circ //@complete(" //", deepCircle, deepCircleField)
+ var _ deepCircle = circ //@deep(" //", deepCircle, deepCircleField)
}
func _() {
@@ -57,7 +57,7 @@
var a deepEmbedA //@item(deepEmbedA, "a", "deepEmbedA", "var")
a.deepEmbedB //@item(deepEmbedB, "a.deepEmbedB", "deepEmbedB", "field")
a.deepEmbedC //@item(deepEmbedC, "a.deepEmbedC", "deepEmbedC", "field")
- wantsC(a) //@complete(")", deepEmbedC, deepEmbedA, deepEmbedB)
+ wantsC(a) //@deep(")", deepEmbedC, deepEmbedA, deepEmbedB)
}
func _() {
@@ -67,7 +67,7 @@
}
nested{
- a: 123, //@complete(" //", deepNestedField)
+ a: 123, //@deep(" //", deepNestedField)
}
}
@@ -86,5 +86,5 @@
// "a.d" should be ranked above the deeper "a.b.c"
var i int
- i = a //@complete(" //", deepAD, deepABC, deepA, deepAB)
+ i = a //@deep(" //", deepAD, deepABC, deepA, deepAB)
}
diff --git a/internal/lsp/testdata/deepcomplete/fuzzymatch/deep_fuzzy.go b/internal/lsp/testdata/fuzzy/fuzzy.go
similarity index 70%
rename from internal/lsp/testdata/deepcomplete/fuzzymatch/deep_fuzzy.go
rename to internal/lsp/testdata/fuzzy/fuzzy.go
index 747f966..73268f5 100644
--- a/internal/lsp/testdata/deepcomplete/fuzzymatch/deep_fuzzy.go
+++ b/internal/lsp/testdata/fuzzy/fuzzy.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package fuzzymatch
+package fuzzy
func _() {
var a struct {
@@ -13,13 +13,13 @@
a.fabar //@item(fuzzFabarField, "a.fabar", "int", "field")
a.fooBar //@item(fuzzFooBarField, "a.fooBar", "string", "field")
- afa //@complete(" //", fuzzFabarField, fuzzFooBarField)
- afb //@complete(" //", fuzzFooBarField, fuzzFabarField)
+ afa //@fuzzy(" //", fuzzFabarField, fuzzFooBarField)
+ afb //@fuzzy(" //", fuzzFooBarField, fuzzFabarField)
- fab //@complete(" //", fuzzFabarField)
+ fab //@fuzzy(" //", fuzzFabarField)
var myString string
- myString = af //@complete(" //", fuzzFooBarField, fuzzFabarField)
+ myString = af //@fuzzy(" //", fuzzFooBarField, fuzzFabarField)
var b struct {
c struct {
@@ -40,9 +40,9 @@
b.c.d.e.abc //@item(fuzzABCstring, "b.c.d.e.abc", "string", "field")
// in depth order by default
- abc //@complete(" //", fuzzABCInt, fuzzABCbool, fuzzABCfloat)
+ abc //@fuzzy(" //", fuzzABCInt, fuzzABCbool, fuzzABCfloat)
// deep candidate that matches expected type should still ranked first
var s string
- s = abc //@complete(" //", fuzzABCstring, fuzzABCInt, fuzzABCbool)
+ s = abc //@fuzzy(" //", fuzzABCstring, fuzzABCInt, fuzzABCbool)
}
diff --git a/internal/lsp/testdata/snippets/literal_snippets.go b/internal/lsp/testdata/snippets/literal_snippets.go
index f2c9472..92fe6a3 100644
--- a/internal/lsp/testdata/snippets/literal_snippets.go
+++ b/internal/lsp/testdata/snippets/literal_snippets.go
@@ -103,7 +103,7 @@
}
func _() {
- "func(...) {}" //@item(litFunc, "func(...) {}", "", "var")
+ _ = "func(...) {}" //@item(litFunc, "func(...) {}", "", "var")
sort.Slice(nil, f) //@snippet(")", litFunc, "func(i, j int) bool {$0\\}", "func(i, j int) bool {$0\\}")
diff --git a/internal/lsp/testdata/unimported/mkunimported.go b/internal/lsp/testdata/unimported/mkunimported.go
index d4210b0..fc1751c 100644
--- a/internal/lsp/testdata/unimported/mkunimported.go
+++ b/internal/lsp/testdata/unimported/mkunimported.go
@@ -91,7 +91,7 @@
marker := strings.ReplaceAll(path, "/", "slash")
markers = append(markers, marker)
}
- outf(" //@complete(\"\", %s)\n", strings.Join(markers, ", "))
+ outf(" //@unimported(\"\", %s)\n", strings.Join(markers, ", "))
outf("}\n")
outf("// Create markers for unimported std lib packages. Only for use by this test.\n")
diff --git a/internal/lsp/testdata/unimported/unimported.go b/internal/lsp/testdata/unimported/unimported.go
index 5166d85..6529943 100644
--- a/internal/lsp/testdata/unimported/unimported.go
+++ b/internal/lsp/testdata/unimported/unimported.go
@@ -3,7 +3,7 @@
package unimported
func _() {
- //@complete("", archiveslashtar, archiveslashzip, bufio, bytes, compressslashbzip2, compressslashflate, compressslashgzip, compressslashlzw, compressslashzlib, containerslashheap, containerslashlist, containerslashring, context, crypto, cryptoslashaes, cryptoslashcipher, cryptoslashdes, cryptoslashdsa, cryptoslashecdsa, cryptoslashed25519, cryptoslashelliptic, cryptoslashhmac, cryptoslashmd5, cryptoslashrand, cryptoslashrc4, cryptoslashrsa, cryptoslashsha1, cryptoslashsha256, cryptoslashsha512, cryptoslashsubtle, cryptoslashtls, cryptoslashx509, cryptoslashx509slashpkix, databaseslashsql, databaseslashsqlslashdriver, debugslashdwarf, debugslashelf, debugslashgosym, debugslashmacho, debugslashpe, debugslashplan9obj, encoding, encodingslashascii85, encodingslashasn1, encodingslashbase32, encodingslashbase64, encodingslashbinary, encodingslashcsv, encodingslashgob, encodingslashhex, encodingslashjson, encodingslashpem, encodingslashxml, errors, expvar, flag, fmt, goslashast, goslashbuild, goslashconstant, goslashdoc, goslashformat, goslashimporter, goslashparser, goslashprinter, goslashscanner, goslashtoken, goslashtypes, hash, hashslashadler32, hashslashcrc32, hashslashcrc64, hashslashfnv, html, htmlslashtemplate, image, imageslashcolor, imageslashcolorslashpalette, imageslashdraw, imageslashgif, imageslashjpeg, imageslashpng, indexslashsuffixarray, io, ioslashioutil, log, logslashsyslog, math, mathslashbig, mathslashbits, mathslashcmplx, mathslashrand, mime, mimeslashmultipart, mimeslashquotedprintable, net, netslashhttp, netslashhttpslashcgi, netslashhttpslashcookiejar, netslashhttpslashfcgi, netslashhttpslashhttptest, netslashhttpslashhttptrace, netslashhttpslashhttputil, netslashhttpslashpprof, netslashmail, netslashrpc, netslashrpcslashjsonrpc, netslashsmtp, netslashtextproto, netslashurl, os, osslashexec, osslashsignal, osslashuser, path, pathslashfilepath, plugin, reflect, regexp, regexpslashsyntax, runtime, runtimeslashdebug, runtimeslashpprof, runtimeslashtrace, sort, strconv, strings, sync, syncslashatomic, syscall, syscallslashjs, testing, testingslashiotest, testingslashquick, textslashscanner, textslashtabwriter, textslashtemplate, textslashtemplateslashparse, time, unicode, unicodeslashutf16, unicodeslashutf8, unsafe)
+ //@unimported("", archiveslashtar, archiveslashzip, bufio, bytes, compressslashbzip2, compressslashflate, compressslashgzip, compressslashlzw, compressslashzlib, containerslashheap, containerslashlist, containerslashring, context, crypto, cryptoslashaes, cryptoslashcipher, cryptoslashdes, cryptoslashdsa, cryptoslashecdsa, cryptoslashed25519, cryptoslashelliptic, cryptoslashhmac, cryptoslashmd5, cryptoslashrand, cryptoslashrc4, cryptoslashrsa, cryptoslashsha1, cryptoslashsha256, cryptoslashsha512, cryptoslashsubtle, cryptoslashtls, cryptoslashx509, cryptoslashx509slashpkix, databaseslashsql, databaseslashsqlslashdriver, debugslashdwarf, debugslashelf, debugslashgosym, debugslashmacho, debugslashpe, debugslashplan9obj, encoding, encodingslashascii85, encodingslashasn1, encodingslashbase32, encodingslashbase64, encodingslashbinary, encodingslashcsv, encodingslashgob, encodingslashhex, encodingslashjson, encodingslashpem, encodingslashxml, errors, expvar, flag, fmt, goslashast, goslashbuild, goslashconstant, goslashdoc, goslashformat, goslashimporter, goslashparser, goslashprinter, goslashscanner, goslashtoken, goslashtypes, hash, hashslashadler32, hashslashcrc32, hashslashcrc64, hashslashfnv, html, htmlslashtemplate, image, imageslashcolor, imageslashcolorslashpalette, imageslashdraw, imageslashgif, imageslashjpeg, imageslashpng, indexslashsuffixarray, io, ioslashioutil, log, logslashsyslog, math, mathslashbig, mathslashbits, mathslashcmplx, mathslashrand, mime, mimeslashmultipart, mimeslashquotedprintable, net, netslashhttp, netslashhttpslashcgi, netslashhttpslashcookiejar, netslashhttpslashfcgi, netslashhttpslashhttptest, netslashhttpslashhttptrace, netslashhttpslashhttputil, netslashhttpslashpprof, netslashmail, netslashrpc, netslashrpcslashjsonrpc, netslashsmtp, netslashtextproto, netslashurl, os, osslashexec, osslashsignal, osslashuser, path, pathslashfilepath, plugin, reflect, regexp, regexpslashsyntax, runtime, runtimeslashdebug, runtimeslashpprof, runtimeslashtrace, sort, strconv, strings, sync, syncslashatomic, syscall, syscallslashjs, testing, testingslashiotest, testingslashquick, textslashscanner, textslashtabwriter, textslashtemplate, textslashtemplateslashparse, time, unicode, unicodeslashutf16, unicodeslashutf8, unsafe)
}
// Create markers for unimported std lib packages. Only for use by this test.
diff --git a/internal/lsp/tests/completion.go b/internal/lsp/tests/completion.go
new file mode 100644
index 0000000..d96b44d
--- /dev/null
+++ b/internal/lsp/tests/completion.go
@@ -0,0 +1,193 @@
+package tests
+
+import (
+ "bytes"
+ "fmt"
+ "sort"
+ "strings"
+
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/lsp/source"
+)
+
+func ToProtocolCompletionItems(items []source.CompletionItem) []protocol.CompletionItem {
+ var result []protocol.CompletionItem
+ for _, item := range items {
+ result = append(result, ToProtocolCompletionItem(item))
+ }
+ return result
+}
+
+func ToProtocolCompletionItem(item source.CompletionItem) protocol.CompletionItem {
+ return protocol.CompletionItem{
+ Label: item.Label,
+ Kind: toProtocolCompletionItemKind(item.Kind),
+ Detail: item.Detail,
+ Documentation: item.Documentation,
+ InsertText: item.InsertText,
+ TextEdit: &protocol.TextEdit{
+ NewText: item.Snippet(),
+ },
+ }
+}
+
+func toProtocolCompletionItemKind(kind source.CompletionItemKind) protocol.CompletionItemKind {
+ switch kind {
+ case source.InterfaceCompletionItem:
+ return protocol.InterfaceCompletion
+ case source.StructCompletionItem:
+ return protocol.StructCompletion
+ case source.TypeCompletionItem:
+ return protocol.TypeParameterCompletion // ??
+ case source.ConstantCompletionItem:
+ return protocol.ConstantCompletion
+ case source.FieldCompletionItem:
+ return protocol.FieldCompletion
+ case source.ParameterCompletionItem, source.VariableCompletionItem:
+ return protocol.VariableCompletion
+ case source.FunctionCompletionItem:
+ return protocol.FunctionCompletion
+ case source.MethodCompletionItem:
+ return protocol.MethodCompletion
+ case source.PackageCompletionItem:
+ return protocol.ModuleCompletion // ??
+ default:
+ return protocol.TextCompletion
+ }
+}
+
+func FilterBuiltins(items []protocol.CompletionItem) []protocol.CompletionItem {
+ var got []protocol.CompletionItem
+ for _, item := range items {
+ if isBuiltin(item.Label, item.Detail, item.Kind) {
+ continue
+ }
+ got = append(got, item)
+ }
+ return got
+}
+
+func isBuiltin(label, detail string, kind protocol.CompletionItemKind) bool {
+ if detail == "" && kind == protocol.TypeParameterCompletion {
+ return true
+ }
+ // Remaining builtin constants, variables, interfaces, and functions.
+ trimmed := label
+ if i := strings.Index(trimmed, "("); i >= 0 {
+ trimmed = trimmed[:i]
+ }
+ switch trimmed {
+ case "append", "cap", "close", "complex", "copy", "delete",
+ "error", "false", "imag", "iota", "len", "make", "new",
+ "nil", "panic", "print", "println", "real", "recover", "true":
+ return true
+ }
+ return false
+}
+
+func CheckCompletionOrder(want, got []protocol.CompletionItem) string {
+ var (
+ matchedIdxs []int
+ lastGotIdx int
+ inOrder = true
+ )
+ for _, w := range want {
+ var found bool
+ for i, g := range got {
+ if w.Label == g.Label && w.Detail == g.Detail && w.Kind == g.Kind {
+ matchedIdxs = append(matchedIdxs, i)
+ found = true
+ if i < lastGotIdx {
+ inOrder = false
+ }
+ lastGotIdx = i
+ break
+ }
+ }
+ if !found {
+ return summarizeCompletionItems(-1, []protocol.CompletionItem{w}, got, "didn't find expected completion")
+ }
+ }
+
+ sort.Ints(matchedIdxs)
+ matched := make([]protocol.CompletionItem, 0, len(matchedIdxs))
+ for _, idx := range matchedIdxs {
+ matched = append(matched, got[idx])
+ }
+
+ if !inOrder {
+ return summarizeCompletionItems(-1, want, matched, "completions out of order")
+ }
+
+ return ""
+}
+
+func DiffSnippets(want string, got *protocol.CompletionItem) string {
+ if want == "" {
+ if got != nil {
+ return fmt.Sprintf("expected no snippet but got %s", got.TextEdit.NewText)
+ }
+ } else {
+ if got == nil {
+ return fmt.Sprintf("couldn't find completion matching %q", want)
+ }
+ if want != got.TextEdit.NewText {
+ return fmt.Sprintf("expected snippet %q, got %q", want, got.TextEdit.NewText)
+ }
+ }
+ return ""
+}
+
+func FindItem(list []protocol.CompletionItem, want source.CompletionItem) *protocol.CompletionItem {
+ for _, item := range list {
+ if item.Label == want.Label {
+ return &item
+ }
+ }
+ return nil
+}
+
+// DiffCompletionItems prints the diff between expected and actual completion
+// test results.
+func DiffCompletionItems(want, got []protocol.CompletionItem) string {
+ if len(got) != len(want) {
+ return summarizeCompletionItems(-1, want, got, "different lengths got %v want %v", len(got), len(want))
+ }
+ for i, w := range want {
+ g := got[i]
+ if w.Label != g.Label {
+ return summarizeCompletionItems(i, want, got, "incorrect Label got %v want %v", g.Label, w.Label)
+ }
+ if w.Detail != g.Detail {
+ return summarizeCompletionItems(i, want, got, "incorrect Detail got %v want %v", g.Detail, w.Detail)
+ }
+ if w.Documentation != "" && !strings.HasPrefix(w.Documentation, "@") {
+ if w.Documentation != g.Documentation {
+ return summarizeCompletionItems(i, want, got, "incorrect Documentation got %v want %v", g.Documentation, w.Documentation)
+ }
+ }
+ if w.Kind != g.Kind {
+ return summarizeCompletionItems(i, want, got, "incorrect Kind got %v want %v", g.Kind, w.Kind)
+ }
+ }
+ return ""
+}
+
+func summarizeCompletionItems(i int, want, got []protocol.CompletionItem, reason string, args ...interface{}) string {
+ msg := &bytes.Buffer{}
+ fmt.Fprint(msg, "completion failed")
+ if i >= 0 {
+ fmt.Fprintf(msg, " at %d", i)
+ }
+ fmt.Fprint(msg, " because of ")
+ fmt.Fprintf(msg, reason, args...)
+ fmt.Fprint(msg, ":\nexpected:\n")
+ for _, d := range want {
+ fmt.Fprintf(msg, " %v\n", d)
+ }
+ fmt.Fprintf(msg, "got:\n")
+ for _, d := range got {
+ fmt.Fprintf(msg, " %v\n", d)
+ }
+ return msg.String()
+}
diff --git a/internal/lsp/tests/diagnostics.go b/internal/lsp/tests/diagnostics.go
new file mode 100644
index 0000000..b4bef05
--- /dev/null
+++ b/internal/lsp/tests/diagnostics.go
@@ -0,0 +1,91 @@
+package tests
+
+import (
+ "bytes"
+ "fmt"
+ "sort"
+ "strings"
+
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/lsp/source"
+ "golang.org/x/tools/internal/span"
+)
+
+// DiffDiagnostics prints the diff between expected and actual diagnostics test
+// results.
+func DiffDiagnostics(uri span.URI, want, got []source.Diagnostic) string {
+ sortDiagnostics(want)
+ sortDiagnostics(got)
+
+ if len(got) != len(want) {
+ return summarizeDiagnostics(-1, want, got, "different lengths got %v want %v", len(got), len(want))
+ }
+ for i, w := range want {
+ g := got[i]
+ if w.Message != g.Message {
+ return summarizeDiagnostics(i, want, got, "incorrect Message got %v want %v", g.Message, w.Message)
+ }
+ if protocol.ComparePosition(w.Range.Start, g.Range.Start) != 0 {
+ return summarizeDiagnostics(i, want, got, "incorrect Start got %v want %v", g.Range.Start, w.Range.Start)
+ }
+ // Special case for diagnostics on parse errors.
+ if strings.Contains(string(uri), "noparse") {
+ if protocol.ComparePosition(g.Range.Start, g.Range.End) != 0 || protocol.ComparePosition(w.Range.Start, g.Range.End) != 0 {
+ return summarizeDiagnostics(i, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.Start)
+ }
+ } else if !protocol.IsPoint(g.Range) { // Accept any 'want' range if the diagnostic returns a zero-length range.
+ if protocol.ComparePosition(w.Range.End, g.Range.End) != 0 {
+ return summarizeDiagnostics(i, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.End)
+ }
+ }
+ if w.Severity != g.Severity {
+ return summarizeDiagnostics(i, want, got, "incorrect Severity got %v want %v", g.Severity, w.Severity)
+ }
+ if w.Source != g.Source {
+ return summarizeDiagnostics(i, want, got, "incorrect Source got %v want %v", g.Source, w.Source)
+ }
+ }
+ return ""
+}
+
+func sortDiagnostics(d []source.Diagnostic) {
+ sort.Slice(d, func(i int, j int) bool {
+ return compareDiagnostic(d[i], d[j]) < 0
+ })
+}
+
+func compareDiagnostic(a, b source.Diagnostic) int {
+ if r := span.CompareURI(a.URI, b.URI); r != 0 {
+ return r
+ }
+ if r := protocol.CompareRange(a.Range, b.Range); r != 0 {
+ return r
+ }
+ if a.Message < b.Message {
+ return -1
+ }
+ if a.Message == b.Message {
+ return 0
+ } else {
+ return 1
+ }
+}
+
+func summarizeDiagnostics(i int, want []source.Diagnostic, got []source.Diagnostic, reason string, args ...interface{}) string {
+ msg := &bytes.Buffer{}
+ fmt.Fprint(msg, "diagnostics failed")
+ if i >= 0 {
+ fmt.Fprintf(msg, " at %d", i)
+ }
+ fmt.Fprint(msg, " because of ")
+ fmt.Fprintf(msg, reason, args...)
+ fmt.Fprint(msg, ":\nexpected:\n")
+ for _, d := range want {
+ fmt.Fprintf(msg, " %s:%v: %s\n", d.URI, d.Range, d.Message)
+ }
+ fmt.Fprintf(msg, "got:\n")
+ for _, d := range got {
+ fmt.Fprintf(msg, " %s:%v: %s\n", d.URI, d.Range, d.Message)
+ }
+ return msg.String()
+}
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index 85e54fe..87cd807 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -2,13 +2,12 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+// Package tests exports functionality to be used across a variety of gopls tests.
package tests
import (
- "bytes"
"context"
"flag"
- "fmt"
"go/ast"
"go/token"
"io/ioutil"
@@ -30,22 +29,26 @@
// We hardcode the expected number of test cases to ensure that all tests
// are being executed. If a test is added, this number must be changed.
const (
- ExpectedCompletionsCount = 165
- ExpectedCompletionSnippetCount = 35
- ExpectedDiagnosticsCount = 21
- ExpectedFormatCount = 6
- ExpectedImportCount = 2
- ExpectedSuggestedFixCount = 1
- ExpectedDefinitionsCount = 39
- ExpectedTypeDefinitionsCount = 2
- ExpectedFoldingRangesCount = 2
- ExpectedHighlightsCount = 2
- ExpectedReferencesCount = 6
- ExpectedRenamesCount = 20
- ExpectedPrepareRenamesCount = 8
- ExpectedSymbolsCount = 1
- ExpectedSignaturesCount = 21
- ExpectedLinksCount = 4
+ ExpectedCompletionsCount = 152
+ ExpectedCompletionSnippetCount = 35
+ ExpectedUnimportedCompletionsCount = 1
+ ExpectedDeepCompletionsCount = 5
+ ExpectedFuzzyCompletionsCount = 6
+ ExpectedRankedCompletionsCount = 1
+ ExpectedDiagnosticsCount = 21
+ ExpectedFormatCount = 6
+ ExpectedImportCount = 2
+ ExpectedSuggestedFixCount = 1
+ ExpectedDefinitionsCount = 39
+ ExpectedTypeDefinitionsCount = 2
+ ExpectedFoldingRangesCount = 2
+ ExpectedHighlightsCount = 2
+ ExpectedReferencesCount = 6
+ ExpectedRenamesCount = 20
+ ExpectedPrepareRenamesCount = 8
+ ExpectedSymbolsCount = 1
+ ExpectedSignaturesCount = 21
+ ExpectedLinksCount = 4
)
const (
@@ -61,6 +64,10 @@
type CompletionItems map[token.Pos]*source.CompletionItem
type Completions map[span.Span]Completion
type CompletionSnippets map[span.Span]CompletionSnippet
+type UnimportedCompletions map[span.Span]Completion
+type DeepCompletions map[span.Span]Completion
+type FuzzyCompletions map[span.Span]Completion
+type RankCompletions map[span.Span]Completion
type FoldingRanges []span.Span
type Formats []span.Span
type Imports []span.Span
@@ -76,25 +83,29 @@
type Links map[span.URI][]Link
type Data struct {
- Config packages.Config
- Exported *packagestest.Exported
- Diagnostics Diagnostics
- CompletionItems CompletionItems
- Completions Completions
- CompletionSnippets CompletionSnippets
- FoldingRanges FoldingRanges
- Formats Formats
- Imports Imports
- SuggestedFixes SuggestedFixes
- Definitions Definitions
- Highlights Highlights
- References References
- Renames Renames
- PrepareRenames PrepareRenames
- Symbols Symbols
- symbolsChildren SymbolsChildren
- Signatures Signatures
- Links Links
+ Config packages.Config
+ Exported *packagestest.Exported
+ Diagnostics Diagnostics
+ CompletionItems CompletionItems
+ Completions Completions
+ CompletionSnippets CompletionSnippets
+ UnimportedCompletions UnimportedCompletions
+ DeepCompletions DeepCompletions
+ FuzzyCompletions FuzzyCompletions
+ RankCompletions RankCompletions
+ FoldingRanges FoldingRanges
+ Formats Formats
+ Imports Imports
+ SuggestedFixes SuggestedFixes
+ Definitions Definitions
+ Highlights Highlights
+ References References
+ Renames Renames
+ PrepareRenames PrepareRenames
+ Symbols Symbols
+ symbolsChildren SymbolsChildren
+ Signatures Signatures
+ Links Links
t testing.TB
fragments map[string]string
@@ -107,7 +118,12 @@
type Tests interface {
Diagnostics(*testing.T, Diagnostics)
- Completion(*testing.T, Completions, CompletionSnippets, CompletionItems)
+ Completion(*testing.T, Completions, CompletionItems)
+ CompletionSnippets(*testing.T, CompletionSnippets, CompletionItems)
+ UnimportedCompletions(*testing.T, UnimportedCompletions, CompletionItems)
+ DeepCompletions(*testing.T, DeepCompletions, CompletionItems)
+ FuzzyCompletions(*testing.T, FuzzyCompletions, CompletionItems)
+ RankCompletions(*testing.T, RankCompletions, CompletionItems)
FoldingRange(*testing.T, FoldingRanges)
Format(*testing.T, Formats)
Import(*testing.T, Imports)
@@ -132,16 +148,24 @@
type CompletionTestType int
const (
- // Full means candidates in test must match full list of candidates.
- CompletionFull CompletionTestType = iota
+ // Default runs the standard completion tests.
+ CompletionDefault = CompletionTestType(iota)
- // Partial means candidates in test must be valid and in the right relative order.
- CompletionPartial
+ // Unimported tests the autocompletion of unimported packages.
+ CompletionUnimported
+
+ // Deep tests deep completion.
+ CompletionDeep
+
+ // Fuzzy tests deep completion and fuzzy matching.
+ CompletionFuzzy
+
+ // CompletionRank candidates in test must be valid and in the right relative order.
+ CompletionRank
)
type Completion struct {
CompletionItems []token.Pos
- Type CompletionTestType
}
type CompletionSnippet struct {
@@ -166,23 +190,42 @@
return context.Background()
}
+func DefaultOptions() source.Options {
+ o := source.DefaultOptions
+ o.SupportedCodeActions = map[source.FileKind]map[protocol.CodeActionKind]bool{
+ source.Go: {
+ protocol.SourceOrganizeImports: true,
+ protocol.QuickFix: true,
+ },
+ source.Mod: {},
+ source.Sum: {},
+ }
+ o.HoverKind = source.SynopsisDocumentation
+ o.InsertTextFormat = protocol.SnippetTextFormat
+ return o
+}
+
func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
t.Helper()
data := &Data{
- Diagnostics: make(Diagnostics),
- CompletionItems: make(CompletionItems),
- Completions: make(Completions),
- CompletionSnippets: make(CompletionSnippets),
- Definitions: make(Definitions),
- Highlights: make(Highlights),
- References: make(References),
- Renames: make(Renames),
- PrepareRenames: make(PrepareRenames),
- Symbols: make(Symbols),
- symbolsChildren: make(SymbolsChildren),
- Signatures: make(Signatures),
- Links: make(Links),
+ Diagnostics: make(Diagnostics),
+ CompletionItems: make(CompletionItems),
+ Completions: make(Completions),
+ CompletionSnippets: make(CompletionSnippets),
+ UnimportedCompletions: make(UnimportedCompletions),
+ DeepCompletions: make(DeepCompletions),
+ FuzzyCompletions: make(FuzzyCompletions),
+ RankCompletions: make(RankCompletions),
+ Definitions: make(Definitions),
+ Highlights: make(Highlights),
+ References: make(References),
+ Renames: make(Renames),
+ PrepareRenames: make(PrepareRenames),
+ Symbols: make(Symbols),
+ symbolsChildren: make(SymbolsChildren),
+ Signatures: make(Signatures),
+ Links: make(Links),
t: t,
dir: dir,
@@ -226,7 +269,7 @@
},
}
data.Exported = packagestest.Export(t, exporter, modules)
- for fragment, _ := range files {
+ for fragment := range files {
filename := data.Exported.File(testModule, fragment)
data.fragments[filename] = fragment
}
@@ -252,25 +295,30 @@
// Collect any data that needs to be used by subsequent tests.
if err := data.Exported.Expect(map[string]interface{}{
- "diag": data.collectDiagnostics,
- "item": data.collectCompletionItems,
- "complete": data.collectCompletions(CompletionFull),
- "completePartial": data.collectCompletions(CompletionPartial),
- "fold": data.collectFoldingRanges,
- "format": data.collectFormats,
- "import": data.collectImports,
- "godef": data.collectDefinitions,
- "typdef": data.collectTypeDefinitions,
- "hover": data.collectHoverDefinitions,
- "highlight": data.collectHighlights,
- "refs": data.collectReferences,
- "rename": data.collectRenames,
- "prepare": data.collectPrepareRenames,
- "symbol": data.collectSymbols,
- "signature": data.collectSignatures,
- "snippet": data.collectCompletionSnippets,
- "link": data.collectLinks,
- "suggestedfix": data.collectSuggestedFixes,
+ "diag": data.collectDiagnostics,
+ "item": data.collectCompletionItems,
+ "complete": data.collectCompletions(CompletionDefault),
+ "unimported": data.collectCompletions(CompletionUnimported),
+ "deep": data.collectCompletions(CompletionDeep),
+ "fuzzy": data.collectCompletions(CompletionFuzzy),
+ "rank": data.collectCompletions(CompletionRank),
+ "snippet": data.collectCompletionSnippets,
+ "fold": data.collectFoldingRanges,
+ "format": data.collectFormats,
+ "import": data.collectImports,
+ "godef": data.collectDefinitions,
+ "typdef": data.collectTypeDefinitions,
+ "hover": data.collectHoverDefinitions,
+ "highlight": data.collectHighlights,
+ "refs": data.collectReferences,
+ "rename": data.collectRenames,
+ "prepare": data.collectPrepareRenames,
+ "symbol": data.collectSymbols,
+ "signature": data.collectSignatures,
+
+ // LSP-only features.
+ "link": data.collectLinks,
+ "suggestedfix": data.collectSuggestedFixes,
}); err != nil {
t.Fatal(err)
}
@@ -292,15 +340,56 @@
func Run(t *testing.T, tests Tests, data *Data) {
t.Helper()
+
t.Run("Completion", func(t *testing.T) {
t.Helper()
if len(data.Completions) != ExpectedCompletionsCount {
t.Errorf("got %v completions expected %v", len(data.Completions), ExpectedCompletionsCount)
}
+ tests.Completion(t, data.Completions, data.CompletionItems)
+ })
+
+ t.Run("CompletionSnippets", func(t *testing.T) {
+ t.Helper()
if len(data.CompletionSnippets) != ExpectedCompletionSnippetCount {
t.Errorf("got %v snippets expected %v", len(data.CompletionSnippets), ExpectedCompletionSnippetCount)
}
- tests.Completion(t, data.Completions, data.CompletionSnippets, data.CompletionItems)
+ if len(data.CompletionSnippets) != ExpectedCompletionSnippetCount {
+ t.Errorf("got %v snippets expected %v", len(data.CompletionSnippets), ExpectedCompletionSnippetCount)
+ }
+ tests.CompletionSnippets(t, data.CompletionSnippets, data.CompletionItems)
+ })
+
+ t.Run("UnimportedCompletion", func(t *testing.T) {
+ t.Helper()
+ if len(data.UnimportedCompletions) != ExpectedUnimportedCompletionsCount {
+ t.Errorf("got %v unimported completions expected %v", len(data.UnimportedCompletions), ExpectedUnimportedCompletionsCount)
+ }
+ tests.UnimportedCompletions(t, data.UnimportedCompletions, data.CompletionItems)
+ })
+
+ t.Run("DeepCompletion", func(t *testing.T) {
+ t.Helper()
+ if len(data.DeepCompletions) != ExpectedDeepCompletionsCount {
+ t.Errorf("got %v deep completions expected %v", len(data.DeepCompletions), ExpectedDeepCompletionsCount)
+ }
+ tests.DeepCompletions(t, data.DeepCompletions, data.CompletionItems)
+ })
+
+ t.Run("FuzzyCompletion", func(t *testing.T) {
+ t.Helper()
+ if len(data.FuzzyCompletions) != ExpectedFuzzyCompletionsCount {
+ t.Errorf("got %v fuzzy completions expected %v", len(data.FuzzyCompletions), ExpectedFuzzyCompletionsCount)
+ }
+ tests.FuzzyCompletions(t, data.FuzzyCompletions, data.CompletionItems)
+ })
+
+ t.Run("RankCompletions", func(t *testing.T) {
+ t.Helper()
+ if len(data.RankCompletions) != ExpectedRankedCompletionsCount {
+ t.Errorf("got %v fuzzy completions expected %v", len(data.RankCompletions), ExpectedRankedCompletionsCount)
+ }
+ tests.RankCompletions(t, data.RankCompletions, data.CompletionItems)
})
t.Run("Diagnostics", func(t *testing.T) {
@@ -528,90 +617,32 @@
data.Diagnostics[spn.URI()] = append(data.Diagnostics[spn.URI()], want)
}
-// diffDiagnostics prints the diff between expected and actual diagnostics test
-// results.
-func DiffDiagnostics(uri span.URI, want, got []source.Diagnostic) string {
- sortDiagnostics(want)
- sortDiagnostics(got)
-
- if len(got) != len(want) {
- return summarizeDiagnostics(-1, want, got, "different lengths got %v want %v", len(got), len(want))
- }
- for i, w := range want {
- g := got[i]
- if w.Message != g.Message {
- return summarizeDiagnostics(i, want, got, "incorrect Message got %v want %v", g.Message, w.Message)
- }
- if protocol.ComparePosition(w.Range.Start, g.Range.Start) != 0 {
- return summarizeDiagnostics(i, want, got, "incorrect Start got %v want %v", g.Range.Start, w.Range.Start)
- }
- // Special case for diagnostics on parse errors.
- if strings.Contains(string(uri), "noparse") {
- if protocol.ComparePosition(g.Range.Start, g.Range.End) != 0 || protocol.ComparePosition(w.Range.Start, g.Range.End) != 0 {
- return summarizeDiagnostics(i, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.Start)
- }
- } else if !protocol.IsPoint(g.Range) { // Accept any 'want' range if the diagnostic returns a zero-length range.
- if protocol.ComparePosition(w.Range.End, g.Range.End) != 0 {
- return summarizeDiagnostics(i, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.End)
- }
- }
- if w.Severity != g.Severity {
- return summarizeDiagnostics(i, want, got, "incorrect Severity got %v want %v", g.Severity, w.Severity)
- }
- if w.Source != g.Source {
- return summarizeDiagnostics(i, want, got, "incorrect Source got %v want %v", g.Source, w.Source)
- }
- }
- return ""
-}
-
-func sortDiagnostics(d []source.Diagnostic) {
- sort.Slice(d, func(i int, j int) bool {
- return compareDiagnostic(d[i], d[j]) < 0
- })
-}
-
-func compareDiagnostic(a, b source.Diagnostic) int {
- if r := span.CompareURI(a.URI, b.URI); r != 0 {
- return r
- }
- if r := protocol.CompareRange(a.Range, b.Range); r != 0 {
- return r
- }
- if a.Message < b.Message {
- return -1
- }
- if a.Message == b.Message {
- return 0
- } else {
- return 1
- }
-}
-
-func summarizeDiagnostics(i int, want []source.Diagnostic, got []source.Diagnostic, reason string, args ...interface{}) string {
- msg := &bytes.Buffer{}
- fmt.Fprint(msg, "diagnostics failed")
- if i >= 0 {
- fmt.Fprintf(msg, " at %d", i)
- }
- fmt.Fprint(msg, " because of ")
- fmt.Fprintf(msg, reason, args...)
- fmt.Fprint(msg, ":\nexpected:\n")
- for _, d := range want {
- fmt.Fprintf(msg, " %s:%v: %s\n", d.URI, d.Range, d.Message)
- }
- fmt.Fprintf(msg, "got:\n")
- for _, d := range got {
- fmt.Fprintf(msg, " %s:%v: %s\n", d.URI, d.Range, d.Message)
- }
- return msg.String()
-}
-
func (data *Data) collectCompletions(typ CompletionTestType) func(span.Span, []token.Pos) {
- return func(src span.Span, expected []token.Pos) {
- data.Completions[src] = Completion{
+ result := func(m map[span.Span]Completion, src span.Span, expected []token.Pos) {
+ m[src] = Completion{
CompletionItems: expected,
- Type: typ,
+ }
+ }
+ switch typ {
+ case CompletionDeep:
+ return func(src span.Span, expected []token.Pos) {
+ result(data.DeepCompletions, src, expected)
+ }
+ case CompletionUnimported:
+ return func(src span.Span, expected []token.Pos) {
+ result(data.UnimportedCompletions, src, expected)
+ }
+ case CompletionFuzzy:
+ return func(src span.Span, expected []token.Pos) {
+ result(data.FuzzyCompletions, src, expected)
+ }
+ case CompletionRank:
+ return func(src span.Span, expected []token.Pos) {
+ result(data.RankCompletions, src, expected)
+ }
+ default:
+ return func(src span.Span, expected []token.Pos) {
+ result(data.Completions, src, expected)
}
}
}