diff --git a/new/dig/dig.go b/new/dig/dig.go new file mode 100644 index 0000000..2333e78 --- /dev/null +++ b/new/dig/dig.go @@ -0,0 +1,176 @@ +package dig + +import ( + "strconv" + "unicode" +) + +type state int + +const ( + start state = iota + unknown + propOrIndex + prop + index + indexEnd + number + doubleQuote + doubleQuoteEscape + singleQuote + singleQuoteEscape +) + +func SplitPath(p string) ([]any, bool) { + path := make([]any, 0) + s := "" + state := start + for _, ch := range p { + switch state { + + case start: + switch { + case ch == 'x': + state = unknown + case ch == '.': + state = propOrIndex + default: + return path, false + } + + case unknown: + switch { + case ch == '.': + state = prop + s = "" + case ch == '[': + state = index + s = "" + default: + return path, false + } + + case propOrIndex: + switch { + case isProp(ch): + state = prop + s = string(ch) + case ch == '[': + state = index + default: + return path, false + } + + case prop: + switch { + case isProp(ch): + s += string(ch) + case ch == '.': + state = prop + path = append(path, s) + s = "" + case ch == '[': + state = index + path = append(path, s) + s = "" + default: + return path, false + } + + case index: + switch { + case unicode.IsDigit(ch): + state = number + s = string(ch) + case ch == '"': + state = doubleQuote + s = "" + case ch == '\'': + state = singleQuote + s = "" + default: + return path, false + } + + case indexEnd: + switch { + case ch == ']': + state = unknown + default: + return path, false + } + + case number: + switch { + case unicode.IsDigit(ch): + s += string(ch) + case ch == ']': + state = unknown + n, err := strconv.Atoi(s) + if err != nil { + return path, false + } + path = append(path, n) + s = "" + default: + return path, false + } + + case doubleQuote: + switch ch { + case '"': + state = indexEnd + path = append(path, s) + s = "" + case '\\': + state = doubleQuoteEscape + default: + s += string(ch) + } + + case doubleQuoteEscape: + switch ch { + case '"': + state = doubleQuote + s += string(ch) + default: + return path, false + } + + case singleQuote: + switch ch { + case '\'': + state = indexEnd + path = append(path, s) + s = "" + case '\\': + state = singleQuoteEscape + s += string(ch) + default: + s += string(ch) + } + + case singleQuoteEscape: + switch ch { + case '\'': + state = singleQuote + s += string(ch) + default: + return path, false + } + } + } + if len(s) > 0 { + if state == prop { + path = append(path, s) + } else { + return path, false + } + + } + return path, true +} + +func isProp(ch rune) bool { + return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '_' || ch == '$' +} diff --git a/new/dig/dig_test.go b/new/dig/dig_test.go new file mode 100644 index 0000000..0423f5e --- /dev/null +++ b/new/dig/dig_test.go @@ -0,0 +1,137 @@ +package dig_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/antonmedv/fx/new/dig" +) + +func Test_SplitPath(t *testing.T) { + tests := []struct { + input string + want []any + }{ + { + input: "", + want: []any{}, + }, + { + input: ".", + want: []any{}, + }, + { + input: "x", + want: []any{}, + }, + { + input: ".foo", + want: []any{"foo"}, + }, + { + input: "x.foo", + want: []any{"foo"}, + }, + { + input: "x[42]", + want: []any{42}, + }, + { + input: ".[42]", + want: []any{42}, + }, + { + input: ".42", + want: []any{"42"}, + }, + { + input: ".физ", + want: []any{"физ"}, + }, + { + input: ".foo.bar", + want: []any{"foo", "bar"}, + }, + { + input: ".foo[42]", + want: []any{"foo", 42}, + }, + { + input: ".foo[42].bar", + want: []any{"foo", 42, "bar"}, + }, + { + input: ".foo[1][2]", + want: []any{"foo", 1, 2}, + }, + { + input: ".foo[\"bar\"]", + want: []any{"foo", "bar"}, + }, + { + input: ".foo[\"bar\\\"\"]", + want: []any{"foo", "bar\""}, + }, + { + input: ".foo['bar']['baz\\'']", + want: []any{"foo", "bar", "baz\\'"}, + }, + } + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + p, ok := dig.SplitPath(tt.input) + require.Equal(t, tt.want, p) + require.True(t, ok) + }) + } +} + +func Test_SplitPath_negative(t *testing.T) { + tests := []struct { + input string + }{ + { + input: "./", + }, + { + input: "x/", + }, + { + input: "1+1", + }, + { + input: "x[42", + }, + { + input: ".i % 2", + }, + { + input: "x[for x]", + }, + { + input: "x['y'.", + }, + { + input: "x[0?", + }, + { + input: "x[\"\\u", + }, + { + input: "x['\\n", + }, + { + input: "x[9999999999999999999999999999999999999]", + }, + { + input: "x[]", + }, + } + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + p, ok := dig.SplitPath(tt.input) + require.False(t, ok, p) + }) + } +} diff --git a/new/go.mod b/new/go.mod index 10cfa42..a49bee0 100644 --- a/new/go.mod +++ b/new/go.mod @@ -5,14 +5,18 @@ go 1.20 require ( github.com/charmbracelet/bubbles v0.16.1 github.com/charmbracelet/bubbletea v0.24.2 - github.com/charmbracelet/lipgloss v0.7.1 - github.com/mattn/go-runewidth v0.0.14 + github.com/charmbracelet/lipgloss v0.8.0 + github.com/mattn/go-runewidth v0.0.15 github.com/mazznoer/colorgrad v0.9.1 + github.com/muesli/termenv v0.15.2 + github.com/stretchr/testify v1.8.4 ) require ( + github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -20,10 +24,11 @@ require ( github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.6.0 // indirect + golang.org/x/sys v0.7.0 // indirect golang.org/x/term v0.6.0 // indirect golang.org/x/text v0.3.8 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/new/go.sum b/new/go.sum index c241acd..1528c02 100644 --- a/new/go.sum +++ b/new/go.sum @@ -1,13 +1,17 @@ +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= -github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= -github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/charmbracelet/lipgloss v0.8.0 h1:IS00fk4XAHcf8uZKc3eHeMUTCxUH6NkaTrdyCQk84RU= +github.com/charmbracelet/lipgloss v0.8.0/go.mod h1:p4eYUZZJ/0oXTuCQKFF8mqyKCz0ja6y+7DniDDw5KKU= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= 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/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= @@ -15,8 +19,8 @@ github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mazznoer/colorgrad v0.9.1 h1:MB80JYVndKWSMEM1beNqnuOowWGhoQc3DXWXkFp6JlM= github.com/mazznoer/colorgrad v0.9.1/go.mod h1:WX2R9wt9B47+txJZVVpM9LY+LAGIdi4lTI5wIyreDH4= github.com/mazznoer/csscolorparser v0.1.2 h1:/UBHuQg792ePmGFzTQAC9u+XbFr7/HzP/Gj70Phyz2A= @@ -27,17 +31,26 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= -github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/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= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/new/keymap.go b/new/keymap.go index 841fc00..16753b4 100644 --- a/new/keymap.go +++ b/new/keymap.go @@ -25,6 +25,7 @@ type KeyMap struct { Search key.Binding Next key.Binding Prev key.Binding + Dig key.Binding } var keyMap KeyMap @@ -119,5 +120,9 @@ func init() { key.WithKeys("N"), key.WithHelp("", "prev search result"), ), + Dig: key.NewBinding( + key.WithKeys("."), + key.WithHelp("", "dig json"), + ), } } diff --git a/new/main.go b/new/main.go index 519ed77..adf06d6 100644 --- a/new/main.go +++ b/new/main.go @@ -9,7 +9,11 @@ import ( "strings" "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "github.com/antonmedv/fx/new/dig" ) var ( @@ -70,10 +74,20 @@ func main() { return } + digInput := textinput.New() + digInput.Prompt = "" + digInput.TextStyle = lipgloss.NewStyle(). + Background(lipgloss.Color("7")). + Foreground(lipgloss.Color("0")) + digInput.Cursor.Style = lipgloss.NewStyle(). + Background(lipgloss.Color("15")). + Foreground(lipgloss.Color("0")) + m := &model{ - head: head, - top: head, - wrap: true, + head: head, + top: head, + wrap: true, + digInput: digInput, } p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion()) @@ -88,7 +102,9 @@ type model struct { head, top *node cursor int // cursor position [0, termHeight) wrap bool + margin int fileName string + digInput textinput.Model } func (m *model) Init() tea.Cmd { @@ -111,6 +127,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.down() case tea.MouseLeft: + m.digInput.Blur() if msg.Y < m.viewHeight() { if m.cursor == msg.Y { to := m.cursorPointsTo() @@ -134,11 +151,30 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case tea.KeyMsg: + if m.digInput.Focused() { + return m.handleDigKey(msg) + } return m.handleKey(msg) } return m, nil } +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: + m.digInput.Blur() + + default: + m.digInput, cmd = m.digInput.Update(msg) + n := m.dig(m.digInput.Value()) + if n != nil { + m.selectNode(n) + } + } + return m, cmd +} + func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch { case key.Matches(msg, keyMap.Quit): @@ -259,8 +295,14 @@ func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { at = at.parent() } m.selectNode(at) - } + case key.Matches(msg, keyMap.Dig): + m.digInput.SetValue(m.cursorPath()) + m.digInput.CursorEnd() + m.digInput.Width = m.termWidth - 1 + m.digInput.Focus() + + } return m, nil } @@ -369,10 +411,15 @@ func (m *model) View() string { screen = append(screen, '\n') } - statusBar := m.cursorPath() + " " - statusBar += strings.Repeat(" ", max(0, m.termWidth-len(statusBar)-len(m.fileName))) - statusBar += m.fileName - screen = append(screen, currentTheme.StatusBar([]byte(statusBar))...) + if m.digInput.Focused() { + screen = append(screen, m.digInput.View()...) + } else { + var statusBar string + statusBar += m.cursorPath() + " " + statusBar += strings.Repeat(" ", max(0, m.termWidth-len(statusBar)-len(m.fileName))) + statusBar += m.fileName + screen = append(screen, currentTheme.StatusBar([]byte(statusBar))...) + } return string(screen) } @@ -455,7 +502,7 @@ func (m *model) cursorPath() string { at := m.cursorPointsTo() for at != nil { if at.prev != nil { - if at.chunk != nil { + if at.chunk != nil && at.value == nil { at = at.parent() } if at.key != nil { @@ -477,3 +524,23 @@ func (m *model) cursorPath() string { } return path } + +func (m *model) dig(value string) *node { + p, ok := dig.SplitPath(value) + if !ok { + return nil + } + n := m.top + for _, part := range p { + if n == nil { + return nil + } + switch part := part.(type) { + case string: + n = n.findChildByKey(part) + case int: + n = n.findChildByIndex(part) + } + } + return n +} diff --git a/new/node.go b/new/node.go index 3d49e2a..a9e76ab 100644 --- a/new/node.go +++ b/new/node.go @@ -1,5 +1,9 @@ package main +import ( + "strconv" +) + type node struct { prev, next, end *node directParent *node @@ -126,3 +130,35 @@ func (n *node) expandRecursively() { at = at.next } } + +func (n *node) findChildByKey(key string) *node { + for at := n.next; at != nil && at != n.end; { + k, err := strconv.Unquote(string(at.key)) + if err != nil { + return nil + } + if k == key { + return at + } + if at.end != nil { + at = at.end.next + } else { + at = at.next + } + } + return nil +} + +func (n *node) findChildByIndex(index int) *node { + for at := n.next; at != nil && at != n.end; { + if at.index == index { + return at + } + if at.end != nil { + at = at.end.next + } else { + at = at.next + } + } + return nil +}