mirror of
https://github.com/golang/go
synced 2024-11-18 21:14:44 -07:00
internal/lsp: enhance document symbols support
Make methods children of their receiver's type symbol. Add struct fields as children of the struct's type symbol. Also identify numeric, boolean, and string types. Updates golang/go#30915 Fixes golang/go#31202 Change-Id: I33c4ea7b953e981ea1e858505b77c7a3ba6ee399 Reviewed-on: https://go-review.googlesource.com/c/tools/+/170198 Run-TryBot: Rebecca Stambler <rstambler@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
parent
db7bebf5ae
commit
2538eef759
@ -93,7 +93,10 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) {
|
|||||||
expectedDefinitions := make(definitions)
|
expectedDefinitions := make(definitions)
|
||||||
expectedTypeDefinitions := make(definitions)
|
expectedTypeDefinitions := make(definitions)
|
||||||
expectedHighlights := make(highlights)
|
expectedHighlights := make(highlights)
|
||||||
expectedSymbols := make(symbols)
|
expectedSymbols := &symbols{
|
||||||
|
m: make(map[span.URI][]protocol.DocumentSymbol),
|
||||||
|
children: make(map[string][]protocol.DocumentSymbol),
|
||||||
|
}
|
||||||
|
|
||||||
// Collect any data that needs to be used by subsequent tests.
|
// Collect any data that needs to be used by subsequent tests.
|
||||||
if err := exported.Expect(map[string]interface{}{
|
if err := exported.Expect(map[string]interface{}{
|
||||||
@ -180,8 +183,8 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) {
|
|||||||
t.Run("Symbols", func(t *testing.T) {
|
t.Run("Symbols", func(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if goVersion111 { // TODO(rstambler): Remove this when we no longer support Go 1.10.
|
if goVersion111 { // TODO(rstambler): Remove this when we no longer support Go 1.10.
|
||||||
if len(expectedSymbols) != expectedSymbolsCount {
|
if len(expectedSymbols.m) != expectedSymbolsCount {
|
||||||
t.Errorf("got %v symbols expected %v", len(expectedSymbols), expectedSymbolsCount)
|
t.Errorf("got %v symbols expected %v", len(expectedSymbols.m), expectedSymbolsCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
expectedSymbols.test(t, s)
|
expectedSymbols.test(t, s)
|
||||||
@ -194,7 +197,10 @@ type completions map[token.Position][]token.Pos
|
|||||||
type formats map[string]string
|
type formats map[string]string
|
||||||
type definitions map[protocol.Location]protocol.Location
|
type definitions map[protocol.Location]protocol.Location
|
||||||
type highlights map[string][]protocol.Location
|
type highlights map[string][]protocol.Location
|
||||||
type symbols map[span.URI][]protocol.DocumentSymbol
|
type symbols struct {
|
||||||
|
m map[span.URI][]protocol.DocumentSymbol
|
||||||
|
children map[string][]protocol.DocumentSymbol
|
||||||
|
}
|
||||||
|
|
||||||
func (d diagnostics) test(t *testing.T, v source.View) int {
|
func (d diagnostics) test(t *testing.T, v source.View) int {
|
||||||
count := 0
|
count := 0
|
||||||
@ -522,7 +528,7 @@ func (h highlights) test(t *testing.T, s *Server) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s symbols) collect(e *packagestest.Exported, fset *token.FileSet, name string, rng span.Range, kind int64) {
|
func (s symbols) collect(e *packagestest.Exported, fset *token.FileSet, name string, rng span.Range, kind int64, parentName string) {
|
||||||
f := fset.File(rng.Start)
|
f := fset.File(rng.Start)
|
||||||
if f == nil {
|
if f == nil {
|
||||||
return
|
return
|
||||||
@ -544,15 +550,20 @@ func (s symbols) collect(e *packagestest.Exported, fset *token.FileSet, name str
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s[spn.URI()] = append(s[spn.URI()], protocol.DocumentSymbol{
|
sym := protocol.DocumentSymbol{
|
||||||
Name: name,
|
Name: name,
|
||||||
Kind: protocol.SymbolKind(kind),
|
Kind: protocol.SymbolKind(kind),
|
||||||
SelectionRange: prng,
|
SelectionRange: prng,
|
||||||
})
|
}
|
||||||
|
if parentName == "" {
|
||||||
|
s.m[spn.URI()] = append(s.m[spn.URI()], sym)
|
||||||
|
} else {
|
||||||
|
s.children[parentName] = append(s.children[parentName], sym)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s symbols) test(t *testing.T, server *Server) {
|
func (s symbols) test(t *testing.T, server *Server) {
|
||||||
for uri, expectedSymbols := range s {
|
for uri, expectedSymbols := range s.m {
|
||||||
params := &protocol.DocumentSymbolParams{
|
params := &protocol.DocumentSymbolParams{
|
||||||
TextDocument: protocol.TextDocumentIdentifier{
|
TextDocument: protocol.TextDocumentIdentifier{
|
||||||
URI: string(uri),
|
URI: string(uri),
|
||||||
@ -564,28 +575,58 @@ func (s symbols) test(t *testing.T, server *Server) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(symbols) != len(expectedSymbols) {
|
if len(symbols) != len(expectedSymbols) {
|
||||||
t.Errorf("want %d symbols in %v, got %d", len(expectedSymbols), uri, len(symbols))
|
t.Errorf("want %d top-level symbols in %v, got %d", len(expectedSymbols), uri, len(symbols))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(symbols, func(i, j int) bool { return symbols[i].Name < symbols[j].Name })
|
sort.Slice(symbols, func(i, j int) bool { return symbols[i].Name < symbols[j].Name })
|
||||||
sort.Slice(expectedSymbols, func(i, j int) bool { return expectedSymbols[i].Name < expectedSymbols[j].Name })
|
sort.Slice(expectedSymbols, func(i, j int) bool { return expectedSymbols[i].Name < expectedSymbols[j].Name })
|
||||||
for i, w := range expectedSymbols {
|
for i := range expectedSymbols {
|
||||||
g := symbols[i]
|
children := s.children[expectedSymbols[i].Name]
|
||||||
if w.Name != g.Name {
|
sort.Slice(children, func(i, j int) bool { return children[i].Name < children[j].Name })
|
||||||
t.Errorf("%s: want symbol %q, got %q", uri, w.Name, g.Name)
|
expectedSymbols[i].Children = children
|
||||||
continue
|
}
|
||||||
}
|
if diff := diffSymbols(uri, expectedSymbols, symbols); diff != "" {
|
||||||
if w.Kind != g.Kind {
|
t.Error(diff)
|
||||||
t.Errorf("%s: want kind %v for %s, got %v", uri, w.Kind, w.Name, g.Kind)
|
|
||||||
}
|
|
||||||
if w.SelectionRange != g.SelectionRange {
|
|
||||||
t.Errorf("%s: want selection range %v for %s, got %v", uri, w.SelectionRange, w.Name, g.SelectionRange)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func diffSymbols(uri span.URI, want, got []protocol.DocumentSymbol) string {
|
||||||
|
if len(got) != len(want) {
|
||||||
|
goto Failed
|
||||||
|
}
|
||||||
|
for i, w := range want {
|
||||||
|
g := got[i]
|
||||||
|
if w.Name != g.Name {
|
||||||
|
goto Failed
|
||||||
|
}
|
||||||
|
if w.Kind != g.Kind {
|
||||||
|
goto Failed
|
||||||
|
}
|
||||||
|
if w.SelectionRange != g.SelectionRange {
|
||||||
|
goto Failed
|
||||||
|
}
|
||||||
|
sort.Slice(g.Children, func(i, j int) bool { return g.Children[i].Name < g.Children[j].Name })
|
||||||
|
if msg := diffSymbols(uri, w.Children, g.Children); msg != "" {
|
||||||
|
return fmt.Sprintf("children of %s: %s", w.Name, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
|
||||||
|
Failed:
|
||||||
|
msg := &bytes.Buffer{}
|
||||||
|
fmt.Fprintf(msg, "document symbols failed for %s:\nexpected:\n", uri)
|
||||||
|
for _, s := range want {
|
||||||
|
fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(msg, "got:\n")
|
||||||
|
for _, s := range got {
|
||||||
|
fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange)
|
||||||
|
}
|
||||||
|
return msg.String()
|
||||||
|
}
|
||||||
|
|
||||||
func testLocation(e *packagestest.Exported, fset *token.FileSet, rng packagestest.Range) (span.Span, *protocol.ColumnMapper) {
|
func testLocation(e *packagestest.Exported, fset *token.FileSet, rng packagestest.Range) (span.Span, *protocol.ColumnMapper) {
|
||||||
spn, err := span.NewRange(fset, rng.Start, rng.End).Span()
|
spn, err := span.NewRange(fset, rng.Start, rng.End).Span()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -6,6 +6,7 @@ package source
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/ast"
|
"go/ast"
|
||||||
"go/token"
|
"go/token"
|
||||||
@ -24,6 +25,10 @@ const (
|
|||||||
FunctionSymbol
|
FunctionSymbol
|
||||||
MethodSymbol
|
MethodSymbol
|
||||||
InterfaceSymbol
|
InterfaceSymbol
|
||||||
|
NumberSymbol
|
||||||
|
StringSymbol
|
||||||
|
BooleanSymbol
|
||||||
|
FieldSymbol
|
||||||
)
|
)
|
||||||
|
|
||||||
type Symbol struct {
|
type Symbol struct {
|
||||||
@ -42,19 +47,30 @@ func DocumentSymbols(ctx context.Context, f File) []Symbol {
|
|||||||
info := pkg.GetTypesInfo()
|
info := pkg.GetTypesInfo()
|
||||||
q := qualifier(file, pkg.GetTypes(), info)
|
q := qualifier(file, pkg.GetTypes(), info)
|
||||||
|
|
||||||
|
methodsToReceiver := make(map[types.Type][]Symbol)
|
||||||
|
symbolsToReceiver := make(map[types.Type]int)
|
||||||
var symbols []Symbol
|
var symbols []Symbol
|
||||||
for _, decl := range file.Decls {
|
for _, decl := range file.Decls {
|
||||||
switch decl := decl.(type) {
|
switch decl := decl.(type) {
|
||||||
case *ast.FuncDecl:
|
case *ast.FuncDecl:
|
||||||
if obj := info.ObjectOf(decl.Name); obj != nil {
|
if obj := info.ObjectOf(decl.Name); obj != nil {
|
||||||
symbols = append(symbols, funcSymbol(decl, obj, fset, q))
|
if fs := funcSymbol(decl, obj, fset, q); fs.Kind == MethodSymbol {
|
||||||
|
// Store methods separately, as we want them to appear as children
|
||||||
|
// of the corresponding type (which we may not have seen yet).
|
||||||
|
rtype := obj.Type().(*types.Signature).Recv().Type()
|
||||||
|
methodsToReceiver[rtype] = append(methodsToReceiver[rtype], fs)
|
||||||
|
} else {
|
||||||
|
symbols = append(symbols, fs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case *ast.GenDecl:
|
case *ast.GenDecl:
|
||||||
for _, spec := range decl.Specs {
|
for _, spec := range decl.Specs {
|
||||||
switch spec := spec.(type) {
|
switch spec := spec.(type) {
|
||||||
case *ast.TypeSpec:
|
case *ast.TypeSpec:
|
||||||
if obj := info.ObjectOf(spec.Name); obj != nil {
|
if obj := info.ObjectOf(spec.Name); obj != nil {
|
||||||
symbols = append(symbols, typeSymbol(spec, obj, fset, q))
|
ts := typeSymbol(spec, obj, fset, q)
|
||||||
|
symbols = append(symbols, ts)
|
||||||
|
symbolsToReceiver[obj.Type()] = len(symbols) - 1
|
||||||
}
|
}
|
||||||
case *ast.ValueSpec:
|
case *ast.ValueSpec:
|
||||||
for _, name := range spec.Names {
|
for _, name := range spec.Names {
|
||||||
@ -66,6 +82,21 @@ func DocumentSymbols(ctx context.Context, f File) []Symbol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attempt to associate methods to the corresponding type symbol.
|
||||||
|
for typ, methods := range methodsToReceiver {
|
||||||
|
if ptr, ok := typ.(*types.Pointer); ok {
|
||||||
|
typ = ptr.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if i, ok := symbolsToReceiver[typ]; ok {
|
||||||
|
symbols[i].Children = append(symbols[i].Children, methods...)
|
||||||
|
} else {
|
||||||
|
// The type definition for the receiver of these methods was not in the document.
|
||||||
|
symbols = append(symbols, methods...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return symbols
|
return symbols
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,24 +133,88 @@ func funcSymbol(decl *ast.FuncDecl, obj types.Object, fset *token.FileSet, q typ
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func typeSymbol(spec *ast.TypeSpec, obj types.Object, fset *token.FileSet, q types.Qualifier) Symbol {
|
func setKind(s *Symbol, typ types.Type, q types.Qualifier) {
|
||||||
s := Symbol{
|
switch typ := typ.Underlying().(type) {
|
||||||
Name: obj.Name(),
|
case *types.Interface:
|
||||||
Kind: StructSymbol,
|
|
||||||
}
|
|
||||||
if types.IsInterface(obj.Type()) {
|
|
||||||
s.Kind = InterfaceSymbol
|
s.Kind = InterfaceSymbol
|
||||||
|
case *types.Struct:
|
||||||
|
s.Kind = StructSymbol
|
||||||
|
case *types.Signature:
|
||||||
|
s.Kind = FunctionSymbol
|
||||||
|
if typ.Recv() != nil {
|
||||||
|
s.Kind = MethodSymbol
|
||||||
|
}
|
||||||
|
case *types.Named:
|
||||||
|
setKind(s, typ.Underlying(), q)
|
||||||
|
case *types.Basic:
|
||||||
|
i := typ.Info()
|
||||||
|
switch {
|
||||||
|
case i&types.IsNumeric != 0:
|
||||||
|
s.Kind = NumberSymbol
|
||||||
|
case i&types.IsBoolean != 0:
|
||||||
|
s.Kind = BooleanSymbol
|
||||||
|
case i&types.IsString != 0:
|
||||||
|
s.Kind = StringSymbol
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
s.Kind = VariableSymbol
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func typeSymbol(spec *ast.TypeSpec, obj types.Object, fset *token.FileSet, q types.Qualifier) Symbol {
|
||||||
|
s := Symbol{Name: obj.Name()}
|
||||||
|
s.Detail, _ = formatType(obj.Type(), q)
|
||||||
|
setKind(&s, obj.Type(), q)
|
||||||
|
|
||||||
if span, err := nodeSpan(spec, fset); err == nil {
|
if span, err := nodeSpan(spec, fset); err == nil {
|
||||||
s.Span = span
|
s.Span = span
|
||||||
}
|
}
|
||||||
if span, err := nodeSpan(spec.Name, fset); err == nil {
|
if span, err := nodeSpan(spec.Name, fset); err == nil {
|
||||||
s.SelectionSpan = span
|
s.SelectionSpan = span
|
||||||
}
|
}
|
||||||
s.Detail, _ = formatType(obj.Type(), q)
|
|
||||||
|
if t, ok := obj.Type().Underlying().(*types.Struct); ok {
|
||||||
|
st := spec.Type.(*ast.StructType)
|
||||||
|
for i := 0; i < t.NumFields(); i++ {
|
||||||
|
f := t.Field(i)
|
||||||
|
child := Symbol{Name: f.Name(), Kind: FieldSymbol}
|
||||||
|
child.Detail, _ = formatType(f.Type(), q)
|
||||||
|
|
||||||
|
spanNode, selectionNode := nodesForStructField(i, st)
|
||||||
|
if span, err := nodeSpan(spanNode, fset); err == nil {
|
||||||
|
child.Span = span
|
||||||
|
}
|
||||||
|
if span, err := nodeSpan(selectionNode, fset); err == nil {
|
||||||
|
child.SelectionSpan = span
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Children = append(s.Children, child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nodesForStructField(i int, st *ast.StructType) (span, selection ast.Node) {
|
||||||
|
j := 0
|
||||||
|
for _, field := range st.Fields.List {
|
||||||
|
if len(field.Names) == 0 {
|
||||||
|
if i == j {
|
||||||
|
return field, field.Type
|
||||||
|
}
|
||||||
|
j++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, name := range field.Names {
|
||||||
|
if i == j {
|
||||||
|
return field, name
|
||||||
|
}
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func varSymbol(decl ast.Node, name *ast.Ident, obj types.Object, fset *token.FileSet, q types.Qualifier) Symbol {
|
func varSymbol(decl ast.Node, name *ast.Ident, obj types.Object, fset *token.FileSet, q types.Qualifier) Symbol {
|
||||||
s := Symbol{
|
s := Symbol{
|
||||||
Name: obj.Name(),
|
Name: obj.Name(),
|
||||||
@ -139,6 +234,9 @@ func varSymbol(decl ast.Node, name *ast.Ident, obj types.Object, fset *token.Fil
|
|||||||
}
|
}
|
||||||
|
|
||||||
func nodeSpan(n ast.Node, fset *token.FileSet) (span.Span, error) {
|
func nodeSpan(n ast.Node, fset *token.FileSet) (span.Span, error) {
|
||||||
|
if n == nil {
|
||||||
|
return span.Span{}, errors.New("no span for nil node")
|
||||||
|
}
|
||||||
r := span.NewRange(fset, n.Pos(), n.End())
|
r := span.NewRange(fset, n.Pos(), n.End())
|
||||||
return r.Span()
|
return r.Span()
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,14 @@ func toProtocolSymbolKind(kind source.SymbolKind) protocol.SymbolKind {
|
|||||||
return protocol.Method
|
return protocol.Method
|
||||||
case source.InterfaceSymbol:
|
case source.InterfaceSymbol:
|
||||||
return protocol.Interface
|
return protocol.Interface
|
||||||
|
case source.NumberSymbol:
|
||||||
|
return protocol.Number
|
||||||
|
case source.StringSymbol:
|
||||||
|
return protocol.String
|
||||||
|
case source.BooleanSymbol:
|
||||||
|
return protocol.Boolean
|
||||||
|
case source.FieldSymbol:
|
||||||
|
return protocol.Field
|
||||||
default:
|
default:
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
38
internal/lsp/testdata/symbols/main.go
vendored
38
internal/lsp/testdata/symbols/main.go
vendored
@ -1,27 +1,43 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
var x = 42 //@symbol("x", "x", 13)
|
import "io"
|
||||||
|
|
||||||
const y = 43 //@symbol("y", "y", 14)
|
var x = 42 //@symbol("x", "x", 13, "")
|
||||||
|
|
||||||
type Foo struct { //@symbol("Foo", "Foo", 23)
|
const y = 43 //@symbol("y", "y", 14, "")
|
||||||
Quux
|
|
||||||
Bar int
|
type Number int //@symbol("Number", "Number", 16, "")
|
||||||
baz string
|
|
||||||
|
type Alias = string //@symbol("Alias", "Alias", 15, "")
|
||||||
|
|
||||||
|
type NumberAlias = Number //@symbol("NumberAlias", "NumberAlias", 16, "")
|
||||||
|
|
||||||
|
type (
|
||||||
|
Boolean bool //@symbol("Boolean", "Boolean", 17, "")
|
||||||
|
BoolAlias = bool //@symbol("BoolAlias", "BoolAlias", 17, "")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Foo struct { //@symbol("Foo", "Foo", 23, "")
|
||||||
|
Quux //@symbol("Quux", "Quux", 8, "Foo")
|
||||||
|
W io.Writer //@symbol("W" , "W", 8, "Foo")
|
||||||
|
Bar int //@symbol("Bar", "Bar", 8, "Foo")
|
||||||
|
baz string //@symbol("baz", "baz", 8, "Foo")
|
||||||
}
|
}
|
||||||
|
|
||||||
type Quux struct { //@symbol("Quux", "Quux", 23)
|
type Quux struct { //@symbol("Quux", "Quux", 23, "")
|
||||||
X float64
|
X, Y float64 //@symbol("X", "X", 8, "Quux"), symbol("Y", "Y", 8, "Quux")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f Foo) Baz() string { //@symbol("Baz", "Baz", 6)
|
func (f Foo) Baz() string { //@symbol("Baz", "Baz", 6, "Foo")
|
||||||
return f.baz
|
return f.baz
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() { //@symbol("main", "main", 12)
|
func (q *Quux) Do() {} //@symbol("Do", "Do", 6, "Quux")
|
||||||
|
|
||||||
|
func main() { //@symbol("main", "main", 12, "")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Stringer interface { //@symbol("Stringer", "Stringer", 11)
|
type Stringer interface { //@symbol("Stringer", "Stringer", 11, "")
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user