Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tools/gopls: add command line support for links #181

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/lsp/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
77 changes: 77 additions & 0 deletions internal/lsp/cmd/links.go
Original file line number Diff line number Diff line change
@@ -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 "<filename>" }
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
}
4 changes: 0 additions & 4 deletions internal/lsp/cmd/test/cmdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,6 @@ func (r *runner) SignatureHelp(t *testing.T, spn span.Span, expectedSignature *s
//TODO: add command line signature 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 CaptureStdOut(t testing.TB, f func()) string {
r, out, err := os.Pipe()
if err != nil {
Expand Down
36 changes: 36 additions & 0 deletions internal/lsp/cmd/test/links.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
39 changes: 3 additions & 36 deletions internal/lsp/lsp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -759,49 +759,16 @@ 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),
},
})
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)
}
}

Expand Down
55 changes: 55 additions & 0 deletions internal/lsp/tests/links.go
Original file line number Diff line number Diff line change
@@ -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 ""
}