diff --git a/internal/lsp/cmd/test/cmdtest.go b/internal/lsp/cmd/test/cmdtest.go index 33523239a1..d31dfe4f02 100644 --- a/internal/lsp/cmd/test/cmdtest.go +++ b/internal/lsp/cmd/test/cmdtest.go @@ -33,7 +33,27 @@ func NewRunner(exporter packagestest.Exporter, data *tests.Data, ctx context.Con } } -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 d1a98f7988..c176be47ad 100644 --- a/internal/lsp/completion.go +++ b/internal/lsp/completion.go @@ -47,11 +47,11 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara // 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 0000000000..be42932a4f --- /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 ba56315fce..c133bacf5c 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -15,7 +15,6 @@ import ( "sort" "strings" "testing" - "time" "golang.org/x/tools/go/packages/packagestest" "golang.org/x/tools/internal/lsp/cache" @@ -51,18 +50,7 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) { 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) Diagnostics(t *testing.T, data tests.Diagnostics) { } } -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 @@ func (r *runner) diffSymbols(t *testing.T, uri span.URI, want []protocol.Documen 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 7e9af00daf..66ef61ef0a 100644 --- a/internal/lsp/source/source_test.go +++ b/internal/lsp/source/source_test.go @@ -49,7 +49,7 @@ func testSource(t *testing.T, exporter packagestest.Exporter) { 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) Diagnostics(t *testing.T, data tests.Diagnostics) { } } -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 83% rename from internal/lsp/testdata/deepcomplete/deep_complete.go rename to internal/lsp/testdata/deep/deep.go index 61b8cd10b3..f6bcbd7a7a 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 wantsDeepB(deepB) {} 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 @@ func _() { 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 @@ func _() { } 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 @@ func _() { 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 @@ func _() { } nested{ - a: 123, //@complete(" //", deepNestedField) + a: 123, //@deep(" //", deepNestedField) } } @@ -86,5 +86,5 @@ func _() { // "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 747f96614e..73268f553e 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 @@ func _() { 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 @@ func _() { 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 f2c947281b..92fe6a389d 100644 --- a/internal/lsp/testdata/snippets/literal_snippets.go +++ b/internal/lsp/testdata/snippets/literal_snippets.go @@ -103,7 +103,7 @@ func _() { } 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 d4210b0c38..fc1751ca0c 100644 --- a/internal/lsp/testdata/unimported/mkunimported.go +++ b/internal/lsp/testdata/unimported/mkunimported.go @@ -91,7 +91,7 @@ func main() { 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 5166d858bb..6529943508 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 0000000000..d96b44d031 --- /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 0000000000..b4bef05cac --- /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 85e54fe4ba..87cd8076f6 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 @@ import ( // 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 Diagnostics map[span.URI][]source.Diagnostic 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 Signatures map[span.Span]*source.SignatureInformation 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 Data struct { 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 Definition struct { 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 @@ func Context(t testing.TB) context.Context { 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 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data { }, } 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 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data { // 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 Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data { 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 @@ func (data *Data) collectDiagnostics(spn span.Span, msgSource, msg string) { 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) } } }