| // 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 |
| } |