Skip to content

Commit

Permalink
internal/lsp: add support for workspace symbol
Browse files Browse the repository at this point in the history
This change adds support for the LSP workspace/symbol. Unlike
documentSymbol, the target is symbols that exist not only in a specific
file, but also in the current or imported packages. It returns symbols
whose name contains the query string of the request(case-insensitive),
or all symbols if the query string is empty.

However, the following is not implemented:
- Setting of deprecated and containerName fields in SymbolInformation
- Consideration of WorkspaceClientCapabilities
- Progress support
- CLI support

Updates golang/go#33844

Change-Id: Id2a8d3c468084b9d44228cc6ed2ad37c4b52c405
Reviewed-on: https://go-review.googlesource.com/c/tools/+/213317
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
  • Loading branch information
daisuzu authored and stamblerre committed Feb 5, 2020
1 parent 9e60d4e commit 6e8b36d
Show file tree
Hide file tree
Showing 21 changed files with 486 additions and 50 deletions.
5 changes: 5 additions & 0 deletions internal/lsp/cmd/test/cmdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

"golang.org/x/tools/go/packages/packagestest"
"golang.org/x/tools/internal/lsp/cmd"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/tests"
"golang.org/x/tools/internal/span"
Expand Down Expand Up @@ -95,6 +96,10 @@ func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completi
//TODO: add command line completions tests when it works
}

func (r *runner) WorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{}) {
//TODO: add command line workspace symbol tests when it works
}

func (r *runner) RunGoplsCmd(t testing.TB, args ...string) (string, string) {
rStdout, wStdout, err := os.Pipe()
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions internal/lsp/general.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func (s *Server) initialize(ctx context.Context, params *protocol.ParamInitializ
ImplementationProvider: true,
DocumentFormattingProvider: true,
DocumentSymbolProvider: true,
WorkspaceSymbolProvider: true,
ExecuteCommandProvider: protocol.ExecuteCommandOptions{
Commands: options.SupportedCommands,
},
Expand Down
18 changes: 18 additions & 0 deletions internal/lsp/lsp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,24 @@ func summarizeSymbols(t *testing.T, i int, want, got []protocol.DocumentSymbol,
return msg.String()
}

func (r *runner) WorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
params := &protocol.WorkspaceSymbolParams{
Query: query,
}
symbols, err := r.server.Symbol(r.ctx, params)
if err != nil {
t.Fatal(err)
}
got := tests.FilterWorkspaceSymbols(symbols, dirs)
if len(got) != len(expectedSymbols) {
t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
return
}
if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
t.Error(diff)
}
}

func (r *runner) SignatureHelp(t *testing.T, spn span.Span, expectedSignature *source.SignatureInformation) {
m, err := r.data.Mapper(spn.URI())
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions internal/lsp/server_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ func (s *Server) SignatureHelp(ctx context.Context, params *protocol.SignatureHe
return s.signatureHelp(ctx, params)
}

func (s *Server) Symbol(context.Context, *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) {
return nil, notImplemented("Symbol")
func (s *Server) Symbol(ctx context.Context, params *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) {
return s.symbol(ctx, params)
}

func (s *Server) TypeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) (protocol.Definition, error) {
Expand Down
15 changes: 15 additions & 0 deletions internal/lsp/source/source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,21 @@ func summarizeSymbols(t *testing.T, i int, want, got []protocol.DocumentSymbol,
return msg.String()
}

func (r *runner) WorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
symbols, err := source.WorkspaceSymbols(r.ctx, []source.View{r.view}, query)
if err != nil {
t.Errorf("symbols failed: %v", err)
}
got := tests.FilterWorkspaceSymbols(symbols, dirs)
if len(got) != len(expectedSymbols) {
t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
return
}
if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
t.Error(diff)
}
}

