internal/lsp: add document link handling for import paths to godoc

Change-Id: Ib2eef50047dfcc64110c264e77d648f959613b88
Reviewed-on: https://go-review.googlesource.com/c/tools/+/173698
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cmd/cmd_test.go b/internal/lsp/cmd/cmd_test.go
index 97eb94f..f91f359 100644
--- a/internal/lsp/cmd/cmd_test.go
+++ b/internal/lsp/cmd/cmd_test.go
@@ -57,6 +57,10 @@
 	//TODO: add command line signature tests when it works
 }
 
+func (r *runner) Link(t *testing.T, data tests.Links) {
+	//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 {
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index b3b4840..ade4cf5 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -84,6 +84,7 @@
 			DocumentSymbolProvider:          true,
 			HoverProvider:                   true,
 			DocumentHighlightProvider:       true,
+			DocumentLinkProvider:            &protocol.DocumentLinkOptions{},
 			SignatureHelpProvider: &protocol.SignatureHelpOptions{
 				TriggerCharacters: []string{"(", ","},
 			},
diff --git a/internal/lsp/link.go b/internal/lsp/link.go
new file mode 100644
index 0000000..33e9188
--- /dev/null
+++ b/internal/lsp/link.go
@@ -0,0 +1,45 @@
+// 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 (
+	"context"
+	"strconv"
+
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/span"
+)
+
+func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	f, m, err := newColumnMap(ctx, view, uri)
+	if err != nil {
+		return nil, err
+	}
+	// find the import block
+	ast := f.GetAST(ctx)
+	var result []protocol.DocumentLink
+	for _, imp := range ast.Imports {
+		spn, err := span.NewRange(f.GetFileSet(ctx), imp.Pos(), imp.End()).Span()
+		if err != nil {
+			return nil, err
+		}
+		rng, err := m.Range(spn)
+		if err != nil {
+			return nil, err
+		}
+		target, err := strconv.Unquote(imp.Path.Value)
+		if err != nil {
+			continue
+		}
+		target = "https://godoc.org/" + target
+		result = append(result, protocol.DocumentLink{
+			Range:  rng,
+			Target: target,
+		})
+	}
+	return result, nil
+}
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index f51dcb0..a1498be 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -536,6 +536,41 @@
 	return ""
 }
 
+func (r *runner) Link(t *testing.T, data tests.Links) {
+	for uri, wantLinks := range data {
+		m := r.mapper(uri)
+		gotLinks, err := r.server.DocumentLink(context.Background(), &protocol.DocumentLinkParams{
+			TextDocument: protocol.TextDocumentIdentifier{
+				URI: protocol.NewURI(uri),
+			},
+		})
+		if err != nil {
+			t.Fatal(err)
+		}
+		links := make(map[span.Span]string, len(wantLinks))
+		for _, link := range wantLinks {
+			links[link.Src] = link.Target
+		}
+		for _, link := range gotLinks {
+			spn, err := m.RangeSpan(link.Range)
+			if err != nil {
+				t.Fatal(err)
+			}
+			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 {
 	fname, err := uri.Filename()
 	if err != nil {
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 63533e8..d94bd93 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -210,8 +210,8 @@
 	return nil, notImplemented("ResolveCodeLens")
 }
 
-func (s *Server) DocumentLink(context.Context, *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
-	return nil, nil // ignore
+func (s *Server) DocumentLink(ctx context.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
+	return s.documentLink(ctx, params)
 }
 
 func (s *Server) ResolveDocumentLink(context.Context, *protocol.DocumentLink) (*protocol.DocumentLink, error) {
diff --git a/internal/lsp/testdata/links/links.go b/internal/lsp/testdata/links/links.go
new file mode 100644
index 0000000..b97da74
--- /dev/null
+++ b/internal/lsp/testdata/links/links.go
@@ -0,0 +1,12 @@
+package links
+
+import (
+	"fmt" //@link(re`".*"`,"https://godoc.org/fmt")
+
+	"golang.org/x/tools/internal/lsp/foo" //@link(re`".*"`,"https://godoc.org/golang.org/x/tools/internal/lsp/foo")
+)
+
+var (
+	_ fmt.Formatter
+	_ foo.StructFoo
+)
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index 4a0363b..69fafca 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -36,6 +36,7 @@
 	ExpectedSymbolsCount           = 1
 	ExpectedSignaturesCount        = 19
 	ExpectedCompletionSnippetCount = 9
+	ExpectedLinksCount             = 2
 )
 
 const (
@@ -57,6 +58,7 @@
 type Symbols map[span.URI][]source.Symbol
 type SymbolsChildren map[string][]source.Symbol
 type Signatures map[span.Span]source.SignatureInformation
+type Links map[span.URI][]Link
 
 type Data struct {
 	Config             packages.Config
@@ -71,6 +73,7 @@
 	Symbols            Symbols
 	symbolsChildren    SymbolsChildren
 	Signatures         Signatures
+	Links              Links
 
 	t         testing.TB
 	fragments map[string]string
@@ -85,6 +88,7 @@
 	Highlight(*testing.T, Highlights)
 	Symbol(*testing.T, Symbols)
 	Signature(*testing.T, Signatures)
+	Link(*testing.T, Links)
 }
 
 type Definition struct {
@@ -101,6 +105,11 @@
 	PlaceholderSnippet string
 }
 
+type Link struct {
+	Src    span.Span
+	Target string
+}
+
 func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
 	t.Helper()
 
@@ -114,6 +123,7 @@
 		Symbols:            make(Symbols),
 		symbolsChildren:    make(SymbolsChildren),
 		Signatures:         make(Signatures),
+		Links:              make(Links),
 
 		t:         t,
 		dir:       dir,
@@ -180,6 +190,7 @@
 		"symbol":    data.collectSymbols,
 		"signature": data.collectSignatures,
 		"snippet":   data.collectCompletionSnippets,
+		"link":      data.collectLinks,
 	}); err != nil {
 		t.Fatal(err)
 	}
@@ -264,6 +275,18 @@
 		}
 		tests.Signature(t, data.Signatures)
 	})
+
+	t.Run("Links", func(t *testing.T) {
+		t.Helper()
+		linksCount := 0
+		for _, want := range data.Links {
+			linksCount += len(want)
+		}
+		if linksCount != ExpectedLinksCount {
+			t.Errorf("got %v links expected %v", linksCount, ExpectedLinksCount)
+		}
+		tests.Link(t, data.Links)
+	})
 }
 
 func (data *Data) Golden(tag string, target string, update func(golden string) error) []byte {
@@ -379,3 +402,11 @@
 		PlaceholderSnippet: placeholder,
 	}
 }
+
+func (data *Data) collectLinks(spn span.Span, link string) {
+	uri := spn.URI()
+	data.Links[uri] = append(data.Links[uri], Link{
+		Src:    spn,
+		Target: link,
+	})
+}