internal/lsp/source: parse symbol queries when using fastfuzzy

Hook up the fastfuzzy symbol matcher to our fzf-style query parsing, for
consistency with the (slow) fuzzy matcher.

In the past I had wanted to implement this natively inside the
SymbolMatcher, but it is much simpler to keep using combinators. In the
common case we'll just be using fuzzy matching.

For golang/go#50016

Change-Id: I1c62c8c8e9d29da570cb1e4034c2b10782529081
Reviewed-on: https://go-review.googlesource.com/c/tools/+/376362
Trust: Robert Findley <rfindley@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Peter Weinberger <pjw@google.com>
This commit is contained in:
Robert Findley 2022-01-07 17:53:08 -05:00
parent 3737ecd836
commit 21ca3b3a93
4 changed files with 66 additions and 30 deletions

View File

@ -7,6 +7,7 @@ package misc
import (
"testing"
"golang.org/x/tools/internal/lsp/protocol"
. "golang.org/x/tools/internal/lsp/regtest"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/testenv"
@ -20,7 +21,7 @@ func TestWorkspaceSymbolMissingMetadata(t *testing.T) {
-- go.mod --
module mod.com
go 1.12
go 1.17
-- a.go --
package p
@ -56,7 +57,7 @@ func TestWorkspaceSymbolSorting(t *testing.T) {
-- go.mod --
module mod.com
go 1.12
go 1.17
-- a/a.go --
package a
@ -82,15 +83,49 @@ const (
"Fooey", // shorter than Fooest, Foobar
"Fooest",
}
syms := env.WorkspaceSymbol("Foo")
if len(syms) != len(want) {
t.Errorf("got %d symbols, want %d", len(syms), len(want))
}
for i := range syms {
if syms[i].Name != want[i] {
t.Errorf("syms[%d] = %q, want %q", i, syms[i].Name, want[i])
}
}
got := env.WorkspaceSymbol("Foo")
compareSymbols(t, got, want)
})
}
func TestWorkspaceSymbolSpecialPatterns(t *testing.T) {
const files = `
-- go.mod --
module mod.com
go 1.17
-- a/a.go --
package a
const (
AxxBxxCxx
ABC
)
`
var symbolMatcher = string(source.SymbolFastFuzzy)
WithOptions(
EditorConfig{
SymbolMatcher: &symbolMatcher,
},
).Run(t, files, func(t *testing.T, env *Env) {
compareSymbols(t, env.WorkspaceSymbol("ABC"), []string{"ABC", "AxxBxxCxx"})
compareSymbols(t, env.WorkspaceSymbol("'ABC"), []string{"ABC"})
compareSymbols(t, env.WorkspaceSymbol("^mod.com"), []string{"mod.com/a.ABC", "mod.com/a.AxxBxxCxx"})
compareSymbols(t, env.WorkspaceSymbol("^mod.com Axx"), []string{"mod.com/a.AxxBxxCxx"})
compareSymbols(t, env.WorkspaceSymbol("C$"), []string{"ABC"})
})
}
func compareSymbols(t *testing.T, got []protocol.SymbolInformation, want []string) {
t.Helper()
if len(got) != len(want) {
t.Errorf("got %d symbols, want %d", len(got), len(want))
}
for i := range got {
if got[i].Name != want[i] {
t.Errorf("got[%d] = %q, want %q", i, got[i].Name, want[i])
}
}
}

View File

@ -49,11 +49,6 @@ const (
//
// Currently this matcher only accepts case-insensitive fuzzy patterns.
//
// TODO(rfindley):
// - implement smart-casing
// - implement space-separated groups
// - implement ', ^, and $ modifiers
//
// An empty pattern matches no input.
func NewSymbolMatcher(pattern string) *SymbolMatcher {
m := &SymbolMatcher{}

View File

@ -177,9 +177,11 @@ func newSymbolCollector(matcher SymbolMatcher, style SymbolStyle, query string)
func buildMatcher(matcher SymbolMatcher, query string) matcherFunc {
switch matcher {
case SymbolFuzzy:
return parseQuery(query)
return parseQuery(query, newFuzzyMatcher)
case SymbolFastFuzzy:
return fuzzy.NewSymbolMatcher(query).Match
return parseQuery(query, func(query string) matcherFunc {
return fuzzy.NewSymbolMatcher(query).Match
})
case SymbolCaseSensitive:
return matchExact(query)
case SymbolCaseInsensitive:
@ -195,6 +197,18 @@ func buildMatcher(matcher SymbolMatcher, query string) matcherFunc {
panic(fmt.Errorf("unknown symbol matcher: %v", matcher))
}
func newFuzzyMatcher(query string) matcherFunc {
fm := fuzzy.NewMatcher(query)
return func(chunks []string) (int, float64) {
score := float64(fm.ScoreChunks(chunks))
ranges := fm.MatchedRanges()
if len(ranges) > 0 {
return ranges[0], score
}
return -1, score
}
}
// 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.
@ -207,7 +221,7 @@ func buildMatcher(matcher SymbolMatcher, query string) matcherFunc {
// 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 {
func parseQuery(q string, newMatcher func(string) matcherFunc) matcherFunc {
fields := strings.Fields(q)
if len(fields) == 0 {
return func([]string) (int, float64) { return -1, 0 }
@ -238,15 +252,7 @@ func parseQuery(q string) matcherFunc {
return -1, 0
})
default:
fm := fuzzy.NewMatcher(field)
f = func(chunks []string) (int, float64) {
score := float64(fm.ScoreChunks(chunks))
ranges := fm.MatchedRanges()
if len(ranges) > 0 {
return ranges[0], score
}
return -1, score
}
f = newMatcher(field)
}
funcs = append(funcs, f)
}

View File

@ -38,7 +38,7 @@ func TestParseQuery(t *testing.T) {
}
for _, test := range tests {
matcher := parseQuery(test.query)
matcher := parseQuery(test.query, newFuzzyMatcher)
if _, score := matcher([]string{test.s}); score > 0 != test.wantMatch {
t.Errorf("parseQuery(%q) match for %q: %.2g, want match: %t", test.query, test.s, score, test.wantMatch)
}