internal/lsp: add an importShortcut configuration

This configuration deals with the fact that VS Code has the same
shortcut for go-to-definition and clicking a link. Many users don't like
the behavior of this shortcut triggering both requests, so we allow
users to choose between each behavior.

Fixes golang/go#39065

Change-Id: I760c99c1fade4d157515d3b0fd647daf0c8314eb
Reviewed-on: https://go-review.googlesource.com/c/tools/+/242457
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 7218b3c..135d457 100644
--- a/internal/lsp/link.go
+++ b/internal/lsp/link.go
@@ -130,28 +130,31 @@
 		return true
 	})
 	var links []protocol.DocumentLink
-	for _, imp := range imports {
-		// For import specs, provide a link to a documentation website, like https://pkg.go.dev.
-		target, err := strconv.Unquote(imp.Path.Value)
-		if err != nil {
-			continue
+	// For import specs, provide a link to a documentation website, like
+	// https://pkg.go.dev.
+	if view.Options().ImportShortcut.ShowLinks() {
+		for _, imp := range imports {
+			target, err := strconv.Unquote(imp.Path.Value)
+			if err != nil {
+				continue
+			}
+			// See golang/go#36998: don't link to modules matching GOPRIVATE.
+			if view.IsGoPrivatePath(target) {
+				continue
+			}
+			if mod, version, ok := moduleAtVersion(ctx, target, ph); ok && strings.ToLower(view.Options().LinkTarget) == "pkg.go.dev" {
+				target = strings.Replace(target, mod, mod+"@"+version, 1)
+			}
+			// Account for the quotation marks in the positions.
+			start := imp.Path.Pos() + 1
+			end := imp.Path.End() - 1
+			target = fmt.Sprintf("https://%s/%s", view.Options().LinkTarget, target)
+			l, err := toProtocolLink(view, m, target, start, end, source.Go)
+			if err != nil {
+				return nil, err
+			}
+			links = append(links, l)
 		}
-		// See golang/go#36998: don't link to modules matching GOPRIVATE.
-		if view.IsGoPrivatePath(target) {
-			continue
-		}
-		if mod, version, ok := moduleAtVersion(ctx, target, ph); ok && strings.ToLower(view.Options().LinkTarget) == "pkg.go.dev" {
-			target = strings.Replace(target, mod, mod+"@"+version, 1)
-		}
-		// Account for the quotation marks in the positions.
-		start := imp.Path.Pos() + 1
-		end := imp.Path.End() - 1
-		target = fmt.Sprintf("https://%s/%s", view.Options().LinkTarget, target)
-		l, err := toProtocolLink(view, m, target, start, end, source.Go)
-		if err != nil {
-			return nil, err
-		}
-		links = append(links, l)
 	}
 	for _, s := range str {
 		l, err := findLinksInString(ctx, view, s.Value, s.Pos(), m, source.Go)
diff --git a/internal/lsp/source/identifier.go b/internal/lsp/source/identifier.go
index 08e1050..198dd5e 100644
--- a/internal/lsp/source/identifier.go
+++ b/internal/lsp/source/identifier.go
@@ -73,9 +73,12 @@
 var ErrNoIdentFound = errors.New("no identifier found")
 
 func findIdentifier(ctx context.Context, s Snapshot, pkg Package, file *ast.File, pos token.Pos) (*IdentifierInfo, error) {
-	// Handle import specs separately, as there is no formal position for a package declaration.
-	if result, err := importSpec(s, pkg, file, pos); result != nil || err != nil {
-		return result, err
+	// Handle import specs separately, as there is no formal position for a
+	// package declaration.
+	if s.View().Options().ImportShortcut.ShowDefinition() {
+		if result, err := importSpec(s, pkg, file, pos); result != nil || err != nil {
+			return result, err
+		}
 	}
 	path := pathEnclosingObjNode(file, pos)
 	if path == nil {
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index 88b840e..5d378cf 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -206,9 +206,14 @@
 	// StaticCheck enables additional analyses from staticcheck.io.
 	StaticCheck bool
 
-	// LinkTarget is the website used for documentation.
+	// LinkTarget is the website used for documentation. If empty, no link is
+	// provided.
 	LinkTarget string
 
+	// ImportShortcut specifies whether import statements should link to
+	// documentation or go to definitions. The default is both.
+	ImportShortcut ImportShortcut
+
 	// LocalPrefix is used to specify goimports's -local behavior.
 	LocalPrefix string
 
@@ -241,6 +246,22 @@
 	Gofumpt bool
 }
 
+type ImportShortcut int
+
+const (
+	Both ImportShortcut = iota
+	Link
+	Definition
+)
+
+func (s ImportShortcut) ShowLinks() bool {
+	return s == Both || s == Link
+}
+
+func (s ImportShortcut) ShowDefinition() bool {
+	return s == Both || s == Definition
+}
+
 type completionOptions struct {
 	deepCompletion    bool
 	unimported        bool
@@ -505,6 +526,18 @@
 	case "linkTarget":
 		result.setString(&o.LinkTarget)
 
+	case "importShortcut":
+		var s string
+		result.setString(&s)
+		switch s {
+		case "both":
+			o.ImportShortcut = Both
+		case "link":
+			o.ImportShortcut = Link
+		case "definition":
+			o.ImportShortcut = Definition
+		}
+
 	case "analyses":
 		result.setBoolMap(&o.UserEnabledAnalyses)