From b8f202ca5eca7fbe2e50f129aa1b4f7802add528 Mon Sep 17 00:00:00 2001 From: Kalman Bekesi Date: Fri, 1 Nov 2019 06:48:57 +0000 Subject: [PATCH] tools/gopls: add command line support for links This adds support for calling links from the gopls command line, e.g. $ gopls links ~/tmp/foo/main.go Optional arguments are: -json, which emits range and uri in JSON With no arguments, a unique list of links are emitted. Updates golang/go#32875 Change-Id: I1e7cbf00a636c05ccf21bd544d9a5b7742d5d70b GitHub-Last-Rev: 7ed1e4612186bce4077d3c73f2407cf6def211d9 GitHub-Pull-Request: golang/tools#181 Reviewed-on: https://go-review.googlesource.com/c/tools/+/203297 Reviewed-by: Rebecca Stambler Run-TryBot: Rebecca Stambler TryBot-Result: Gobot Gobot --- internal/lsp/cmd/cmd.go | 1 + internal/lsp/cmd/links.go | 77 ++++++++++++++++++++++++++++++++ internal/lsp/cmd/test/cmdtest.go | 4 -- internal/lsp/cmd/test/links.go | 36 +++++++++++++++ internal/lsp/lsp_test.go | 39 ++-------------- internal/lsp/tests/links.go | 55 +++++++++++++++++++++++ 6 files changed, 172 insertions(+), 40 deletions(-) create mode 100644 internal/lsp/cmd/links.go create mode 100644 internal/lsp/cmd/test/links.go create mode 100644 internal/lsp/tests/links.go diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go index 519c196258..0d00c38fc0 100644 --- a/internal/lsp/cmd/cmd.go +++ b/internal/lsp/cmd/cmd.go @@ -143,6 +143,7 @@ func (app *Application) commands() []tool.Application { &bug{}, &check{app: app}, &format{app: app}, + &links{app: app}, &imports{app: app}, &query{app: app}, &references{app: app}, diff --git a/internal/lsp/cmd/links.go b/internal/lsp/cmd/links.go new file mode 100644 index 0000000000..a93ae8fdb9 --- /dev/null +++ b/internal/lsp/cmd/links.go @@ -0,0 +1,77 @@ +// Copyright 2019 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 cmd + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + + "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/tool" + errors "golang.org/x/xerrors" +) + +// links implements the links verb for gopls. +type links struct { + JSON bool `flag:"json" help:"emit document links in JSON format"` + + app *Application +} + +func (l *links) Name() string { return "links" } +func (l *links) Usage() string { return "" } +func (l *links) ShortHelp() string { return "list links in a file" } +func (l *links) DetailedHelp(f *flag.FlagSet) { + fmt.Fprintf(f.Output(), ` +Example: list links contained within a file: + +  $ gopls links internal/lsp/cmd/check.go + +gopls links flags are: +`) + f.PrintDefaults() +} + +// Run finds all the links within a document +// - if -json is specified, outputs location range and uri +// - otherwise, prints the a list of unique links +func (l *links) Run(ctx context.Context, args ...string) error { + if len(args) != 1 { + return tool.CommandLineErrorf("links expects 1 argument") + } + conn, err := l.app.connect(ctx) + if err != nil { + return err + } + defer conn.terminate(ctx) + + from := span.Parse(args[0]) + uri := from.URI() + file := conn.AddFile(ctx, uri) + if file.err != nil { + return file.err + } + results, err := conn.DocumentLink(ctx, &protocol.DocumentLinkParams{ + TextDocument: protocol.TextDocumentIdentifier{ + URI: protocol.NewURI(uri), + }, + }) + if err != nil { + return errors.Errorf("%v: %v", from, err) + } + if l.JSON { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", "\t") + return enc.Encode(results) + } + for _, v := range results { + fmt.Println(v.Target) + } + return nil +} diff --git a/internal/lsp/cmd/test/cmdtest.go b/internal/lsp/cmd/test/cmdtest.go index 2a6276a1f1..adced2a303 100644 --- a/internal/lsp/cmd/test/cmdtest.go +++ b/internal/lsp/cmd/test/cmdtest.go @@ -82,10 +82,6 @@ func (r *runner) Symbol(t *testing.T, uri span.URI, expectedSymbols []protocol.D //TODO: add command line symbol tests when it works } -func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) { - //TODO: add command line link tests when it works -} - func (r *runner) Implementation(t *testing.T, spn span.Span, imp tests.Implementations) { //TODO: add implements tests when it works } diff --git a/internal/lsp/cmd/test/links.go b/internal/lsp/cmd/test/links.go new file mode 100644 index 0000000000..79a679965c --- /dev/null +++ b/internal/lsp/cmd/test/links.go @@ -0,0 +1,36 @@ +// Copyright 2019 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 cmdtest + +import ( + "encoding/json" + "testing" + + "golang.org/x/tools/internal/lsp/cmd" + "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/tool" +) + +func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) { + m, err := r.data.Mapper(uri) + if err != nil { + t.Fatal(err) + } + args := []string{"links", "-json", uri.Filename()} + app := cmd.New("gopls-test", r.data.Config.Dir, r.data.Exported.Config.Env, r.options) + out := CaptureStdOut(t, func() { + _ = tool.Run(r.ctx, app, args) + }) + var got []protocol.DocumentLink + err = json.Unmarshal([]byte(out), &got) + if err != nil { + t.Fatal(err) + } + if diff := tests.DiffLinks(m, wantLinks, got); diff != "" { + t.Error(diff) + } +} diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index 0a8fc0fbaa..7e3a555c7b 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -797,7 +797,7 @@ func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) { if err != nil { t.Fatal(err) } - gotLinks, err := r.server.DocumentLink(r.ctx, &protocol.DocumentLinkParams{ + got, err := r.server.DocumentLink(r.ctx, &protocol.DocumentLinkParams{ TextDocument: protocol.TextDocumentIdentifier{ URI: protocol.NewURI(uri), }, @@ -805,41 +805,8 @@ func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) { 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) + if diff := tests.DiffLinks(m, wantLinks, got); diff != "" { + t.Error(diff) } } diff --git a/internal/lsp/tests/links.go b/internal/lsp/tests/links.go new file mode 100644 index 0000000000..07fc3ef17f --- /dev/null +++ b/internal/lsp/tests/links.go @@ -0,0 +1,55 @@ +// Copyright 2019 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 tests + +import ( + "fmt" + "go/token" + + "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/span" +) + +// DiffLinks takes the links we got and checks if they are located within the source or a Note. +// If the link is within a Note, the link is removed. +// Returns an diff comment if there are differences and empty string if no diffs +func DiffLinks(mapper *protocol.ColumnMapper, wantLinks []Link, gotLinks []protocol.DocumentLink) string { + 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 := mapper.RangeSpan(link.Range) + if err != nil { + return fmt.Sprintf("%v", 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 { + return fmt.Sprintf("for %v want %v, got %v\n", spn, link.Target, target) + } + } else { + return fmt.Sprintf("unexpected link %v:%v\n", spn, link.Target) + } + } + for spn, target := range links { + return fmt.Sprintf("missing link %v:%v\n", spn, target) + } + return "" +}