mirror of https://github.com/golang/go.git
595 lines
17 KiB
Go
595 lines
17 KiB
Go
// 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"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/token"
|
|
"go/types"
|
|
"sort"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"golang.org/x/tools/internal/event"
|
|
"golang.org/x/tools/internal/lsp/fuzzy"
|
|
"golang.org/x/tools/internal/lsp/protocol"
|
|
"golang.org/x/tools/internal/span"
|
|
)
|
|
|
|
// maxSymbols defines the maximum number of symbol results that should ever be
|
|
// sent in response to a client.
|
|
const maxSymbols = 100
|
|
|
|
// WorkspaceSymbols matches symbols across all views using the given query,
|
|
// according to the match semantics parameterized by matcherType and style.
|
|
//
|
|
// 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
|
|
}
|
|
sc := newSymbolCollector(matcherType, style, query)
|
|
return sc.walk(ctx, views)
|
|
}
|
|
|
|
// A matcherFunc determines the matching score of a symbol.
|
|
//
|
|
// See the comment for symbolCollector for more information.
|
|
type matcherFunc func(name string) float64
|
|
|
|
// A symbolizer returns the best symbol match for a name with pkg, according to
|
|
// some heuristic. The symbol name is passed as the slice nameParts of logical
|
|
// name pieces. For example, for myType.field the caller can pass either
|
|
// []string{"myType.field"} or []string{"myType.", "field"}.
|
|
//
|
|
// See the comment for symbolCollector for more information.
|
|
type symbolizer func(nameParts []string, pkg Package, m matcherFunc) (string, float64)
|
|
|
|
func fullyQualifiedSymbolMatch(nameParts []string, pkg Package, matcher matcherFunc) (string, float64) {
|
|
_, score := dynamicSymbolMatch(nameParts, pkg, matcher)
|
|
path := append([]string{pkg.PkgPath() + "."}, nameParts...)
|
|
if score > 0 {
|
|
return strings.Join(path, ""), score
|
|
}
|
|
return "", 0
|
|
}
|
|
|
|
func dynamicSymbolMatch(nameParts []string, pkg Package, matcher matcherFunc) (string, float64) {
|
|
var best string
|
|
fullName := strings.Join(nameParts, "")
|
|
var score float64
|
|
var name string
|
|
|
|
// Compute the match score by finding the highest scoring suffix. In these
|
|
// cases the matched symbol is still the full name: it is confusing to match
|
|
// an unqualified nested field or method.
|
|
if match := bestMatch("", nameParts, matcher); match > score {
|
|
best = fullName
|
|
score = match
|
|
}
|
|
|
|
// Next: try to match a package-qualified name.
|
|
name = pkg.Name() + "." + fullName
|
|
if match := matcher(name); match > score {
|
|
best = name
|
|
score = match
|
|
}
|
|
|
|
// Finally: consider a fully qualified name.
|
|
prefix := pkg.PkgPath() + "."
|
|
fullyQualified := prefix + fullName
|
|
// As with field/method selectors, consider suffixes from right to left, but
|
|
// always return a fully-qualified symbol.
|
|
pathParts := strings.SplitAfter(prefix, "/")
|
|
if match := bestMatch(fullName, pathParts, matcher); match > score {
|
|
best = fullyQualified
|
|
score = match
|
|
}
|
|
return best, score
|
|
}
|
|
|
|
func bestMatch(name string, prefixParts []string, matcher matcherFunc) float64 {
|
|
var score float64
|
|
for i := len(prefixParts) - 1; i >= 0; i-- {
|
|
name = prefixParts[i] + name
|
|
if match := matcher(name); match > score {
|
|
score = match
|
|
}
|
|
}
|
|
return score
|
|
}
|
|
|
|
func packageSymbolMatch(components []string, pkg Package, matcher matcherFunc) (string, float64) {
|
|
path := append([]string{pkg.Name() + "."}, components...)
|
|
qualified := strings.Join(path, "")
|
|
if matcher(qualified) > 0 {
|
|
return qualified, 1
|
|
}
|
|
return "", 0
|
|
}
|
|
|
|
// symbolCollector holds context as we walk Packages, gathering symbols that
|
|
// match a given query.
|
|
//
|
|
// How we match symbols is parameterized by two interfaces:
|
|
// * A matcherFunc determines how well a string symbol matches a query. It
|
|
// returns a non-negative score indicating the quality of the match. A score
|
|
// of zero indicates no match.
|
|
// * A symbolizer determines how we extract the symbol for an object. This
|
|
// enables the 'symbolStyle' configuration option.
|
|
type symbolCollector struct {
|
|
// These types parameterize the symbol-matching pass.
|
|
matcher matcherFunc
|
|
symbolizer symbolizer
|
|
|
|
// current holds metadata for the package we are currently walking.
|
|
current *pkgView
|
|
curFile *ParsedGoFile
|
|
|
|
res [maxSymbols]symbolInformation
|
|
}
|
|
|
|
func newSymbolCollector(matcher SymbolMatcher, style SymbolStyle, query string) *symbolCollector {
|
|
var m matcherFunc
|
|
switch matcher {
|
|
case SymbolFuzzy:
|
|
m = parseQuery(query)
|
|
case SymbolCaseSensitive:
|
|
m = func(s string) float64 {
|
|
if strings.Contains(s, query) {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
case SymbolCaseInsensitive:
|
|
q := strings.ToLower(query)
|
|
m = func(s string) float64 {
|
|
if strings.Contains(strings.ToLower(s), q) {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
default:
|
|
panic(fmt.Errorf("unknown symbol matcher: %v", matcher))
|
|
}
|
|
var s symbolizer
|
|
switch style {
|
|
case DynamicSymbols:
|
|
s = dynamicSymbolMatch
|
|
case FullyQualifiedSymbols:
|
|
s = fullyQualifiedSymbolMatch
|
|
case PackageQualifiedSymbols:
|
|
s = packageSymbolMatch
|
|
default:
|
|
panic(fmt.Errorf("unknown symbol style: %v", style))
|
|
}
|
|
return &symbolCollector{
|
|
matcher: m,
|
|
symbolizer: s,
|
|
}
|
|
}
|
|
|
|
// parseQuery parses a field-separated symbol query, extracting the special
|
|
// characters listed below, and returns a matcherFunc corresponding to the AND
|
|
// of all field queries.
|
|
//
|
|
// Special characters:
|
|
// ^ match exact prefix
|
|
// $ match exact suffix
|
|
// ' match exact
|
|
//
|
|
// In all three of these special queries, matches are 'smart-cased', meaning
|
|
// they are case sensitive if the symbol query contains any upper-case
|
|
// characters, and case insensitive otherwise.
|
|
func parseQuery(q string) matcherFunc {
|
|
fields := strings.Fields(q)
|
|
if len(fields) == 0 {
|
|
return func(string) float64 { return 0 }
|
|
}
|
|
var funcs []matcherFunc
|
|
for _, field := range fields {
|
|
var f matcherFunc
|
|
switch {
|
|
case strings.HasPrefix(field, "^"):
|
|
prefix := field[1:]
|
|
f = smartCase(prefix, func(s string) float64 {
|
|
if strings.HasPrefix(s, prefix) {
|
|
return 1
|
|
}
|
|
return 0
|
|
})
|
|
case strings.HasPrefix(field, "'"):
|
|
exact := field[1:]
|
|
f = smartCase(exact, func(s string) float64 {
|
|
if strings.Contains(s, exact) {
|
|
return 1
|
|
}
|
|
return 0
|
|
})
|
|
case strings.HasSuffix(field, "$"):
|
|
suffix := field[0 : len(field)-1]
|
|
f = smartCase(suffix, func(s string) float64 {
|
|
if strings.HasSuffix(s, suffix) {
|
|
return 1
|
|
}
|
|
return 0
|
|
})
|
|
default:
|
|
fm := fuzzy.NewMatcher(field)
|
|
f = func(s string) float64 {
|
|
return float64(fm.Score(s))
|
|
}
|
|
}
|
|
funcs = append(funcs, f)
|
|
}
|
|
return comboMatcher(funcs).match
|
|
}
|
|
|
|
// smartCase returns a matcherFunc that is case-sensitive if q contains any
|
|
// upper-case characters, and case-insensitive otherwise.
|
|
func smartCase(q string, m matcherFunc) matcherFunc {
|
|
insensitive := strings.ToLower(q) == q
|
|
return func(s string) float64 {
|
|
if insensitive {
|
|
s = strings.ToLower(s)
|
|
}
|
|
return m(s)
|
|
}
|
|
}
|
|
|
|
type comboMatcher []matcherFunc
|
|
|
|
func (c comboMatcher) match(s string) float64 {
|
|
score := 1.0
|
|
for _, f := range c {
|
|
score *= f(s)
|
|
}
|
|
return score
|
|
}
|
|
|
|
// walk walks views, gathers symbols, and returns the results.
|
|
func (sc *symbolCollector) walk(ctx context.Context, views []View) (_ []protocol.SymbolInformation, err error) {
|
|
toWalk, err := sc.collectPackages(ctx, views)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Make sure we only walk files once (we might see them more than once due to
|
|
// build constraints).
|
|
seen := make(map[span.URI]bool)
|
|
for _, pv := range toWalk {
|
|
sc.current = pv
|
|
for _, pgf := range pv.pkg.CompiledGoFiles() {
|
|
if seen[pgf.URI] {
|
|
continue
|
|
}
|
|
seen[pgf.URI] = true
|
|
sc.curFile = pgf
|
|
sc.walkFilesDecls(pgf.File.Decls)
|
|
}
|
|
}
|
|
return sc.results(), nil
|
|
}
|
|
|
|
func (sc *symbolCollector) results() []protocol.SymbolInformation {
|
|
var res []protocol.SymbolInformation
|
|
for _, si := range sc.res {
|
|
if si.score <= 0 {
|
|
return res
|
|
}
|
|
res = append(res, si.asProtocolSymbolInformation())
|
|
}
|
|
return res
|
|
}
|
|
|
|
// collectPackages gathers all known packages and sorts for stability.
|
|
func (sc *symbolCollector) collectPackages(ctx context.Context, views []View) ([]*pkgView, error) {
|
|
var toWalk []*pkgView
|
|
for _, v := range views {
|
|
snapshot, release := v.Snapshot(ctx)
|
|
defer release()
|
|
knownPkgs, err := snapshot.KnownPackages(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
workspacePackages, err := snapshot.WorkspacePackages(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
isWorkspacePkg := make(map[Package]bool)
|
|
for _, wp := range workspacePackages {
|
|
isWorkspacePkg[wp] = true
|
|
}
|
|
for _, pkg := range knownPkgs {
|
|
toWalk = append(toWalk, &pkgView{
|
|
pkg: pkg,
|
|
isWorkspace: isWorkspacePkg[pkg],
|
|
})
|
|
}
|
|
}
|
|
// Now sort for stability of results. We order by
|
|
// (pkgView.isWorkspace, pkgView.p.ID())
|
|
sort.Slice(toWalk, func(i, j int) bool {
|
|
lhs := toWalk[i]
|
|
rhs := toWalk[j]
|
|
switch {
|
|
case lhs.isWorkspace == rhs.isWorkspace:
|
|
return lhs.pkg.ID() < rhs.pkg.ID()
|
|
case lhs.isWorkspace:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
})
|
|
return toWalk, nil
|
|
}
|
|
|
|
func (sc *symbolCollector) walkFilesDecls(decls []ast.Decl) {
|
|
for _, decl := range decls {
|
|
switch decl := decl.(type) {
|
|
case *ast.FuncDecl:
|
|
kind := protocol.Function
|
|
var recv *ast.Ident
|
|
if decl.Recv.NumFields() > 0 {
|
|
kind = protocol.Method
|
|
recv = unpackRecv(decl.Recv.List[0].Type)
|
|
}
|
|
if recv != nil {
|
|
sc.match(decl.Name.Name, kind, decl.Name, recv)
|
|
} else {
|
|
sc.match(decl.Name.Name, kind, decl.Name)
|
|
}
|
|
case *ast.GenDecl:
|
|
for _, spec := range decl.Specs {
|
|
switch spec := spec.(type) {
|
|
case *ast.TypeSpec:
|
|
sc.match(spec.Name.Name, typeToKind(sc.current.pkg.GetTypesInfo().TypeOf(spec.Type)), spec.Name)
|
|
sc.walkType(spec.Type, spec.Name)
|
|
case *ast.ValueSpec:
|
|
for _, name := range spec.Names {
|
|
kind := protocol.Variable
|
|
if decl.Tok == token.CONST {
|
|
kind = protocol.Constant
|
|
}
|
|
sc.match(name.Name, kind, name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func unpackRecv(rtyp ast.Expr) *ast.Ident {
|
|
// Extract the receiver identifier. Lifted from go/types/resolver.go
|
|
L:
|
|
for {
|
|
switch t := rtyp.(type) {
|
|
case *ast.ParenExpr:
|
|
rtyp = t.X
|
|
case *ast.StarExpr:
|
|
rtyp = t.X
|
|
default:
|
|
break L
|
|
}
|
|
}
|
|
if name, _ := rtyp.(*ast.Ident); name != nil {
|
|
return name
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// walkType processes symbols related to a type expression. path is path of
|
|
// nested type identifiers to the type expression.
|
|
func (sc *symbolCollector) walkType(typ ast.Expr, path ...*ast.Ident) {
|
|
switch st := typ.(type) {
|
|
case *ast.StructType:
|
|
for _, field := range st.Fields.List {
|
|
sc.walkField(field, protocol.Field, protocol.Field, path...)
|
|
}
|
|
case *ast.InterfaceType:
|
|
for _, field := range st.Methods.List {
|
|
sc.walkField(field, protocol.Interface, protocol.Method, path...)
|
|
}
|
|
}
|
|
}
|
|
|
|
// walkField processes symbols related to the struct field or interface method.
|
|
//
|
|
// unnamedKind and namedKind are the symbol kinds if the field is resp. unnamed
|
|
// or named. path is the path of nested identifiers containing the field.
|
|
func (sc *symbolCollector) walkField(field *ast.Field, unnamedKind, namedKind protocol.SymbolKind, path ...*ast.Ident) {
|
|
if len(field.Names) == 0 {
|
|
switch typ := field.Type.(type) {
|
|
case *ast.SelectorExpr:
|
|
// embedded qualified type
|
|
sc.match(typ.Sel.Name, unnamedKind, field, path...)
|
|
default:
|
|
sc.match(types.ExprString(field.Type), unnamedKind, field, path...)
|
|
}
|
|
}
|
|
for _, name := range field.Names {
|
|
sc.match(name.Name, namedKind, name, path...)
|
|
sc.walkType(field.Type, append(path, name)...)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// match finds matches and gathers the symbol identified by name, kind and node
|
|
// via the symbolCollector's matcher after first de-duping against previously
|
|
// seen symbols.
|
|
//
|
|
// path specifies the identifier path to a nested field or interface method.
|
|
func (sc *symbolCollector) match(name string, kind protocol.SymbolKind, node ast.Node, path ...*ast.Ident) {
|
|
if !node.Pos().IsValid() || !node.End().IsValid() {
|
|
return
|
|
}
|
|
|
|
isExported := isExported(name)
|
|
var names []string
|
|
for _, ident := range path {
|
|
names = append(names, ident.Name+".")
|
|
if !ident.IsExported() {
|
|
isExported = false
|
|
}
|
|
}
|
|
names = append(names, name)
|
|
|
|
// Factors to apply to the match score for the purpose of downranking
|
|
// results.
|
|
//
|
|
// These numbers were crudely calibrated based on trial-and-error using a
|
|
// small number of sample queries. Adjust as necessary.
|
|
//
|
|
// All factors are multiplicative, meaning if more than one applies they are
|
|
// multiplied together.
|
|
const (
|
|
// nonWorkspaceFactor is applied to symbols outside of any active
|
|
// workspace. Developers are less likely to want to jump to code that they
|
|
// are not actively working on.
|
|
nonWorkspaceFactor = 0.5
|
|
// nonWorkspaceUnexportedFactor is applied to unexported symbols outside of
|
|
// any active workspace. Since one wouldn't usually jump to unexported
|
|
// symbols to understand a package API, they are particularly irrelevant.
|
|
nonWorkspaceUnexportedFactor = 0.5
|
|
// fieldFactor is applied to fields and interface methods. One would
|
|
// typically jump to the type definition first, so ranking fields highly
|
|
// can be noisy.
|
|
fieldFactor = 0.5
|
|
)
|
|
symbol, score := sc.symbolizer(names, sc.current.pkg, sc.matcher)
|
|
|
|
// Downrank symbols outside of the workspace.
|
|
if !sc.current.isWorkspace {
|
|
score *= nonWorkspaceFactor
|
|
if !isExported {
|
|
score *= nonWorkspaceUnexportedFactor
|
|
}
|
|
}
|
|
|
|
// Downrank fields.
|
|
if len(path) > 0 {
|
|
score *= fieldFactor
|
|
}
|
|
|
|
// Avoid the work below if we know this score will not be sorted into the
|
|
// results.
|
|
if score <= sc.res[len(sc.res)-1].score {
|
|
return
|
|
}
|
|
|
|
rng, err := fileRange(sc.curFile, node.Pos(), node.End())
|
|
if err != nil {
|
|
return
|
|
}
|
|
si := symbolInformation{
|
|
score: score,
|
|
name: name,
|
|
symbol: symbol,
|
|
container: sc.current.pkg.PkgPath(),
|
|
kind: kind,
|
|
location: protocol.Location{
|
|
URI: protocol.URIFromSpanURI(sc.curFile.URI),
|
|
Range: rng,
|
|
},
|
|
}
|
|
insertAt := sort.Search(len(sc.res), func(i int) bool {
|
|
return sc.res[i].score < score
|
|
})
|
|
if insertAt < len(sc.res)-1 {
|
|
copy(sc.res[insertAt+1:], sc.res[insertAt:len(sc.res)-1])
|
|
}
|
|
sc.res[insertAt] = si
|
|
}
|
|
|
|
func fileRange(pgf *ParsedGoFile, start, end token.Pos) (protocol.Range, error) {
|
|
s, err := span.FileSpan(pgf.Tok, pgf.Mapper.Converter, start, end)
|
|
if err != nil {
|
|
return protocol.Range{}, nil
|
|
}
|
|
return pgf.Mapper.Range(s)
|
|
}
|
|
|
|
// isExported reports if a token is exported. Copied from
|
|
// token.IsExported (go1.13+).
|
|
//
|
|
// TODO: replace usage with token.IsExported once go1.12 is no longer
|
|
// supported.
|
|
func isExported(name string) bool {
|
|
ch, _ := utf8.DecodeRuneInString(name)
|
|
return unicode.IsUpper(ch)
|
|
}
|
|
|
|
// pkgView holds information related to a package that we are going to walk.
|
|
type pkgView struct {
|
|
pkg Package
|
|
isWorkspace bool
|
|
}
|
|
|
|
// symbolInformation is a cut-down version of protocol.SymbolInformation that
|
|
// allows struct values of this type to be used as map keys.
|
|
type symbolInformation struct {
|
|
score float64
|
|
name string
|
|
symbol string
|
|
container string
|
|
kind protocol.SymbolKind
|
|
location protocol.Location
|
|
}
|
|
|
|
// asProtocolSymbolInformation converts s to a protocol.SymbolInformation value.
|
|
//
|
|
// TODO: work out how to handle tags if/when they are needed.
|
|
func (s symbolInformation) asProtocolSymbolInformation() protocol.SymbolInformation {
|
|
return protocol.SymbolInformation{
|
|
Name: s.symbol,
|
|
Kind: s.kind,
|
|
Location: s.location,
|
|
ContainerName: s.container,
|
|
}
|
|
}
|