diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index 7fc584f2c1a..bcc04c032ce 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go @@ -18,6 +18,7 @@ import ( "go/version" "io/fs" "path/filepath" + "slices" "sort" "strconv" "strings" @@ -305,7 +306,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro return protocol.Range{}, nil, fmt.Errorf("re-parsing declaration of %s: %v", obj.Name(), err) } decl, spec, field := findDeclInfo([]*ast.File{declPGF.File}, declPos) // may be nil^3 - comment := chooseDocComment(decl, spec, field) + comment := chooseDocComment(pkg.FileSet(), decl, spec, field) docText := comment.Text() // By default, types.ObjectString provides a reasonable signature. @@ -1147,10 +1148,10 @@ func HoverDocForObject(ctx context.Context, snapshot *cache.Snapshot, fset *toke } decl, spec, field := findDeclInfo([]*ast.File{pgf.File}, pos) - return chooseDocComment(decl, spec, field), nil + return chooseDocComment(fset, decl, spec, field), nil } -func chooseDocComment(decl ast.Decl, spec ast.Spec, field *ast.Field) *ast.CommentGroup { +func chooseDocComment(fset *token.FileSet, decl ast.Decl, spec ast.Spec, field *ast.Field) *ast.CommentGroup { if field != nil { if field.Doc != nil { return field.Doc @@ -1164,28 +1165,87 @@ func chooseDocComment(decl ast.Decl, spec ast.Spec, field *ast.Field) *ast.Comme case *ast.FuncDecl: return decl.Doc case *ast.GenDecl: - switch spec := spec.(type) { - case *ast.ValueSpec: - if spec.Doc != nil { - return spec.Doc - } - if decl.Doc != nil { - return decl.Doc - } - return spec.Comment - case *ast.TypeSpec: - if spec.Doc != nil { - return spec.Doc - } - if decl.Doc != nil { - return decl.Doc - } - return spec.Comment + group := specGroupComment(fset, decl, spec) + doc := getDocComment(spec) + line := getLineComment(spec) + + cg := &ast.CommentGroup{} + if group != nil { + cg.List = append(cg.List, group) + } + if doc != nil { + cg.List = append(cg.List, doc.List...) + } + if line != nil { + cg.List = append(cg.List, line.List...) + } + + if len(cg.List) != 0 { + // Group comment might be part of a Doc comment. + cg.List = slices.Compact(cg.List) + return cg } + + return decl.Doc } return nil } +func specGroupComment(fset *token.FileSet, decl *ast.GenDecl, spec ast.Spec) *ast.Comment { + var groupComment *ast.Comment + var prevEndPos token.Pos + for _, s := range decl.Specs { + doc := getDocComment(s) + + startPos := s.Pos() + if doc != nil { + startPos = doc.Pos() + } + + if prevEndPos.IsValid() && fset.PositionFor(startPos, false).Line != fset.PositionFor(prevEndPos, false).Line+1 { + groupComment = nil + } + + if doc != nil && strings.HasPrefix(doc.List[0].Text, "/*") { + groupComment = doc.List[0] + } + + if s == spec { + return groupComment + } + + prevEndPos = s.End() + if lc := getLineComment(s); lc != nil { + prevEndPos = lc.End() + } + } + + // The provided spec is not part of the decl.Spec slice. + panic("unreachable") +} + +func getLineComment(spec ast.Spec) *ast.CommentGroup { + switch spec := spec.(type) { + case *ast.ValueSpec: + return spec.Comment + case *ast.TypeSpec: + return spec.Comment + default: + return nil + } +} + +func getDocComment(spec ast.Spec) *ast.CommentGroup { + switch spec := spec.(type) { + case *ast.ValueSpec: + return spec.Doc + case *ast.TypeSpec: + return spec.Doc + default: + return nil + } +} + // parseFull fully parses the file corresponding to position pos (for // which fset provides file/line information). // diff --git a/gopls/internal/golang/hover_test.go b/gopls/internal/golang/hover_test.go new file mode 100644 index 00000000000..3ab7cabaf98 --- /dev/null +++ b/gopls/internal/golang/hover_test.go @@ -0,0 +1,173 @@ +package golang + +import ( + "go/ast" + "go/parser" + "go/token" + "strconv" + "strings" + "testing" +) + +func TestGroupComment(t *testing.T) { + cases := []struct { + src string + groupComment []string + }{ + { + src: `package test +const ( + A = iota + B + C +) +`, + groupComment: []string{"", "", ""}, + }, + { + src: `package test +const ( + // doc comment + A = iota + B + C +) +`, + groupComment: []string{"", "", ""}, + }, + { + src: `package test +const ( + // doc comment + /* doc comment */ + A = iota + B + C +) +`, + groupComment: []string{"", "", ""}, + }, + { + src: `package test +const ( + /* group */ + A = iota + B + C +) +`, + groupComment: []string{"/* group */", "/* group */", "/* group */"}, + }, + { + src: `package test +const ( + /* group */ + // doc comment + A = iota // line comment + B // line comment + C // line comment +) +`, + groupComment: []string{"/* group */", "/* group */", "/* group */"}, + }, + { + src: `package test +const ( + /* group */ + A = iota + B + C + + D + E + F +) +`, + groupComment: []string{"/* group */", "/* group */", "/* group */", "", "", ""}, + }, + { + src: `package test +const ( + /* foo */ + A = iota + C + + /* bar */ + D + E +) +`, + groupComment: []string{"foo", "foo", "bar", "bar"}, + }, + { + src: `package test +const ( + /* foo */ + A = iota + B + /* bar */ + D + E +) +`, + groupComment: []string{"foo", "foo", "bar", "bar"}, + }, + { + src: `package test +const ( + /* foo */ + A = iota + // doc comment + C + + /* bar */ + D + // doc comment + E // line comment +) +`, + groupComment: []string{"foo", "foo", "bar", "bar"}, + }, + { + src: `package test +const ( + /* foo */ + A = iota + B + + /* bar */ + D + E + + F +) +`, + groupComment: []string{"foo", "foo", "bar", "bar", ""}, + }, + } + + for i, tt := range cases { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "test.go", tt.src, parser.SkipObjectResolution|parser.ParseComments) + if err != nil { + t.Fatal(err) + } + + t.Run(strconv.Itoa(i), func(t *testing.T) { + if testing.Verbose() { + t.Logf("src:\n%s", tt.src) + } + decl := f.Decls[0].(*ast.GenDecl) + for i, expect := range tt.groupComment { + gc := specGroupComment(fset, decl, decl.Specs[i]) + var got string + if gc != nil { + got = gc.Text + } + if !strings.Contains(got, expect) { + t.Errorf("specGroupComment(%v) = %q; want = %q", i, got, expect) + } + } + }) + } +}