// 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 lsp import ( "bytes" "context" "fmt" "go/token" "os" "os/exec" "path/filepath" "sort" "strings" "testing" "golang.org/x/tools/go/packages/packagestest" "golang.org/x/tools/internal/lsp/cache" "golang.org/x/tools/internal/lsp/diff" "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" "golang.org/x/tools/internal/testenv" ) func TestMain(m *testing.M) { testenv.ExitIfSmallMachine() os.Exit(m.Run()) } func TestLSP(t *testing.T) { packagestest.TestAll(t, testLSP) } type runner struct { server *Server data *tests.Data ctx context.Context } const viewName = "lsp_test" func testLSP(t *testing.T, exporter packagestest.Exporter) { ctx := tests.Context(t) data := tests.Load(t, exporter, "testdata") defer data.Exported.Cleanup() cache := cache.New() session := cache.NewSession(ctx) view := session.NewView(ctx, viewName, span.FileURI(data.Config.Dir)) view.SetEnv(data.Config.Env) for filename, content := range data.Config.Overlay { session.SetOverlay(span.FileURI(filename), content) } 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 session.SetOptions(options) r := &runner{ server: &Server{ session: session, undelivered: make(map[span.URI][]source.Diagnostic), }, data: data, ctx: ctx, } tests.Run(t, r, data) } // TODO: Actually test the LSP diagnostics function in this test. func (r *runner) Diagnostics(t *testing.T, data tests.Diagnostics) { v := r.server.session.View(viewName) for uri, want := range data { f, err := v.GetFile(r.ctx, uri) if err != nil { t.Fatalf("no file for %s: %v", f, err) } gof, ok := f.(source.GoFile) if !ok { t.Fatalf("%s is not a Go file: %v", uri, err) } results, err := source.Diagnostics(r.ctx, v, gof, nil) if err != nil { t.Fatal(err) } got := results[uri] // 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 } if diff := tests.DiffDiagnostics(uri, want, got); diff != "" { t.Error(diff) } } } func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests.CompletionSnippets, items tests.CompletionItems) { original := r.server.session.Options() modified := original defer func() { r.server.session.SetOptions(original) }() // Set this as a default. modified.Completion.Documentation = true for src, itemList := range data { var want []source.CompletionItem for _, pos := range itemList { 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") r.server.session.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) } if diff := diffCompletionItems(t, src, want, got); diff != "" { t.Errorf("%s: %s", src, diff) } } modified.InsertTextFormat = protocol.SnippetTextFormat for _, usePlaceholders := range []bool{true, false} { modified.UsePlaceholders = usePlaceholders for src, want := range snippets { 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") r.server.session.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 } } if got == nil { t.Fatalf("%s: couldn't find completion matching %q", src.URI(), wantItem.Label) } var expected string if usePlaceholders { expected = want.PlaceholderSnippet } else { expected = want.PlainSnippet } if expected != got.TextEdit.NewText { t.Errorf("%s: expected snippet %q, got %q", src, expected, got.TextEdit.NewText) } } } } 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(t *testing.T, spn span.Span, 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 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) { original := r.server.session.Options() modified := original defer func() { r.server.session.SetOptions(original) }() for _, spn := range data { uri := spn.URI() // Test all folding ranges. modified.LineFoldingOnly = false r.server.session.SetOptions(modified) ranges, err := r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{ TextDocument: protocol.TextDocumentIdentifier{ URI: protocol.NewURI(uri), }, }) if err != nil { t.Error(err) continue } r.foldingRanges(t, "foldingRange", uri, ranges) // Test folding ranges with lineFoldingOnly = true. modified.LineFoldingOnly = true r.server.session.SetOptions(modified) ranges, err = r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{ TextDocument: protocol.TextDocumentIdentifier{ URI: protocol.NewURI(uri), }, }) if err != nil { t.Error(err) continue } r.foldingRanges(t, "foldingRange-lineFolding", uri, ranges) } } func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, ranges []protocol.FoldingRange) { m, err := r.mapper(uri) if err != nil { t.Fatal(err) } // Fold all ranges. nonOverlapping := nonOverlappingRanges(ranges) for i, rngs := range nonOverlapping { got, err := foldRanges(m, string(m.Content), rngs) if err != nil { t.Error(err) continue } tag := fmt.Sprintf("%s-%d", prefix, i) want := string(r.data.Golden(tag, uri.Filename(), func() ([]byte, error) { return []byte(got), nil })) if want != got { t.Errorf("%s: foldingRanges failed for %s, expected:\n%v\ngot:\n%v", tag, uri.Filename(), want, got) } } // Filter by kind. kinds := []protocol.FoldingRangeKind{protocol.Imports, protocol.Comment} for _, kind := range kinds { var kindOnly []protocol.FoldingRange for _, fRng := range ranges { if fRng.Kind == string(kind) { kindOnly = append(kindOnly, fRng) } } nonOverlapping := nonOverlappingRanges(kindOnly) for i, rngs := range nonOverlapping { got, err := foldRanges(m, string(m.Content), rngs) if err != nil { t.Error(err) continue } tag := fmt.Sprintf("%s-%s-%d", prefix, kind, i) want := string(r.data.Golden(tag, uri.Filename(), func() ([]byte, error) { return []byte(got), nil })) if want != got { t.Errorf("%s: foldingRanges failed for %s, expected:\n%v\ngot:\n%v", tag, uri.Filename(), want, got) } } } } func nonOverlappingRanges(ranges []protocol.FoldingRange) (res [][]protocol.FoldingRange) { for _, fRng := range ranges { setNum := len(res) for i := 0; i < len(res); i++ { canInsert := true for _, rng := range res[i] { if conflict(rng, fRng) { canInsert = false break } } if canInsert { setNum = i break } } if setNum == len(res) { res = append(res, []protocol.FoldingRange{}) } res[setNum] = append(res[setNum], fRng) } return res } func conflict(a, b protocol.FoldingRange) bool { // a start position is <= b start positions return (a.StartLine < b.StartLine || (a.StartLine == b.StartLine && a.StartCharacter <= b.StartCharacter)) && (a.EndLine > b.StartLine || (a.EndLine == b.StartLine && a.EndCharacter > b.StartCharacter)) } func foldRanges(m *protocol.ColumnMapper, contents string, ranges []protocol.FoldingRange) (string, error) { foldedText := "<>" res := contents // Apply the edits from the end of the file forward // to preserve the offsets for i := len(ranges) - 1; i >= 0; i-- { fRange := ranges[i] spn, err := m.RangeSpan(protocol.Range{ Start: protocol.Position{ Line: fRange.StartLine, Character: fRange.StartCharacter, }, End: protocol.Position{ Line: fRange.EndLine, Character: fRange.EndCharacter, }, }) if err != nil { return "", err } start := spn.Start().Offset() end := spn.End().Offset() tmp := res[0:start] + foldedText res = tmp + res[end:] } return res, nil } func (r *runner) Format(t *testing.T, data tests.Formats) { for _, spn := range data { uri := spn.URI() filename := uri.Filename() 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 })) edits, err := r.server.Formatting(r.ctx, &protocol.DocumentFormattingParams{ TextDocument: protocol.TextDocumentIdentifier{ URI: protocol.NewURI(uri), }, }) if err != nil { if gofmted != "" { t.Error(err) } continue } m, err := r.mapper(uri) if err != nil { t.Fatal(err) } sedits, err := source.FromProtocolEdits(m, edits) if err != nil { t.Error(err) } got := diff.ApplyEdits(string(m.Content), sedits) if gofmted != got { t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", filename, gofmted, got) } } } func (r *runner) Import(t *testing.T, data tests.Imports) { for _, spn := range data { uri := spn.URI() filename := uri.Filename() 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 })) actions, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{ TextDocument: protocol.TextDocumentIdentifier{ URI: protocol.NewURI(uri), }, }) if err != nil { if goimported != "" { t.Error(err) } continue } m, err := r.mapper(uri) if err != nil { t.Fatal(err) } var edits []protocol.TextEdit for _, a := range actions { if a.Title == "Organize Imports" { edits = (*a.Edit.Changes)[string(uri)] } } sedits, err := source.FromProtocolEdits(m, edits) if err != nil { t.Error(err) } got := diff.ApplyEdits(string(m.Content), sedits) if goimported != got { t.Errorf("import failed for %s, expected:\n%v\ngot:\n%v", filename, goimported, got) } } } func (r *runner) Definition(t *testing.T, data tests.Definitions) { for _, d := range data { sm, err := r.mapper(d.Src.URI()) if err != nil { t.Fatal(err) } loc, err := sm.Location(d.Src) if err != nil { t.Fatalf("failed for %v: %v", d.Src, err) } tdpp := protocol.TextDocumentPositionParams{ TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, Position: loc.Range.Start, } var locs []protocol.Location var hover *protocol.Hover if d.IsType { params := &protocol.TypeDefinitionParams{ tdpp, protocol.WorkDoneProgressParams{}, protocol.PartialResultParams{}, } locs, err = r.server.TypeDefinition(r.ctx, params) } else { params := &protocol.DefinitionParams{ tdpp, protocol.WorkDoneProgressParams{}, protocol.PartialResultParams{}, } locs, err = r.server.Definition(r.ctx, params) if err != nil { t.Fatalf("failed for %v: %+v", d.Src, err) } v := &protocol.HoverParams{ tdpp, protocol.WorkDoneProgressParams{}, } hover, err = r.server.Hover(r.ctx, v) } if err != nil { t.Fatalf("failed for %v: %v", d.Src, err) } if len(locs) != 1 { t.Errorf("got %d locations for definition, expected 1", len(locs)) } if hover != nil { tag := fmt.Sprintf("%s-hover", d.Name) expectHover := string(r.data.Golden(tag, d.Src.URI().Filename(), func() ([]byte, error) { return []byte(hover.Contents.Value), nil })) if hover.Contents.Value != expectHover { t.Errorf("for %v got %q want %q", d.Src, hover.Contents.Value, expectHover) } } else if !d.OnlyHover { locURI := span.NewURI(locs[0].URI) lm, err := r.mapper(locURI) if err != nil { t.Fatal(err) } if def, err := lm.Span(locs[0]); err != nil { t.Fatalf("failed for %v: %v", locs[0], 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()) } } } func (r *runner) Highlight(t *testing.T, data tests.Highlights) { for name, locations := range data { m, err := r.mapper(locations[0].URI()) if err != nil { t.Fatal(err) } loc, err := m.Location(locations[0]) if err != nil { t.Fatalf("failed for %v: %v", locations[0], err) } tdpp := protocol.TextDocumentPositionParams{ TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, Position: loc.Range.Start, } params := &protocol.DocumentHighlightParams{ tdpp, protocol.WorkDoneProgressParams{}, protocol.PartialResultParams{}, } highlights, err := r.server.DocumentHighlight(r.ctx, params) if err != nil { t.Fatal(err) } if len(highlights) != len(locations) { t.Fatalf("got %d highlights for %s, expected %d", len(highlights), name, len(locations)) } for i := range highlights { if h, err := m.RangeSpan(highlights[i].Range); err != nil { t.Fatalf("failed for %v: %v", highlights[i], err) } else if h != locations[i] { t.Errorf("want %v, got %v\n", locations[i], h) } } } } func (r *runner) Reference(t *testing.T, data tests.References) { for src, itemList := range data { sm, err := r.mapper(src.URI()) if err != nil { t.Fatal(err) } loc, err := sm.Location(src) if err != nil { t.Fatalf("failed for %v: %v", src, err) } want := make(map[protocol.Location]bool) for _, pos := range itemList { m, err := r.mapper(pos.URI()) if err != nil { t.Fatal(err) } loc, err := m.Location(pos) if err != nil { t.Fatalf("failed for %v: %v", src, err) } want[loc] = true } params := &protocol.ReferenceParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, Position: loc.Range.Start, }, } got, err := r.server.References(r.ctx, params) if err != nil { t.Fatalf("failed for %v: %v", src, err) } if len(got) != len(want) { t.Errorf("references failed: different lengths got %v want %v", len(got), len(want)) } for _, loc := range got { if !want[loc] { t.Errorf("references failed: incorrect references got %v want %v", loc, want) } } } } func (r *runner) Rename(t *testing.T, data tests.Renames) { for spn, newText := range data { tag := fmt.Sprintf("%s-rename", newText) uri := spn.URI() filename := uri.Filename() sm, err := r.mapper(uri) if err != nil { t.Fatal(err) } loc, err := sm.Location(spn) if err != nil { t.Fatalf("failed for %v: %v", spn, err) } workspaceEdits, err := r.server.Rename(r.ctx, &protocol.RenameParams{ TextDocument: protocol.TextDocumentIdentifier{ URI: protocol.NewURI(uri), }, Position: loc.Range.Start, NewName: newText, }) if err != nil { renamed := string(r.data.Golden(tag, 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) } continue } var res []string for uri, edits := range *workspaceEdits.Changes { m, err := r.mapper(span.URI(uri)) if err != nil { t.Fatal(err) } sedits, err := source.FromProtocolEdits(m, edits) if err != nil { t.Error(err) } filename := filepath.Base(m.URI.Filename()) contents := applyEdits(string(m.Content), sedits) res = append(res, fmt.Sprintf("%s:\n%s", filename, contents)) } // Sort on filename sort.Strings(res) var got string for i, val := range res { if i != 0 { got += "\n" } got += val } renamed := string(r.data.Golden(tag, filename, func() ([]byte, error) { return []byte(got), nil })) if renamed != got { t.Errorf("rename failed for %s, expected:\n%v\ngot:\n%v", newText, renamed, got) } } } func (r *runner) PrepareRename(t *testing.T, data tests.PrepareRenames) { for src, want := range data { m, err := r.mapper(src.URI()) if err != nil { t.Fatal(err) } loc, err := m.Location(src) if err != nil { t.Fatalf("failed for %v: %v", src, err) } tdpp := protocol.TextDocumentPositionParams{ TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, Position: loc.Range.Start, } params := &protocol.PrepareRenameParams{ tdpp, protocol.WorkDoneProgressParams{}, } got, err := r.server.PrepareRename(context.Background(), params) if err != nil { t.Errorf("prepare rename failed for %v: got error: %v", src, err) continue } if got == nil { if want.Text != "" { // expected an ident. t.Errorf("prepare rename failed for %v: got nil", src) } continue } if protocol.CompareRange(*got, want.Range) != 0 { t.Errorf("prepare rename failed: incorrect range got %v want %v", *got, want.Range) } } } func applyEdits(contents string, edits []diff.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 } func (r *runner) Symbol(t *testing.T, data tests.Symbols) { for uri, expectedSymbols := range data { params := &protocol.DocumentSymbolParams{ TextDocument: protocol.TextDocumentIdentifier{ URI: string(uri), }, } symbols, err := r.server.DocumentSymbol(r.ctx, params) if err != nil { t.Fatal(err) } 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(t, uri, expectedSymbols, symbols); diff != "" { t.Error(diff) } } } func (r *runner) diffSymbols(t *testing.T, uri span.URI, want []protocol.DocumentSymbol, got []protocol.DocumentSymbol) 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(t, -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(t, i, want, got, "incorrect name got %v want %v", g.Name, w.Name) } if w.Kind != g.Kind { return summarizeSymbols(t, i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind) } if protocol.CompareRange(g.SelectionRange, w.SelectionRange) != 0 { return summarizeSymbols(t, i, want, got, "incorrect span got %v want %v", g.SelectionRange, w.SelectionRange) } if msg := r.diffSymbols(t, uri, w.Children, g.Children); msg != "" { return fmt.Sprintf("children of %s: %s", w.Name, msg) } } return "" } func summarizeSymbols(t *testing.T, i int, want []protocol.DocumentSymbol, got []protocol.DocumentSymbol, 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.SelectionRange) } fmt.Fprintf(msg, "got:\n") for _, s := range got { fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange) } return msg.String() } func (r *runner) SignatureHelp(t *testing.T, data tests.Signatures) { for spn, expectedSignatures := range data { m, err := r.mapper(spn.URI()) if err != nil { t.Fatal(err) } loc, err := m.Location(spn) if err != nil { t.Fatalf("failed for %v: %v", loc, err) } tdpp := protocol.TextDocumentPositionParams{ TextDocument: protocol.TextDocumentIdentifier{ URI: protocol.NewURI(spn.URI()), }, Position: loc.Range.Start, } params := &protocol.SignatureHelpParams{ tdpp, protocol.WorkDoneProgressParams{}, } gotSignatures, err := r.server.SignatureHelp(r.ctx, params) if err != nil { // Only fail if we got an error we did not expect. if expectedSignatures != nil { t.Fatal(err) } continue } if expectedSignatures == nil { if gotSignatures != nil { t.Errorf("expected no signature, got %v", gotSignatures) } continue } if diff := diffSignatures(spn, expectedSignatures, gotSignatures); diff != "" { t.Error(diff) } } } func diffSignatures(spn span.Span, want *source.SignatureInformation, got *protocol.SignatureHelp) string { decorate := func(f string, args ...interface{}) string { return fmt.Sprintf("Invalid signature at %s: %s", spn, fmt.Sprintf(f, args...)) } if len(got.Signatures) != 1 { return decorate("wanted 1 signature, got %d", len(got.Signatures)) } if got.ActiveSignature != 0 { return decorate("wanted active signature of 0, got %f", got.ActiveSignature) } if want.ActiveParameter != int(got.ActiveParameter) { return decorate("wanted active parameter of %d, got %f", want.ActiveParameter, got.ActiveParameter) } gotSig := got.Signatures[int(got.ActiveSignature)] if want.Label != gotSig.Label { return decorate("wanted label %q, got %q", want.Label, gotSig.Label) } var paramParts []string for _, p := range gotSig.Parameters { paramParts = append(paramParts, p.Label) } paramsStr := strings.Join(paramParts, ", ") if !strings.Contains(gotSig.Label, paramsStr) { return decorate("expected signature %q to contain params %q", gotSig.Label, paramsStr) } return "" } func (r *runner) Link(t *testing.T, data tests.Links) { for uri, wantLinks := range data { m, err := r.mapper(uri) if err != nil { t.Fatal(err) } gotLinks, err := r.server.DocumentLink(r.ctx, &protocol.DocumentLinkParams{ TextDocument: protocol.TextDocumentIdentifier{ URI: protocol.NewURI(uri), }, }) if err != nil { t.Fatal(err) } var notePositions []token.Position links := make(map[span.Span]string, len(wantLinks)) for _, link := range wantLinks { links[link.Src] = link.Target notePositions = append(notePositions, link.NotePosition) } for _, link := range gotLinks { spn, err := m.RangeSpan(link.Range) if err != nil { t.Fatal(err) } linkInNote := false for _, notePosition := range notePositions { // Drop the links found inside expectation notes arguments as this links are not collected by expect package if notePosition.Line == spn.Start().Line() && notePosition.Column <= spn.Start().Column() { delete(links, spn) linkInNote = true } } if linkInNote { continue } if target, ok := links[spn]; ok { delete(links, spn) if target != link.Target { t.Errorf("for %v want %v, got %v\n", spn, link.Target, target) } } else { t.Errorf("unexpected link %v:%v\n", spn, link.Target) } } for spn, target := range links { t.Errorf("missing link %v:%v\n", spn, target) } } } func (r *runner) mapper(uri span.URI) (*protocol.ColumnMapper, error) { filename := uri.Filename() fset := r.data.Exported.ExpectFileSet var f *token.File fset.Iterate(func(check *token.File) bool { if check.Name() == filename { f = check return false } return true }) if f == nil { return nil, fmt.Errorf("no token.File for %s", uri) } content, err := r.data.Exported.FileContents(f.Name()) if err != nil { return nil, err } return protocol.NewColumnMapper(uri, filename, fset, f, content), nil } func TestBytesOffset(t *testing.T) { tests := []struct { text string pos protocol.Position want int }{ {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 0}, want: 0}, {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 1}, want: 1}, {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 2}, want: 1}, {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 3}, want: 5}, {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 4}, want: 6}, {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 5}, want: -1}, {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 3}, want: 3}, {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 4}, want: 3}, {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 0}, want: 4}, {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 3}, want: 7}, {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 4}, want: 7}, {text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, {text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 1}, want: -1}, {text: "aaa\nbbb\n\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, } for i, test := range tests { fname := fmt.Sprintf("test %d", i) fset := token.NewFileSet() f := fset.AddFile(fname, -1, len(test.text)) f.SetLinesForContent([]byte(test.text)) mapper := protocol.NewColumnMapper(span.FileURI(fname), fname, fset, f, []byte(test.text)) got, err := mapper.Point(test.pos) if err != nil && test.want != -1 { t.Errorf("unexpected error: %v", err) } if err == nil && got.Offset() != test.want { t.Errorf("want %d for %q(Line:%d,Character:%d), but got %d", test.want, test.text, int(test.pos.Line), int(test.pos.Character), got.Offset()) } } }