mirror of https://github.com/golang/go.git
internal/lsp/source: support some fzf-like tokens in symbol matching
It's useful to be able to switch between case sensitive, case insensitive, and fuzzy matching for symbol without having to reload gopls. FZF has some nice syntax for this: https://github.com/junegunn/fzf#search-syntax Adopt a subset of this syntax for our symbol search: ' for exact matching ^ for prefix matching $ for suffix matching It would be straightforward to also support inversion, using '!', but I deemed this unnecessary. I think we should adopt this, since none of these symbols conflicts with Go identifiers, or (AFAIK) with special syntax in major LSP clients. Change-Id: If2e4d372d4a45ace5ab5d4e76c460f1dcca0bc2b Reviewed-on: https://go-review.googlesource.com/c/tools/+/248418 Run-TryBot: Robert Findley <rfindley@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
parent
242af255f0
commit
ea3a2cdbfb
|
|
@ -188,4 +188,4 @@ Defines the algorithm that is used when calculating workspace symbol results. Mu
|
|||
* `"caseSensitive"`
|
||||
* `"caseInsensitive"`
|
||||
|
||||
Default: `"caseInsensitive"`.
|
||||
Default: `"fuzzy"`.
|
||||
|
|
|
|||
|
|
@ -105,6 +105,22 @@ See [Settings](settings.md) for more information about the available configurati
|
|||
|
||||
This contains exactly the same set of values that are in the global configuration, but it is fetched for every workspace folder separately. The editor can choose to respond with different values per-folder.
|
||||
|
||||
## Special Features
|
||||
|
||||
### Symbol Queries
|
||||
|
||||
Gopls supports some extended syntax for `workspace/symbol` requests, when using
|
||||
the `fuzzy` symbol matcher (the default). Inspired by the popular fuzzy matcher
|
||||
[FZF](https://github.com/junegunn/fzf), the following special characters are
|
||||
supported within symbol queries:
|
||||
|
||||
| Character | Usage | Match |
|
||||
| --------- | --------- | ------------ |
|
||||
| `'` | `'abc` | exact |
|
||||
| `^` | `^printf` | exact prefix |
|
||||
| `$` | `printf$` | exact suffix |
|
||||
|
||||
|
||||
## Command line support
|
||||
|
||||
Much of the functionality of `gopls` is available through a command line interface.
|
||||
|
|
|
|||
|
|
@ -155,10 +155,7 @@ func newSymbolCollector(matcher SymbolMatcher, style SymbolStyle, query string)
|
|||
var m matcherFunc
|
||||
switch matcher {
|
||||
case SymbolFuzzy:
|
||||
fm := fuzzy.NewMatcher(query)
|
||||
m = func(s string) float64 {
|
||||
return float64(fm.Score(s))
|
||||
}
|
||||
m = parseQuery(query)
|
||||
case SymbolCaseSensitive:
|
||||
m = func(s string) float64 {
|
||||
if strings.Contains(s, query) {
|
||||
|
|
@ -194,6 +191,84 @@ func newSymbolCollector(matcher SymbolMatcher, style SymbolStyle, query string)
|
|||
}
|
||||
}
|
||||
|
||||
// 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, release, err := sc.collectPackages(ctx, views)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,43 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func TestParseQuery(t *testing.T) {
|
||||
tests := []struct {
|
||||
query, s string
|
||||
wantMatch bool
|
||||
}{
|
||||
{"", "anything", false},
|
||||
{"any", "anything", true},
|
||||
{"any$", "anything", false},
|
||||
{"ing$", "anything", true},
|
||||
{"ing$", "anythinG", true},
|
||||
{"inG$", "anything", false},
|
||||
{"^any", "anything", true},
|
||||
{"^any", "Anything", true},
|
||||
{"^Any", "anything", false},
|
||||
{"at", "anything", true},
|
||||
// TODO: this appears to be a bug in the fuzzy matching algorithm. 'At'
|
||||
// should cause a case-sensitive match.
|
||||
// {"At", "anything", false},
|
||||
{"At", "Anything", true},
|
||||
{"'yth", "Anything", true},
|
||||
{"'yti", "Anything", false},
|
||||
{"'any 'thing", "Anything", true},
|
||||
{"anythn nythg", "Anything", true},
|
||||
{"ntx", "Anything", false},
|
||||
{"anythn", "anything", true},
|
||||
{"ing", "anything", true},
|
||||
{"anythn nythgx", "anything", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
matcher := parseQuery(test.query)
|
||||
if score := matcher(test.s); score > 0 != test.wantMatch {
|
||||
t.Errorf("parseQuery(%q) match for %q: %.2g, want match: %t", test.query, test.s, score, test.wantMatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBestMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
|
|
|
|||
Loading…
Reference in New Issue