Separate jsonx package

pull/300/head
Anton Medvedev 3 months ago
parent 58b646c3f4
commit 1bb2803186
No known key found for this signature in database

@ -1,6 +1,6 @@
module github.com/antonmedv/fx
go 1.20
go 1.21
require (
github.com/antonmedv/clipboard v1.0.1

@ -34,13 +34,17 @@ github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I=
github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 h1:pUa4ghanp6q4IJHwE9RwLgmVFfReJN+KbQ8ExNEUUoQ=
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
@ -54,7 +58,9 @@ 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/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
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-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -91,6 +97,7 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=

@ -1,4 +1,4 @@
package main
package jsonx
import (
"fmt"
@ -17,7 +17,7 @@ type jsonParser struct {
skipFirstIdent bool
}
func parse(data []byte) (head *node, err error) {
func Parse(data []byte) (head *Node, err error) {
p := &jsonParser{
data: data,
lineNumber: 1,
@ -29,14 +29,14 @@ func parse(data []byte) (head *node, err error) {
}
}()
p.next()
var next *node
var next *Node
for p.lastChar != 0 {
value := p.parseValue()
if head == nil {
head = value
next = head
} else {
value.index = -1
value.Index = -1
next.adjacent(value)
next = value
}
@ -57,10 +57,10 @@ func (p *jsonParser) next() {
p.sourceTail.writeByte(p.lastChar)
}
func (p *jsonParser) parseValue() *node {
func (p *jsonParser) parseValue() *Node {
p.skipWhitespace()
var l *node
var l *Node
switch p.lastChar {
case '"':
l = p.parseString()
@ -84,8 +84,8 @@ func (p *jsonParser) parseValue() *node {
return l
}
func (p *jsonParser) parseString() *node {
str := &node{depth: p.depth}
func (p *jsonParser) parseString() *Node {
str := &Node{Depth: p.depth}
start := p.end - 1
p.next()
escaped := false
@ -122,13 +122,13 @@ func (p *jsonParser) parseString() *node {
p.next()
}
str.value = p.data[start:p.end]
str.Value = p.data[start:p.end]
p.next()
return str
}
func (p *jsonParser) parseNumber() *node {
num := &node{depth: p.depth}
func (p *jsonParser) parseNumber() *Node {
num := &Node{Depth: p.depth}
start := p.end - 1
// Handle negative numbers
@ -173,20 +173,20 @@ func (p *jsonParser) parseNumber() *node {
}
}
num.value = p.data[start : p.end-1]
num.Value = p.data[start : p.end-1]
return num
}
func (p *jsonParser) parseObject() *node {
object := &node{depth: p.depth}
object.value = []byte{'{'}
func (p *jsonParser) parseObject() *Node {
object := &Node{Depth: p.depth}
object.Value = []byte{'{'}
p.next()
p.skipWhitespace()
// Empty object
if p.lastChar == '}' {
object.value = append(object.value, '}')
object.Value = append(object.Value, '}')
p.next()
return object
}
@ -199,8 +199,8 @@ func (p *jsonParser) parseObject() *node {
p.depth++
key := p.parseString()
key.key, key.value = key.value, nil
object.size += 1
key.Key, key.Value = key.Value, nil
object.Size += 1
key.directParent = object
p.skipWhitespace()
@ -216,34 +216,34 @@ func (p *jsonParser) parseObject() *node {
value := p.parseValue()
p.depth--
key.value = value.value
key.size = value.size
key.next = value.next
if key.next != nil {
key.next.prev = key
key.Value = value.Value
key.Size = value.Size
key.Next = value.Next
if key.Next != nil {
key.Next.Prev = key
}
key.end = value.end
key.End = value.End
value.indirectParent = key
object.append(key)
p.skipWhitespace()
if p.lastChar == ',' {
object.end.comma = true
object.End.Comma = true
p.next()
p.skipWhitespace()
if p.lastChar == '}' {
object.end.comma = false
object.End.Comma = false
} else {
continue
}
}
if p.lastChar == '}' {
closeBracket := &node{depth: p.depth}
closeBracket.value = []byte{'}'}
closeBracket := &Node{Depth: p.depth}
closeBracket.Value = []byte{'}'}
closeBracket.directParent = object
closeBracket.index = -1
closeBracket.Index = -1
object.append(closeBracket)
p.next()
return object
@ -253,15 +253,15 @@ func (p *jsonParser) parseObject() *node {
}
}
func (p *jsonParser) parseArray() *node {
arr := &node{depth: p.depth}
arr.value = []byte{'['}
func (p *jsonParser) parseArray() *Node {
arr := &Node{Depth: p.depth}
arr.Value = []byte{'['}
p.next()
p.skipWhitespace()
if p.lastChar == ']' {
arr.value = append(arr.value, ']')
arr.Value = append(arr.Value, ']')
p.next()
return arr
}
@ -270,29 +270,29 @@ func (p *jsonParser) parseArray() *node {
p.depth++
value := p.parseValue()
value.directParent = arr
arr.size += 1
value.index = i
arr.Size += 1
value.Index = i
p.depth--
arr.append(value)
p.skipWhitespace()
if p.lastChar == ',' {
arr.end.comma = true
arr.End.Comma = true
p.next()
p.skipWhitespace()
if p.lastChar == ']' {
arr.end.comma = false
arr.End.Comma = false
} else {
continue
}
}
if p.lastChar == ']' {
closeBracket := &node{depth: p.depth}
closeBracket.value = []byte{']'}
closeBracket := &Node{Depth: p.depth}
closeBracket.Value = []byte{']'}
closeBracket.directParent = arr
closeBracket.index = -1
closeBracket.Index = -1
arr.append(closeBracket)
p.next()
return arr
@ -302,7 +302,7 @@ func (p *jsonParser) parseArray() *node {
}
}
func (p *jsonParser) parseKeyword(name string) *node {
func (p *jsonParser) parseKeyword(name string) *Node {
for i := 1; i < len(name); i++ {
p.next()
if p.lastChar != name[i] {
@ -313,8 +313,8 @@ func (p *jsonParser) parseKeyword(name string) *node {
nextCharIsSpecial := isWhitespace(p.lastChar) || p.lastChar == ',' || p.lastChar == '}' || p.lastChar == ']' || p.lastChar == 0
if nextCharIsSpecial {
keyword := &node{depth: p.depth}
keyword.value = []byte(name)
keyword := &Node{Depth: p.depth}
keyword.Value = []byte(name)
return keyword
}

@ -0,0 +1,259 @@
package jsonx
import (
"strconv"
jsonpath "github.com/antonmedv/fx/path"
)
type Node struct {
Prev, Next, End *Node
directParent *Node
indirectParent *Node
Collapsed *Node
Depth uint8
Key []byte
Value []byte
Size int
Chunk []byte
ChunkEnd *Node
Comma bool
Index int
}
// append ands a node as a child to the current node (body of {...} or [...]).
func (n *Node) append(child *Node) {
if n.End == nil {
n.End = n
}
n.End.Next = child
child.Prev = n.End
if child.End == nil {
n.End = child
} else {
n.End = child.End
}
}
// adjacent adds a node as a sibling to the current node ({}{}{} or [][][]).
func (n *Node) adjacent(child *Node) {
end := n.End
if end == nil {
end = n
}
end.Next = child
child.Prev = end
}
func (n *Node) insertChunk(chunk *Node) {
if n.ChunkEnd == nil {
n.insertAfter(chunk)
} else {
n.ChunkEnd.insertAfter(chunk)
}
n.ChunkEnd = chunk
}
func (n *Node) insertAfter(child *Node) {
if n.Next == nil {
n.Next = child
child.Prev = n
} else {
old := n.Next
n.Next = child
child.Prev = n
child.Next = old
old.Prev = child
}
}
func (n *Node) dropChunks() {
if n.ChunkEnd == nil {
return
}
n.Chunk = nil
n.Next = n.ChunkEnd.Next
if n.Next != nil {
n.Next.Prev = n
}
n.ChunkEnd = nil
}
func (n *Node) HasChildren() bool {
return n.End != nil
}
func (n *Node) Parent() *Node {
if n.directParent == nil {
return nil
}
parent := n.directParent
if parent.indirectParent != nil {
parent = parent.indirectParent
}
return parent
}
func (n *Node) IsCollapsed() bool {
return n.Collapsed != nil
}
func (n *Node) Collapse() *Node {
if n.End != nil && !n.IsCollapsed() {
n.Collapsed = n.Next
n.Next = n.End.Next
if n.Next != nil {
n.Next.Prev = n
}
}
return n
}
func (n *Node) CollapseRecursively() {
var at *Node
if n.IsCollapsed() {
at = n.Collapsed
} else {
at = n.Next
}
for at != nil && at != n.End {
if at.HasChildren() {
at.CollapseRecursively()
at.Collapse()
}
at = at.Next
}
}
func (n *Node) Expand() {
if n.IsCollapsed() {
if n.Next != nil {
n.Next.Prev = n.End
}
n.Next = n.Collapsed
n.Collapsed = nil
}
}
func (n *Node) ExpandRecursively(level, maxLevel int) {
if level >= maxLevel {
return
}
if n.IsCollapsed() {
n.Expand()
}
it := n.Next
for it != nil && it != n.End {
if it.HasChildren() {
it.ExpandRecursively(level+1, maxLevel)
it = it.End.Next
} else {
it = it.Next
}
}
}
func (n *Node) FindChildByKey(key string) *Node {
it := n.Next
for it != nil && it != n.End {
if it.Key != nil {
k, err := strconv.Unquote(string(it.Key))
if err != nil {
return nil
}
if k == key {
return it
}
}
if it.ChunkEnd != nil {
it = it.ChunkEnd.Next
} else if it.End != nil {
it = it.End.Next
} else {
it = it.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
}
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 && jsonpath.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 {
key := string(it.Key)
unquoted, err := strconv.Unquote(key)
if err == nil {
key = unquoted
}
paths = append(paths, key)
nodes = append(nodes, it)
}
if it.HasChildren() {
it = it.End.Next
} else {
it = it.Next
}
}
return paths, nodes
}

@ -1,4 +1,4 @@
package main
package jsonx
import (
"testing"
@ -8,28 +8,28 @@ import (
)
func TestNode_paths(t *testing.T) {
n, err := parse([]byte(`{"a": 1, "b": {"f": 2}, "c": [3, 4]}`))
n, err := Parse([]byte(`{"a": 1, "b": {"f": 2}, "c": [3, 4]}`))
require.NoError(t, err)
var paths []string
var nodes []*node
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]}`))
n, err := Parse([]byte(`{"a": 1, "b": {"f": 2}, "c": [3, 4]}`))
require.NoError(t, err)
paths, _ := n.children()
paths, _ := n.Children()
assert.Equal(t, []string{"a", "b", "c"}, paths)
}
func TestNode_expandRecursively(t *testing.T) {
n, err := parse([]byte(`{"a": {"b": {"c": 1}}}`))
n, err := Parse([]byte(`{"a": {"b": {"c": 1}}}`))
require.NoError(t, err)
n.collapseRecursively()
n.expandRecursively(0, 3)
assert.Equal(t, `"c"`, string(n.next.next.next.key))
n.CollapseRecursively()
n.ExpandRecursively(0, 3)
assert.Equal(t, `"c"`, string(n.Next.Next.Next.Key))
}

@ -1,4 +1,4 @@
package main
package jsonx
import (
"strings"

@ -0,0 +1,9 @@
package jsonx
func isHexDigit(ch byte) bool {
return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')
}
func isDigit(ch byte) bool {
return ch >= '0' && ch <= '9'
}

@ -1,4 +1,4 @@
package main
package jsonx
import (
"unicode/utf8"
@ -6,56 +6,56 @@ import (
"github.com/mattn/go-runewidth"
)
func dropWrapAll(n *node) {
func DropWrapAll(n *Node) {
for n != nil {
if n.value != nil && n.value[0] == '"' {
if n.Value != nil && n.Value[0] == '"' {
n.dropChunks()
}
if n.isCollapsed() {
n = n.collapsed
if n.IsCollapsed() {
n = n.Collapsed
} else {
n = n.next
n = n.Next
}
}
}
func wrapAll(n *node, termWidth int) {
func WrapAll(n *Node, termWidth int) {
if termWidth <= 0 {
return
}
for n != nil {
if n.value != nil && n.value[0] == '"' {
if n.Value != nil && n.Value[0] == '"' {
n.dropChunks()
lines, count := doWrap(n, termWidth)
if count > 1 {
n.chunk = lines[0]
n.Chunk = lines[0]
for i := 1; i < count; i++ {
child := &node{
child := &Node{
directParent: n,
depth: n.depth,
chunk: lines[i],
Depth: n.Depth,
Chunk: lines[i],
}
if n.comma && i == count-1 {
child.comma = true
if n.Comma && i == count-1 {
child.Comma = true
}
n.insertChunk(child)
}
}
}
if n.isCollapsed() {
n = n.collapsed
if n.IsCollapsed() {
n = n.Collapsed
} else {
n = n.next
n = n.Next
}
}
}
func doWrap(n *node, termWidth int) ([][]byte, int) {
func doWrap(n *Node, termWidth int) ([][]byte, int) {
lines := make([][]byte, 0, 1)
width := int(n.depth) * 2
width := int(n.Depth) * 2
if n.key != nil {
for _, ch := range string(n.key) {
if n.Key != nil {
for _, ch := range string(n.Key) {
width += runewidth.RuneWidth(ch)
}
width += 2 // for ": "
@ -63,15 +63,15 @@ func doWrap(n *node, termWidth int) ([][]byte, int) {
linesCount := 0
start, end := 0, 0
b := n.value
b := n.Value
for len(b) > 0 {
r, size := utf8.DecodeRune(b)
w := runewidth.RuneWidth(r)
if width+w > termWidth {
lines = append(lines, n.value[start:end])
lines = append(lines, n.Value[start:end])
start = end
width = int(n.depth) * 2
width = int(n.Depth) * 2
linesCount++
}
width += w
@ -80,7 +80,7 @@ func doWrap(n *node, termWidth int) ([][]byte, int) {
}
if start < end {
lines = append(lines, n.value[start:])
lines = append(lines, n.Value[start:])
linesCount++
}

@ -26,6 +26,7 @@ import (
"github.com/sahilm/fuzzy"
"github.com/antonmedv/fx/internal/complete"
. "github.com/antonmedv/fx/internal/jsonx"
jsonpath "github.com/antonmedv/fx/path"
)
@ -143,7 +144,7 @@ func main() {
}
}
head, err := parse(data)
head, err := Parse(data)
if err != nil {
fmt.Print(err.Error())
os.Exit(1)
@ -200,7 +201,7 @@ func main() {
type model struct {
termWidth, termHeight int
head, top *node
head, top *Node
cursor int // cursor position [0, termHeight)
showCursor bool
wrap bool
@ -229,7 +230,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.help.Height = m.termHeight - 1
m.preview.Width = m.termWidth
m.preview.Height = m.termHeight - 1
wrapAll(m.top, m.termWidth)
WrapAll(m.top, m.termWidth)
m.redoSearch()
}
@ -257,18 +258,18 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.cursor == msg.Y {
to := m.cursorPointsTo()
if to != nil {
if to.isCollapsed() {
to.expand()
if to.IsCollapsed() {
to.Expand()
} else {
to.collapse()
to.Collapse()
}
}
} else {
to := m.at(msg.Y)
if to != nil {
m.cursor = msg.Y
if to.isCollapsed() {
to.expand()
if to.IsCollapsed() {
to.Expand()
}
}
}
@ -490,11 +491,11 @@ func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case key.Matches(msg, keyMap.NextSibling):
pointsTo := m.cursorPointsTo()
var nextSibling *node
if pointsTo.end != nil && pointsTo.end.next != nil {
nextSibling = pointsTo.end.next
var nextSibling *Node
if pointsTo.End != nil && pointsTo.End.Next != nil {
nextSibling = pointsTo.End.Next
} else {
nextSibling = pointsTo.next
nextSibling = pointsTo.Next
}
if nextSibling != nil {
m.selectNode(nextSibling)
@ -502,13 +503,13 @@ func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case key.Matches(msg, keyMap.PrevSibling):
pointsTo := m.cursorPointsTo()
var prevSibling *node
if pointsTo.parent() != nil && pointsTo.parent().end == pointsTo {
prevSibling = pointsTo.parent()
} else if pointsTo.prev != nil {
prevSibling = pointsTo.prev
parent := prevSibling.parent()
if parent != nil && parent.end == prevSibling {
var prevSibling *Node
if pointsTo.Parent() != nil && pointsTo.Parent().End == pointsTo {
prevSibling = pointsTo.Parent()
} else if pointsTo.Prev != nil {
prevSibling = pointsTo.Prev
parent := prevSibling.Parent()
if parent != nil && parent.End == prevSibling {
prevSibling = parent
}
}
@ -518,41 +519,41 @@ func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case key.Matches(msg, keyMap.Collapse):
n := m.cursorPointsTo()
if n.hasChildren() && !n.isCollapsed() {
n.collapse()
if n.HasChildren() && !n.IsCollapsed() {
n.Collapse()
} else {
if n.parent() != nil {
n = n.parent()
if n.Parent() != nil {
n = n.Parent()
}
}
m.selectNode(n)
case key.Matches(msg, keyMap.Expand):
m.cursorPointsTo().expand()
m.cursorPointsTo().Expand()
m.showCursor = true
case key.Matches(msg, keyMap.CollapseRecursively):
n := m.cursorPointsTo()
if n.hasChildren() {
n.collapseRecursively()
if n.HasChildren() {
n.CollapseRecursively()
}
m.showCursor = true
case key.Matches(msg, keyMap.ExpandRecursively):
n := m.cursorPointsTo()
if n.hasChildren() {
n.expandRecursively(0, math.MaxInt)
if n.HasChildren() {
n.ExpandRecursively(0, math.MaxInt)
}
m.showCursor = true
case key.Matches(msg, keyMap.CollapseAll):
n := m.top
for n != nil {
n.collapseRecursively()
if n.end == nil {
n.CollapseRecursively()
if n.End == nil {
n = nil
} else {
n = n.end.next
n = n.End.Next
}
}
m.cursor = 0
@ -563,21 +564,21 @@ func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
at := m.cursorPointsTo()
n := m.top
for n != nil {
n.expandRecursively(0, math.MaxInt)
if n.end == nil {
n.ExpandRecursively(0, math.MaxInt)
if n.End == nil {
n = nil
} else {
n = n.end.next
n = n.End.Next
}
}
m.selectNode(at)
case key.Matches(msg, keyMap.CollapseLevel):
at := m.cursorPointsTo()
if at != nil && at.hasChildren() {
if at != nil && at.HasChildren() {
toLevel, _ := strconv.Atoi(msg.String())
at.collapseRecursively()
at.expandRecursively(0, toLevel)
at.CollapseRecursively()
at.ExpandRecursively(0, toLevel)
m.showCursor = true
}
@ -585,12 +586,12 @@ func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
at := m.cursorPointsTo()
m.wrap = !m.wrap
if m.wrap {
wrapAll(m.top, m.termWidth)
WrapAll(m.top, m.termWidth)
} else {
dropWrapAll(m.top)
DropWrapAll(m.top)
}
if at.chunk != nil && at.value == nil {
at = at.parent()
if at.Chunk != nil && at.Value == nil {
at = at.Parent()
}
m.redoSearch()
m.selectNode(at)
@ -632,8 +633,8 @@ func (m *model) up() {
m.cursor--
if m.cursor < 0 {
m.cursor = 0
if m.head.prev != nil {
m.head = m.head.prev
if m.head.Prev != nil {
m.head = m.head.Prev
}
}
}
@ -648,8 +649,8 @@ func (m *model) down() {
}
if m.cursor >= m.viewHeight() {
m.cursor = m.viewHeight() - 1
if m.head.next != nil {
m.head = m.head.next
if m.head.Next != nil {
m.head = m.head.Next
}
}
}
@ -659,7 +660,7 @@ func (m *model) visibleLines() int {
n := m.head
for n != nil && visibleLines < m.viewHeight() {
visibleLines++
n = n.next
n = n.Next
}
return visibleLines
}
@ -669,10 +670,10 @@ func (m *model) scrollIntoView() {
if m.cursor >= visibleLines {
m.cursor = visibleLines - 1
}
for visibleLines < m.viewHeight() && m.head.prev != nil {
for visibleLines < m.viewHeight() && m.head.Prev != nil {
visibleLines++
m.cursor++
m.head = m.head.prev
m.head = m.head.Prev
}
}
@ -695,7 +696,7 @@ func (m *model) View() string {
if n == nil {
break
}
for ident := 0; ident < int(n.depth); ident++ {
for ident := 0; ident < int(n.Depth); ident++ {
screen = append(screen, ' ', ' ')
}
@ -704,7 +705,7 @@ func (m *model) View() string {
isSelected = false // don't highlight the cursor while iterating search results
}
if n.key != nil {
if n.Key != nil {
screen = append(screen, m.prettyKey(n, isSelected)...)
screen = append(screen, colon...)
isSelected = false // don't highlight the key's value
@ -712,35 +713,35 @@ func (m *model) View() string {
screen = append(screen, m.prettyPrint(n, isSelected)...)
if n.isCollapsed() {
if n.value[0] == '{' {
if n.collapsed.key != nil {
screen = append(screen, currentTheme.Preview(n.collapsed.key)...)
if n.IsCollapsed() {
if n.Value[0] == '{' {
if n.Collapsed.Key != nil {
screen = append(screen, currentTheme.Preview(n.Collapsed.Key)...)
screen = append(screen, colonPreview...)
}
screen = append(screen, dot3...)
screen = append(screen, closeCurlyBracket...)
} else if n.value[0] == '[' {
} else if n.Value[0] == '[' {
screen = append(screen, dot3...)
screen = append(screen, closeSquareBracket...)
}
if n.end != nil && n.end.comma {
if n.End != nil && n.End.Comma {
screen = append(screen, comma...)
}
}
if n.comma {
if n.Comma {
screen = append(screen, comma...)
}
if showSizes && len(n.value) > 0 && (n.value[0] == '{' || n.value[0] == '[') {
if n.isCollapsed() || n.size > 1 {
screen = append(screen, currentTheme.Size([]byte(fmt.Sprintf(" // %d", n.size)))...)
if showSizes && len(n.Value) > 0 && (n.Value[0] == '{' || n.Value[0] == '[') {
if n.IsCollapsed() || n.Size > 1 {
screen = append(screen, currentTheme.Size([]byte(fmt.Sprintf(" // %d", n.Size)))...)
}
}
screen = append(screen, '\n')
printedLines++
n = n.next
n = n.Next
}
for i := printedLines; i < m.viewHeight(); i++ {
@ -781,8 +782,8 @@ func (m *model) View() string {
return string(screen)
}
func (m *model) prettyKey(node *node, selected bool) []byte {
b := node.key
func (m *model) prettyKey(node *Node, selected bool) []byte {
b := node.Key
style := currentTheme.Key
if selected {
@ -806,19 +807,19 @@ func (m *model) prettyKey(node *node, selected bool) []byte {
}
}
func (m *model) prettyPrint(node *node, selected bool) []byte {
func (m *model) prettyPrint(node *Node, selected bool) []byte {
var b []byte
if node.chunk != nil {
b = node.chunk
if node.Chunk != nil {
b = node.Chunk
} else {
b = node.value
b = node.Value
}
if len(b) == 0 {
return b
}
style := valueStyle(b, selected, node.chunk != nil)
style := valueStyle(b, selected, node.Chunk != nil)
if indexes, ok := m.search.values[node]; ok {
var out []byte
@ -847,34 +848,34 @@ func (m *model) viewHeight() int {
return m.termHeight - 1
}
func (m *model) cursorPointsTo() *node {
func (m *model) cursorPointsTo() *Node {
return m.at(m.cursor)
}
func (m *model) at(pos int) *node {
func (m *model) at(pos int) *Node {
head := m.head
for i := 0; i < pos; i++ {
if head == nil {
break
}
head = head.next
head = head.Next
}
return head
}
func (m *model) findBottom() *node {
func (m *model) findBottom() *Node {
n := m.head
for n.next != nil {
if n.end != nil {
n = n.end
for n.Next != nil {
if n.End != nil {
n = n.End
} else {
n = n.next
n = n.Next
}
}
return n
}
func (m *model) nodeInsideView(n *node) bool {
func (m *model) nodeInsideView(n *Node) bool {
if n == nil {
return false
}
@ -886,12 +887,12 @@ func (m *model) nodeInsideView(n *node) bool {
if head == n {
return true
}
head = head.next
head = head.Next
}
return false
}
func (m *model) selectNodeInView(n *node) {
func (m *model) selectNodeInView(n *Node) {
head := m.head
for i := 0; i < m.viewHeight(); i++ {
if head == nil {
@ -901,11 +902,11 @@ func (m *model) selectNodeInView(n *node) {
m.cursor = i
return
}
head = head.next
head = head.Next
}
}
func (m *model) selectNode(n *node) {
func (m *model) selectNode(n *Node) {
m.showCursor = true
if m.nodeInsideView(n) {
m.selectNodeInView(n)
@ -915,10 +916,10 @@ func (m *model) selectNode(n *node) {
m.head = n
m.scrollIntoView()
}
parent := n.parent()
parent := n.Parent()
for parent != nil {
parent.expand()
parent = parent.parent()
parent.Expand()
parent = parent.Parent()
}
}
@ -926,23 +927,23 @@ func (m *model) cursorPath() string {
path := ""
at := m.cursorPointsTo()
for at != nil {
if at.prev != nil {
if at.chunk != nil && at.value == nil {
at = at.parent()
if at.Prev != nil {
if at.Chunk != nil && at.Value == nil {
at = at.Parent()
}
if at.key != nil {
quoted := string(at.key)
if at.Key != nil {
quoted := string(at.Key)
unquoted, err := strconv.Unquote(quoted)
if err == nil && jsonpath.Identifier.MatchString(unquoted) {
path = "." + unquoted + path
} else {
path = "[" + quoted + "]" + path
}
} else if at.index >= 0 {
path = "[" + strconv.Itoa(at.index) + "]" + path
} else if at.Index >= 0 {
path = "[" + strconv.Itoa(at.Index) + "]" + path
}
}
at = at.parent()
at = at.Parent()
}
return path
}
@ -952,55 +953,55 @@ func (m *model) cursorValue() string {
if at == nil {
return ""
}
parent := at.parent()
parent := at.Parent()
if parent != nil {
// wrapped string part
if at.chunk != nil && at.value == nil {
if at.Chunk != nil && at.Value == nil {
at = parent
}
if len(at.value) == 1 && at.value[0] == '}' || at.value[0] == ']' {
if len(at.Value) == 1 && at.Value[0] == '}' || at.Value[0] == ']' {
at = parent
}
}
if len(at.value) > 0 && at.value[0] == '"' {
str, err := strconv.Unquote(string(at.value))
if len(at.Value) > 0 && at.Value[0] == '"' {
str, err := strconv.Unquote(string(at.Value))
if err == nil {
return str
}
return string(at.value)
return string(at.Value)
}
var out strings.Builder
out.Write(at.value)
out.Write(at.Value)
out.WriteString("\n")
if at.hasChildren() {
it := at.next
if at.isCollapsed() {
it = at.collapsed
if at.HasChildren() {
it := at.Next
if at.IsCollapsed() {
it = at.Collapsed
}
for it != nil {
out.WriteString(strings.Repeat(" ", int(it.depth-at.depth)))
if it.key != nil {
out.Write(it.key)
out.WriteString(strings.Repeat(" ", int(it.Depth-at.Depth)))
if it.Key != nil {
out.Write(it.Key)
out.WriteString(": ")
}
if it.value != nil {
out.Write(it.value)
if it.Value != nil {
out.Write(it.Value)
}
if it == at.end {
if it == at.End {
break
}
if it.comma {
if it.Comma {
out.WriteString(",")
}
out.WriteString("\n")
if it.chunkEnd != nil {
it = it.chunkEnd.next
} else if it.isCollapsed() {
it = it.collapsed
if it.ChunkEnd != nil {
it = it.ChunkEnd.Next
} else if it.IsCollapsed() {
it = it.Collapsed
} else {
it = it.next
it = it.Next
}
}
}
@ -1012,16 +1013,16 @@ func (m *model) cursorKey() string {
if at == nil {
return ""
}
if at.key != nil {
if at.Key != nil {
var v string
_ = json.Unmarshal(at.key, &v)
_ = json.Unmarshal(at.Key, &v)
return v
}
return strconv.Itoa(at.index)
return strconv.Itoa(at.Index)
}
func (m *model) selectByPath(path []any) *node {
func (m *model) selectByPath(path []any) *Node {
n := m.currentTopNode()
for _, part := range path {
if n == nil {
@ -1029,21 +1030,21 @@ func (m *model) selectByPath(path []any) *node {
}
switch part := part.(type) {
case string:
n = n.findChildByKey(part)
n = n.FindChildByKey(part)
case int:
n = n.findChildByIndex(part)
n = n.FindChildByIndex(part)
}
}
return n
}
func (m *model) currentTopNode() *node {
func (m *model) currentTopNode() *Node {
at := m.cursorPointsTo()
if at == nil {
return nil
}
for at.parent() != nil {
at = at.parent()
for at.Parent() != nil {
at = at.Parent()
}
return at
}
@ -1069,8 +1070,8 @@ func (m *model) doSearch(s string) {
n := m.top
searchIndex := 0
for n != nil {
if n.key != nil {
indexes := re.FindAllIndex(n.key, -1)
if n.Key != nil {
indexes := re.FindAllIndex(n.Key, -1)
if len(indexes) > 0 {
for i, pair := range indexes {
m.search.results = append(m.search.results, n)
@ -1079,24 +1080,24 @@ func (m *model) doSearch(s string) {
searchIndex += len(indexes)
}
}
indexes := re.FindAllIndex(n.value, -1)
indexes := re.FindAllIndex(n.Value, -1)
if len(indexes) > 0 {
for range indexes {
m.search.results = append(m.search.results, n)
}
if n.chunk != nil {
if n.Chunk != nil {
// String can be split into chunks, so we need to map the indexes to the chunks.
chunks := [][]byte{n.chunk}
chunkNodes := []*node{n}
chunks := [][]byte{n.Chunk}
chunkNodes := []*Node{n}
it := n.next
it := n.Next
for it != nil {
chunkNodes = append(chunkNodes, it)
chunks = append(chunks, it.chunk)
if it == n.chunkEnd {
chunks = append(chunks, it.Chunk)
if it == n.ChunkEnd {
break
}
it = it.next
it = it.Next
}
chunkMatches := splitIndexesToChunks(chunks, indexes, searchIndex)
@ -1111,10 +1112,10 @@ func (m *model) doSearch(s string) {
searchIndex += len(indexes)
}
if n.isCollapsed() {
n = n.collapsed
if n.IsCollapsed() {
n = n.Collapsed
} else {
n = n.next
n = n.Next
}
}
@ -1145,7 +1146,7 @@ func (m *model) redoSearch() {
}
}
func (m *model) dig(v string) *node {
func (m *model) dig(v string) *Node {
p, ok := jsonpath.Split(v)
if !ok {
return nil
@ -1167,7 +1168,7 @@ func (m *model) dig(v string) *node {
return nil
}
keys, nodes := at.children()
keys, nodes := at.Children()
matches := fuzzy.Find(searchTerm, keys)
if len(matches) == 0 {

@ -13,6 +13,8 @@ import (
"github.com/charmbracelet/x/exp/teatest"
"github.com/muesli/termenv"
"github.com/stretchr/testify/require"
"github.com/antonmedv/fx/internal/jsonx"
)
func init() {
@ -26,7 +28,7 @@ func prepare(t *testing.T) *teatest.TestModel {
json, err := io.ReadAll(file)
require.NoError(t, err)
head, err := parse(json)
head, err := jsonx.Parse(json)
require.NoError(t, err)
m := &model{

@ -1,259 +0,0 @@
package main
import (
"strconv"
jsonpath "github.com/antonmedv/fx/path"
)
type node struct {
prev, next, end *node
directParent *node
indirectParent *node
collapsed *node
depth uint8
key []byte
value []byte
size int
chunk []byte
chunkEnd *node
comma bool
index int
}
// append ands a node as a child to the current node (body of {...} or [...]).
func (n *node) append(child *node) {
if n.end == nil {
n.end = n
}
n.end.next = child
child.prev = n.end
if child.end == nil {
n.end = child
} else {
n.end = child.end
}
}
// adjacent adds a node as a sibling to the current node ({}{}{} or [][][]).
func (n *node) adjacent(child *node) {
end := n.end
if end == nil {
end = n
}
end.next = child
child.prev = end
}
func (n *node) insertChunk(chunk *node) {
if n.chunkEnd == nil {
n.insertAfter(chunk)
} else {
n.chunkEnd.insertAfter(chunk)
}
n.chunkEnd = chunk
}
func (n *node) insertAfter(child *node) {
if n.next == nil {
n.next = child
child.prev = n
} else {
old := n.next
n.next = child
child.prev = n
child.next = old
old.prev = child
}
}
func (n *node) dropChunks() {
if n.chunkEnd == nil {
return
}
n.chunk = nil
n.next = n.chunkEnd.next
if n.next != nil {
n.next.prev = n
}
n.chunkEnd = nil
}
func (n *node) hasChildren() bool {
return n.end != nil
}
func (n *node) parent() *node {
if n.directParent == nil {
return nil
}
parent := n.directParent
if parent.indirectParent != nil {
parent = parent.indirectParent
}
return parent
}
func (n *node) isCollapsed() bool {
return n.collapsed != nil
}
func (n *node) collapse() *node {
if n.end != nil && !n.isCollapsed() {
n.collapsed = n.next
n.next = n.end.next
if n.next != nil {
n.next.prev = n
}
}
return n
}
func (n *node) collapseRecursively() {
var at *node
if n.isCollapsed() {
at = n.collapsed
} else {
at = n.next
}
for at != nil && at != n.end {
if at.hasChildren() {
at.collapseRecursively()
at.collapse()
}
at = at.next
}
}
func (n *node) expand() {
if n.isCollapsed() {
if n.next != nil {
n.next.prev = n.end
}
n.next = n.collapsed
n.collapsed = nil
}
}
func (n *node) expandRecursively(level, maxLevel int) {
if level >= maxLevel {
return
}
if n.isCollapsed() {
n.expand()
}
it := n.next
for it != nil && it != n.end {
if it.hasChildren() {
it.expandRecursively(level+1, maxLevel)
it = it.end.next
} else {
it = it.next
}
}
}
func (n *node) findChildByKey(key string) *node {
it := n.next
for it != nil && it != n.end {
if it.key != nil {
k, err := strconv.Unquote(string(it.key))
if err != nil {
return nil
}
if k == key {
return it
}
}
if it.chunkEnd != nil {
it = it.chunkEnd.next
} else if it.end != nil {
it = it.end.next
} else {
it = it.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
}
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 && jsonpath.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 {
key := string(it.key)
unquoted, err := strconv.Unquote(key)
if err == nil {
key = unquoted
}
paths = append(paths, key)
nodes = append(nodes, it)
}
if it.hasChildren() {
it = it.end.next
} else {
it = it.next
}
}
return paths, nodes
}

@ -1,18 +1,22 @@
package main
import (
. "github.com/antonmedv/fx/internal/jsonx"
)
type search struct {
err error
results []*node
results []*Node
cursor int
values map[*node][]match
keys map[*node][]match
values map[*Node][]match
keys map[*Node][]match
}
func newSearch() *search {
return &search{
results: make([]*node, 0),
values: make(map[*node][]match),
keys: make(map[*node][]match),
results: make([]*Node, 0),
values: make(map[*Node][]match),
keys: make(map[*Node][]match),
}
}

@ -4,21 +4,10 @@ import (
"strings"
)
func isHexDigit(ch byte) bool {
return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')
}
func isDigit(ch byte) bool {
return ch >= '0' && ch <= '9'
}
func max(i, j int) int {
if i > j {
return i
}
return j
}
func regexCase(code string) (string, bool) {
if strings.HasSuffix(code, "/i") {
return code[:len(code)-2], true

Loading…
Cancel
Save