diff --git a/gopls/internal/regtest/misc/workspace_symbol_test.go b/gopls/internal/regtest/misc/workspace_symbol_test.go index 38a9854334..a21d47312d 100644 --- a/gopls/internal/regtest/misc/workspace_symbol_test.go +++ b/gopls/internal/regtest/misc/workspace_symbol_test.go @@ -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]) + } + } +} diff --git a/internal/lsp/fuzzy/symbol.go b/internal/lsp/fuzzy/symbol.go index 2a91ed2204..df9fbd5141 100644 --- a/internal/lsp/fuzzy/symbol.go +++ b/internal/lsp/fuzzy/symbol.go @@ -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{} diff --git a/internal/lsp/source/workspace_symbol.go b/internal/lsp/source/workspace_symbol.go index b73d0fb5d3..364119530f 100644 --- a/internal/lsp/source/workspace_symbol.go +++ b/internal/lsp/source/workspace_symbol.go @@ -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) } diff --git a/internal/lsp/source/workspace_symbol_test.go b/internal/lsp/source/workspace_symbol_test.go index 89c754db09..314ef785df 100644 --- a/internal/lsp/source/workspace_symbol_test.go +++ b/internal/lsp/source/workspace_symbol_test.go @@ -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) }