tools/gopls: provide markdown for completion and signature help

If the client prefers markdown, provide markdown.

There is a change to the LSP stubs, with Documentation fields becoming
Or-types (string|MarkupContent). If the client prefers, the returned
Documentation is markdown.

Fixes: golang/go#57300
Change-Id: I57300146333552da3849c1b6bfb97793042faee2
Reviewed-on: https://go-review.googlesource.com/c/tools/+/463377
Reviewed-by: Robert Findley <rfindley@google.com>
Run-TryBot: Peter Weinberger <pjw@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/gopls/internal/lsp/cmd/signature.go b/gopls/internal/lsp/cmd/signature.go
index 64c892e..4d47cd2 100644
--- a/gopls/internal/lsp/cmd/signature.go
+++ b/gopls/internal/lsp/cmd/signature.go
@@ -73,8 +73,15 @@
 	// see toProtocolSignatureHelp in lsp/signature_help.go
 	signature := s.Signatures[0]
 	fmt.Printf("%s\n", signature.Label)
-	if signature.Documentation != "" {
-		fmt.Printf("\n%s\n", signature.Documentation)
+	switch x := signature.Documentation.Value.(type) {
+	case string:
+		if x != "" {
+			fmt.Printf("\n%s\n", x)
+		}
+	case protocol.MarkupContent:
+		if x.Value != "" {
+			fmt.Printf("\n%s\n", x.Value)
+		}
 	}
 
 	return nil
diff --git a/gopls/internal/lsp/completion.go b/gopls/internal/lsp/completion.go
index d63e3a3..b2e50cc 100644
--- a/gopls/internal/lsp/completion.go
+++ b/gopls/internal/lsp/completion.go
@@ -101,6 +101,15 @@
 			continue
 		}
 
+		doc := &protocol.Or_CompletionItem_documentation{
+			Value: protocol.MarkupContent{
+				Kind:  protocol.Markdown,
+				Value: source.CommentToMarkdown(candidate.Documentation),
+			},
+		}
+		if options.PreferredContentFormat != protocol.Markdown {
+			doc.Value = candidate.Documentation
+		}
 		item := protocol.CompletionItem{
 			Label:  candidate.Label,
 			Detail: candidate.Detail,
@@ -121,7 +130,7 @@
 			FilterText: strings.TrimLeft(candidate.InsertText, "&*"),
 
 			Preselect:     i == 0,
-			Documentation: candidate.Documentation,
+			Documentation: doc,
 			Tags:          candidate.Tags,
 			Deprecated:    candidate.Deprecated,
 		}
