diff --git a/langserver/hover.go b/langserver/hover.go index e5a29d5c..e53012c1 100644 --- a/langserver/hover.go +++ b/langserver/hover.go @@ -59,7 +59,7 @@ func (h *LangHandler) handleHover(ctx context.Context, conn jsonrpc2.JSONRPC2, r r := rangeForNode(fset, node) if pkgName := packageStatementName(fset, pkg.Files, node); pkgName != "" { return &lsp.Hover{ - Contents: maybeAddComments(comments, []lsp.MarkedString{{Language: "go", Value: "package " + pkgName}}), + Contents: maybeAddComments(comments, []lsp.MarkupContent{lsp.MarkedString{Language: "go", Value: "package " + pkgName}}), Range: &r, }, nil } @@ -137,7 +137,7 @@ func (h *LangHandler) handleHover(ctx context.Context, conn jsonrpc2.JSONRPC2, r return doc.Text() } - contents := maybeAddComments(findComments(o), []lsp.MarkedString{{Language: "go", Value: s}}) + contents := maybeAddComments(findComments(o), []lsp.MarkupContent{lsp.MarkedString{Language: "go", Value: s}}) if extra != "" { // If we have extra info, ensure it comes after the usually // more useful documentation @@ -164,7 +164,7 @@ func packageStatementName(fset *token.FileSet, files []*ast.File, node *ast.Iden // maybeAddComments appends the specified comments converted to Markdown godoc // form to the specified contents slice, if the comments string is not empty. -func maybeAddComments(comments string, contents []lsp.MarkedString) []lsp.MarkedString { +func maybeAddComments(comments string, contents []lsp.MarkupContent) []lsp.MarkupContent { if comments == "" { return contents } @@ -294,7 +294,7 @@ func (h *LangHandler) handleHoverGodef(ctx context.Context, conn jsonrpc2.JSONRP comments := packageDoc(pkgFiles, bpkg.Name) return &lsp.Hover{ - Contents: maybeAddComments(comments, []lsp.MarkedString{{Language: "go", Value: fmt.Sprintf("package %s (%q)", bpkg.Name, bpkg.ImportPath)}}), + Contents: maybeAddComments(comments, []lsp.MarkupContent{lsp.MarkedString{Language: "go", Value: fmt.Sprintf("package %s (%q)", bpkg.Name, bpkg.ImportPath)}}), // TODO(slimsag): I think we can add Range here, but not exactly // sure. res.Start and res.End are only present if it's a package @@ -437,7 +437,7 @@ func findDocTarget(fset *token.FileSet, target token.Position, in interface{}) i // *doc.Type // *doc.Func // -func fmtDocObject(fset *token.FileSet, x interface{}, target token.Position) ([]lsp.MarkedString, ast.Node) { +func fmtDocObject(fset *token.FileSet, x interface{}, target token.Position) ([]lsp.MarkupContent, ast.Node) { switch v := x.(type) { case *doc.Value: // Vars and Consts // Sort the specs by distance to find the one nearest to target. @@ -455,7 +455,7 @@ func fmtDocObject(fset *token.FileSet, x interface{}, target token.Position) ([] cpy := *spec cpy.Doc = nil value := v.Decl.Tok.String() + " " + fmtNode(fset, &cpy) - return maybeAddComments(doc, []lsp.MarkedString{{Language: "go", Value: value}}), spec + return maybeAddComments(doc, []lsp.MarkupContent{lsp.MarkedString{Language: "go", Value: value}}), spec case *doc.Type: // Type declarations spec := v.Decl.Specs[0].(*ast.TypeSpec) @@ -468,7 +468,7 @@ func fmtDocObject(fset *token.FileSet, x interface{}, target token.Position) ([] if fset.Position(field.Pos()).Offset == target.Offset { // An exact match. value := fmt.Sprintf("func (%s).%s%s", spec.Name.Name, field.Names[0].Name, strings.TrimPrefix(fmtNode(fset, field.Type), "func")) - return maybeAddComments(field.Doc.Text(), []lsp.MarkedString{{Language: "go", Value: value}}), field + return maybeAddComments(field.Doc.Text(), []lsp.MarkupContent{lsp.MarkedString{Language: "go", Value: value}}), field } } @@ -478,14 +478,14 @@ func fmtDocObject(fset *token.FileSet, x interface{}, target token.Position) ([] if fset.Position(field.Pos()).Offset == target.Offset { // An exact match. value := fmt.Sprintf("struct field %s %s", field.Names[0], fmtNode(fset, field.Type)) - return maybeAddComments(field.Doc.Text(), []lsp.MarkedString{{Language: "go", Value: value}}), field + return maybeAddComments(field.Doc.Text(), []lsp.MarkupContent{lsp.MarkedString{Language: "go", Value: value}}), field } } } // Formatting of all type declarations: structs, interfaces, integers, etc. name := v.Decl.Tok.String() + " " + spec.Name.Name + " " + typeName(fset, spec.Type) - res := []lsp.MarkedString{{Language: "go", Value: name}} + res := []lsp.MarkupContent{lsp.MarkedString{Language: "go", Value: name}} doc := spec.Doc.Text() if doc == "" { @@ -499,7 +499,7 @@ func fmtDocObject(fset *token.FileSet, x interface{}, target token.Position) ([] return res, spec case *doc.Func: // Functions - return maybeAddComments(v.Doc, []lsp.MarkedString{{Language: "go", Value: fmtNode(fset, v.Decl)}}), v.Decl + return maybeAddComments(v.Doc, []lsp.MarkupContent{lsp.MarkedString{Language: "go", Value: fmtNode(fset, v.Decl)}}), v.Decl default: panic("unreachable") } diff --git a/pkg/lsp/service.go b/pkg/lsp/service.go index cb697a63..b2cde118 100644 --- a/pkg/lsp/service.go +++ b/pkg/lsp/service.go @@ -296,12 +296,19 @@ type CompletionParams struct { } type Hover struct { - Contents []MarkedString `json:"contents,omitempty"` - Range *Range `json:"range,omitempty"` + Contents []MarkupContent `json:"contents,omitempty"` + Range *Range `json:"range,omitempty"` } +type MarkupContent interface { + MarshalJSON() ([]byte, error) +} + +// Deprecated: Use MarkdownString instead. type MarkedString markedString +type MarkdownString markdownString + type markedString struct { Language string `json:"language"` Value string `json:"value"` @@ -338,6 +345,37 @@ func RawMarkedString(s string) MarkedString { return MarkedString{Value: s, isRawString: true} } +type markdownString struct { + // The markdown string. + Value string `json:"value"` + // Indicates that this markdown string is from a trusted source. Only *trusted* + // markdown supports links that execute commands, e.g. `[Run it](command:myCommandId)` + IsTrusted bool `json:"isTrusted"` +} + +func (m *MarkdownString) UnmarshalJSON(data []byte) error { + if d := strings.TrimSpace(string(data)); len(d) > 0 && d[0] == '"' { + // Raw string + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + m.Value = s + m.IsTrusted = true + return nil + } + // Language string + ms := (*markdownString)(m) + return json.Unmarshal(data, ms) +} + +func (m MarkdownString) MarshalJSON() ([]byte, error) { + if m.IsTrusted { + return json.Marshal(m.Value) + } + return json.Marshal((markdownString)(m)) +} + type SignatureHelp struct { Signatures []SignatureInformation `json:"signatures"` ActiveSignature int `json:"activeSignature"` diff --git a/pkg/lsp/service_test.go b/pkg/lsp/service_test.go index d242710e..fcd34ad2 100644 --- a/pkg/lsp/service_test.go +++ b/pkg/lsp/service_test.go @@ -98,3 +98,30 @@ func TestMarkedString_MarshalUnmarshalJSON(t *testing.T) { } } } + +func TestMarkdownString_MarshalUnmarshalJSON(t *testing.T) { + tests := []struct { + data []byte + want MarkdownString + }{{ + data: []byte(`{"value":"## h2 heading"}`), + want: MarkdownString{Value: "## h2 heading", IsTrusted: false}, + }, { + data: []byte(`"# h1 heading"`), + want: MarkdownString{Value: "# h1 heading", IsTrusted: true}, + }, + } + + for _, test := range tests { + var m MarkdownString + if err := json.Unmarshal(test.data, &m); err != nil { + t.Errorf("json.Unmarshal error: %s", err) + continue + } + if !reflect.DeepEqual(test.want, m) { + t.Errorf("Unmarshaled %q, expected %+v, but got %+v", string(test.data), test.want, m) + continue + } + + } +}