func (r *runner) SignatureHelp(t *testing.T, spn span.Span, expectedSignature *source.SignatureInformation) {
_, rng, err := spanToRange(r.data, spn)
if err != nil {
Expand Down
193 changes: 193 additions & 0 deletions internal/lsp/source/workspace_symbol.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package source

import (
"context"
"go/ast"
"go/token"
"go/types"
"strings"

"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/telemetry/log"
"golang.org/x/tools/internal/telemetry/trace"
)

func WorkspaceSymbols(ctx context.Context, views []View, query string) ([]protocol.SymbolInformation, error) {
ctx, done := trace.StartSpan(ctx, "source.WorkspaceSymbols")
defer done()

q := strings.ToLower(query)
matcher := func(s string) bool {
return strings.Contains(strings.ToLower(s), q)
}

seen := make(map[string]struct{})
var symbols []protocol.SymbolInformation
for _, view := range views {
knownPkgs, err := view.Snapshot().KnownPackages(ctx)
if err != nil {
return nil, err
}
for _, ph := range knownPkgs {
pkg, err := ph.Check(ctx)
if err != nil {
return nil, err
}
if _, ok := seen[pkg.PkgPath()]; ok {
continue
}
seen[pkg.PkgPath()] = struct{}{}
for _, fh := range pkg.CompiledGoFiles() {
file, _, _, err := fh.Cached()
if err != nil {
return nil, err
}
for _, si := range findSymbol(file.Decls, pkg.GetTypesInfo(), matcher) {
rng, err := nodeToProtocolRange(view, pkg, si.node)
if err != nil {
log.Error(ctx, "Error getting range for node", err)
continue
}
symbols = append(symbols, protocol.SymbolInformation{
Name: si.name,
Kind: si.kind,
Location: protocol.Location{
URI: protocol.NewURI(fh.File().Identity().URI),
Range: rng,
},
})
}
}
}
}
return symbols, nil
}

type symbolInformation struct {
name string
kind protocol.SymbolKind
node ast.Node
}

type matcherFunc func(string) bool

func findSymbol(decls []ast.Decl, info *types.Info, matcher matcherFunc) []symbolInformation {
var result []symbolInformation
for _, decl := range decls {
switch decl := decl.(type) {
case *ast.FuncDecl:
if matcher(decl.Name.Name) {
kind := protocol.Function
if decl.Recv != nil {
kind = protocol.Method
}
result = append(result, symbolInformation{
name: decl.Name.Name,
kind: kind,
node: decl.Name,
})
}
case *ast.GenDecl:
for _, spec := range decl.Specs {
switch spec := spec.(type) {
case *ast.TypeSpec:
if matcher(spec.Name.Name) {
result = append(result, symbolInformation{
name: spec.Name.Name,
kind: typeToKind(info.TypeOf(spec.Type)),
node: spec.Name,
})
}
switch st := spec.Type.(type) {
case *ast.StructType:
for _, field := range st.Fields.List {
result = append(result, findFieldSymbol(field, protocol.Field, matcher)...)
}
case *ast.InterfaceType:
for _, field := range st.Methods.List {
kind := protocol.Method
if len(field.Names) == 0 {
kind = protocol.Interface
}
result = append(result, findFieldSymbol(field, kind, matcher)...)
}
}
case *ast.ValueSpec:
for _, name := range spec.Names {
if matcher(name.Name) {
kind := protocol.Variable
if decl.Tok == token.CONST {
kind = protocol.Constant
}
result = append(result, symbolInformation{
name: name.Name,
kind: kind,
node: name,
})
}
}
}
}
}
}
return result
}

func typeToKind(typ types.Type) protocol.SymbolKind {
switch typ := typ.Underlying().(type) {
case *types.Interface:
return protocol.Interface
case *types.Struct:
return protocol.Struct
case *types.Signature:
if typ.Recv() != nil {
return protocol.Method
}
return protocol.Function
case *types.Named:
return typeToKind(typ.Underlying())
case *types.Basic:
i := typ.Info()
switch {
case i&types.IsNumeric != 0:
return protocol.Number
case i&types.IsBoolean != 0:
return protocol.Boolean
case i&types.IsString != 0:
return protocol.String
}
}
return protocol.Variable
}

func findFieldSymbol(field *ast.Field, kind protocol.SymbolKind, matcher matcherFunc) []symbolInformation {
var result []symbolInformation

if len(field.Names) == 0 {
name := types.ExprString(field.Type)
if matcher(name) {
result = append(result, symbolInformation{
name: name,
kind: kind,
node: field,
})
}
return result
}

for _, name := range field.Names {
if matcher(name.Name) {
result = append(result, symbolInformation{
name: name.Name,
kind: kind,
node: name,
})
}
}

return result
}
2 changes: 1 addition & 1 deletion internal/lsp/testdata/godef/a/a.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ var (
x string //@x,hover("x", x)
)

type A string //@A
type A string //@mark(AString, "A")

func AStuff() { //@AStuff
x := 5
Expand Down
4 changes: 2 additions & 2 deletions internal/lsp/testdata/godef/a/a.go.golden
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ var err error
"start": {
"line": 23,
"column": 6,
"offset": 287
"offset": 304
},
"end": {
"line": 23,
"column": 9,
"offset": 290
"offset": 307
}
},
"description": "```go\nvar err error\n```"
Expand Down
8 changes: 4 additions & 4 deletions internal/lsp/testdata/godef/b/b.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@ import (
type S1 struct { //@S1
F1 int //@mark(S1F1, "F1")
S2 //@godef("S2", S2), mark(S1S2, "S2")
a.A //@godef("A", A)
a.A //@godef("A", AString)
}

type S2 struct { //@S2
F1 string //@mark(S2F1, "F1")
F2 int //@mark(S2F2, "F2")
*a.A //@godef("A", A),godef("a",AImport)
*a.A //@godef("A", AString),godef("a",AImport)
}

type S3 struct {
F1 struct {
a.A //@godef("A", A)
a.A //@godef("A", AString)
}
}

Expand All @@ -34,4 +34,4 @@ func Bar() {
var _ *myFoo.StructFoo //@godef("myFoo", myFoo)
}

const X = 0 //@mark(X, "X"),godef("X", X)
const X = 0 //@mark(bX, "X"),godef("X", bX)
Loading

0 comments on commit 6e8b36d

Please sign in to comment.