diff --git a/gopls/internal/lsp/completion_test.go b/gopls/internal/lsp/completion_test.go
index 9a72f12..cd3bcec 100644
--- a/gopls/internal/lsp/completion_test.go
+++ b/gopls/internal/lsp/completion_test.go
@@ -115,11 +115,13 @@
 
 	toProtocolCompletionItem := func(item *completion.CompletionItem) protocol.CompletionItem {
 		pItem := protocol.CompletionItem{
-			Label:         item.Label,
-			Kind:          item.Kind,
-			Detail:        item.Detail,
-			Documentation: item.Documentation,
-			InsertText:    item.InsertText,
+			Label:  item.Label,
+			Kind:   item.Kind,
+			Detail: item.Detail,
+			Documentation: &protocol.Or_CompletionItem_documentation{
+				Value: item.Documentation,
+			},
+			InsertText: item.InsertText,
 			TextEdit: &protocol.TextEdit{
 				NewText: item.Snippet(),
 			},
diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go
index 70d5e5e..eaeca85 100644
--- a/gopls/internal/lsp/fake/editor.go
+++ b/gopls/internal/lsp/fake/editor.go
@@ -1184,6 +1184,23 @@
 	return e.Server.Implementation(ctx, params)
 }
 
+func (e *Editor) SignatureHelp(ctx context.Context, loc protocol.Location) (*protocol.SignatureHelp, error) {
+	if e.Server == nil {
+		return nil, nil
+	}
+	path := e.sandbox.Workdir.URIToPath(loc.URI)
+	e.mu.Lock()
+	_, ok := e.buffers[path]
+	e.mu.Unlock()
+	if !ok {
+		return nil, fmt.Errorf("buffer %q is not open", path)
+	}
+	params := &protocol.SignatureHelpParams{
+		TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc),
+	}
+	return e.Server.SignatureHelp(ctx, params)
+}
+
 func (e *Editor) RenameFile(ctx context.Context, oldPath, newPath string) error {
 	closed, opened, err := e.renameBuffers(ctx, oldPath, newPath)
 	if err != nil {
diff --git a/gopls/internal/lsp/protocol/generate/tables.go b/gopls/internal/lsp/protocol/generate/tables.go
index 93a0436..838990c 100644
--- a/gopls/internal/lsp/protocol/generate/tables.go
+++ b/gopls/internal/lsp/protocol/generate/tables.go
@@ -128,11 +128,10 @@
 
 // For gopls compatibility, use a different, typically more restrictive, type for some fields.
 var renameProp = map[prop]string{
-	{"CancelParams", "id"}:              "interface{}",
-	{"Command", "arguments"}:            "[]json.RawMessage",
-	{"CompletionItem", "documentation"}: "string",
-	{"CompletionItem", "textEdit"}:      "TextEdit",
-	{"Diagnostic", "code"}:              "interface{}",
+	{"CancelParams", "id"}:         "interface{}",
+	{"Command", "arguments"}:       "[]json.RawMessage",
+	{"CompletionItem", "textEdit"}: "TextEdit",
+	{"Diagnostic", "code"}:         "interface{}",
 
 	{"DocumentDiagnosticReportPartialResult", "relatedDocuments"}: "map[DocumentURI]interface{}",
 
@@ -181,7 +180,6 @@
 	{"ServerCapabilities", "typeDefinitionProvider"}:          "interface{}",
 	{"ServerCapabilities", "typeHierarchyProvider"}:           "interface{}",
 	{"ServerCapabilities", "workspaceSymbolProvider"}:         "bool",
-	{"SignatureInformation", "documentation"}:                 "string",
 	{"TextDocumentEdit", "edits"}:                             "[]TextEdit",
 	{"TextDocumentSyncOptions", "save"}:                       "SaveOptions",
 	{"WorkspaceEdit", "documentChanges"}:                      "[]DocumentChanges",
diff --git a/gopls/internal/lsp/protocol/tsclient.go b/gopls/internal/lsp/protocol/tsclient.go
index 116a81b..b51b1f6 100644
--- a/gopls/internal/lsp/protocol/tsclient.go
+++ b/gopls/internal/lsp/protocol/tsclient.go
@@ -7,7 +7,7 @@
 package protocol
 
 // Code generated from version 3.17.0 of protocol/metaModel.json.
-// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-14)
+// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-23)
 
 import (
 	"context"
diff --git a/gopls/internal/lsp/protocol/tsjson.go b/gopls/internal/lsp/protocol/tsjson.go
index a0f11c6..0c8bf10 100644
--- a/gopls/internal/lsp/protocol/tsjson.go
+++ b/gopls/internal/lsp/protocol/tsjson.go
@@ -7,7 +7,7 @@
 package protocol
 
 // Code generated from version 3.17.0 of protocol/metaModel.json.
-// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-14)
+// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-23)
 
 import "encoding/json"
 
diff --git a/gopls/internal/lsp/protocol/tsprotocol.go b/gopls/internal/lsp/protocol/tsprotocol.go
index becb255..54a878c 100644
--- a/gopls/internal/lsp/protocol/tsprotocol.go
+++ b/gopls/internal/lsp/protocol/tsprotocol.go
@@ -7,7 +7,7 @@
 package protocol
 
 // Code generated from version 3.17.0 of protocol/metaModel.json.
-// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-14)
+// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-23)
 
 import "encoding/json"
 
@@ -667,7 +667,7 @@
 	 */
 	Detail string `json:"detail,omitempty"`
 	// A human-readable string that represents a doc-comment.
-	Documentation string `json:"documentation,omitempty"`
+	Documentation *Or_CompletionItem_documentation `json:"documentation,omitempty"`
 	/*
 	 * Indicates if this item is deprecated.
 	 * @deprecated Use `tags` instead.
@@ -2970,8 +2970,8 @@
 	Text string `json:"text"`
 }
 
-// created for Literal (Lit_TextDocumentFilter_Item1)
-type Msg_TextDocumentFilter struct { // line 14193
+// created for Literal (Lit_TextDocumentFilter_Item0)
+type Msg_TextDocumentFilter struct { // line 14160
 	// A language id, like `typescript`.
 	Language string `json:"language"`
 	// A Uri [scheme](#Uri.scheme), like `file` or `untitled`.
@@ -4885,7 +4885,7 @@
 	 * The human-readable doc-comment of this signature. Will be shown
 	 * in the UI but can be omitted.
 	 */
-	Documentation string `json:"documentation,omitempty"`
+	Documentation *Or_SignatureInformation_documentation `json:"documentation,omitempty"`
 	// The parameters of this signature.
 	Parameters []ParameterInformation `json:"parameters,omitempty"`
 	/*
diff --git a/gopls/internal/lsp/protocol/tsserver.go b/gopls/internal/lsp/protocol/tsserver.go
index 6446a01..d968010 100644
--- a/gopls/internal/lsp/protocol/tsserver.go
+++ b/gopls/internal/lsp/protocol/tsserver.go
@@ -7,7 +7,7 @@
 package protocol
 
 // Code generated from version 3.17.0 of protocol/metaModel.json.
-// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-14)
+// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-23)
 
 import (
 	"context"
diff --git a/gopls/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go
index 1207a14..cdd39e5 100644
--- a/gopls/internal/lsp/regtest/wrappers.go
+++ b/gopls/internal/lsp/regtest/wrappers.go
@@ -417,6 +417,16 @@
 	}
 }
 
+// SignatureHelp wraps Editor.SignatureHelp, calling t.Fatal on error
+func (e *Env) SignatureHelp(loc protocol.Location) *protocol.SignatureHelp {
+	e.T.Helper()
+	sighelp, err := e.Editor.SignatureHelp(e.Ctx, loc)
+	if err != nil {
+		e.T.Fatal(err)
+	}
+	return sighelp
+}
+
 // Completion executes a completion request on the server.
 func (e *Env) Completion(loc protocol.Location) *protocol.CompletionList {
 	e.T.Helper()
diff --git a/gopls/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go
index 1d43dc5..4902496 100644
--- a/gopls/internal/lsp/source/signature_help.go
+++ b/gopls/internal/lsp/source/signature_help.go
@@ -10,6 +10,7 @@
 	"go/ast"
 	"go/token"
 	"go/types"
+	"strings"
 
 	"golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
@@ -117,7 +118,7 @@
 	}
 	return &protocol.SignatureInformation{
 		Label:         name + s.Format(),
-		Documentation: s.doc,
+		Documentation: stringToSigInfoDocumentation(s.doc, snapshot.View().Options()),
 		Parameters:    paramInfo,
 	}, activeParam, nil
 }
@@ -134,7 +135,7 @@
 	activeParam := activeParameter(callExpr, len(sig.params), sig.variadic, pos)
 	return &protocol.SignatureInformation{
 		Label:         sig.name + sig.Format(),
-		Documentation: sig.doc,
+		Documentation: stringToSigInfoDocumentation(sig.doc, snapshot.View().Options()),
 		Parameters:    paramInfo,
 	}, activeParam, nil
 
@@ -165,3 +166,21 @@
 	}
 	return activeParam
 }
+
+func stringToSigInfoDocumentation(s string, options *Options) *protocol.Or_SignatureInformation_documentation {
+	v := s
+	k := protocol.PlainText
+	if options.PreferredContentFormat == protocol.Markdown {
+		v = CommentToMarkdown(s)
+		// whether or not content is newline terminated may not matter for LSP clients,
+		// but our tests expect trailing newlines to be stripped.
+		v = strings.TrimSuffix(v, "\n") // TODO(pjw): change the golden files
+		k = protocol.Markdown
+	}
+	return &protocol.Or_SignatureInformation_documentation{
+		Value: protocol.MarkupContent{
+			Kind:  k,
+			Value: v,
+		},
+	}
+}
diff --git a/gopls/internal/regtest/misc/hover_test.go b/gopls/internal/regtest/misc/hover_test.go
index 6b1d243..7054993 100644
--- a/gopls/internal/regtest/misc/hover_test.go
+++ b/gopls/internal/regtest/misc/hover_test.go
@@ -10,7 +10,9 @@
 	"testing"
 
 	"golang.org/x/tools/gopls/internal/lsp/fake"
+	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	. "golang.org/x/tools/gopls/internal/lsp/regtest"
+	"golang.org/x/tools/internal/testenv"
 )
 
 func TestHoverUnexported(t *testing.T) {
@@ -270,3 +272,62 @@
 		env.Hover(env.RegexpSearch("go.mod", "go")) // no panic
 	})
 }
+
+func TestHoverCompletionMarkdown(t *testing.T) {
+	testenv.NeedsGo1Point(t, 19)
+	const source = `
+-- go.mod --
+module mod.com
+go 1.19
+-- main.go --
+package main
+// Just says [hello].
+//
+// [hello]: https://en.wikipedia.org/wiki/Hello
+func Hello() string {
+	Hello() //Here
+    return "hello"
+}
+`
+	Run(t, source, func(t *testing.T, env *Env) {
+		// Hover, Completion, and SignatureHelp should all produce markdown
+		// check that the markdown for SignatureHelp and Completion are
+		// the same, and contained in that for Hover (up to trailing \n)
+		env.OpenFile("main.go")
+		loc := env.RegexpSearch("main.go", "func (Hello)")
+		hover, _ := env.Hover(loc)
+		hoverContent := hover.Value
+
+		loc = env.RegexpSearch("main.go", "//Here")
+		loc.Range.Start.Character -= 3 // Hello(_) //Here
+		completions := env.Completion(loc)
+		signatures := env.SignatureHelp(loc)
+
+		if len(completions.Items) != 1 {
+			t.Errorf("got %d completions, expected 1", len(completions.Items))
+		}
+		if len(signatures.Signatures) != 1 {
+			t.Errorf("got %d signatures, expected 1", len(signatures.Signatures))
+		}
+		item := completions.Items[0].Documentation.Value
+		var itemContent string
+		if x, ok := item.(protocol.MarkupContent); !ok || x.Kind != protocol.Markdown {
+			t.Fatalf("%#v is not markdown", item)
+		} else {
+			itemContent = strings.Trim(x.Value, "\n")
+		}
+		sig := signatures.Signatures[0].Documentation.Value
+		var sigContent string
+		if x, ok := sig.(protocol.MarkupContent); !ok || x.Kind != protocol.Markdown {
+			t.Fatalf("%#v is not markdown", item)
+		} else {
+			sigContent = x.Value
+		}
+		if itemContent != sigContent {
+			t.Errorf("item:%q not sig:%q", itemContent, sigContent)
+		}
+		if !strings.Contains(hoverContent, itemContent) {
+			t.Errorf("hover:%q does not containt sig;%q", hoverContent, sigContent)
+		}
+	})
+}