internal/lsp: fix link handling for links without schemes

Unfortunately, this can't be tested until golang/go#35880 is resolved,
because only the third-party xurls library detects links without
schemes.

Change-Id: I620581d6ce99c79ae89f3f22ea8ee634c3850a8e
Reviewed-on: https://go-review.googlesource.com/c/tools/+/214280
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/internal/lsp/link.go b/internal/lsp/link.go
index 96984b7..1352727 100644
--- a/internal/lsp/link.go
+++ b/internal/lsp/link.go
@@ -9,6 +9,7 @@
 	"fmt"
 	"go/ast"
 	"go/token"
+	"net/url"
 	"regexp"
 	"strconv"
 	"sync"
@@ -17,7 +18,6 @@
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/span"
 	"golang.org/x/tools/internal/telemetry/log"
-	"golang.org/x/tools/internal/telemetry/tag"
 )
 
 func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
@@ -30,6 +30,7 @@
 	if err != nil {
 		return nil, err
 	}
+	// TODO(golang/go#36501): Support document links for go.mod files.
 	if fh.Identity().Kind == source.Mod {
 		return nil, nil
 	}
@@ -41,60 +42,56 @@
 	ast.Inspect(file, func(node ast.Node) bool {
 		switch n := node.(type) {
 		case *ast.ImportSpec:
-			target, err := strconv.Unquote(n.Path.Value)
-			if err != nil {
-				log.Error(ctx, "cannot unquote import path", err, tag.Of("Path", n.Path.Value))
-				return false
+			// For import specs, provide a link to a documentation website, like https://pkg.go.dev.
+			if target, err := strconv.Unquote(n.Path.Value); err == nil {
+				target = fmt.Sprintf("https://%s/%s", view.Options().LinkTarget, target)
+
+				// Account for the quotation marks in the positions.
+				start, end := n.Path.Pos()+1, n.Path.End()-1
+				if l, err := toProtocolLink(view, m, target, start, end); err == nil {
+					links = append(links, l)
+				} else {
+					log.Error(ctx, "failed to create protocol link", err)
+				}
 			}
-			if target == "" {
-				return false
-			}
-			target = fmt.Sprintf("https://%s/%s", view.Options().LinkTarget, target)
-			l, err := toProtocolLink(view, m, target, n.Path.Pos()+1, n.Path.End()-1)
-			if err != nil {
-				log.Error(ctx, "cannot initialize DocumentLink", err, tag.Of("Path", n.Path.Value))
-				return false
-			}
-			links = append(links, l)
 			return false
 		case *ast.BasicLit:
-			if n.Kind != token.STRING {
-				return false
+			// Look for links in string literals.
+			if n.Kind == token.STRING {
+				links = append(links, findLinksInString(ctx, view, n.Value, n.Pos(), m)...)
 			}
-			l, err := findLinksInString(view, n.Value, n.Pos(), m)
-			if err != nil {
-				log.Error(ctx, "cannot find links in string", err)
-				return false
-			}
-			links = append(links, l...)
 			return false
 		}
 		return true
 	})
-
+	// Look for links in comments.
 	for _, commentGroup := range file.Comments {
 		for _, comment := range commentGroup.List {
-			l, err := findLinksInString(view, comment.Text, comment.Pos(), m)
-			if err != nil {
-				log.Error(ctx, "cannot find links in comment", err)
-				continue
-			}
-			links = append(links, l...)
+			links = append(links, findLinksInString(ctx, view, comment.Text, comment.Pos(), m)...)
 		}
 	}
 	return links, nil
 }
 
-func findLinksInString(view source.View, src string, pos token.Pos, m *protocol.ColumnMapper) ([]protocol.DocumentLink, error) {
+func findLinksInString(ctx context.Context, view source.View, src string, pos token.Pos, m *protocol.ColumnMapper) []protocol.DocumentLink {
 	var links []protocol.DocumentLink
 	for _, index := range view.Options().URLRegexp.FindAllIndex([]byte(src), -1) {
 		start, end := index[0], index[1]
 		startPos := token.Pos(int(pos) + start)
 		endPos := token.Pos(int(pos) + end)
-		target := src[start:end]
-		l, err := toProtocolLink(view, m, target, startPos, endPos)
+		url, err := url.Parse(src[start:end])
 		if err != nil {
-			return nil, err
+			log.Error(ctx, "failed to parse matching URL", err)
+			continue
+		}
+		// If the URL has no scheme, use https.
+		if url.Scheme == "" {
+			url.Scheme = "https"
+		}
+		l, err := toProtocolLink(view, m, url.String(), startPos, endPos)
+		if err != nil {
+			log.Error(ctx, "failed to create protocol link", err)
+			continue
 		}
 		links = append(links, l)
 	}
@@ -112,11 +109,12 @@
 		target := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number)
 		l, err := toProtocolLink(view, m, target, startPos, endPos)
 		if err != nil {
-			return nil, err
+			log.Error(ctx, "failed to create protocol link", err)
+			continue
 		}
 		links = append(links, l)
 	}
-	return links, nil
+	return links
 }
 
 func getIssueRegexp() *regexp.Regexp {
@@ -140,9 +138,8 @@
 	if err != nil {
 		return protocol.DocumentLink{}, err
 	}
-	l := protocol.DocumentLink{
+	return protocol.DocumentLink{
 		Range:  rng,
 		Target: target,
-	}
-	return l, nil
+	}, nil
 }