From 41f5d50b0ddcff896dab26a47ea5db5a8b34a5fc Mon Sep 17 00:00:00 2001 From: Anton Medvedev Date: Tue, 19 Sep 2023 09:57:51 +0200 Subject: [PATCH] Add fuzzy key matching --- go.mod | 1 + go.sum | 3 +++ keymap.go | 2 ++ main.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++------ node.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++ node_test.go | 26 ++++++++++++++++++++ 6 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 node_test.go diff --git a/go.mod b/go.mod index d1b1288..33c8fa4 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/mattn/go-runewidth v0.0.15 github.com/mazznoer/colorgrad v0.9.1 github.com/muesli/termenv v0.15.2 + github.com/sahilm/fuzzy v0.1.0 github.com/stretchr/testify v1.8.4 ) diff --git a/go.sum b/go.sum index 3e578af..bdcd513 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,7 @@ github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2 github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= 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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 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= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= @@ -44,6 +45,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= +github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= diff --git a/keymap.go b/keymap.go index 361b463..002f0a2 100644 --- a/keymap.go +++ b/keymap.go @@ -131,4 +131,6 @@ var ( yankValue = key.NewBinding(key.WithKeys("y")) yankKey = key.NewBinding(key.WithKeys("k")) yankPath = key.NewBinding(key.WithKeys("p")) + arrowUp = key.NewBinding(key.WithKeys("up")) + arrowDown = key.NewBinding(key.WithKeys("down")) ) diff --git a/main.go b/main.go index dfde17d..024d069 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/mattn/go-isatty" + "github.com/sahilm/fuzzy" jsonpath "github.com/antonmedv/fx/path" ) @@ -219,9 +220,33 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *model) handleDigKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch { - case msg.Type == tea.KeyEscape, msg.Type == tea.KeyEnter: + case key.Matches(msg, arrowUp): + m.up() + m.digInput.SetValue(m.cursorPath()) + m.digInput.CursorEnd() + + case key.Matches(msg, arrowDown): + m.down() + m.digInput.SetValue(m.cursorPath()) + m.digInput.CursorEnd() + + case msg.Type == tea.KeyEscape: m.digInput.Blur() + case msg.Type == tea.KeyTab: + m.digInput.SetValue(m.cursorPath()) + m.digInput.CursorEnd() + + case msg.Type == tea.KeyEnter: + m.digInput.Blur() + digPath, ok := jsonpath.Split(m.digInput.Value()) + if ok { + n := m.selectByPath(digPath) + if n != nil { + m.selectNode(n) + } + } + default: m.digInput, cmd = m.digInput.Update(msg) n := m.dig(m.digInput.Value()) @@ -396,7 +421,7 @@ func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.yank = true case key.Matches(msg, keyMap.Dig): - m.digInput.SetValue(m.cursorPath()) + m.digInput.SetValue(m.cursorPath() + ".") m.digInput.CursorEnd() m.digInput.Width = m.termWidth - 1 m.digInput.Focus() @@ -777,13 +802,9 @@ func (m *model) cursorKey() string { } -func (m *model) dig(value string) *node { - p, ok := jsonpath.Split(value) - if !ok { - return nil - } +func (m *model) selectByPath(path []any) *node { n := m.top - for _, part := range p { + for _, part := range path { if n == nil { return nil } @@ -893,3 +914,35 @@ func (m *model) redoSearch() { m.selectSearchResult(cursor) } } + +func (m *model) dig(v string) *node { + p, ok := jsonpath.Split(v) + if !ok { + return nil + } + at := m.selectByPath(p) + if at != nil { + return at + } + + lastPart := p[len(p)-1] + searchTerm, ok := lastPart.(string) + if !ok { + return nil + } + p = p[:len(p)-1] + + at = m.selectByPath(p) + if at == nil { + return nil + } + + keys, nodes := at.children() + + matches := fuzzy.Find(searchTerm, keys) + if len(matches) == 0 { + return nil + } + + return nodes[matches[0].Index] +} diff --git a/node.go b/node.go index c23edfa..3bd5998 100644 --- a/node.go +++ b/node.go @@ -168,3 +168,70 @@ func (n *node) findChildByIndex(index int) *node { } return nil } + +func (n *node) paths(prefix string, paths *[]string, nodes *[]*node) { + it := n.next + for it != nil && it != n.end { + var path string + + if it.key != nil { + quoted := string(it.key) + unquoted, err := strconv.Unquote(quoted) + if err != nil { + panic(err) + } + if identifier.MatchString(unquoted) { + path = prefix + "." + unquoted + } else { + path = prefix + "[" + quoted + "]" + } + } else if it.index >= 0 { + path = prefix + "[" + strconv.Itoa(it.index) + "]" + } + + *paths = append(*paths, path) + *nodes = append(*nodes, it) + + if it.hasChildren() { + it.paths(path, paths, nodes) + it = it.end.next + } else { + it = it.next + } + } +} + +func (n *node) children() ([]string, []*node) { + if !n.hasChildren() { + return nil, nil + } + + var paths []string + var nodes []*node + + var it *node + if n.isCollapsed() { + it = n.collapsed + } else { + it = n.next + } + + for it != nil && it != n.end { + if it.key != nil { + unquoted, err := strconv.Unquote(string(it.key)) + if err != nil { + panic(err) + } + paths = append(paths, unquoted) + nodes = append(nodes, it) + } + + if it.hasChildren() { + it = it.end.next + } else { + it = it.next + } + } + + return paths, nodes +} diff --git a/node_test.go b/node_test.go new file mode 100644 index 0000000..6bfb30f --- /dev/null +++ b/node_test.go @@ -0,0 +1,26 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNode_paths(t *testing.T) { + n, err := parse([]byte(`{"a": 1, "b": {"f": 2}, "c": [3, 4]}`)) + require.NoError(t, err) + + var paths []string + var nodes []*node + n.paths("", &paths, &nodes) + assert.Equal(t, []string{".a", ".b", ".b.f", ".c", ".c[0]", ".c[1]"}, paths) +} + +func TestNode_children(t *testing.T) { + n, err := parse([]byte(`{"a": 1, "b": {"f": 2}, "c": [3, 4]}`)) + require.NoError(t, err) + + paths, _ := n.children() + assert.Equal(t, []string{"a", "b", "c"}, paths) +}