Skip to content

Commit 7f68387

Browse files
committed
internal/lsp/source: workspace symbol improvements for selectors
This CL adds various improvements for matching nested fields and methods: - Limit the symbols we produce to not show unqualified fields/methods, and not show partial package paths. - Handle embedded selectors, by trimming the package path. - Improve the internal API used by symbolizers to operate on named chunks. Fixes golang/go#46997 Change-Id: I86cbe998adbb8e52549c937e330896134c375ed7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/334531 Trust: Robert Findley <[email protected]> Run-TryBot: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]> TryBot-Result: Go Bot <[email protected]>
1 parent 7aa8294 commit 7f68387

File tree

2 files changed

+64
-115
lines changed

2 files changed

+64
-115
lines changed

internal/lsp/source/workspace_symbol.go

+64-64
Original file line numberDiff line numberDiff line change
@@ -57,77 +57,75 @@ func WorkspaceSymbols(ctx context.Context, matcherType SymbolMatcher, style Symb
5757
// See the comment for symbolCollector for more information.
5858
type matcherFunc func(name string) float64
5959

60-
// A symbolizer returns the best symbol match for name with pkg, according to
61-
// some heuristic.
60+
// A symbolizer returns the best symbol match for a name with pkg, according to
61+
// some heuristic. The symbol name is passed as the slice nameParts of logical
62+
// name pieces. For example, for myType.field the caller can pass either
63+
// []string{"myType.field"} or []string{"myType.", "field"}.
6264
//
6365
// See the comment for symbolCollector for more information.
64-
type symbolizer func(name string, pkg Package, m matcherFunc) (string, float64)
66+
type symbolizer func(nameParts []string, pkg Package, m matcherFunc) (string, float64)
6567

66-
func fullyQualifiedSymbolMatch(name string, pkg Package, matcher matcherFunc) (string, float64) {
67-
_, score := dynamicSymbolMatch(name, pkg, matcher)
68+
func fullyQualifiedSymbolMatch(nameParts []string, pkg Package, matcher matcherFunc) (string, float64) {
69+
_, score := dynamicSymbolMatch(nameParts, pkg, matcher)
70+
path := append([]string{pkg.PkgPath() + "."}, nameParts...)
6871
if score > 0 {
69-
return pkg.PkgPath() + "." + name, score
72+
return strings.Join(path, ""), score
7073
}
7174
return "", 0
7275
}
7376

74-
func dynamicSymbolMatch(name string, pkg Package, matcher matcherFunc) (string, float64) {
75-
// Prefer any package-qualified match.
76-
pkgQualified := pkg.Name() + "." + name
77-
if match, score := bestMatch(pkgQualified, matcher); match != "" {
78-
return match, score
77+
func dynamicSymbolMatch(nameParts []string, pkg Package, matcher matcherFunc) (string, float64) {
78+
var best string
79+
fullName := strings.Join(nameParts, "")
80+
var score float64
81+
var name string
82+
83+
// Compute the match score by finding the highest scoring suffix. In these
84+
// cases the matched symbol is still the full name: it is confusing to match
85+
// an unqualified nested field or method.
86+
if match := bestMatch("", nameParts, matcher); match > score {
87+
best = fullName
88+
score = match
7989
}
80-
fullyQualified := pkg.PkgPath() + "." + name
81-
if match, score := bestMatch(fullyQualified, matcher); match != "" {
82-
return match, score
90+
91+
// Next: try to match a package-qualified name.
92+
name = pkg.Name() + "." + fullName
93+
if match := matcher(name); match > score {
94+
best = name
95+
score = match
8396
}
84-
return "", 0
85-
}
8697

87-
func packageSymbolMatch(name string, pkg Package, matcher matcherFunc) (string, float64) {
88-
qualified := pkg.Name() + "." + name
89-
if matcher(qualified) > 0 {
90-
return qualified, 1
98+
// Finally: consider a fully qualified name.
99+
prefix := pkg.PkgPath() + "."
100+
fullyQualified := prefix + fullName
101+
// As with field/method selectors, consider suffixes from right to left, but
102+
// always return a fully-qualified symbol.
103+
pathParts := strings.SplitAfter(prefix, "/")
104+
if match := bestMatch(fullName, pathParts, matcher); match > score {
105+
best = fullyQualified
106+
score = match
91107
}
92-
return "", 0
108+
return best, score
93109
}
94110

95-
// bestMatch returns the highest scoring symbol suffix of fullPath, starting
96-
// from the right and splitting on selectors and path components.
97-
//
98-
// e.g. given a symbol path of the form 'host.com/dir/pkg.type.field', we
99-
// check the match quality of the following:
100-
// - field
101-
// - type.field
102-
// - pkg.type.field
103-
// - dir/pkg.type.field
104-
// - host.com/dir/pkg.type.field
105-
//
106-
// and return the best match, along with its score.
107-
//
108-
// This is used to implement the 'dynamic' symbol style.
109-
func bestMatch(fullPath string, matcher matcherFunc) (string, float64) {
110-
pathParts := strings.Split(fullPath, "/")
111-
dottedParts := strings.Split(pathParts[len(pathParts)-1], ".")
112-
113-
var best string
111+
func bestMatch(name string, prefixParts []string, matcher matcherFunc) float64 {
114112
var score float64
115-
116-
for i := 0; i < len(dottedParts); i++ {
117-
path := strings.Join(dottedParts[len(dottedParts)-1-i:], ".")
118-
if match := matcher(path); match > score {
119-
best = path
113+
for i := len(prefixParts) - 1; i >= 0; i-- {
114+
name = prefixParts[i] + name
115+
if match := matcher(name); match > score {
120116
score = match
121117
}
122118
}
123-
for i := 0; i < len(pathParts); i++ {
124-
path := strings.Join(pathParts[len(pathParts)-1-i:], "/")
125-
if match := matcher(path); match > score {
126-
best = path
127-
score = match
128-
}
119+
return score
120+
}
121+
122+
func packageSymbolMatch(components []string, pkg Package, matcher matcherFunc) (string, float64) {
123+
path := append([]string{pkg.Name() + "."}, components...)
124+
qualified := strings.Join(path, "")
125+
if matcher(qualified) > 0 {
126+
return qualified, 1
129127
}
130-
return best, score
128+
return "", 0
131129
}
132130

133131
// symbolCollector holds context as we walk Packages, gathering symbols that
@@ -420,7 +418,13 @@ func (sc *symbolCollector) walkType(typ ast.Expr, path ...*ast.Ident) {
420418
// or named. path is the path of nested identifiers containing the field.
421419
func (sc *symbolCollector) walkField(field *ast.Field, unnamedKind, namedKind protocol.SymbolKind, path ...*ast.Ident) {
422420
if len(field.Names) == 0 {
423-
sc.match(types.ExprString(field.Type), unnamedKind, field, path...)
421+
switch typ := field.Type.(type) {
422+
case *ast.SelectorExpr:
423+
// embedded qualified type
424+
sc.match(typ.Sel.Name, unnamedKind, field, path...)
425+
default:
426+
sc.match(types.ExprString(field.Type), unnamedKind, field, path...)
427+
}
424428
}
425429
for _, name := range field.Names {
426430
sc.match(name.Name, namedKind, name, path...)
@@ -466,18 +470,14 @@ func (sc *symbolCollector) match(name string, kind protocol.SymbolKind, node ast
466470
}
467471

468472
isExported := isExported(name)
469-
if len(path) > 0 {
470-
var nameBuilder strings.Builder
471-
for _, ident := range path {
472-
nameBuilder.WriteString(ident.Name)
473-
nameBuilder.WriteString(".")
474-
if !ident.IsExported() {
475-
isExported = false
476-
}
473+
var names []string
474+
for _, ident := range path {
475+
names = append(names, ident.Name+".")
476+
if !ident.IsExported() {
477+
isExported = false
477478
}
478-
nameBuilder.WriteString(name)
479-
name = nameBuilder.String()
480479
}
480+
names = append(names, name)
481481

482482
// Factors to apply to the match score for the purpose of downranking
483483
// results.
@@ -501,7 +501,7 @@ func (sc *symbolCollector) match(name string, kind protocol.SymbolKind, node ast
501501
// can be noisy.
502502
fieldFactor = 0.5
503503
)
504-
symbol, score := sc.symbolizer(name, sc.current.pkg, sc.matcher)
504+
symbol, score := sc.symbolizer(names, sc.current.pkg, sc.matcher)
505505

506506
// Downrank symbols outside of the workspace.
507507
if !sc.current.isWorkspace {

internal/lsp/source/workspace_symbol_test.go

-51
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package source
66

77
import (
8-
"strings"
98
"testing"
109
)
1110

@@ -45,53 +44,3 @@ func TestParseQuery(t *testing.T) {
4544
}
4645
}
4746
}
48-
49-
func TestBestMatch(t *testing.T) {
50-
tests := []struct {
51-
desc string
52-
symbol string
53-
matcher matcherFunc
54-
wantMatch string
55-
wantScore float64
56-
}{
57-
{
58-
desc: "shortest match",
59-
symbol: "foo/bar/baz.quux",
60-
matcher: func(string) float64 { return 1.0 },
61-
wantMatch: "quux",
62-
wantScore: 1.0,
63-
},
64-
{
65-
desc: "partial match",
66-
symbol: "foo/bar/baz.quux",
67-
matcher: func(s string) float64 {
68-
if strings.HasPrefix(s, "bar") {
69-
return 1.0
70-
}
71-
return 0.0
72-
},
73-
wantMatch: "bar/baz.quux",
74-
wantScore: 1.0,
75-
},
76-
{
77-
desc: "longest match",
78-
symbol: "foo/bar/baz.quux",
79-
matcher: func(s string) float64 {
80-
parts := strings.Split(s, "/")
81-
return float64(len(parts))
82-
},
83-
wantMatch: "foo/bar/baz.quux",
84-
wantScore: 3.0,
85-
},
86-
}
87-
88-
for _, test := range tests {
89-
test := test
90-
t.Run(test.desc, func(t *testing.T) {
91-
gotMatch, gotScore := bestMatch(test.symbol, test.matcher)
92-
if gotMatch != test.wantMatch || gotScore != test.wantScore {
93-
t.Errorf("bestMatch(%q, matcher) = (%q, %.2g), want (%q, %.2g)", test.symbol, gotMatch, gotScore, test.wantMatch, test.wantScore)
94-
}
95-
})
96-
}
97-
}

0 commit comments

Comments
 (0)