2019-05-08 14:04:29 -06:00
|
|
|
// Copyright 2018 The Go Authors. All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
package source_test
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"os/exec"
|
2019-07-08 19:53:01 -06:00
|
|
|
"path/filepath"
|
2019-05-08 14:04:29 -06:00
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
"golang.org/x/tools/go/packages/packagestest"
|
|
|
|
"golang.org/x/tools/internal/lsp/cache"
|
|
|
|
"golang.org/x/tools/internal/lsp/diff"
|
internal/lsp: add fuzzy completion matching
Make use of the existing fuzzy matcher to perform server side fuzzy
completion matching. Previously the server did exact prefix matching
for completion candidates and left fancy filtering to the
client. Having the server do fuzzy matching has two main benefits:
- Deep completions now update as you type. The completion candidates
returned to the client are marked "incomplete", causing the client
to refresh the candidates after every keystroke. This lets the
server pick the most relevant set of deep completion candidates.
- All editors get fuzzy matching for free. VSCode has fuzzy matching
out of the box, but some editors either don't provide it, or it can
be difficult to set up.
I modified the fuzzy matcher to allow matches where the input doesn't
match the final segment of the candidate. For example, previously "ab"
would not match "abc.def" because the "b" in "ab" did not match the
final segment "def". I can see how this is useful when the text
matching happens in a vacuum and candidate's final segment is the most
specific part. But, in our case, we have various other methods to
order candidates, so we don't want to exclude them just because the
final segment doesn't match. For example, if we know our candidate
needs to be type "context.Context" and "foo.ctx" is of the right type,
we want to suggest "foo.ctx" as soon as the user starts inputting
"foo", even though "foo" doesn't match "ctx" at all.
Note that fuzzy matching is behind the "useDeepCompletions" config
flag for the time being.
Change-Id: Ic7674f0cf885af770c30daef472f2e3c5ac4db78
Reviewed-on: https://go-review.googlesource.com/c/tools/+/190099
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2019-08-13 14:45:19 -06:00
|
|
|
"golang.org/x/tools/internal/lsp/fuzzy"
|
2019-05-08 14:04:29 -06:00
|
|
|
"golang.org/x/tools/internal/lsp/source"
|
|
|
|
"golang.org/x/tools/internal/lsp/tests"
|
|
|
|
"golang.org/x/tools/internal/span"
|
2019-08-13 13:07:39 -06:00
|
|
|
"golang.org/x/tools/internal/telemetry/log"
|
2019-05-08 14:04:29 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
func TestSource(t *testing.T) {
|
|
|
|
packagestest.TestAll(t, testSource)
|
|
|
|
}
|
|
|
|
|
|
|
|
type runner struct {
|
2019-05-14 21:04:23 -06:00
|
|
|
view source.View
|
2019-05-08 14:04:29 -06:00
|
|
|
data *tests.Data
|
2019-07-10 19:11:23 -06:00
|
|
|
ctx context.Context
|
2019-05-08 14:04:29 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
func testSource(t *testing.T, exporter packagestest.Exporter) {
|
2019-07-10 19:11:23 -06:00
|
|
|
ctx := tests.Context(t)
|
2019-05-08 14:04:29 -06:00
|
|
|
data := tests.Load(t, exporter, "../testdata")
|
|
|
|
defer data.Exported.Cleanup()
|
|
|
|
|
2019-08-13 11:35:13 -06:00
|
|
|
log.AddLogger(log.NullLogger)
|
|
|
|
|
2019-05-15 10:24:49 -06:00
|
|
|
cache := cache.New()
|
2019-07-10 19:01:12 -06:00
|
|
|
session := cache.NewSession(ctx)
|
2019-05-08 14:04:29 -06:00
|
|
|
r := &runner{
|
2019-07-10 19:11:23 -06:00
|
|
|
view: session.NewView(ctx, "source_test", span.FileURI(data.Config.Dir)),
|
2019-05-08 14:04:29 -06:00
|
|
|
data: data,
|
2019-07-10 19:11:23 -06:00
|
|
|
ctx: ctx,
|
2019-05-08 14:04:29 -06:00
|
|
|
}
|
2019-05-17 08:51:19 -06:00
|
|
|
r.view.SetEnv(data.Config.Env)
|
|
|
|
for filename, content := range data.Config.Overlay {
|
2019-05-23 11:51:56 -06:00
|
|
|
session.SetOverlay(span.FileURI(filename), content)
|
2019-05-17 08:51:19 -06:00
|
|
|
}
|
2019-05-08 14:04:29 -06:00
|
|
|
tests.Run(t, r, data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *runner) Diagnostics(t *testing.T, data tests.Diagnostics) {
|
|
|
|
for uri, want := range data {
|
2019-07-10 19:11:23 -06:00
|
|
|
f, err := r.view.GetFile(r.ctx, uri)
|
2019-05-23 13:03:11 -06:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2019-07-10 19:11:23 -06:00
|
|
|
results, err := source.Diagnostics(r.ctx, r.view, f.(source.GoFile), nil)
|
2019-05-08 14:04:29 -06:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
got := results[uri]
|
2019-08-02 17:45:56 -06:00
|
|
|
// A special case to test that there are no diagnostics for a file.
|
|
|
|
if len(want) == 1 && want[0].Source == "no_diagnostics" {
|
|
|
|
if len(got) != 0 {
|
|
|
|
t.Errorf("expected no diagnostics for %s, got %v", uri, got)
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
2019-05-08 14:04:29 -06:00
|
|
|
if diff := diffDiagnostics(uri, want, got); diff != "" {
|
|
|
|
t.Error(diff)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func sortDiagnostics(d []source.Diagnostic) {
|
|
|
|
sort.Slice(d, func(i int, j int) bool {
|
|
|
|
if r := span.Compare(d[i].Span, d[j].Span); r != 0 {
|
|
|
|
return r < 0
|
|
|
|
}
|
|
|
|
return d[i].Message < d[j].Message
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 span.ComparePoint(w.Start(), g.Start()) != 0 {
|
|
|
|
return summarizeDiagnostics(i, want, got, "incorrect Start got %v want %v", g.Start(), w.Start())
|
|
|
|
}
|
|
|
|
// Special case for diagnostics on parse errors.
|
|
|
|
if strings.Contains(string(uri), "noparse") {
|
|
|
|
if span.ComparePoint(g.Start(), g.End()) != 0 || span.ComparePoint(w.Start(), g.End()) != 0 {
|
|
|
|
return summarizeDiagnostics(i, want, got, "incorrect End got %v want %v", g.End(), w.Start())
|
|
|
|
}
|
|
|
|
} else if !g.IsPoint() { // Accept any 'want' range if the diagnostic returns a zero-length range.
|
|
|
|
if span.ComparePoint(w.End(), g.End()) != 0 {
|
|
|
|
return summarizeDiagnostics(i, want, got, "incorrect End got %v want %v", g.End(), w.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 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, " %v\n", d)
|
|
|
|
}
|
|
|
|
fmt.Fprintf(msg, "got:\n")
|
|
|
|
for _, d := range got {
|
|
|
|
fmt.Fprintf(msg, " %v\n", d)
|
|
|
|
}
|
|
|
|
return msg.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests.CompletionSnippets, items tests.CompletionItems) {
|
2019-07-10 19:11:23 -06:00
|
|
|
ctx := r.ctx
|
2019-05-08 14:04:29 -06:00
|
|
|
for src, itemList := range data {
|
|
|
|
var want []source.CompletionItem
|
|
|
|
for _, pos := range itemList {
|
|
|
|
want = append(want, *items[pos])
|
|
|
|
}
|
|
|
|
f, err := r.view.GetFile(ctx, src.URI())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", src, err)
|
|
|
|
}
|
2019-07-11 19:05:55 -06:00
|
|
|
tok, err := f.(source.GoFile).GetToken(ctx)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed to get token for %s: %v", src.URI(), err)
|
2019-05-17 08:51:19 -06:00
|
|
|
}
|
2019-05-08 14:04:29 -06:00
|
|
|
pos := tok.Pos(src.Start().Offset())
|
internal/lsp: add fuzzy completion matching
Make use of the existing fuzzy matcher to perform server side fuzzy
completion matching. Previously the server did exact prefix matching
for completion candidates and left fancy filtering to the
client. Having the server do fuzzy matching has two main benefits:
- Deep completions now update as you type. The completion candidates
returned to the client are marked "incomplete", causing the client
to refresh the candidates after every keystroke. This lets the
server pick the most relevant set of deep completion candidates.
- All editors get fuzzy matching for free. VSCode has fuzzy matching
out of the box, but some editors either don't provide it, or it can
be difficult to set up.
I modified the fuzzy matcher to allow matches where the input doesn't
match the final segment of the candidate. For example, previously "ab"
would not match "abc.def" because the "b" in "ab" did not match the
final segment "def". I can see how this is useful when the text
matching happens in a vacuum and candidate's final segment is the most
specific part. But, in our case, we have various other methods to
order candidates, so we don't want to exclude them just because the
final segment doesn't match. For example, if we know our candidate
needs to be type "context.Context" and "foo.ctx" is of the right type,
we want to suggest "foo.ctx" as soon as the user starts inputting
"foo", even though "foo" doesn't match "ctx" at all.
Note that fuzzy matching is behind the "useDeepCompletions" config
flag for the time being.
Change-Id: Ic7674f0cf885af770c30daef472f2e3c5ac4db78
Reviewed-on: https://go-review.googlesource.com/c/tools/+/190099
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2019-08-13 14:45:19 -06:00
|
|
|
deepComplete := strings.Contains(string(src.URI()), "deepcomplete")
|
2019-06-27 11:50:01 -06:00
|
|
|
list, surrounding, err := source.Completion(ctx, r.view, f.(source.GoFile), pos, source.CompletionOptions{
|
internal/lsp: add fuzzy completion matching
Make use of the existing fuzzy matcher to perform server side fuzzy
completion matching. Previously the server did exact prefix matching
for completion candidates and left fancy filtering to the
client. Having the server do fuzzy matching has two main benefits:
- Deep completions now update as you type. The completion candidates
returned to the client are marked "incomplete", causing the client
to refresh the candidates after every keystroke. This lets the
server pick the most relevant set of deep completion candidates.
- All editors get fuzzy matching for free. VSCode has fuzzy matching
out of the box, but some editors either don't provide it, or it can
be difficult to set up.
I modified the fuzzy matcher to allow matches where the input doesn't
match the final segment of the candidate. For example, previously "ab"
would not match "abc.def" because the "b" in "ab" did not match the
final segment "def". I can see how this is useful when the text
matching happens in a vacuum and candidate's final segment is the most
specific part. But, in our case, we have various other methods to
order candidates, so we don't want to exclude them just because the
final segment doesn't match. For example, if we know our candidate
needs to be type "context.Context" and "foo.ctx" is of the right type,
we want to suggest "foo.ctx" as soon as the user starts inputting
"foo", even though "foo" doesn't match "ctx" at all.
Note that fuzzy matching is behind the "useDeepCompletions" config
flag for the time being.
Change-Id: Ic7674f0cf885af770c30daef472f2e3c5ac4db78
Reviewed-on: https://go-review.googlesource.com/c/tools/+/190099
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2019-08-13 14:45:19 -06:00
|
|
|
DeepComplete: deepComplete,
|
2019-08-06 16:51:17 -06:00
|
|
|
WantDocumentaton: true,
|
2019-06-27 11:50:01 -06:00
|
|
|
})
|
2019-05-08 14:04:29 -06:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", src, err)
|
|
|
|
}
|
internal/lsp: add fuzzy completion matching
Make use of the existing fuzzy matcher to perform server side fuzzy
completion matching. Previously the server did exact prefix matching
for completion candidates and left fancy filtering to the
client. Having the server do fuzzy matching has two main benefits:
- Deep completions now update as you type. The completion candidates
returned to the client are marked "incomplete", causing the client
to refresh the candidates after every keystroke. This lets the
server pick the most relevant set of deep completion candidates.
- All editors get fuzzy matching for free. VSCode has fuzzy matching
out of the box, but some editors either don't provide it, or it can
be difficult to set up.
I modified the fuzzy matcher to allow matches where the input doesn't
match the final segment of the candidate. For example, previously "ab"
would not match "abc.def" because the "b" in "ab" did not match the
final segment "def". I can see how this is useful when the text
matching happens in a vacuum and candidate's final segment is the most
specific part. But, in our case, we have various other methods to
order candidates, so we don't want to exclude them just because the
final segment doesn't match. For example, if we know our candidate
needs to be type "context.Context" and "foo.ctx" is of the right type,
we want to suggest "foo.ctx" as soon as the user starts inputting
"foo", even though "foo" doesn't match "ctx" at all.
Note that fuzzy matching is behind the "useDeepCompletions" config
flag for the time being.
Change-Id: Ic7674f0cf885af770c30daef472f2e3c5ac4db78
Reviewed-on: https://go-review.googlesource.com/c/tools/+/190099
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2019-08-13 14:45:19 -06:00
|
|
|
var (
|
|
|
|
prefix string
|
|
|
|
fuzzyMatcher *fuzzy.Matcher
|
|
|
|
)
|
2019-06-27 14:28:22 -06:00
|
|
|
if surrounding != nil {
|
|
|
|
prefix = strings.ToLower(surrounding.Prefix())
|
internal/lsp: add fuzzy completion matching
Make use of the existing fuzzy matcher to perform server side fuzzy
completion matching. Previously the server did exact prefix matching
for completion candidates and left fancy filtering to the
client. Having the server do fuzzy matching has two main benefits:
- Deep completions now update as you type. The completion candidates
returned to the client are marked "incomplete", causing the client
to refresh the candidates after every keystroke. This lets the
server pick the most relevant set of deep completion candidates.
- All editors get fuzzy matching for free. VSCode has fuzzy matching
out of the box, but some editors either don't provide it, or it can
be difficult to set up.
I modified the fuzzy matcher to allow matches where the input doesn't
match the final segment of the candidate. For example, previously "ab"
would not match "abc.def" because the "b" in "ab" did not match the
final segment "def". I can see how this is useful when the text
matching happens in a vacuum and candidate's final segment is the most
specific part. But, in our case, we have various other methods to
order candidates, so we don't want to exclude them just because the
final segment doesn't match. For example, if we know our candidate
needs to be type "context.Context" and "foo.ctx" is of the right type,
we want to suggest "foo.ctx" as soon as the user starts inputting
"foo", even though "foo" doesn't match "ctx" at all.
Note that fuzzy matching is behind the "useDeepCompletions" config
flag for the time being.
Change-Id: Ic7674f0cf885af770c30daef472f2e3c5ac4db78
Reviewed-on: https://go-review.googlesource.com/c/tools/+/190099
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2019-08-13 14:45:19 -06:00
|
|
|
if deepComplete && prefix != "" {
|
|
|
|
fuzzyMatcher = fuzzy.NewMatcher(surrounding.Prefix(), fuzzy.Symbol)
|
|
|
|
}
|
2019-06-27 14:28:22 -06:00
|
|
|
}
|
2019-05-08 14:04:29 -06:00
|
|
|
wantBuiltins := strings.Contains(string(src.URI()), "builtins")
|
|
|
|
var got []source.CompletionItem
|
|
|
|
for _, item := range list {
|
|
|
|
if !wantBuiltins && isBuiltin(item) {
|
|
|
|
continue
|
|
|
|
}
|
internal/lsp: add fuzzy completion matching
Make use of the existing fuzzy matcher to perform server side fuzzy
completion matching. Previously the server did exact prefix matching
for completion candidates and left fancy filtering to the
client. Having the server do fuzzy matching has two main benefits:
- Deep completions now update as you type. The completion candidates
returned to the client are marked "incomplete", causing the client
to refresh the candidates after every keystroke. This lets the
server pick the most relevant set of deep completion candidates.
- All editors get fuzzy matching for free. VSCode has fuzzy matching
out of the box, but some editors either don't provide it, or it can
be difficult to set up.
I modified the fuzzy matcher to allow matches where the input doesn't
match the final segment of the candidate. For example, previously "ab"
would not match "abc.def" because the "b" in "ab" did not match the
final segment "def". I can see how this is useful when the text
matching happens in a vacuum and candidate's final segment is the most
specific part. But, in our case, we have various other methods to
order candidates, so we don't want to exclude them just because the
final segment doesn't match. For example, if we know our candidate
needs to be type "context.Context" and "foo.ctx" is of the right type,
we want to suggest "foo.ctx" as soon as the user starts inputting
"foo", even though "foo" doesn't match "ctx" at all.
Note that fuzzy matching is behind the "useDeepCompletions" config
flag for the time being.
Change-Id: Ic7674f0cf885af770c30daef472f2e3c5ac4db78
Reviewed-on: https://go-review.googlesource.com/c/tools/+/190099
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2019-08-13 14:45:19 -06:00
|
|
|
|
|
|
|
// If deep completion is enabled, we need to use the fuzzy matcher to match
|
|
|
|
// the code's behvaior.
|
|
|
|
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
|
|
|
|
}
|
2019-05-08 14:04:29 -06:00
|
|
|
}
|
|
|
|
got = append(got, item)
|
|
|
|
}
|
|
|
|
if diff := diffCompletionItems(t, src, want, got); diff != "" {
|
|
|
|
t.Errorf("%s: %s", src, diff)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, usePlaceholders := range []bool{true, false} {
|
2019-05-14 20:02:51 -06:00
|
|
|
for src, want := range snippets {
|
2019-05-08 14:04:29 -06:00
|
|
|
f, err := r.view.GetFile(ctx, src.URI())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", src, err)
|
|
|
|
}
|
2019-07-11 19:05:55 -06:00
|
|
|
tok, err := f.(source.GoFile).GetToken(ctx)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed to get token for %s: %v", src.URI(), err)
|
|
|
|
}
|
2019-05-08 14:04:29 -06:00
|
|
|
pos := tok.Pos(src.Start().Offset())
|
2019-06-27 11:50:01 -06:00
|
|
|
list, _, err := source.Completion(ctx, r.view, f.(source.GoFile), pos, source.CompletionOptions{
|
|
|
|
DeepComplete: strings.Contains(string(src.URI()), "deepcomplete"),
|
|
|
|
})
|
2019-05-08 14:04:29 -06:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", src, err)
|
|
|
|
}
|
2019-05-14 20:02:51 -06:00
|
|
|
wantItem := items[want.CompletionItem]
|
2019-05-14 12:15:18 -06:00
|
|
|
var got *source.CompletionItem
|
2019-05-08 14:04:29 -06:00
|
|
|
for _, item := range list {
|
2019-05-14 20:02:51 -06:00
|
|
|
if item.Label == wantItem.Label {
|
2019-05-14 12:15:18 -06:00
|
|
|
got = &item
|
2019-05-08 14:04:29 -06:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2019-05-14 12:15:18 -06:00
|
|
|
if got == nil {
|
2019-05-14 20:02:51 -06:00
|
|
|
t.Fatalf("%s: couldn't find completion matching %q", src.URI(), wantItem.Label)
|
2019-05-08 14:04:29 -06:00
|
|
|
}
|
2019-05-14 12:15:18 -06:00
|
|
|
expected := want.PlainSnippet
|
2019-05-08 14:04:29 -06:00
|
|
|
if usePlaceholders {
|
2019-05-13 14:49:29 -06:00
|
|
|
expected = want.PlaceholderSnippet
|
2019-05-08 14:04:29 -06:00
|
|
|
}
|
2019-05-14 20:02:51 -06:00
|
|
|
if actual := got.Snippet(usePlaceholders); expected != actual {
|
|
|
|
t.Errorf("%s: expected placeholder snippet %q, got %q", src, expected, actual)
|
2019-05-08 14:04:29 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
// 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(t *testing.T, spn span.Span, want []source.CompletionItem, got []source.CompletionItem) string {
|
|
|
|
sort.SliceStable(got, func(i, j int) bool {
|
|
|
|
return got[i].Score > got[j].Score
|
|
|
|
})
|
2019-06-27 11:50:01 -06:00
|
|
|
|
|
|
|
// 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 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
seenDeepCompletions++
|
|
|
|
}
|
|
|
|
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))
|
|
|
|
}
|
2019-05-08 14:04:29 -06:00
|
|
|
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)
|
|
|
|
}
|
2019-08-06 16:51:17 -06:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2019-05-08 14:04:29 -06:00
|
|
|
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 []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)
|
|
|
|
}
|
|
|
|
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) Format(t *testing.T, data tests.Formats) {
|
2019-07-10 19:11:23 -06:00
|
|
|
ctx := r.ctx
|
2019-05-08 14:04:29 -06:00
|
|
|
for _, spn := range data {
|
|
|
|
uri := spn.URI()
|
2019-06-06 11:51:00 -06:00
|
|
|
filename := uri.Filename()
|
2019-05-08 14:04:29 -06:00
|
|
|
gofmted := string(r.data.Golden("gofmt", filename, func() ([]byte, error) {
|
|
|
|
cmd := exec.Command("gofmt", filename)
|
|
|
|
out, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files
|
|
|
|
return out, nil
|
|
|
|
}))
|
|
|
|
f, err := r.view.GetFile(ctx, uri)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", spn, err)
|
|
|
|
}
|
2019-07-11 19:05:55 -06:00
|
|
|
tok, err := f.(source.GoFile).GetToken(ctx)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed to get token for %s: %v", spn.URI(), err)
|
|
|
|
}
|
|
|
|
rng, err := spn.Range(span.NewTokenConverter(f.FileSet(), tok))
|
2019-05-08 14:04:29 -06:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", spn, err)
|
|
|
|
}
|
2019-05-03 22:04:18 -06:00
|
|
|
edits, err := source.Format(ctx, f.(source.GoFile), rng)
|
2019-05-08 14:04:29 -06:00
|
|
|
if err != nil {
|
|
|
|
if gofmted != "" {
|
|
|
|
t.Error(err)
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
ops := source.EditsToDiff(edits)
|
2019-06-03 23:04:18 -06:00
|
|
|
data, _, err := f.Handle(ctx).Read(ctx)
|
|
|
|
if err != nil {
|
2019-05-17 10:15:22 -06:00
|
|
|
t.Error(err)
|
|
|
|
continue
|
|
|
|
}
|
2019-06-03 23:04:18 -06:00
|
|
|
got := strings.Join(diff.ApplyEdits(diff.SplitLines(string(data)), ops), "")
|
2019-05-08 14:04:29 -06:00
|
|
|
if gofmted != got {
|
|
|
|
t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", filename, gofmted, got)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-31 20:42:59 -06:00
|
|
|
func (r *runner) Import(t *testing.T, data tests.Imports) {
|
2019-07-10 19:11:23 -06:00
|
|
|
ctx := r.ctx
|
2019-05-31 20:42:59 -06:00
|
|
|
for _, spn := range data {
|
|
|
|
uri := spn.URI()
|
2019-06-06 11:51:00 -06:00
|
|
|
filename := uri.Filename()
|
2019-05-31 20:42:59 -06:00
|
|
|
goimported := string(r.data.Golden("goimports", filename, func() ([]byte, error) {
|
|
|
|
cmd := exec.Command("goimports", filename)
|
|
|
|
out, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files
|
|
|
|
return out, nil
|
|
|
|
}))
|
|
|
|
f, err := r.view.GetFile(ctx, uri)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", spn, err)
|
|
|
|
}
|
2019-07-11 19:05:55 -06:00
|
|
|
tok, err := f.(source.GoFile).GetToken(ctx)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed to get token for %s: %v", spn.URI(), err)
|
|
|
|
}
|
|
|
|
rng, err := spn.Range(span.NewTokenConverter(f.FileSet(), tok))
|
2019-05-31 20:42:59 -06:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", spn, err)
|
|
|
|
}
|
2019-06-28 14:21:07 -06:00
|
|
|
edits, err := source.Imports(ctx, r.view, f.(source.GoFile), rng)
|
2019-05-31 20:42:59 -06:00
|
|
|
if err != nil {
|
|
|
|
if goimported != "" {
|
|
|
|
t.Error(err)
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
ops := source.EditsToDiff(edits)
|
2019-06-03 23:04:18 -06:00
|
|
|
data, _, err := f.Handle(ctx).Read(ctx)
|
|
|
|
if err != nil {
|
2019-05-31 20:42:59 -06:00
|
|
|
t.Error(err)
|
|
|
|
continue
|
|
|
|
}
|
2019-06-03 23:04:18 -06:00
|
|
|
got := strings.Join(diff.ApplyEdits(diff.SplitLines(string(data)), ops), "")
|
2019-05-31 20:42:59 -06:00
|
|
|
if goimported != got {
|
|
|
|
t.Errorf("import failed for %s, expected:\n%v\ngot:\n%v", filename, goimported, got)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-08 14:04:29 -06:00
|
|
|
func (r *runner) Definition(t *testing.T, data tests.Definitions) {
|
2019-07-10 19:11:23 -06:00
|
|
|
ctx := r.ctx
|
2019-05-08 14:04:29 -06:00
|
|
|
for _, d := range data {
|
|
|
|
f, err := r.view.GetFile(ctx, d.Src.URI())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", d.Src, err)
|
|
|
|
}
|
2019-07-11 19:05:55 -06:00
|
|
|
tok, err := f.(source.GoFile).GetToken(ctx)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed to get token for %s: %v", d.Src.URI(), err)
|
|
|
|
}
|
2019-05-08 14:04:29 -06:00
|
|
|
pos := tok.Pos(d.Src.Start().Offset())
|
2019-08-07 14:03:45 -06:00
|
|
|
ident, err := source.Identifier(ctx, f.(source.GoFile), pos)
|
2019-05-08 14:04:29 -06:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", d.Src, err)
|
|
|
|
}
|
2019-08-12 14:59:23 -06:00
|
|
|
h, err := ident.Hover(ctx)
|
2019-05-08 14:04:29 -06:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", d.Src, err)
|
|
|
|
}
|
2019-08-12 14:59:23 -06:00
|
|
|
var hover string
|
|
|
|
if h.Synopsis != "" {
|
|
|
|
hover += h.Synopsis + "\n"
|
|
|
|
}
|
|
|
|
hover += h.Signature
|
2019-06-04 23:16:23 -06:00
|
|
|
rng := ident.DeclarationRange()
|
2019-05-08 14:04:29 -06:00
|
|
|
if d.IsType {
|
|
|
|
rng = ident.Type.Range
|
|
|
|
hover = ""
|
|
|
|
}
|
|
|
|
if hover != "" {
|
|
|
|
tag := fmt.Sprintf("%s-hover", d.Name)
|
2019-06-06 11:51:00 -06:00
|
|
|
expectHover := string(r.data.Golden(tag, d.Src.URI().Filename(), func() ([]byte, error) {
|
2019-05-08 14:04:29 -06:00
|
|
|
return []byte(hover), nil
|
|
|
|
}))
|
|
|
|
if hover != expectHover {
|
|
|
|
t.Errorf("for %v got %q want %q", d.Src, hover, expectHover)
|
|
|
|
}
|
2019-05-15 15:58:16 -06:00
|
|
|
} else if !d.OnlyHover {
|
|
|
|
if def, err := rng.Span(); err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", rng, err)
|
|
|
|
} else if def != d.Def {
|
|
|
|
t.Errorf("for %v got %v want %v", d.Src, def, d.Def)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
t.Errorf("no tests ran for %s", d.Src.URI())
|
2019-05-08 14:04:29 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *runner) Highlight(t *testing.T, data tests.Highlights) {
|
2019-07-10 19:11:23 -06:00
|
|
|
ctx := r.ctx
|
2019-05-08 14:04:29 -06:00
|
|
|
for name, locations := range data {
|
|
|
|
src := locations[0]
|
|
|
|
f, err := r.view.GetFile(ctx, src.URI())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", src, err)
|
|
|
|
}
|
2019-07-11 19:05:55 -06:00
|
|
|
tok, err := f.(source.GoFile).GetToken(ctx)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed to get token for %s: %v", src.URI(), err)
|
|
|
|
}
|
2019-05-08 14:04:29 -06:00
|
|
|
pos := tok.Pos(src.Start().Offset())
|
2019-06-21 15:00:02 -06:00
|
|
|
highlights, err := source.Highlight(ctx, f.(source.GoFile), pos)
|
|
|
|
if err != nil {
|
|
|
|
t.Errorf("highlight failed for %s: %v", src.URI(), err)
|
|
|
|
}
|
2019-05-08 14:04:29 -06:00
|
|
|
if len(highlights) != len(locations) {
|
2019-06-21 15:00:02 -06:00
|
|
|
t.Errorf("got %d highlights for %s, expected %d", len(highlights), name, len(locations))
|
2019-05-08 14:04:29 -06:00
|
|
|
}
|
|
|
|
for i, h := range highlights {
|
|
|
|
if h != locations[i] {
|
|
|
|
t.Errorf("want %v, got %v\n", locations[i], h)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-07 08:04:22 -06:00
|
|
|
func (r *runner) Reference(t *testing.T, data tests.References) {
|
2019-07-10 19:11:23 -06:00
|
|
|
ctx := r.ctx
|
2019-06-07 08:04:22 -06:00
|
|
|
for src, itemList := range data {
|
|
|
|
f, err := r.view.GetFile(ctx, src.URI())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", src, err)
|
|
|
|
}
|
2019-07-11 19:05:55 -06:00
|
|
|
tok, err := f.(source.GoFile).GetToken(ctx)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed to get token for %s: %v", src.URI(), err)
|
|
|
|
}
|
2019-06-07 08:04:22 -06:00
|
|
|
pos := tok.Pos(src.Start().Offset())
|
2019-08-07 14:03:45 -06:00
|
|
|
ident, err := source.Identifier(ctx, f.(source.GoFile), pos)
|
2019-06-07 08:04:22 -06:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", src, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
want := make(map[span.Span]bool)
|
|
|
|
for _, pos := range itemList {
|
|
|
|
want[pos] = true
|
|
|
|
}
|
|
|
|
|
2019-06-26 16:05:29 -06:00
|
|
|
refs, err := ident.References(ctx)
|
2019-06-07 08:04:22 -06:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", src, err)
|
|
|
|
}
|
|
|
|
|
2019-06-26 16:05:29 -06:00
|
|
|
got := make(map[span.Span]bool)
|
|
|
|
for _, refInfo := range refs {
|
2019-06-07 08:04:22 -06:00
|
|
|
refSpan, err := refInfo.Range.Span()
|
|
|
|
if err != nil {
|
|
|
|
t.Errorf("failed for %v item %v: %v", src, refInfo.Name, err)
|
|
|
|
}
|
2019-06-26 16:05:29 -06:00
|
|
|
got[refSpan] = true
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(got) != len(want) {
|
|
|
|
t.Errorf("references failed: different lengths got %v want %v", len(got), len(want))
|
|
|
|
}
|
|
|
|
|
|
|
|
for spn, _ := range got {
|
|
|
|
if !want[spn] {
|
2019-06-07 08:04:22 -06:00
|
|
|
t.Errorf("references failed: incorrect references got %v want locations %v", got, want)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-18 08:23:37 -06:00
|
|
|
func (r *runner) Rename(t *testing.T, data tests.Renames) {
|
2019-07-10 19:11:23 -06:00
|
|
|
ctx := r.ctx
|
2019-06-18 08:23:37 -06:00
|
|
|
for spn, newText := range data {
|
2019-07-02 16:35:35 -06:00
|
|
|
tag := fmt.Sprintf("%s-rename", newText)
|
|
|
|
|
2019-06-18 08:23:37 -06:00
|
|
|
f, err := r.view.GetFile(ctx, spn.URI())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", spn, err)
|
|
|
|
}
|
2019-07-11 19:05:55 -06:00
|
|
|
tok, err := f.(source.GoFile).GetToken(ctx)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed to get token for %s: %v", spn.URI(), err)
|
|
|
|
}
|
2019-06-18 08:23:37 -06:00
|
|
|
pos := tok.Pos(spn.Start().Offset())
|
2019-08-07 14:03:45 -06:00
|
|
|
ident, err := source.Identifier(r.ctx, f.(source.GoFile), pos)
|
2019-06-21 15:00:02 -06:00
|
|
|
if err != nil {
|
|
|
|
t.Error(err)
|
2019-07-15 23:27:56 -06:00
|
|
|
continue
|
2019-06-21 15:00:02 -06:00
|
|
|
}
|
2019-07-10 19:11:23 -06:00
|
|
|
changes, err := ident.Rename(r.ctx, newText)
|
2019-06-18 08:23:37 -06:00
|
|
|
if err != nil {
|
2019-07-02 16:35:35 -06:00
|
|
|
renamed := string(r.data.Golden(tag, spn.URI().Filename(), func() ([]byte, error) {
|
|
|
|
return []byte(err.Error()), nil
|
|
|
|
}))
|
|
|
|
if err.Error() != renamed {
|
|
|
|
t.Errorf("rename failed for %s, expected:\n%v\ngot:\n%v\n", newText, renamed, err)
|
|
|
|
}
|
2019-06-18 08:23:37 -06:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2019-07-08 19:53:01 -06:00
|
|
|
var res []string
|
|
|
|
for editSpn, edits := range changes {
|
|
|
|
f, err := r.view.GetFile(ctx, editSpn)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", spn, err)
|
|
|
|
}
|
2019-06-18 08:23:37 -06:00
|
|
|
|
2019-07-08 19:53:01 -06:00
|
|
|
data, _, err := f.Handle(ctx).Read(ctx)
|
|
|
|
if err != nil {
|
|
|
|
t.Error(err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
filename := filepath.Base(editSpn.Filename())
|
|
|
|
contents := applyEdits(string(data), edits)
|
|
|
|
res = append(res, fmt.Sprintf("%s:\n%s", filename, contents))
|
2019-06-18 08:23:37 -06:00
|
|
|
}
|
2019-07-08 19:53:01 -06:00
|
|
|
|
|
|
|
// Sort on filename
|
|
|
|
sort.Strings(res)
|
|
|
|
|
|
|
|
var got string
|
|
|
|
for i, val := range res {
|
|
|
|
if i != 0 {
|
|
|
|
got += "\n"
|
|
|
|
}
|
|
|
|
got += val
|
2019-06-18 08:23:37 -06:00
|
|
|
}
|
|
|
|
|
2019-07-08 19:53:01 -06:00
|
|
|
renamed := string(r.data.Golden(tag, spn.URI().Filename(), func() ([]byte, error) {
|
2019-06-18 08:23:37 -06:00
|
|
|
return []byte(got), nil
|
|
|
|
}))
|
|
|
|
|
2019-07-08 19:53:01 -06:00
|
|
|
if renamed != got {
|
|
|
|
t.Errorf("rename failed for %s, expected:\n%v\ngot:\n%v", newText, renamed, got)
|
2019-06-18 08:23:37 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func applyEdits(contents string, edits []source.TextEdit) string {
|
|
|
|
res := contents
|
|
|
|
|
|
|
|
// Apply the edits from the end of the file forward
|
|
|
|
// to preserve the offsets
|
|
|
|
for i := len(edits) - 1; i >= 0; i-- {
|
|
|
|
edit := edits[i]
|
|
|
|
start := edit.Span.Start().Offset()
|
|
|
|
end := edit.Span.End().Offset()
|
|
|
|
tmp := res[0:start] + edit.NewText
|
|
|
|
res = tmp + res[end:]
|
|
|
|
}
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
2019-05-08 14:04:29 -06:00
|
|
|
func (r *runner) Symbol(t *testing.T, data tests.Symbols) {
|
2019-07-10 19:11:23 -06:00
|
|
|
ctx := r.ctx
|
2019-05-08 14:04:29 -06:00
|
|
|
for uri, expectedSymbols := range data {
|
|
|
|
f, err := r.view.GetFile(ctx, uri)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", uri, err)
|
|
|
|
}
|
2019-06-21 15:00:02 -06:00
|
|
|
symbols, err := source.DocumentSymbols(ctx, f.(source.GoFile))
|
|
|
|
if err != nil {
|
|
|
|
t.Errorf("symbols failed for %s: %v", uri, err)
|
|
|
|
}
|
2019-05-08 14:04:29 -06:00
|
|
|
if len(symbols) != len(expectedSymbols) {
|
|
|
|
t.Errorf("want %d top-level symbols in %v, got %d", len(expectedSymbols), uri, len(symbols))
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if diff := r.diffSymbols(uri, expectedSymbols, symbols); diff != "" {
|
|
|
|
t.Error(diff)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *runner) diffSymbols(uri span.URI, want []source.Symbol, got []source.Symbol) string {
|
|
|
|
sort.Slice(want, func(i, j int) bool { return want[i].Name < want[j].Name })
|
|
|
|
sort.Slice(got, func(i, j int) bool { return got[i].Name < got[j].Name })
|
|
|
|
if len(got) != len(want) {
|
|
|
|
return summarizeSymbols(-1, want, got, "different lengths got %v want %v", len(got), len(want))
|
|
|
|
}
|
|
|
|
for i, w := range want {
|
|
|
|
g := got[i]
|
|
|
|
if w.Name != g.Name {
|
|
|
|
return summarizeSymbols(i, want, got, "incorrect name got %v want %v", g.Name, w.Name)
|
|
|
|
}
|
|
|
|
if w.Kind != g.Kind {
|
|
|
|
return summarizeSymbols(i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind)
|
|
|
|
}
|
|
|
|
if w.SelectionSpan != g.SelectionSpan {
|
|
|
|
return summarizeSymbols(i, want, got, "incorrect span got %v want %v", g.SelectionSpan, w.SelectionSpan)
|
|
|
|
}
|
|
|
|
if msg := r.diffSymbols(uri, w.Children, g.Children); msg != "" {
|
|
|
|
return fmt.Sprintf("children of %s: %s", w.Name, msg)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func summarizeSymbols(i int, want []source.Symbol, got []source.Symbol, reason string, args ...interface{}) string {
|
|
|
|
msg := &bytes.Buffer{}
|
|
|
|
fmt.Fprint(msg, "document symbols 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 _, s := range want {
|
|
|
|
fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionSpan)
|
|
|
|
}
|
|
|
|
fmt.Fprintf(msg, "got:\n")
|
|
|
|
for _, s := range got {
|
|
|
|
fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionSpan)
|
|
|
|
}
|
|
|
|
return msg.String()
|
|
|
|
}
|
|
|
|
|
2019-05-14 19:20:41 -06:00
|
|
|
func (r *runner) SignatureHelp(t *testing.T, data tests.Signatures) {
|
2019-07-10 19:11:23 -06:00
|
|
|
ctx := r.ctx
|
2019-06-28 19:27:41 -06:00
|
|
|
for spn, expectedSignature := range data {
|
2019-05-08 14:04:29 -06:00
|
|
|
f, err := r.view.GetFile(ctx, spn.URI())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", spn, err)
|
|
|
|
}
|
2019-07-11 19:05:55 -06:00
|
|
|
tok, err := f.(source.GoFile).GetToken(ctx)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed to get token for %s: %v", spn.URI(), err)
|
|
|
|
}
|
2019-05-08 14:04:29 -06:00
|
|
|
pos := tok.Pos(spn.Start().Offset())
|
2019-05-03 22:04:18 -06:00
|
|
|
gotSignature, err := source.SignatureHelp(ctx, f.(source.GoFile), pos)
|
2019-05-08 14:04:29 -06:00
|
|
|
if err != nil {
|
2019-06-28 19:27:41 -06:00
|
|
|
// Only fail if we got an error we did not expect.
|
|
|
|
if expectedSignature != nil {
|
|
|
|
t.Fatalf("failed for %v: %v", spn, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if expectedSignature == nil {
|
|
|
|
if gotSignature != nil {
|
|
|
|
t.Errorf("expected no signature, got %v", gotSignature)
|
|
|
|
}
|
|
|
|
continue
|
2019-05-08 14:04:29 -06:00
|
|
|
}
|
2019-06-28 19:27:41 -06:00
|
|
|
if diff := diffSignatures(spn, expectedSignature, gotSignature); diff != "" {
|
2019-05-08 14:04:29 -06:00
|
|
|
t.Error(diff)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-28 19:27:41 -06:00
|
|
|
func diffSignatures(spn span.Span, want *source.SignatureInformation, got *source.SignatureInformation) string {
|
2019-05-08 14:04:29 -06:00
|
|
|
decorate := func(f string, args ...interface{}) string {
|
|
|
|
return fmt.Sprintf("Invalid signature at %s: %s", spn, fmt.Sprintf(f, args...))
|
|
|
|
}
|
|
|
|
if want.ActiveParameter != got.ActiveParameter {
|
|
|
|
return decorate("wanted active parameter of %d, got %f", want.ActiveParameter, got.ActiveParameter)
|
|
|
|
}
|
|
|
|
if want.Label != got.Label {
|
|
|
|
return decorate("wanted label %q, got %q", want.Label, got.Label)
|
|
|
|
}
|
|
|
|
var paramParts []string
|
|
|
|
for _, p := range got.Parameters {
|
|
|
|
paramParts = append(paramParts, p.Label)
|
|
|
|
}
|
|
|
|
paramsStr := strings.Join(paramParts, ", ")
|
|
|
|
if !strings.Contains(got.Label, paramsStr) {
|
|
|
|
return decorate("expected signature %q to contain params %q", got.Label, paramsStr)
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *runner) Link(t *testing.T, data tests.Links) {
|
2019-06-28 19:27:41 -06:00
|
|
|
// This is a pure LSP feature, no source level functionality to be tested.
|
2019-05-08 14:04:29 -06:00
|
|
|
}
|