2020-01-05 03:46:20 -07:00
|
|
|
// 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"
|
|
|
|
|
2020-04-17 07:32:56 -06:00
|
|
|
"golang.org/x/tools/internal/event"
|
2020-02-09 05:36:49 -07:00
|
|
|
"golang.org/x/tools/internal/lsp/fuzzy"
|
2020-01-05 03:46:20 -07:00
|
|
|
"golang.org/x/tools/internal/lsp/protocol"
|
|
|
|
)
|
|
|
|
|
2020-02-25 14:18:16 -07:00
|
|
|
const maxSymbols = 100
|
|
|
|
|
2020-04-19 10:07:45 -06:00
|
|
|
// WorkspaceSymbols matches symbols across views using the given query,
|
2020-04-08 15:56:24 -06:00
|
|
|
// according to the SymbolMatcher matcher.
|
2020-04-19 10:07:45 -06:00
|
|
|
//
|
|
|
|
// The workspace symbol method is defined in the spec as follows:
|
|
|
|
//
|
|
|
|
// > The workspace symbol request is sent from the client to the server to
|
|
|
|
// > list project-wide symbols matching the query string.
|
|
|
|
//
|
|
|
|
// It is unclear what "project-wide" means here, but given the parameters of
|
|
|
|
// workspace/symbol do not include any workspace identifier, then it has to be
|
|
|
|
// assumed that "project-wide" means "across all workspaces". Hence why
|
|
|
|
// WorkspaceSymbols receives the views []View.
|
|
|
|
//
|
|
|
|
// However, it then becomes unclear what it would mean to call WorkspaceSymbols
|
2020-04-08 15:56:24 -06:00
|
|
|
// with a different configured SymbolMatcher per View. Therefore we assume that
|
|
|
|
// Session level configuration will define the SymbolMatcher to be used for the
|
2020-04-19 10:07:45 -06:00
|
|
|
// WorkspaceSymbols method.
|
2020-06-07 19:50:35 -06:00
|
|
|
func WorkspaceSymbols(ctx context.Context, matcherType SymbolMatcher, style SymbolStyle, views []View, query string) ([]protocol.SymbolInformation, error) {
|
2020-04-20 10:14:12 -06:00
|
|
|
ctx, done := event.Start(ctx, "source.WorkspaceSymbols")
|
2020-01-05 03:46:20 -07:00
|
|
|
defer done()
|
2020-05-08 14:36:41 -06:00
|
|
|
if query == "" {
|
|
|
|
return nil, nil
|
|
|
|
}
|
2020-01-05 03:46:20 -07:00
|
|
|
|
2020-06-07 19:50:35 -06:00
|
|
|
queryMatcher := makeQueryMatcher(matcherType, query)
|
2020-01-05 03:46:20 -07:00
|
|
|
seen := make(map[string]struct{})
|
|
|
|
var symbols []protocol.SymbolInformation
|
2020-02-25 14:18:16 -07:00
|
|
|
outer:
|
2020-01-05 03:46:20 -07:00
|
|
|
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)
|
2020-06-07 19:50:35 -06:00
|
|
|
symbolMatcher := makePackageSymbolMatcher(style, pkg, queryMatcher)
|
2020-01-05 03:46:20 -07:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if _, ok := seen[pkg.PkgPath()]; ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
seen[pkg.PkgPath()] = struct{}{}
|
|
|
|
for _, fh := range pkg.CompiledGoFiles() {
|
2020-02-10 21:10:59 -07:00
|
|
|
file, _, _, _, err := fh.Cached()
|
2020-01-05 03:46:20 -07:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-06-07 19:50:35 -06:00
|
|
|
for _, si := range findSymbol(file.Decls, pkg.GetTypesInfo(), symbolMatcher) {
|
2020-03-04 12:10:32 -07:00
|
|
|
mrng, err := posToMappedRange(view, pkg, si.node.Pos(), si.node.End())
|
2020-01-05 03:46:20 -07:00
|
|
|
if err != nil {
|
2020-03-07 19:28:21 -07:00
|
|
|
event.Error(ctx, "Error getting mapped range for node", err)
|
2020-03-04 12:10:32 -07:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
rng, err := mrng.Range()
|
|
|
|
if err != nil {
|
2020-03-07 19:28:21 -07:00
|
|
|
event.Error(ctx, "Error getting range from mapped range", err)
|
2020-01-05 03:46:20 -07:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
symbols = append(symbols, protocol.SymbolInformation{
|
|
|
|
Name: si.name,
|
|
|
|
Kind: si.kind,
|
|
|
|
Location: protocol.Location{
|
2020-03-04 12:10:32 -07:00
|
|
|
URI: protocol.URIFromSpanURI(mrng.URI()),
|
2020-01-05 03:46:20 -07:00
|
|
|
Range: rng,
|
|
|
|
},
|
2020-06-07 19:50:35 -06:00
|
|
|
ContainerName: pkg.PkgPath(),
|
2020-01-05 03:46:20 -07:00
|
|
|
})
|
2020-02-25 14:18:16 -07:00
|
|
|
if len(symbols) > maxSymbols {
|
|
|
|
break outer
|
|
|
|
}
|
2020-01-05 03:46:20 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return symbols, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type symbolInformation struct {
|
|
|
|
name string
|
|
|
|
kind protocol.SymbolKind
|
|
|
|
node ast.Node
|
|
|
|
}
|
|
|
|
|
|
|
|
type matcherFunc func(string) bool
|
|
|
|
|
2020-06-07 19:50:35 -06:00
|
|
|
func makeQueryMatcher(m SymbolMatcher, query string) matcherFunc {
|
2020-02-09 05:36:49 -07:00
|
|
|
switch m {
|
2020-04-08 15:56:24 -06:00
|
|
|
case SymbolFuzzy:
|
2020-02-09 05:36:49 -07:00
|
|
|
fm := fuzzy.NewMatcher(query)
|
|
|
|
return func(s string) bool {
|
|
|
|
return fm.Score(s) > 0
|
|
|
|
}
|
2020-04-08 15:56:24 -06:00
|
|
|
case SymbolCaseSensitive:
|
2020-02-09 05:36:49 -07:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-07 19:50:35 -06:00
|
|
|
// packageSymbolMatcher matches (possibly partially) qualified symbols within a
|
|
|
|
// package scope.
|
|
|
|
//
|
|
|
|
// The given symbolizer controls how symbol names are extracted from the
|
|
|
|
// package scope.
|
|
|
|
type packageSymbolMatcher struct {
|
|
|
|
queryMatcher matcherFunc
|
|
|
|
pkg Package
|
|
|
|
symbolize symbolizer
|
|
|
|
}
|
|
|
|
|
|
|
|
// symbolMatch returns the package symbol for name that matches the underlying
|
|
|
|
// query, or the empty string if no match is found.
|
|
|
|
func (s packageSymbolMatcher) symbolMatch(name string) string {
|
|
|
|
return s.symbolize(name, s.pkg, s.queryMatcher)
|
|
|
|
}
|
|
|
|
|
|
|
|
func makePackageSymbolMatcher(style SymbolStyle, pkg Package, matcher matcherFunc) func(string) string {
|
|
|
|
var s symbolizer
|
|
|
|
switch style {
|
|
|
|
case DynamicSymbols:
|
|
|
|
s = dynamicSymbolMatch
|
|
|
|
case FullyQualifiedSymbols:
|
|
|
|
s = fullyQualifiedSymbolMatch
|
|
|
|
default:
|
|
|
|
s = packageSymbolMatch
|
|
|
|
}
|
|
|
|
return packageSymbolMatcher{queryMatcher: matcher, pkg: pkg, symbolize: s}.symbolMatch
|
|
|
|
}
|
|
|
|
|
|
|
|
// A symbolizer returns a qualified symbol match for the unqualified name
|
|
|
|
// within pkg, if one exists, or the empty string if no match is found.
|
|
|
|
type symbolizer func(name string, pkg Package, m matcherFunc) string
|
|
|
|
|
|
|
|
func fullyQualifiedSymbolMatch(name string, pkg Package, matcher matcherFunc) string {
|
|
|
|
// TODO: this should probably include pkg.Name() as well.
|
|
|
|
fullyQualified := pkg.PkgPath() + "." + name
|
|
|
|
if matcher(fullyQualified) {
|
|
|
|
return fullyQualified
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func dynamicSymbolMatch(name string, pkg Package, matcher matcherFunc) string {
|
|
|
|
pkgQualified := pkg.Name() + "." + name
|
|
|
|
if match := shortestMatch(pkgQualified, matcher); match != "" {
|
|
|
|
return match
|
|
|
|
}
|
|
|
|
fullyQualified := pkg.PkgPath() + "." + name
|
|
|
|
if match := shortestMatch(fullyQualified, matcher); match != "" {
|
|
|
|
return match
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func packageSymbolMatch(name string, pkg Package, matcher matcherFunc) string {
|
|
|
|
qualified := pkg.Name() + "." + name
|
|
|
|
if matcher(qualified) {
|
|
|
|
return qualified
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func shortestMatch(fullPath string, matcher func(string) bool) string {
|
|
|
|
pathParts := strings.Split(fullPath, "/")
|
|
|
|
dottedParts := strings.Split(pathParts[len(pathParts)-1], ".")
|
|
|
|
// First match the smallest package identifier.
|
|
|
|
if m := matchRight(dottedParts, ".", matcher); m != "" {
|
|
|
|
return m
|
|
|
|
}
|
|
|
|
// Then match the shortest subpath.
|
|
|
|
return matchRight(pathParts, "/", matcher)
|
|
|
|
}
|
|
|
|
|
|
|
|
func matchRight(parts []string, sep string, matcher func(string) bool) string {
|
|
|
|
for i := 0; i < len(parts); i++ {
|
|
|
|
path := strings.Join(parts[len(parts)-1-i:], sep)
|
|
|
|
if matcher(path) {
|
|
|
|
return path
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func findSymbol(decls []ast.Decl, info *types.Info, symbolMatch func(string) string) []symbolInformation {
|
2020-01-05 03:46:20 -07:00
|
|
|
var result []symbolInformation
|
|
|
|
for _, decl := range decls {
|
|
|
|
switch decl := decl.(type) {
|
|
|
|
case *ast.FuncDecl:
|
2020-04-14 13:16:05 -06:00
|
|
|
fn := decl.Name.Name
|
|
|
|
kind := protocol.Function
|
|
|
|
if decl.Recv != nil {
|
|
|
|
kind = protocol.Method
|
|
|
|
switch typ := decl.Recv.List[0].Type.(type) {
|
|
|
|
case *ast.StarExpr:
|
|
|
|
fn = typ.X.(*ast.Ident).Name + "." + fn
|
|
|
|
case *ast.Ident:
|
|
|
|
fn = typ.Name + "." + fn
|
2020-01-05 03:46:20 -07:00
|
|
|
}
|
2020-04-14 13:16:05 -06:00
|
|
|
}
|
2020-06-07 19:50:35 -06:00
|
|
|
if m := symbolMatch(fn); m != "" {
|
2020-01-05 03:46:20 -07:00
|
|
|
result = append(result, symbolInformation{
|
2020-06-07 19:50:35 -06:00
|
|
|
name: m,
|
2020-01-05 03:46:20 -07:00
|
|
|
kind: kind,
|
|
|
|
node: decl.Name,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
case *ast.GenDecl:
|
|
|
|
for _, spec := range decl.Specs {
|
|
|
|
switch spec := spec.(type) {
|
|
|
|
case *ast.TypeSpec:
|
2020-06-07 19:50:35 -06:00
|
|
|
target := spec.Name.Name
|
|
|
|
if m := symbolMatch(target); m != "" {
|
2020-01-05 03:46:20 -07:00
|
|
|
result = append(result, symbolInformation{
|
2020-06-07 19:50:35 -06:00
|
|
|
name: m,
|
2020-01-05 03:46:20 -07:00
|
|
|
kind: typeToKind(info.TypeOf(spec.Type)),
|
|
|
|
node: spec.Name,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
switch st := spec.Type.(type) {
|
|
|
|
case *ast.StructType:
|
|
|
|
for _, field := range st.Fields.List {
|
2020-06-07 19:50:35 -06:00
|
|
|
result = append(result, findFieldSymbol(field, protocol.Field, symbolMatch, target)...)
|
2020-01-05 03:46:20 -07:00
|
|
|
}
|
|
|
|
case *ast.InterfaceType:
|
|
|
|
for _, field := range st.Methods.List {
|
|
|
|
kind := protocol.Method
|
|
|
|
if len(field.Names) == 0 {
|
|
|
|
kind = protocol.Interface
|
|
|
|
}
|
2020-06-07 19:50:35 -06:00
|
|
|
result = append(result, findFieldSymbol(field, kind, symbolMatch, target)...)
|
2020-01-05 03:46:20 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
case *ast.ValueSpec:
|
|
|
|
for _, name := range spec.Names {
|
2020-06-07 19:50:35 -06:00
|
|
|
if m := symbolMatch(name.Name); m != "" {
|
2020-01-05 03:46:20 -07:00
|
|
|
kind := protocol.Variable
|
|
|
|
if decl.Tok == token.CONST {
|
|
|
|
kind = protocol.Constant
|
|
|
|
}
|
|
|
|
result = append(result, symbolInformation{
|
2020-06-07 19:50:35 -06:00
|
|
|
name: m,
|
2020-01-05 03:46:20 -07:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-06-07 19:50:35 -06:00
|
|
|
func findFieldSymbol(field *ast.Field, kind protocol.SymbolKind, symbolMatch func(string) string, prefix string) []symbolInformation {
|
2020-01-05 03:46:20 -07:00
|
|
|
var result []symbolInformation
|
|
|
|
|
|
|
|
if len(field.Names) == 0 {
|
|
|
|
name := types.ExprString(field.Type)
|
2020-04-14 13:16:05 -06:00
|
|
|
target := prefix + "." + name
|
2020-06-07 19:50:35 -06:00
|
|
|
if m := symbolMatch(target); m != "" {
|
2020-01-05 03:46:20 -07:00
|
|
|
result = append(result, symbolInformation{
|
2020-06-07 19:50:35 -06:00
|
|
|
name: m,
|
2020-01-05 03:46:20 -07:00
|
|
|
kind: kind,
|
|
|
|
node: field,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, name := range field.Names {
|
2020-04-14 13:16:05 -06:00
|
|
|
target := prefix + "." + name.Name
|
2020-06-07 19:50:35 -06:00
|
|
|
if m := symbolMatch(target); m != "" {
|
2020-01-05 03:46:20 -07:00
|
|
|
result = append(result, symbolInformation{
|
2020-06-07 19:50:35 -06:00
|
|
|
name: m,
|
2020-01-05 03:46:20 -07:00
|
|
|
kind: kind,
|
|
|
|
node: name,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|