blob: d631cacbb1023e9bbbdfb8d8f6be00f044d29459 [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/event"
"golang.org/x/tools/internal/lsp/fuzzy"
"golang.org/x/tools/internal/lsp/protocol"
)
const maxSymbols = 100
// WorkspaceSymbols matches symbols across views using the given query,
// according to the SymbolMatcher matcher.
//
// 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
// with a different configured SymbolMatcher per View. Therefore we assume that
// Session level configuration will define the SymbolMatcher to be used for the
// WorkspaceSymbols method.
func WorkspaceSymbols(ctx context.Context, matcherType SymbolMatcher, style SymbolStyle, views []View, query string) ([]protocol.SymbolInformation, error) {
ctx, done := event.Start(ctx, "source.WorkspaceSymbols")
defer done()
if query == "" {
return nil, nil
}
queryMatcher := makeQueryMatcher(matcherType, query)
seen := make(map[string]struct{})
var symbols []protocol.SymbolInformation
outer:
for _, view := range views {
snapshot, release := view.Snapshot(ctx)
defer release() // TODO: refactor so this runs promptly instead of at the end of the function
knownPkgs, err := snapshot.KnownPackages(ctx)
if err != nil {
return nil, err
}
// TODO: apply some kind of ordering to the search, and sort the results.
for _, pkg := range knownPkgs {
symbolMatcher := makePackageSymbolMatcher(style, pkg, queryMatcher)
if err != nil {
return nil, err
}
if _, ok := seen[pkg.PkgPath()]; ok {
continue
}
seen[pkg.PkgPath()] = struct{}{}
for _, pgf := range pkg.CompiledGoFiles() {
for _, si := range findSymbol(pgf.File.Decls, pkg.GetTypesInfo(), symbolMatcher) {
mrng, err := posToMappedRange(snapshot, 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,
},
ContainerName: pkg.PkgPath(),
})
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 makeQueryMatcher(m SymbolMatcher, query string) matcherFunc {
switch m {
case SymbolFuzzy:
fm := fuzzy.NewMatcher(query)
return func(s string) bool {
return fm.Score(s) > 0
}
case SymbolCaseSensitive:
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)
}
}
}
// 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 {
var result []symbolInformation
for _, decl := range decls {
switch decl := decl.(type) {
case *ast.FuncDecl:
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
}
}
if m := symbolMatch(fn); m != "" {
result = append(result, symbolInformation{
name: m,
kind: kind,
node: decl.Name,
})
}
case *ast.GenDecl:
for _, spec := range decl.Specs {
switch spec := spec.(type) {
case *ast.TypeSpec:
target := spec.Name.Name
if m := symbolMatch(target); m != "" {
result = append(result, symbolInformation{
name: m,
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, symbolMatch, target)...)
}
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, symbolMatch, target)...)
}
}
case *ast.ValueSpec:
for _, name := range spec.Names {
if m := symbolMatch(name.Name); m != "" {
kind := protocol.Variable
if decl.Tok == token.CONST {
kind = protocol.Constant
}
result = append(result, symbolInformation{
name: m,
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, symbolMatch func(string) string, prefix string) []symbolInformation {
var result []symbolInformation
if len(field.Names) == 0 {
name := types.ExprString(field.Type)
target := prefix + "." + name
if m := symbolMatch(target); m != "" {
result = append(result, symbolInformation{
name: m,
kind: kind,
node: field,
})
}
return result
}
for _, name := range field.Names {
target := prefix + "." + name.Name
if m := symbolMatch(target); m != "" {
result = append(result, symbolInformation{
name: m,
kind: kind,
node: name,
})
}
}
return result
}