pkg/terminal: Allow fuzzy searching tab completions (#2633)

This patch implements fuzzy searching for tab completions in the
terminal client. Under the hood it is using a trie data structure
(https://en.wikipedia.org/wiki/Trie) to perform very fast prefix / fuzzy
searches.
This commit is contained in:
Derek Parker 2021-08-05 10:55:27 -07:00 committed by GitHub
parent 985eca462c
commit 43d50202f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 439 additions and 20 deletions

1
go.mod

@ -6,6 +6,7 @@ require (
github.com/aquasecurity/libbpfgo v0.1.2-0.20210708203834-4928d36fafac
github.com/cosiner/argv v0.1.0
github.com/creack/pty v1.1.9
github.com/derekparker/trie v0.0.0-20200317170641-1fdf38b7b0e9
github.com/google/go-dap v0.5.0
github.com/hashicorp/golang-lru v0.5.4
github.com/mattn/go-colorable v0.0.9

2
go.sum

@ -44,6 +44,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/derekparker/trie v0.0.0-20200317170641-1fdf38b7b0e9 h1:G765iDCq7bP5opdrPkXk+4V3yfkgV9iGFuheWZ/X/zY=
github.com/derekparker/trie v0.0.0-20200317170641-1fdf38b7b0e9/go.mod h1:D6ICZm05D9VN1n/8iOtBxLpXtoGp6HDFUJ1RNVieOSE=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=

@ -590,10 +590,10 @@ func (c *Commands) Register(cmdstr string, cf cmdfunc, helpMsg string) {
// Find will look up the command function for the given command input.
// If it cannot find the command it will default to noCmdAvailable().
// If the command is an empty string it will replay the last command.
func (c *Commands) Find(cmdstr string, prefix cmdPrefix) cmdfunc {
func (c *Commands) Find(cmdstr string, prefix cmdPrefix) command {
// If <enter> use last command, if there was one.
if cmdstr == "" {
return nullCommand
return command{aliases: []string{"nullcmd"}, cmdFn: nullCommand}
}
for _, v := range c.cmds {
@ -601,11 +601,11 @@ func (c *Commands) Find(cmdstr string, prefix cmdPrefix) cmdfunc {
if prefix != noPrefix && v.allowedPrefixes&prefix == 0 {
continue
}
return v.cmdFn
return v
}
}
return noCmdAvailable
return command{aliases: []string{"nocmd"}, cmdFn: noCmdAvailable}
}
// CallWithContext takes a command and a context that command should be executed in.
@ -616,7 +616,7 @@ func (c *Commands) CallWithContext(cmdstr string, t *Term, ctx callContext) erro
if len(vals) > 1 {
args = strings.TrimSpace(vals[1])
}
return c.Find(cmdname, ctx.Prefix)(t, ctx, args)
return c.Find(cmdname, ctx.Prefix).cmdFn(t, ctx, args)
}
// Call takes a command to execute.

@ -178,7 +178,7 @@ func withTestTerminalBuildFlags(name string, t testing.TB, buildFlags test.Build
func TestCommandDefault(t *testing.T) {
var (
cmds = Commands{}
cmd = cmds.Find("non-existant-command", noPrefix)
cmd = cmds.Find("non-existant-command", noPrefix).cmdFn
)
err := cmd(nil, callContext{}, "")
@ -194,7 +194,7 @@ func TestCommandDefault(t *testing.T) {
func TestCommandReplayWithoutPreviousCommand(t *testing.T) {
var (
cmds = DebugCommands(nil)
cmd = cmds.Find("", noPrefix)
cmd = cmds.Find("", noPrefix).cmdFn
err = cmd(nil, callContext{}, "")
)
@ -206,7 +206,7 @@ func TestCommandReplayWithoutPreviousCommand(t *testing.T) {
func TestCommandThread(t *testing.T) {
var (
cmds = DebugCommands(nil)
cmd = cmds.Find("thread", noPrefix)
cmd = cmds.Find("thread", noPrefix).cmdFn
)
err := cmd(nil, callContext{}, "")

@ -10,6 +10,7 @@ import (
"sync"
"syscall"
"github.com/derekparker/trie"
"github.com/peterh/liner"
"github.com/go-delve/delve/pkg/config"
@ -210,21 +211,32 @@ func (t *Term) Run() (int, error) {
signal.Notify(ch, syscall.SIGINT)
go t.sigintGuard(ch, multiClient)
t.line.SetCompleter(func(line string) (c []string) {
if strings.HasPrefix(line, "break ") || strings.HasPrefix(line, "b ") {
filter := line[strings.Index(line, " ")+1:]
funcs, _ := t.client.ListFunctions(filter)
for _, f := range funcs {
c = append(c, "break "+f)
}
return
fns := trie.New()
cmds := trie.New()
funcs, _ := t.client.ListFunctions("")
for _, fn := range funcs {
fns.Add(fn, nil)
}
for _, cmd := range t.cmds.cmds {
for _, alias := range cmd.aliases {
cmds.Add(alias, nil)
}
for _, cmd := range t.cmds.cmds {
for _, alias := range cmd.aliases {
if strings.HasPrefix(alias, strings.ToLower(line)) {
c = append(c, alias)
}
t.line.SetCompleter(func(line string) (c []string) {
cmd := t.cmds.Find(strings.Split(line, " ")[0], noPrefix)
switch cmd.aliases[0] {
case "break", "trace", "continue":
if spc := strings.LastIndex(line, " "); spc > 0 {
prefix := line[:spc] + " "
funcs := fns.FuzzySearch(line[spc+1:])
for _, f := range funcs {
c = append(c, prefix+f)
}
}
case "nullcmd", "nocmd":
commands := cmds.FuzzySearch(strings.ToLower(line))
c = append(c, commands...)
}
return
})

13
vendor/github.com/derekparker/trie/.deepsource.toml generated vendored Normal file

@ -0,0 +1,13 @@
version = 1
test_patterns = ["*_test.go"]
exclude_patterns = ["vendor/*"]
[[analyzers]]
name = "go"
enabled = true
[analyzers.meta]
import_path = "github.com/derekparker/trie"
dependencies_vendored = true

1
vendor/github.com/derekparker/trie/.gitignore generated vendored Normal file

@ -0,0 +1 @@
*.test

20
vendor/github.com/derekparker/trie/LICENSE generated vendored Normal file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014 Derek Parker
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

62
vendor/github.com/derekparker/trie/README.md generated vendored Normal file

@ -0,0 +1,62 @@
[![GoDoc](https://godoc.org/github.com/derekparker/trie?status.svg)](https://godoc.org/github.com/derekparker/trie)
# Trie
Data structure and relevant algorithms for extremely fast prefix/fuzzy string searching.
## Usage
Create a Trie with:
```Go
t := trie.New()
```
Add Keys with:
```Go
// Add can take in meta information which can be stored with the key.
// i.e. you could store any information you would like to associate with
// this particular key.
t.Add("foobar", 1)
```
Find a key with:
```Go
node, ok := t.Find("foobar")
meta := node.Meta()
// use meta with meta.(type)
```
Remove Keys with:
```Go
t.Remove("foobar")
```
Prefix search with:
```Go
t.PrefixSearch("foo")
```
Fast test for valid prefix:
```Go
t.HasKeysWithPrefix("foo")
```
Fuzzy search with:
```Go
t.FuzzySearch("fb")
```
## Contributing
Fork this repo and run tests with:
go test
Create a feature branch, write your tests and code and submit a pull request.
## License
MIT

306
vendor/github.com/derekparker/trie/trie.go generated vendored Normal file

@ -0,0 +1,306 @@
// Implementation of an R-Way Trie data structure.
//
// A Trie has a root Node which is the base of the tree.
// Each subsequent Node has a letter and children, which are
// nodes that have letter values associated with them.
package trie
import (
"sort"
"sync"
)
type Node struct {
val rune
path string
term bool
depth int
meta interface{}
mask uint64
parent *Node
children map[rune]*Node
termCount int
}
type Trie struct {
mu sync.Mutex
root *Node
size int
}
type ByKeys []string
func (a ByKeys) Len() int { return len(a) }
func (a ByKeys) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByKeys) Less(i, j int) bool { return len(a[i]) < len(a[j]) }
const nul = 0x0
// Creates a new Trie with an initialized root Node.
func New() *Trie {
return &Trie{
root: &Node{children: make(map[rune]*Node), depth: 0},
size: 0,
}
}
// Returns the root node for the Trie.
func (t *Trie) Root() *Node {
return t.root
}
// Adds the key to the Trie, including meta data. Meta data
// is stored as `interface{}` and must be type cast by
// the caller.
func (t *Trie) Add(key string, meta interface{}) *Node {
t.mu.Lock()
t.size++
runes := []rune(key)
bitmask := maskruneslice(runes)
node := t.root
node.mask |= bitmask
node.termCount++
for i := range runes {
r := runes[i]
bitmask = maskruneslice(runes[i:])
if n, ok := node.children[r]; ok {
node = n
node.mask |= bitmask
} else {
node = node.NewChild(r, "", bitmask, nil, false)
}
node.termCount++
}
node = node.NewChild(nul, key, 0, meta, true)
t.mu.Unlock()
return node
}
// Finds and returns meta data associated
// with `key`.
func (t *Trie) Find(key string) (*Node, bool) {
node := findNode(t.Root(), []rune(key))
if node == nil {
return nil, false
}
node, ok := node.Children()[nul]
if !ok || !node.term {
return nil, false
}
return node, true
}
func (t *Trie) HasKeysWithPrefix(key string) bool {
node := findNode(t.Root(), []rune(key))
return node != nil
}
// Removes a key from the trie, ensuring that
// all bitmasks up to root are appropriately recalculated.
func (t *Trie) Remove(key string) {
var (
i int
rs = []rune(key)
node = findNode(t.Root(), []rune(key))
)
t.mu.Lock()
t.size--
for n := node.Parent(); n != nil; n = n.Parent() {
i++
if len(n.Children()) > 1 {
r := rs[len(rs)-i]
n.RemoveChild(r)
break
}
}
t.mu.Unlock()
}
// Returns all the keys currently stored in the trie.
func (t *Trie) Keys() []string {
if t.size == 0 {
return []string{}
}
return t.PrefixSearch("")
}
// Performs a fuzzy search against the keys in the trie.
func (t Trie) FuzzySearch(pre string) []string {
keys := fuzzycollect(t.Root(), []rune(pre))
sort.Sort(ByKeys(keys))
return keys
}
// Performs a prefix search against the keys in the trie.
func (t Trie) PrefixSearch(pre string) []string {
node := findNode(t.Root(), []rune(pre))
if node == nil {
return nil
}
return collect(node)
}
// Creates and returns a pointer to a new child for the node.
func (parent *Node) NewChild(val rune, path string, bitmask uint64, meta interface{}, term bool) *Node {
node := &Node{
val: val,
path: path,
mask: bitmask,
term: term,
meta: meta,
parent: parent,
children: make(map[rune]*Node),
depth: parent.depth + 1,
}
parent.children[node.val] = node
parent.mask |= bitmask
return node
}
func (n *Node) RemoveChild(r rune) {
delete(n.children, r)
for nd := n.parent; nd != nil; nd = nd.parent {
nd.mask ^= nd.mask
nd.mask |= uint64(1) << uint64(nd.val-'a')
for _, c := range nd.children {
nd.mask |= c.mask
}
}
}
// Returns the parent of this node.
func (n Node) Parent() *Node {
return n.parent
}
// Returns the meta information of this node.
func (n Node) Meta() interface{} {
return n.meta
}
// Returns the children of this node.
func (n Node) Children() map[rune]*Node {
return n.children
}
func (n Node) Terminating() bool {
return n.term
}
func (n Node) Val() rune {
return n.val
}
func (n Node) Depth() int {
return n.depth
}
// Returns a uint64 representing the current
// mask of this node.
func (n Node) Mask() uint64 {
return n.mask
}
func findNode(node *Node, runes []rune) *Node {
if node == nil {
return nil
}
if len(runes) == 0 {
return node
}
n, ok := node.Children()[runes[0]]
if !ok {
return nil
}
var nrunes []rune
if len(runes) > 1 {
nrunes = runes[1:]
} else {
nrunes = runes[0:0]
}
return findNode(n, nrunes)
}
func maskruneslice(rs []rune) uint64 {
var m uint64
for _, r := range rs {
m |= uint64(1) << uint64(r-'a')
}
return m
}
func collect(node *Node) []string {
var (
n *Node
i int
)
keys := make([]string, 0, node.termCount)
nodes := make([]*Node, 1, len(node.children))
nodes[0] = node
for l := len(nodes); l != 0; l = len(nodes) {
i = l - 1
n = nodes[i]
nodes = nodes[:i]
for _, c := range n.children {
nodes = append(nodes, c)
}
if n.term {
word := n.path
keys = append(keys, word)
}
}
return keys
}
type potentialSubtree struct {
idx int
node *Node
}
func fuzzycollect(node *Node, partial []rune) []string {
if len(partial) == 0 {
return collect(node)
}
var (
m uint64
i int
p potentialSubtree
keys []string
)
potential := []potentialSubtree{potentialSubtree{node: node, idx: 0}}
for l := len(potential); l > 0; l = len(potential) {
i = l - 1
p = potential[i]
potential = potential[:i]
m = maskruneslice(partial[p.idx:])
if (p.node.mask & m) != m {
continue
}
if p.node.val == partial[p.idx] {
p.idx++
if p.idx == len(partial) {
keys = append(keys, collect(p.node)...)
continue
}
}
for _, c := range p.node.children {
potential = append(potential, potentialSubtree{node: c, idx: p.idx})
}
}
return keys
}

2
vendor/modules.txt vendored

@ -10,6 +10,8 @@ github.com/cpuguy83/go-md2man/v2/md2man
# github.com/creack/pty v1.1.9
## explicit
github.com/creack/pty
# github.com/derekparker/trie v0.0.0-20200317170641-1fdf38b7b0e9
github.com/derekparker/trie
# github.com/google/go-dap v0.5.0
## explicit
github.com/google/go-dap