1
0
mirror of https://github.com/golang/go synced 2024-11-19 04:54:41 -07:00
go/internal/lsp/source/source_test.go

929 lines
26 KiB
Go
Raw Normal View History

// 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 (
"context"
"fmt"
"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/diff/myers"
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"
"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"
errors "golang.org/x/xerrors"
)
func TestMain(m *testing.M) {
testenv.ExitIfSmallMachine()
os.Exit(m.Run())
}
func TestSource(t *testing.T) {
packagestest.TestAll(t, testSource)
}
type runner struct {
view source.View
data *tests.Data
ctx context.Context
}
func testSource(t *testing.T, exporter packagestest.Exporter) {
ctx := tests.Context(t)
data := tests.Load(t, exporter, "../testdata")
for _, datum := range data {
defer datum.Exported.Cleanup()
cache := cache.New(ctx, nil)
session := cache.NewSession(ctx)
options := tests.DefaultOptions()
options.Env = datum.Config.Env
view, snapshot, err := session.NewView(ctx, "source_test", span.URIFromPath(datum.Config.Dir), options)
if err != nil {
t.Fatal(err)
}
// Enable type error analyses for tests.
// TODO(golang/go#38212): Delete this once they are enabled by default.
tests.EnableAllAnalyzers(snapshot, &options)
view.SetOptions(ctx, options)
r := &runner{
view: view,
data: datum,
ctx: ctx,
}
var modifications []source.FileModification
for filename, content := range datum.Config.Overlay {
kind := source.DetectLanguage("", filename)
if kind != source.Go {
continue
}
modifications = append(modifications, source.FileModification{
URI: span.URIFromPath(filename),
Action: source.Open,
Version: -1,
Text: content,
LanguageID: "go",
})
}
if _, err := session.DidModifyFiles(ctx, modifications); err != nil {
t.Fatal(err)
}
t.Run(tests.FormatFolderName(datum.Folder), func(t *testing.T) {
t.Helper()
tests.Run(t, r, datum)
})
}
}
func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnostic) {
snapshot := r.view.Snapshot()
fileID, got, err := source.FileDiagnostics(r.ctx, snapshot, uri)
if err != nil {
t.Fatal(err)
}
// 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)
}
return
}
if diff := tests.DiffDiagnostics(fileID.URI, want, got); diff != "" {
t.Error(diff)
}
}
func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
var want []protocol.CompletionItem
for _, pos := range test.CompletionItems {
want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
}
_, got := r.callCompletion(t, src, func(opts *source.Options) {
opts.Matcher = source.CaseInsensitive
opts.DeepCompletion = false
opts.UnimportedCompletion = false
opts.InsertTextFormat = protocol.PlainTextTextFormat
// Only enable literal completions if in the completion literals tests.
// TODO(rstambler): Separate out literal completion tests.
if strings.Contains(string(src.URI()), "literal") {
opts.InsertTextFormat = protocol.SnippetTextFormat
}
})
got = tests.FilterBuiltins(src, got)
if diff := tests.DiffCompletionItems(want, got); diff != "" {
t.Errorf("%s: %s", src, diff)
}
}
func (r *runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.CompletionSnippet, placeholders bool, items tests.CompletionItems) {
_, list := r.callCompletion(t, src, func(opts *source.Options) {
opts.Placeholders = placeholders
opts.DeepCompletion = true
opts.UnimportedCompletion = false
})
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)
}
}
func (r *runner) UnimportedCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
var want []protocol.CompletionItem
for _, pos := range test.CompletionItems {
want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
}
_, got := r.callCompletion(t, src, func(opts *source.Options) {})
got = tests.FilterBuiltins(src, got)
if diff := tests.CheckCompletionOrder(want, got, false); diff != "" {
t.Errorf("%s: %s", src, diff)
}
}
func (r *runner) DeepCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
var want []protocol.CompletionItem
for _, pos := range test.CompletionItems {
want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
}
prefix, list := r.callCompletion(t, src, func(opts *source.Options) {
opts.DeepCompletion = true
opts.Matcher = source.CaseInsensitive
opts.UnimportedCompletion = false
})
list = tests.FilterBuiltins(src, list)
fuzzyMatcher := fuzzy.NewMatcher(prefix)
var got []protocol.CompletionItem
for _, item := range list {
if fuzzyMatcher.Score(item.Label) <= 0 {
continue
}
got = append(got, item)
}
if msg := tests.DiffCompletionItems(want, got); msg != "" {
t.Errorf("%s: %s", src, msg)
}
}
func (r *runner) FuzzyCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
var want []protocol.CompletionItem
for _, pos := range test.CompletionItems {
want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
}
_, got := r.callCompletion(t, src, func(opts *source.Options) {
opts.DeepCompletion = true
opts.Matcher = source.Fuzzy
opts.UnimportedCompletion = false
})
got = tests.FilterBuiltins(src, got)
if msg := tests.DiffCompletionItems(want, got); msg != "" {
t.Errorf("%s: %s", src, msg)
}
}
func (r *runner) CaseSensitiveCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
var want []protocol.CompletionItem
for _, pos := range test.CompletionItems {
want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
}
_, list := r.callCompletion(t, src, func(opts *source.Options) {
opts.Matcher = source.CaseSensitive
opts.UnimportedCompletion = false
})
list = tests.FilterBuiltins(src, list)
if diff := tests.DiffCompletionItems(want, list); diff != "" {
t.Errorf("%s: %s", src, diff)
}
}
func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
var want []protocol.CompletionItem
for _, pos := range test.CompletionItems {
want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
}
_, got := r.callCompletion(t, src, func(opts *source.Options) {
opts.DeepCompletion = true
opts.Matcher = source.Fuzzy
})
if msg := tests.CheckCompletionOrder(want, got, true); msg != "" {
t.Errorf("%s: %s", src, msg)
}
}
func (r *runner) callCompletion(t *testing.T, src span.Span, options func(*source.Options)) (string, []protocol.CompletionItem) {
fh, err := r.view.Snapshot().GetFile(src.URI())
if err != nil {
t.Fatal(err)
}
original := r.view.Options()
modified := original
options(&modified)
view, err := r.view.SetOptions(r.ctx, modified)
if err != nil {
t.Fatal(err)
}
defer r.view.SetOptions(r.ctx, original)
list, surrounding, err := source.Completion(r.ctx, view.Snapshot(), fh, protocol.Position{
Line: float64(src.Start().Line() - 1),
Character: float64(src.Start().Column() - 1),
})
if err != nil && !errors.As(err, &source.ErrIsDefinition{}) {
t.Fatalf("failed for %v: %v", src, err)
}
var prefix string
if surrounding != nil {
prefix = strings.ToLower(surrounding.Prefix())
}
var numDeepCompletionsSeen int
var items []source.CompletionItem
// Apply deep completion filtering.
for _, item := range list {
if item.Depth > 0 {
if !modified.DeepCompletion {
continue
}
if numDeepCompletionsSeen >= source.MaxDeepCompletions {
continue
}
numDeepCompletionsSeen++
}
items = append(items, item)
}
return prefix, tests.ToProtocolCompletionItems(items)
}
func (r *runner) FoldingRanges(t *testing.T, spn span.Span) {
uri := spn.URI()
fh, err := r.view.Snapshot().GetFile(spn.URI())
if err != nil {
t.Fatal(err)
}
data, _, err := fh.Read(r.ctx)
if err != nil {
t.Error(err)
return
}
// Test all folding ranges.
ranges, err := source.FoldingRange(r.ctx, r.view.Snapshot(), fh, false)
if err != nil {
t.Error(err)
return
}
r.foldingRanges(t, "foldingRange", uri, string(data), ranges)
// Test folding ranges with lineFoldingOnly
ranges, err = source.FoldingRange(r.ctx, r.view.Snapshot(), fh, true)
if err != nil {
t.Error(err)
return
}
r.foldingRanges(t, "foldingRange-lineFolding", uri, string(data), ranges)
}
func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, data string, ranges []*source.FoldingRangeInfo) {
t.Helper()
// Fold all ranges.
nonOverlapping := nonOverlappingRanges(t, ranges)
for i, rngs := range nonOverlapping {
got, err := foldRanges(string(data), 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 []*source.FoldingRangeInfo
for _, fRng := range ranges {
if fRng.Kind == kind {
kindOnly = append(kindOnly, fRng)
}
}
nonOverlapping := nonOverlappingRanges(t, kindOnly)
for i, rngs := range nonOverlapping {
got, err := foldRanges(string(data), 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: failed for %s, expected:\n%v\ngot:\n%v", tag, uri.Filename(), want, got)
}
}
}
}
func nonOverlappingRanges(t *testing.T, ranges []*source.FoldingRangeInfo) (res [][]*source.FoldingRangeInfo) {
for _, fRng := range ranges {
setNum := len(res)
for i := 0; i < len(res); i++ {
canInsert := true
for _, rng := range res[i] {
if conflict(t, rng, fRng) {
canInsert = false
break
}
}
if canInsert {
setNum = i
break
}
}
if setNum == len(res) {
res = append(res, []*source.FoldingRangeInfo{})
}
res[setNum] = append(res[setNum], fRng)
}
return res
}
func conflict(t *testing.T, a, b *source.FoldingRangeInfo) bool {
arng, err := a.Range()
if err != nil {
t.Fatal(err)
}
brng, err := b.Range()
if err != nil {
t.Fatal(err)
}
// a start position is <= b start positions
return protocol.ComparePosition(arng.Start, brng.Start) <= 0 && protocol.ComparePosition(arng.End, brng.Start) > 0
}
func foldRanges(contents string, ranges []*source.FoldingRangeInfo) (string, error) {
foldedText := "<>"
res := contents
// Apply the folds from the end of the file forward
// to preserve the offsets.
for i := len(ranges) - 1; i >= 0; i-- {
fRange := ranges[i]
spn, err := fRange.Span()
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, spn span.Span) {
gofmted := string(r.data.Golden("gofmt", spn.URI().Filename(), func() ([]byte, error) {
cmd := exec.Command("gofmt", spn.URI().Filename())
out, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files
return out, nil
}))
fh, err := r.view.Snapshot().GetFile(spn.URI())
if err != nil {
t.Fatal(err)
}
edits, err := source.Format(r.ctx, r.view.Snapshot(), fh)
if err != nil {
if gofmted != "" {
t.Error(err)
}
return
}
data, _, err := fh.Read(r.ctx)
if err != nil {
t.Fatal(err)
}
m, err := r.data.Mapper(spn.URI())
if err != nil {
t.Fatal(err)
}
diffEdits, err := source.FromProtocolEdits(m, edits)
if err != nil {
t.Error(err)
}
got := diff.ApplyEdits(string(data), diffEdits)
if gofmted != got {
t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", spn.URI().Filename(), gofmted, got)
}
}
func (r *runner) Import(t *testing.T, spn span.Span) {
fh, err := r.view.Snapshot().GetFile(spn.URI())
if err != nil {
t.Fatal(err)
}
edits, _, err := source.AllImportsFixes(r.ctx, r.view.Snapshot(), fh)
if err != nil {
t.Error(err)
}
data, _, err := fh.Read(r.ctx)
if err != nil {
t.Fatal(err)
}
m, err := r.data.Mapper(fh.Identity().URI)
if err != nil {
t.Fatal(err)
}
diffEdits, err := source.FromProtocolEdits(m, edits)
if err != nil {
t.Error(err)
}
got := diff.ApplyEdits(string(data), diffEdits)
want := string(r.data.Golden("goimports", spn.URI().Filename(), func() ([]byte, error) {
return []byte(got), nil
}))
if want != got {
d := myers.ComputeEdits(spn.URI(), want, got)
t.Errorf("import failed for %s: %s", spn.URI().Filename(), diff.ToUnified("want", "got", want, d))
}
}
func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string) {}
func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) {
_, srcRng, err := spanToRange(r.data, d.Src)
if err != nil {
t.Fatal(err)
}
fh, err := r.view.Snapshot().GetFile(spn.URI())
if err != nil {
t.Fatal(err)
}
ident, err := source.Identifier(r.ctx, r.view.Snapshot(), fh, srcRng.Start)
if err != nil {
t.Fatalf("failed for %v: %v", d.Src, err)
}
h, err := ident.Hover(r.ctx)
if err != nil {
t.Fatalf("failed for %v: %v", d.Src, err)
}
hover, err := source.FormatHover(h, r.view.Options())
if err != nil {
t.Fatal(err)
}
rng, err := ident.Declaration.MappedRange[0].Range()
if err != nil {
t.Fatal(err)
}
if d.IsType {
rng, err = ident.Type.Range()
if err != nil {
t.Fatal(err)
}
hover = ""
}
didSomething := false
if hover != "" {
didSomething = true
tag := fmt.Sprintf("%s-hover", d.Name)
expectHover := string(r.data.Golden(tag, d.Src.URI().Filename(), func() ([]byte, error) {
return []byte(hover), nil
}))
if hover != expectHover {
t.Errorf("for %v got %q want %q", d.Src, hover, expectHover)
}
}
if !d.OnlyHover {
didSomething = true
if _, defRng, err := spanToRange(r.data, d.Def); err != nil {
t.Fatal(err)
} else if rng != defRng {
t.Errorf("for %v got %v want %v", d.Src, rng, defRng)
}
}
if !didSomething {
t.Errorf("no tests ran for %s", d.Src.URI())
}
}
func (r *runner) Implementation(t *testing.T, spn span.Span, impls []span.Span) {
sm, err := r.data.Mapper(spn.URI())
if err != nil {
t.Fatal(err)
}
loc, err := sm.Location(spn)
if err != nil {
t.Fatalf("failed for %v: %v", spn, err)
}
fh, err := r.view.Snapshot().GetFile(spn.URI())
if err != nil {
t.Fatal(err)
}
locs, err := source.Implementation(r.ctx, r.view.Snapshot(), fh, loc.Range.Start)
if err != nil {
t.Fatalf("failed for %v: %v", spn, err)
}
if len(locs) != len(impls) {
t.Fatalf("got %d locations for implementation, expected %d", len(locs), len(impls))
}
var results []span.Span
for i := range locs {
locURI := locs[i].URI.SpanURI()
lm, err := r.data.Mapper(locURI)
if err != nil {
t.Fatal(err)
}
imp, err := lm.Span(locs[i])
if err != nil {
t.Fatalf("failed for %v: %v", locs[i], err)
}
results = append(results, imp)
}
// Sort results and expected to make tests deterministic.
sort.SliceStable(results, func(i, j int) bool {
return span.Compare(results[i], results[j]) == -1
})
sort.SliceStable(impls, func(i, j int) bool {
return span.Compare(impls[i], impls[j]) == -1
})
for i := range results {
if results[i] != impls[i] {
t.Errorf("for %dth implementation of %v got %v want %v", i, spn, results[i], impls[i])
}
}
}
func (r *runner) Highlight(t *testing.T, src span.Span, locations []span.Span) {
ctx := r.ctx
m, srcRng, err := spanToRange(r.data, src)
if err != nil {
t.Fatal(err)
}
fh, err := r.view.Snapshot().GetFile(src.URI())
if err != nil {
t.Fatal(err)
}
highlights, err := source.Highlight(ctx, r.view.Snapshot(), fh, srcRng.Start)
if err != nil {
t.Errorf("highlight failed for %s: %v", src.URI(), err)
}
if len(highlights) != len(locations) {
t.Errorf("got %d highlights for highlight at %v:%v:%v, expected %d", len(highlights), src.URI().Filename(), src.Start().Line(), src.Start().Column(), len(locations))
}
// Check to make sure highlights have a valid range.
var results []span.Span
for i := range highlights {
h, err := m.RangeSpan(highlights[i])
if err != nil {
t.Fatalf("failed for %v: %v", highlights[i], err)
}
results = append(results, h)
}
// Sort results to make tests deterministic since DocumentHighlight uses a map.
sort.SliceStable(results, func(i, j int) bool {
return span.Compare(results[i], results[j]) == -1
})
// Check to make sure all the expected highlights are found.
for i := range results {
if results[i] != locations[i] {
t.Errorf("want %v, got %v\n", locations[i], results[i])
}
}
}
func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) {
ctx := r.ctx
_, srcRng, err := spanToRange(r.data, src)
if err != nil {
t.Fatal(err)
}
snapshot := r.view.Snapshot()
fh, err := snapshot.GetFile(src.URI())
if err != nil {
t.Fatal(err)
}
for _, includeDeclaration := range []bool{true, false} {
t.Run(fmt.Sprintf("refs-declaration-%v", includeDeclaration), func(t *testing.T) {
want := make(map[span.Span]bool)
for i, pos := range itemList {
// We don't want the first result if we aren't including the declaration.
if i == 0 && !includeDeclaration {
continue
}
want[pos] = true
}
refs, err := source.References(ctx, snapshot, fh, srcRng.Start, includeDeclaration)
if err != nil {
t.Fatalf("failed for %s: %v", src, err)
}
got := make(map[span.Span]bool)
for _, refInfo := range refs {
refSpan, err := refInfo.Span()
if err != nil {
t.Fatal(err)
}
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] {
t.Errorf("references failed: incorrect references got %v want locations %v", got, want)
}
}
})
}
}
func (r *runner) Rename(t *testing.T, spn span.Span, newText string) {
ctx := r.ctx
tag := fmt.Sprintf("%s-rename", newText)
_, srcRng, err := spanToRange(r.data, spn)
if err != nil {
t.Fatal(err)
}
fh, err := r.view.Snapshot().GetFile(spn.URI())
if err != nil {
t.Fatal(err)
}
internal/lsp: refactor find-references and rename The main goal is to push the package variant logic from internal/lsp into internal/lsp/source so all users of internal/lsp/source benefit. "references" and "rename" now have top-level source.References() and source.Rename() entry points (as opposed to hanging off source.Identifier()). I expanded objectsAtProtocolPos() to know about implicit objects (type switch and import spec), and to handle *ast.ImportSpec generically. This gets rid of special case handling of *types.PkgName in various places. The biggest practical benefit, though, is that "references" no longer needs to compute the objectpath for every types.Object comparison it does, instead using direct types.Object equality. This speeds up "references" and "rename" a lot. Two other notable improvements that fell out of not using source.Identifier()'s logic: - Finding references on an embedded field now shows references to the field, not the type being embedded. - Finding references on an imported object now works correctly (previously it searched the importing package's dependents rather than the imported package's dependents). Finally, I refactored findIdentifier() to use pathEnclosingObjNode() instead of astutil.PathEnclosingInterval. Now we only need a single call to get the path because pathEnclosingObjNode() has the "try pos || try pos-1" logic built in. Change-Id: I667be9bed6ad83912404b90257c5c1485b3a7025 Reviewed-on: https://go-review.googlesource.com/c/tools/+/211999 Run-TryBot: Muir Manders <muir@mnd.rs> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2019-12-17 22:06:31 -07:00
changes, err := source.Rename(r.ctx, r.view.Snapshot(), fh, srcRng.Start, newText)
if err != nil {
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)
}
return
}
var res []string
for editURI, edits := range changes {
fh, err := r.view.Snapshot().GetFile(editURI)
if err != nil {
t.Fatal(err)
}
data, _, err := fh.Read(ctx)
if err != nil {
t.Fatal(err)
}
m, err := r.data.Mapper(fh.Identity().URI)
if err != nil {
t.Fatal(err)
}
diffEdits, err := source.FromProtocolEdits(m, edits)
if err != nil {
t.Fatal(err)
}
contents := applyEdits(string(data), diffEdits)
if len(changes) > 1 {
filename := filepath.Base(editURI.Filename())
contents = fmt.Sprintf("%s:\n%s", filename, contents)
}
res = append(res, 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, spn.URI().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 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) PrepareRename(t *testing.T, src span.Span, want *source.PrepareItem) {
_, srcRng, err := spanToRange(r.data, src)
if err != nil {
t.Fatal(err)
}
// Find the identifier at the position.
fh, err := r.view.Snapshot().GetFile(src.URI())
if err != nil {
t.Fatal(err)
}
internal/lsp: refactor find-references and rename The main goal is to push the package variant logic from internal/lsp into internal/lsp/source so all users of internal/lsp/source benefit. "references" and "rename" now have top-level source.References() and source.Rename() entry points (as opposed to hanging off source.Identifier()). I expanded objectsAtProtocolPos() to know about implicit objects (type switch and import spec), and to handle *ast.ImportSpec generically. This gets rid of special case handling of *types.PkgName in various places. The biggest practical benefit, though, is that "references" no longer needs to compute the objectpath for every types.Object comparison it does, instead using direct types.Object equality. This speeds up "references" and "rename" a lot. Two other notable improvements that fell out of not using source.Identifier()'s logic: - Finding references on an embedded field now shows references to the field, not the type being embedded. - Finding references on an imported object now works correctly (previously it searched the importing package's dependents rather than the imported package's dependents). Finally, I refactored findIdentifier() to use pathEnclosingObjNode() instead of astutil.PathEnclosingInterval. Now we only need a single call to get the path because pathEnclosingObjNode() has the "try pos || try pos-1" logic built in. Change-Id: I667be9bed6ad83912404b90257c5c1485b3a7025 Reviewed-on: https://go-review.googlesource.com/c/tools/+/211999 Run-TryBot: Muir Manders <muir@mnd.rs> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2019-12-17 22:06:31 -07:00
item, err := source.PrepareRename(r.ctx, r.view.Snapshot(), fh, srcRng.Start)
if err != nil {
if want.Text != "" { // expected an ident.
t.Errorf("prepare rename failed for %v: got error: %v", src, err)
}
return
}
if item == nil {
if want.Text != "" {
t.Errorf("prepare rename failed for %v: got nil", src)
}
return
}
if want.Text == "" {
t.Errorf("prepare rename failed for %v: expected nil, got %v", src, item)
return
}
if item.Range.Start == item.Range.End {
// Special case for 0-length ranges. Marks can't specify a 0-length range,
// so just compare the start.
if item.Range.Start != want.Range.Start {
t.Errorf("prepare rename failed: incorrect point, got %v want %v", item.Range.Start, want.Range.Start)
}
} else {
if protocol.CompareRange(item.Range, want.Range) != 0 {
t.Errorf("prepare rename failed: incorrect range got %v want %v", item.Range, want.Range)
}
}
}
func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) {
fh, err := r.view.Snapshot().GetFile(uri)
if err != nil {
t.Fatal(err)
}
symbols, err := source.DocumentSymbols(r.ctx, r.view.Snapshot(), fh)
if err != nil {
t.Errorf("symbols failed for %s: %v", uri, err)
}
if len(symbols) != len(expectedSymbols) {
t.Errorf("want %d top-level symbols in %v, got %d", len(expectedSymbols), uri, len(symbols))
return
}
if diff := tests.DiffSymbols(t, uri, expectedSymbols, symbols); diff != "" {
t.Error(diff)
}
}
func (r *runner) WorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
got := r.callWorkspaceSymbols(t, query, func(opts *source.Options) {
opts.Matcher = source.CaseInsensitive
})
got = tests.FilterWorkspaceSymbols(got, dirs)
if len(got) != len(expectedSymbols) {
t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
return
}
if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
t.Error(diff)
}
}
func (r *runner) FuzzyWorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
got := r.callWorkspaceSymbols(t, query, func(opts *source.Options) {
opts.Matcher = source.Fuzzy
})
got = tests.FilterWorkspaceSymbols(got, dirs)
if len(got) != len(expectedSymbols) {
t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
return
}
if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
t.Error(diff)
}
}
func (r *runner) CaseSensitiveWorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
got := r.callWorkspaceSymbols(t, query, func(opts *source.Options) {
opts.Matcher = source.CaseSensitive
})
got = tests.FilterWorkspaceSymbols(got, dirs)
if len(got) != len(expectedSymbols) {
t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
return
}
if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
t.Error(diff)
}
}
func (r *runner) callWorkspaceSymbols(t *testing.T, query string, options func(*source.Options)) []protocol.SymbolInformation {
t.Helper()
original := r.view.Options()
modified := original
options(&modified)
view, err := r.view.SetOptions(r.ctx, modified)
if err != nil {
t.Fatal(err)
}
defer r.view.SetOptions(r.ctx, original)
got, err := source.WorkspaceSymbols(r.ctx, []source.View{view}, query)
if err != nil {
t.Fatal(err)
}
return got
}
func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.SignatureHelp) {
_, rng, err := spanToRange(r.data, spn)
if err != nil {
t.Fatal(err)
}
fh, err := r.view.Snapshot().GetFile(spn.URI())
if err != nil {
t.Fatal(err)
}
gotSignature, gotActiveParameter, err := source.SignatureHelp(r.ctx, r.view.Snapshot(), fh, rng.Start)
if err != nil {
// Only fail if we got an error we did not expect.
if want != nil {
t.Fatalf("failed for %v: %v", spn, err)
}
return
}
if gotSignature == nil {
if want != nil {
t.Fatalf("got nil signature, but expected %v", want)
}
return
}
got := &protocol.SignatureHelp{
Signatures: []protocol.SignatureInformation{*gotSignature},
ActiveParameter: float64(gotActiveParameter),
}
if diff := tests.DiffSignatures(spn, got, want); diff != "" {
t.Error(diff)
}
}
func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) {
// This is a pure LSP feature, no source level functionality to be tested.
}
func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {
fh, err := r.view.Snapshot().GetFile(uri)
if err != nil {
t.Fatal(err)
}
got, err := source.CodeLens(r.ctx, r.view.Snapshot(), fh)
if err != nil {
t.Fatal(err)
}
if diff := tests.DiffCodeLens(uri, want, got); diff != "" {
t.Error(diff)
}
}
func spanToRange(data *tests.Data, spn span.Span) (*protocol.ColumnMapper, protocol.Range, error) {
m, err := data.Mapper(spn.URI())
if err != nil {
return nil, protocol.Range{}, err
}
srcRng, err := m.Range(spn)
if err != nil {
return nil, protocol.Range{}, err
}
return m, srcRng, nil
}