blob: 5d458a5ce5f1abd2b4da8e0425f6d7f70d3f977f [file] [log] [blame]
// 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/fuzzy"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/telemetry/event"
)
const maxSymbols = 100
func WorkspaceSymbols(ctx context.Context, views []View, query string) ([]protocol.SymbolInformation, error) {
ctx, done := event.StartSpan(ctx, "source.WorkspaceSymbols")
defer done()
seen := make(map[string]struct{})
var symbols []protocol.SymbolInformation
outer:
for _, view := range views {
knownPkgs, err := view.Snapshot().KnownPackages(ctx)
if err != nil {
return nil, err
}
matcher := makeMatcher(view.Options().Matcher, query)
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) {
mrng, err := posToMappedRange(view, pkg, si.node.Pos(), si.node.End())
if err != nil {
event.Error(ctx, "Error getting mapped range for node", err)
continue
}
rng, err := mrng.Range()
if err != nil {
event.Error(ctx, "Error getting range from mapped range", err)
continue
}
symbols = append(symbols, protocol.SymbolInformation{
Name: si.name,
Kind: si.kind,
Location: protocol.Location{
URI: protocol.URIFromSpanURI(mrng.URI()),
Range: rng,
},
})
if len(symbols) > maxSymbols {
break outer
}
}
}
}
}
return symbols, nil
}
type symbolInformation struct {
name string
kind protocol.SymbolKind
node ast.Node
}
type matcherFunc func(string) bool
func makeMatcher(m Matcher, query string) matcherFunc {
switch m {
case Fuzzy:
fm := fuzzy.NewMatcher(query)
return func(s string) bool {
return fm.Score(s) > 0
}
case CaseSensitive:
return func(s string) bool {
return strings.Contains(s, query)
}
default:
q := strings.ToLower(query)
return func(s string) bool {
return strings.Contains(strings.ToLower(s), q)
}
}
}
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
}