Skip to content

Commit

Permalink
Add fuzzy filtering (#586)
Browse files Browse the repository at this point in the history
- Add a new fuzzy filter command for fuzzy string matching (e.g.
`srnm` would match `some resource name`)
- Allow setting default key bindings with modifiers. Using `/` for
fuzzy filter (compared to `Alt+/` for non-fuzzy filtering)
- Allow uesr-configured key bindings to include modifiers
  • Loading branch information
stuartleeks authored Aug 8, 2024
2 parents 605d23d + aa7b59b commit a59a8dc
Show file tree
Hide file tree
Showing 17 changed files with 1,048 additions and 23 deletions.
7 changes: 6 additions & 1 deletion cmd/azbrowse/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,14 @@ func setupViewsAndKeybindings(ctx context.Context, g *gocui.Gui, settings *confi
// Special handler/hack required by view because `/` doesn't trigger correctly in itemWidget
// this causes an ordering issue as the ItemWidget needs the command panel and the command panel needs the views as inputs
// to work around this we use two hacky methods of `Set*Widget`
commandPanelFilterCommand := keybindings.NewCommandPanelFilterHandler(commandPanel)
commandPanelFilterCommand := keybindings.NewCommandPanelFilterHandler(commandPanel, false)
commandPanelFilterFuzzyCommand := keybindings.NewCommandPanelFilterHandler(commandPanel, true)
content := views.NewItemWidget(leftColumnWidth+2, 0, 0, -4, settings.HideGuids, settings.ShouldRender, "", commandPanelFilterCommand.InvokeWithStartString)
list := views.NewListWidget(ctx, 1, 0, leftColumnWidth, -4, []string{"Loading..."}, 0, content, status, settings.EnableTracing, "Subscriptions", settings.ShouldRender, g)
commandPanelFilterCommand.SetItemWidget(content)
commandPanelFilterCommand.SetListWidget(list)
commandPanelFilterFuzzyCommand.SetItemWidget(content)
commandPanelFilterFuzzyCommand.SetListWidget(list)

copyCommand := keybindings.NewCopyHandler(content, status)
toggleDemoModeCommand := keybindings.NewToggleDemoModeHandler(settings, list, status, content)
Expand All @@ -252,6 +255,7 @@ func setupViewsAndKeybindings(ctx context.Context, g *gocui.Gui, settings *confi

commands := []keybindings.Command{
commandPanelFilterCommand,
commandPanelFilterFuzzyCommand,
copyCommand,
commandPanelAzureSearchQueryCommand,
commandPanelContainerAppLogsCommand,
Expand Down Expand Up @@ -285,6 +289,7 @@ func setupViewsAndKeybindings(ctx context.Context, g *gocui.Gui, settings *confi
keybindings.AddHandler(keybindings.NewClearPendingDeleteHandler(notifications))
keybindings.AddHandler(keybindings.NewOpenCommandPanelHandler(g, commandPanel, commands))
keybindings.AddHandler(commandPanelFilterCommand)
keybindings.AddHandler(commandPanelFilterFuzzyCommand)
keybindings.AddHandler(keybindings.NewCloseCommandPanelHandler(commandPanel))
keybindings.AddHandler(keybindings.NewCommandPanelDownHandler(commandPanel))
keybindings.AddHandler(keybindings.NewCommandPanelUpHandler(commandPanel))
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0
github.com/lithammer/fuzzysearch v1.1.8
)

require (
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
Expand Down Expand Up @@ -202,6 +204,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
Expand All @@ -213,6 +216,7 @@ golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
Expand All @@ -224,6 +228,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down Expand Up @@ -253,6 +258,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
Expand All @@ -264,6 +270,7 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
48 changes: 47 additions & 1 deletion internal/pkg/keybindings/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import (
"github.com/lawrencegripper/azbrowse/internal/pkg/config"
)

// KeyWithModifier represents a key with a modifier and is used when configuring keybinding defaults etc
type KeyWithModifier struct {
Key interface{}
Modifier gocui.Modifier
}

// KeyMap reprsents the current mappings from Handler -> Key
type KeyMap map[string][]interface{}

Expand Down Expand Up @@ -99,7 +105,13 @@ func bindHandlerToKey(g *gocui.Gui, hnd KeyHandler) error {
return err
}

err := g.SetKeybinding(hnd.Widget(), key, gocui.ModNone, hnd.Fn())
// If key is a KeyWithModifier then unwrap
modifier := gocui.ModNone
if keyWithModifier, ok := key.(KeyWithModifier); ok {
key = keyWithModifier.Key
modifier = keyWithModifier.Modifier
}
err := g.SetKeybinding(hnd.Widget(), key, modifier, hnd.Fn())
if err != nil {
return err
}
Expand Down Expand Up @@ -186,10 +198,29 @@ func parseKey(key string) (string, error) {
func parseValue(value string) (interface{}, error) {
// TODO Parse semantics properly
target := cleanValue(value)

// Handle modifiers (alt/shift)
if strings.HasPrefix(strings.ToLower(target), "alt+") {
key, err := parseValue(target[4:])
if err != nil {
return nil, err
}
return KeyWithModifier{Key: key, Modifier: gocui.ModAlt}, nil
}
if strings.HasPrefix(strings.ToLower(target), "shift+") {
key, err := parseValue(target[6:])
if err != nil {
return nil, err
}
return KeyWithModifier{Key: key, Modifier: gocui.ModShift}, nil
}

// Parse key as string (e.g. "Ctrl+D")
if val, ok := StrToGocuiKey[target]; ok {
return val, nil
}

// Parse key as rune (e.g. "/")
if len(target) == 1 {
// attempt as rune
return rune(target[0]), nil
Expand Down Expand Up @@ -219,7 +250,22 @@ func keyToString(key interface{}) string {
return GocuiKeyToStr[key.(gocui.Key)]
case rune:
return strings.ToUpper(string(key.(rune)))
case KeyWithModifier:
keyWithModifier := key.(KeyWithModifier)
return modifierToString(keyWithModifier.Modifier) + "+" + keyToString(keyWithModifier.Key)
default:
panic(fmt.Sprintf("Unhandled key type: %v\n", t))
}
}

func modifierToString(modifier gocui.Modifier) string {
switch modifier {
case gocui.ModNone:
return ""
case gocui.ModAlt:
return "Alt"
case gocui.ModShift:
return "Shift"
}
panic(fmt.Sprintf("Unhandled modifier type: %v\n", modifier))
}
3 changes: 2 additions & 1 deletion internal/pkg/keybindings/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ var DefaultKeys = map[string]interface{}{
"commandpaneldown": gocui.KeyArrowDown,
"commandpanelup": gocui.KeyArrowUp,
"commandpanelenter": gocui.KeyEnter,
"filter": rune('/'),
"filter": KeyWithModifier{Key: rune('/'), Modifier: gocui.ModAlt},
"filterfuzzy": rune('/'),
"commandpanelclose": gocui.KeyEsc,
"azuresearchquery": gocui.KeyCtrlR,
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,22 @@ type CommandPanelFilterHandler struct {
commandPanelWidget *views.CommandPanelWidget
list *views.ListWidget
itemView *views.ItemWidget
fuzzyFilter bool
}

var _ Command = &CommandPanelFilterHandler{}

func NewCommandPanelFilterHandler(commandPanelWidget *views.CommandPanelWidget) *CommandPanelFilterHandler {
func NewCommandPanelFilterHandler(commandPanelWidget *views.CommandPanelWidget, fuzzyFilter bool) *CommandPanelFilterHandler {

handler := &CommandPanelFilterHandler{
commandPanelWidget: commandPanelWidget,
fuzzyFilter: fuzzyFilter,
}
if fuzzyFilter {
handler.id = HandlerIDFilterFuzzy
} else {
handler.id = HandlerIDFilter
}
handler.id = HandlerIDFilter
return handler
}

Expand All @@ -40,25 +46,37 @@ func (h *CommandPanelFilterHandler) Fn() func(g *gocui.Gui, v *gocui.View) error
}
}
func (h *CommandPanelFilterHandler) DisplayText() string {
return "Filter"
if h.fuzzyFilter {
return "Filter (Fuzzy)"
} else {
return "Filter"
}
}
func (h *CommandPanelFilterHandler) IsEnabled() bool {
return true
}
func (h *CommandPanelFilterHandler) getTitle() string {
if h.fuzzyFilter {
return "Filter (fuzzy)"
} else {
return "Filter"
}
}
func (h *CommandPanelFilterHandler) Invoke() error {
h.commandPanelWidget.ShowWithText("Filter", "", nil, h.CommandPanelNotification)
h.commandPanelWidget.ShowWithText(h.getTitle(), "", nil, h.CommandPanelNotification)
return nil
}

func (h *CommandPanelFilterHandler) InvokeWithStartString(s string) error {
h.commandPanelWidget.ShowWithText("Filter", s, nil, h.CommandPanelNotification)
h.commandPanelWidget.ShowWithText(h.getTitle(), s, nil, h.CommandPanelNotification)
return nil
}
func (h *CommandPanelFilterHandler) CommandPanelNotification(state interfaces.CommandPanelNotification) {
switch h.commandPanelWidget.PreviousViewName {
case "listWidget":
h.list.SetFilter(state.CurrentText)
h.list.SetFilter(state.CurrentText, h.fuzzyFilter)
case "itemWidget":
h.itemView.SetFilter(state.CurrentText)
h.itemView.SetFilter(state.CurrentText, h.fuzzyFilter)
}
if state.EnterPressed {
h.commandPanelWidget.Hide()
Expand Down
1 change: 1 addition & 0 deletions internal/pkg/keybindings/keyhandlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const (
HandlerIDCommandPanelUp HandlerID = "commandpanelup" //nolint:golint
HandlerIDCommandPanelEnter HandlerID = "commandpanelenter" //nolint:golint
HandlerIDFilter HandlerID = "filter" //nolint:golint
HandlerIDFilterFuzzy HandlerID = "filterfuzzy" //nolint:golint
HandlerIDAzureSearchQuery HandlerID = "azuresearchquery" //nolist:golint
HandlerIDToggleDemoMode HandlerID = "toggledemomode" //nolist:golint
HandlerIDListSort HandlerID = "listsort" //nolint:golint
Expand Down
9 changes: 6 additions & 3 deletions internal/pkg/views/itemview.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/lawrencegripper/azbrowse/internal/pkg/eventing"
"github.com/lawrencegripper/azbrowse/internal/pkg/expanders"
"github.com/lawrencegripper/azbrowse/internal/pkg/interfaces"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/stuartleeks/colorjson"

"github.com/alecthomas/chroma"
Expand Down Expand Up @@ -174,7 +175,7 @@ func (w *ItemWidget) PageUp() {
}

// SetFilter sets the filter to be applied to list items
func (w *ItemWidget) SetFilter(filterString string) {
func (w *ItemWidget) SetFilter(filterString string, filterFuzzy bool) {
w.filterString = filterString

var currentContent []string
Expand All @@ -192,8 +193,10 @@ func (w *ItemWidget) SetFilter(filterString string) {
var filterStringLower = strings.ToLower(filterString)

for _, line := range currentContent {
if strings.Contains(strings.ToLower(line), filterStringLower) {
line = highlightText(line, filterString)
if (filterFuzzy && fuzzy.MatchFold(filterString, line)) ||
(!filterFuzzy && strings.Contains(strings.ToLower(line), filterStringLower)) {
// Got a match - highlight and add to filtered results
line = highlightText(line, filterString, filterFuzzy)
filteredResult.WriteString(line + "\n")
}
}
Expand Down
Loading

0 comments on commit a59a8dc

Please sign in to comment.