diff --git a/main.go b/main.go index 19d0ccb..44844b9 100644 --- a/main.go +++ b/main.go @@ -262,6 +262,31 @@ func (m *model) handleDigKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } } + case key.Matches(msg, textinput.DefaultKeyMap.WordBackward): + value := m.digInput.Value() + pth, ok := jsonpath.Split(value[0:m.digInput.Position()]) + if ok { + if len(pth) > 0 { + pth = pth[:len(pth)-1] + m.digInput.SetCursor(len(jsonpath.Join(pth))) + } else { + m.digInput.CursorStart() + } + } + + case key.Matches(msg, textinput.DefaultKeyMap.WordForward): + value := m.digInput.Value() + fullPath, ok1 := jsonpath.Split(value) + pth, ok2 := jsonpath.Split(value[0:m.digInput.Position()]) + if ok1 && ok2 { + if len(pth) < len(fullPath) { + pth = append(pth, fullPath[len(pth)]) + m.digInput.SetCursor(len(jsonpath.Join(pth))) + } else { + m.digInput.CursorEnd() + } + } + default: if key.Matches(msg, key.NewBinding(key.WithKeys("."))) { m.digInput.SetValue(m.cursorPath()) @@ -752,7 +777,7 @@ func (m *model) cursorPath() string { if at.key != nil { quoted := string(at.key) unquoted, err := strconv.Unquote(quoted) - if err == nil && identifier.MatchString(unquoted) { + if err == nil && jsonpath.Identifier.MatchString(unquoted) { path = "." + unquoted + path } else { path = "[" + quoted + "]" + path diff --git a/node.go b/node.go index eb90b62..2925b0d 100644 --- a/node.go +++ b/node.go @@ -2,6 +2,8 @@ package main import ( "strconv" + + jsonpath "github.com/antonmedv/fx/path" ) type node struct { @@ -177,7 +179,7 @@ func (n *node) paths(prefix string, paths *[]string, nodes *[]*node) { if it.key != nil { quoted := string(it.key) unquoted, err := strconv.Unquote(quoted) - if err == nil && identifier.MatchString(unquoted) { + if err == nil && jsonpath.Identifier.MatchString(unquoted) { path = prefix + "." + unquoted } else { path = prefix + "[" + quoted + "]" diff --git a/path/path.go b/path/path.go index 602f448..3dfe21e 100644 --- a/path/path.go +++ b/path/path.go @@ -1,6 +1,7 @@ package path import ( + "regexp" "strconv" "unicode" ) @@ -174,3 +175,22 @@ func Split(p string) ([]any, bool) { func isProp(ch rune) bool { return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '_' || ch == '$' } + +var Identifier = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`) + +func Join(path []any) string { + s := "" + for _, v := range path { + switch v := v.(type) { + case string: + if Identifier.MatchString(v) { + s += "." + v + } else { + s += "[" + strconv.Quote(v) + "]" + } + case int: + s += "[" + strconv.Itoa(v) + "]" + } + } + return s +} diff --git a/path/path_test.go b/path/path_test.go index 09c4177..9360ed2 100644 --- a/path/path_test.go +++ b/path/path_test.go @@ -135,3 +135,52 @@ func Test_SplitPath_negative(t *testing.T) { }) } } + +func TestJoin(t *testing.T) { + tests := []struct { + input []any + want string + }{ + { + input: []any{}, + want: "", + }, + { + input: []any{"foo"}, + want: ".foo", + }, + { + input: []any{"foo", "bar"}, + want: ".foo.bar", + }, + { + input: []any{"foo", 42}, + want: ".foo[42]", + }, + { + input: []any{"foo", "bar", 42}, + want: ".foo.bar[42]", + }, + { + input: []any{"foo", "bar", 42, "baz"}, + want: ".foo.bar[42].baz", + }, + { + input: []any{"foo", "bar", 42, "baz", 1}, + want: ".foo.bar[42].baz[1]", + }, + { + input: []any{"foo", "bar", 42, "baz", 1, "qux"}, + want: ".foo.bar[42].baz[1].qux", + }, + { + input: []any{"foo bar"}, + want: "[\"foo bar\"]", + }, + } + for _, tt := range tests { + t.Run(tt.want, func(t *testing.T) { + require.Equal(t, tt.want, path.Join(tt.input)) + }) + } +} diff --git a/utils.go b/utils.go index 5831410..af354a1 100644 --- a/utils.go +++ b/utils.go @@ -1,12 +1,9 @@ package main import ( - "regexp" "strings" ) -var identifier = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`) - func isHexDigit(ch byte) bool { return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F') }