Add fallback engine (#300)

pull/297/head
Anton Medvedev 1 month ago committed by GitHub
parent b7c4bab9f1
commit 04f79a71d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -13,7 +13,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
go-version: 1.21
- uses: snapcore/action-build@v1
id: build

@ -15,7 +15,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
go-version: 1.21
- name: Test
run: go test ./...

@ -1,6 +1,6 @@
module github.com/antonmedv/fx
go 1.20
go 1.21
require (
github.com/antonmedv/clipboard v1.0.1
@ -23,10 +23,10 @@ require (
github.com/aymanbagabas/go-udiff v0.1.3 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect

@ -23,8 +23,9 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
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/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20240220182346-e401ed450204 h1:O7I1iuzEA7SG+dK8ocOBSlYAA9jBUmCYl/Qa7ey7JAM=
github.com/dop251/goja v0.0.0-20240220182346-e401ed450204/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
@ -33,15 +34,20 @@ 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/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
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=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@ -52,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=
@ -89,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,6 +1,7 @@
package complete
import (
_ "embed"
"fmt"
"os"
"path/filepath"
@ -10,6 +11,7 @@ import (
"github.com/dop251/goja"
"github.com/goccy/go-yaml"
"github.com/antonmedv/fx/internal/engine"
"github.com/antonmedv/fx/internal/shlex"
)
@ -53,6 +55,9 @@ var globals = []string{
"skip",
}
//go:embed prelude.js
var prelude string
func Complete() bool {
compLine, ok := os.LookupEnv("COMP_LINE")
@ -145,7 +150,7 @@ func doComplete(compLine string, compWord string) {
}
}
codeComplete(string(input), args, compWord)
codeComplete(input, args, compWord)
}
}
@ -163,7 +168,7 @@ func globalsComplete(compWord string) bool {
return false
}
func codeComplete(input string, args []string, compWord string) {
func codeComplete(input []byte, args []string, compWord string) {
args = args[2:] // Drop binary & file from the args.
if compWord == "" {
@ -180,21 +185,24 @@ func codeComplete(input string, args []string, compWord string) {
var code strings.Builder
code.WriteString(prelude)
code.WriteString(fmt.Sprintf("let json = %s\n", input))
code.WriteString(engine.Stdlib)
code.WriteString("let json = ")
code.Write(input)
for _, arg := range args {
if arg == "" {
if arg == "" { // After dropTail, we can have empty strings.
continue
}
code.WriteString(Transform(arg))
code.WriteString(engine.Transform(arg))
}
code.WriteString("\n__keys\n")
out, err := goja.New().RunString(code.String())
vm := goja.New()
value, err := vm.RunString(code.String())
if err != nil {
return
}
if array, ok := out.Export().([]interface{}); ok {
if array, ok := value.Export().([]interface{}); ok {
prefix := dropTail(compWord)
var reply []string
for _, key := range array {

@ -0,0 +1,10 @@
const __keys = new Set()
Object.prototype.__keys = function () {
if (Array.isArray(this)) return
if (typeof this === 'string') return
if (this instanceof String) return
if (typeof this === 'object' && this !== null)
Object.keys(this).forEach(x => __keys.add(x))
}

@ -0,0 +1,126 @@
package engine
import (
_ "embed"
"fmt"
"io"
"os"
"strconv"
"strings"
"github.com/dop251/goja"
"github.com/goccy/go-yaml"
"github.com/antonmedv/fx/internal/jsonx"
)
//go:embed stdlib.js
var Stdlib string
//go:embed prelude.js
var prelude string
func Reduce(args []string) {
if len(args) < 1 {
panic("args must have at least one element")
}
var (
flagYaml bool
flagRaw bool
flagSlurp bool
)
var src io.Reader = os.Stdin
if isFile(args[0]) {
src = open(args[0], &flagYaml)
args = args[1:]
} else if isFile(args[len(args)-1]) {
src = open(args[len(args)-1], &flagYaml)
args = args[:len(args)-1]
}
var fns []string
for _, arg := range args {
switch arg {
case "--yaml":
flagYaml = true
case "--raw", "-r":
flagRaw = true
case "--slurp", "-s":
flagSlurp = true
case "-rs", "-sr":
flagRaw = true
flagSlurp = true
default:
fns = append(fns, arg)
}
}
if flagSlurp {
println("Error: Built-in JS engine does not support \"--slurp\" flag. Install Node.js or Deno to use this flag.")
os.Exit(1)
}
data, err := io.ReadAll(src)
if err != nil {
panic(err)
}
if flagRaw {
data = []byte(strconv.Quote(string(data)))
} else if flagYaml {
data, err = yaml.YAMLToJSON(data)
if err != nil {
println(err.Error())
os.Exit(1)
}
} else {
node, err := jsonx.Parse(data)
if err != nil {
println(err.Error())
os.Exit(1)
}
data = []byte(node.String())
}
var code strings.Builder
code.WriteString(prelude)
code.WriteString(Stdlib)
code.WriteString(fmt.Sprintf("let json = JSON.parse(%q)\n", data))
for _, fn := range fns {
code.WriteString(Transform(fn))
}
code.WriteString("JSON.stringify(json)")
vm := goja.New()
vm.Set("println", func(s string) any {
fmt.Println(s)
return nil
})
value, err := vm.RunString(code.String())
if err != nil {
println(err.Error())
os.Exit(1)
}
output, ok := value.Export().(string)
if !ok {
println("undefined")
return
}
node, err := jsonx.Parse([]byte(output))
if err != nil {
println(err.Error())
os.Exit(1)
}
if len(node.Value) > 0 && node.Value[0] == '"' {
s, _ := strconv.Unquote(string(node.Value))
fmt.Println(s)
return
}
fmt.Print(node.PrettyPrint())
}

@ -0,0 +1,9 @@
const console = {
log: function (...args) {
const parts = []
for (const arg of args) {
parts.push(typeof arg === 'string' ? arg : JSON.stringify(arg, null, 2))
}
println(parts.join(' '))
},
}

@ -1,16 +1,3 @@
package complete
const prelude = `
const __keys = new Set()
Object.prototype.__keys = function () {
if (Array.isArray(this)) return
if (typeof this === 'string') return
if (this instanceof String) return
if (typeof this === 'object' && this !== null)
Object.keys(this).forEach(x => __keys.add(x))
}
function apply(fn, ...args) {
if (typeof fn === 'function') return fn(...args)
return fn
@ -20,23 +7,23 @@ function len(x) {
if (Array.isArray(x)) return x.length
if (typeof x === 'string') return x.length
if (typeof x === 'object' && x !== null) return Object.keys(x).length
throw new Error()
throw new Error(`Cannot get length of ${typeof x}`)
}
function uniq(x) {
if (Array.isArray(x)) return [...new Set(x)]
throw new Error()
throw new Error(`Cannot get unique values of ${typeof x}`)
}
function sort(x) {
if (Array.isArray(x)) return x.sort()
throw new Error()
throw new Error(`Cannot sort ${typeof x}`)
}
function map(fn) {
return function (x) {
if (Array.isArray(x)) return x.map((v, i) => fn(v, i))
throw new Error()
throw new Error(`Cannot map ${typeof x}`)
}
}
@ -47,7 +34,7 @@ function sortBy(fn) {
const fb = fn(b)
return fa < fb ? -1 : fa > fb ? 1 : 0
})
throw new Error()
throw new Error(`Cannot sort ${typeof x}`)
}
}
@ -85,21 +72,20 @@ function zip(...x) {
function flatten(x) {
if (Array.isArray(x)) return x.flat()
throw new Error()
throw new Error(`Cannot flatten ${typeof x}`)
}
function reverse(x) {
if (Array.isArray(x)) return x.reverse()
throw new Error()
throw new Error(`Cannot reverse ${typeof x}`)
}
function keys(x) {
if (typeof x === 'object' && x !== null) return Object.keys(x)
throw new Error()
throw new Error(`Cannot get keys of ${typeof x}`)
}
function values(x) {
if (typeof x === 'object' && x !== null) return Object.values(x)
throw new Error()
throw new Error(`Cannot get values of ${typeof x}`)
}
`

@ -1,4 +1,4 @@
package complete
package engine
import (
"fmt"

@ -0,0 +1,36 @@
package engine
import (
"errors"
"io/fs"
"os"
"path"
"regexp"
)
func isFile(name string) bool {
stat, err := os.Stat(name)
if err != nil {
return false
}
return !stat.IsDir()
}
func open(filePath string, flagYaml *bool) *os.File {
f, err := os.Open(filePath)
if err != nil {
var pathError *fs.PathError
if errors.As(err, &pathError) {
println(err.Error())
os.Exit(1)
} else {
panic(err)
}
}
fileName := path.Base(filePath)
hasYamlExt, _ := regexp.MatchString(`(?i)\.ya?ml$`, fileName)
if !*flagYaml && hasYamlExt {
*flagYaml = true
}
return f
}

@ -1,10 +1,12 @@
package main
package jsonx
import (
"fmt"
"strconv"
"strings"
"unicode/utf8"
"github.com/antonmedv/fx/internal/utils"
)
type jsonParser struct {
@ -17,7 +19,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 +31,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 +59,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 +86,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
@ -96,7 +98,7 @@ func (p *jsonParser) parseString() *node {
var unicode string
for i := 0; i < 4; i++ {
p.next()
if !isHexDigit(p.lastChar) {
if !utils.IsHexDigit(p.lastChar) {
panic(fmt.Sprintf("Invalid Unicode escape sequence '\\u%s%c'", unicode, p.lastChar))
}
unicode += string(p.lastChar)
@ -122,19 +124,19 @@ 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
if p.lastChar == '-' {
p.next()
if !isDigit(p.lastChar) {
if !utils.IsDigit(p.lastChar) {
panic(fmt.Sprintf("Invalid character %q in number", p.lastChar))
}
}
@ -143,7 +145,7 @@ func (p *jsonParser) parseNumber() *node {
if p.lastChar == '0' {
p.next()
} else {
for isDigit(p.lastChar) {
for utils.IsDigit(p.lastChar) {
p.next()
}
}
@ -151,10 +153,10 @@ func (p *jsonParser) parseNumber() *node {
// Decimal portion
if p.lastChar == '.' {
p.next()
if !isDigit(p.lastChar) {
if !utils.IsDigit(p.lastChar) {
panic(fmt.Sprintf("Invalid character %q in number", p.lastChar))
}
for isDigit(p.lastChar) {
for utils.IsDigit(p.lastChar) {
p.next()
}
}
@ -165,28 +167,28 @@ func (p *jsonParser) parseNumber() *node {
if p.lastChar == '+' || p.lastChar == '-' {
p.next()
}
if !isDigit(p.lastChar) {
if !utils.IsDigit(p.lastChar) {
panic(fmt.Sprintf("Invalid character %q in number", p.lastChar))
}
for isDigit(p.lastChar) {
for utils.IsDigit(p.lastChar) {
p.next()
}
}
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 +201,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 +218,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 +255,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 +272,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 +304,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 +315,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,61 @@
package jsonx
import (
"strings"
"github.com/antonmedv/fx/internal/theme"
)
func (n *Node) String() string {
var out strings.Builder
it := n
for it != nil {
if it.Key != nil {
out.Write(it.Key)
out.WriteByte(':')
}
if it.Value != nil {
out.Write(it.Value)
}
if it.Comma {
out.WriteByte(',')
}
if it.IsCollapsed() {
it = it.Collapsed
} else {
it = it.Next
}
}
return out.String()
}
func (n *Node) PrettyPrint() string {
var out strings.Builder
it := n
for it != nil {
for ident := 0; ident < int(it.Depth); ident++ {
out.WriteString(" ")
}
if it.Key != nil {
out.Write(theme.CurrentTheme.Key(it.Key))
out.Write(theme.Colon)
}
if it.Value != nil {
out.Write(theme.Value(it.Value, false, false)(it.Value))
}
if it.Comma {
out.Write(theme.Comma)
}
out.WriteByte('\n')
if it.IsCollapsed() {
it = it.Collapsed
} else {
it = it.Next
}
}
return out.String()
}

@ -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++
}

@ -1,4 +1,4 @@
package main
package theme
import (
"encoding/json"
@ -10,42 +10,44 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"github.com/antonmedv/fx/internal/utils"
)
type theme struct {
Cursor color
Syntax color
Preview color
StatusBar color
Search color
Key color
String color
Null color
Boolean color
Number color
Size color
type Theme struct {
Cursor Color
Syntax Color
Preview Color
StatusBar Color
Search Color
Key Color
String Color
Null Color
Boolean Color
Number Color
Size Color
}
type color func(s []byte) []byte
type Color func(s []byte) []byte
func valueStyle(b []byte, selected, chunk bool) color {
func Value(b []byte, selected, chunk bool) Color {
if selected {
return currentTheme.Cursor
return CurrentTheme.Cursor
} else if chunk {
return currentTheme.String
return CurrentTheme.String
} else {
switch b[0] {
case '"':
return currentTheme.String
return CurrentTheme.String
case 't', 'f':
return currentTheme.Boolean
return CurrentTheme.Boolean
case 'n':
return currentTheme.Null
return CurrentTheme.Null
case '{', '[', '}', ']':
return currentTheme.Syntax
return CurrentTheme.Syntax
default:
if isDigit(b[0]) || b[0] == '-' {
return currentTheme.Number
if utils.IsDigit(b[0]) || b[0] == '-' {
return CurrentTheme.Number
}
return noColor
}
@ -53,7 +55,7 @@ func valueStyle(b []byte, selected, chunk bool) color {
}
var (
termOutput = termenv.NewOutput(os.Stderr)
TermOutput = termenv.NewOutput(os.Stderr)
)
func init() {
@ -71,51 +73,51 @@ func init() {
showSizesValue, ok := os.LookupEnv("FX_SHOW_SIZE")
if ok {
showSizesValue := strings.ToLower(showSizesValue)
showSizes = showSizesValue == "true" || showSizesValue == "yes" || showSizesValue == "on" || showSizesValue == "1"
ShowSizes = showSizesValue == "true" || showSizesValue == "yes" || showSizesValue == "on" || showSizesValue == "1"
}
currentTheme, ok = themes[themeId]
CurrentTheme, ok = themes[themeId]
if !ok {
_, _ = fmt.Fprintf(os.Stderr, "fx: unknown theme %q, available themes: %v\n", themeId, themeNames)
os.Exit(1)
}
if termOutput.ColorProfile() == termenv.Ascii {
currentTheme = themes["0"]
if TermOutput.ColorProfile() == termenv.Ascii {
CurrentTheme = themes["0"]
}
colon = currentTheme.Syntax([]byte{':', ' '})
colonPreview = currentTheme.Preview([]byte{':'})
comma = currentTheme.Syntax([]byte{','})
empty = currentTheme.Preview([]byte{'~'})
dot3 = currentTheme.Preview([]byte("…"))
closeCurlyBracket = currentTheme.Syntax([]byte{'}'})
closeSquareBracket = currentTheme.Syntax([]byte{']'})
Colon = CurrentTheme.Syntax([]byte{':', ' '})
ColonPreview = CurrentTheme.Preview([]byte{':'})
Comma = CurrentTheme.Syntax([]byte{','})
Empty = CurrentTheme.Preview([]byte{'~'})
Dot3 = CurrentTheme.Preview([]byte("…"))
CloseCurlyBracket = CurrentTheme.Syntax([]byte{'}'})
CloseSquareBracket = CurrentTheme.Syntax([]byte{']'})
}
var (
themeNames []string
currentTheme theme
CurrentTheme Theme
defaultCursor = toColor(lipgloss.NewStyle().Reverse(true).Render)
defaultPreview = toColor(lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render)
defaultStatusBar = toColor(lipgloss.NewStyle().Background(lipgloss.Color("7")).Foreground(lipgloss.Color("0")).Render)
defaultSearch = toColor(lipgloss.NewStyle().Background(lipgloss.Color("11")).Foreground(lipgloss.Color("16")).Render)
defaultNull = fg("243")
defaultSize = toColor(lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render)
showSizes = false
ShowSizes = false
)
var (
colon []byte
colonPreview []byte
comma []byte
empty []byte
dot3 []byte
closeCurlyBracket []byte
closeSquareBracket []byte
Colon []byte
ColonPreview []byte
Comma []byte
Empty []byte
Dot3 []byte
CloseCurlyBracket []byte
CloseSquareBracket []byte
)
var themes = map[string]theme{
var themes = map[string]Theme{
"0": {
Cursor: defaultCursor,
Syntax: noColor,
@ -268,21 +270,21 @@ func noColor(s []byte) []byte {
return s
}
func toColor(f func(s ...string) string) color {
func toColor(f func(s ...string) string) Color {
return func(s []byte) []byte {
return []byte(f(string(s)))
}
}
func fg(color string) color {
func fg(color string) Color {
return toColor(lipgloss.NewStyle().Foreground(lipgloss.Color(color)).Render)
}
func boldFg(color string) color {
func boldFg(color string) Color {
return toColor(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(color)).Render)
}
func themeTester() {
func ThemeTester() {
title := lipgloss.NewStyle().Bold(true)
for _, name := range themeNames {
t := themes[name]
@ -319,7 +321,7 @@ func themeTester() {
}
}
func exportThemes() {
func ExportThemes() {
lipgloss.SetColorProfile(termenv.ANSI256) // Export in Terminal.app compatible colors
placeholder := []byte{'_'}
extract := func(b []byte) string {

@ -0,0 +1,9 @@
package utils
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'
}

@ -26,6 +26,8 @@ import (
"github.com/sahilm/fuzzy"
"github.com/antonmedv/fx/internal/complete"
. "github.com/antonmedv/fx/internal/jsonx"
"github.com/antonmedv/fx/internal/theme"
jsonpath "github.com/antonmedv/fx/path"
)
@ -71,10 +73,10 @@ func main() {
fmt.Println(version)
return
case "--themes":
themeTester()
theme.ThemeTester()
return
case "--export-themes":
exportThemes()
theme.ExportThemes()
return
default:
args = append(args, arg)
@ -143,7 +145,7 @@ func main() {
}
}
head, err := parse(data)
head, err := Parse(data)
if err != nil {
fmt.Print(err.Error())
os.Exit(1)
@ -176,7 +178,7 @@ func main() {
search: newSearch(),
}
lipgloss.SetColorProfile(termOutput.ColorProfile())
lipgloss.SetColorProfile(theme.TermOutput.ColorProfile())
withMouse := tea.WithMouseCellMotion()
if _, ok := os.LookupEnv("FX_NO_MOUSE"); ok {
@ -200,7 +202,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 +231,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 +259,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 +492,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 +504,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 +520,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 +565,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 +587,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 +634,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 +650,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 +661,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,22 +671,22 @@ 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
}
}
func (m *model) View() string {
if m.showHelp {
statusBar := flex(m.termWidth, ": press q or ? to close help", "")
return m.help.View() + "\n" + string(currentTheme.StatusBar([]byte(statusBar)))
return m.help.View() + "\n" + string(theme.CurrentTheme.StatusBar([]byte(statusBar)))
}
if m.showPreview {
statusBar := flex(m.termWidth, m.cursorPath(), m.fileName)
return m.preview.View() + "\n" + string(currentTheme.StatusBar([]byte(statusBar)))
return m.preview.View() + "\n" + string(theme.CurrentTheme.StatusBar([]byte(statusBar)))
}
var screen []byte
@ -695,7 +697,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,47 +706,47 @@ 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...)
screen = append(screen, theme.Colon...)
isSelected = false // don't highlight the key's value
}
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)...)
screen = append(screen, colonPreview...)
if n.IsCollapsed() {
if n.Value[0] == '{' {
if n.Collapsed.Key != nil {
screen = append(screen, theme.CurrentTheme.Preview(n.Collapsed.Key)...)
screen = append(screen, theme.ColonPreview...)
}
screen = append(screen, dot3...)
screen = append(screen, closeCurlyBracket...)
} else if n.value[0] == '[' {
screen = append(screen, dot3...)
screen = append(screen, closeSquareBracket...)
screen = append(screen, theme.Dot3...)
screen = append(screen, theme.CloseCurlyBracket...)
} else if n.Value[0] == '[' {
screen = append(screen, theme.Dot3...)
screen = append(screen, theme.CloseSquareBracket...)
}
if n.end != nil && n.end.comma {
screen = append(screen, comma...)
if n.End != nil && n.End.Comma {
screen = append(screen, theme.Comma...)
}
}
if n.comma {
screen = append(screen, comma...)
if n.Comma {
screen = append(screen, theme.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 theme.ShowSizes && len(n.Value) > 0 && (n.Value[0] == '{' || n.Value[0] == '[') {
if n.IsCollapsed() || n.Size > 1 {
screen = append(screen, theme.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++ {
screen = append(screen, empty...)
screen = append(screen, theme.Empty...)
screen = append(screen, '\n')
}
@ -752,7 +754,7 @@ func (m *model) View() string {
screen = append(screen, m.digInput.View()...)
} else {
statusBar := flex(m.termWidth, m.cursorPath(), m.fileName)
screen = append(screen, currentTheme.StatusBar([]byte(statusBar))...)
screen = append(screen, theme.CurrentTheme.StatusBar([]byte(statusBar))...)
}
if m.yank {
@ -781,12 +783,12 @@ 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
style := theme.CurrentTheme.Key
if selected {
style = currentTheme.Cursor
style = theme.CurrentTheme.Cursor
}
if indexes, ok := m.search.keys[node]; ok {
@ -795,9 +797,9 @@ func (m *model) prettyKey(node *node, selected bool) []byte {
if i%2 == 0 {
out = append(out, style(p.b)...)
} else if p.index == m.search.cursor {
out = append(out, currentTheme.Cursor(p.b)...)
out = append(out, theme.CurrentTheme.Cursor(p.b)...)
} else {
out = append(out, currentTheme.Search(p.b)...)
out = append(out, theme.CurrentTheme.Search(p.b)...)
}
}
return out
@ -806,19 +808,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 := theme.Value(b, selected, node.Chunk != nil)
if indexes, ok := m.search.values[node]; ok {
var out []byte
@ -826,9 +828,9 @@ func (m *model) prettyPrint(node *node, selected bool) []byte {
if i%2 == 0 {
out = append(out, style(p.b)...)
} else if p.index == m.search.cursor {
out = append(out, currentTheme.Cursor(p.b)...)
out = append(out, theme.CurrentTheme.Cursor(p.b)...)
} else {
out = append(out, currentTheme.Search(p.b)...)
out = append(out, theme.CurrentTheme.Search(p.b)...)
}
}
return out
@ -847,34 +849,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 +888,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 +903,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 +917,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 +928,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 +954,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 +1014,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 +1031,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 +1071,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 +1081,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 +1113,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 +1147,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 +1169,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,9 @@ import (
"github.com/charmbracelet/x/exp/teatest"
"github.com/muesli/termenv"
"github.com/stretchr/testify/require"
"github.com/antonmedv/fx/internal/jsonx"
"github.com/antonmedv/fx/internal/theme"
)
func init() {
@ -26,7 +29,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{
@ -103,8 +106,8 @@ func TestCollapseRecursive(t *testing.T) {
}
func TestCollapseRecursiveWithSizes(t *testing.T) {
showSizes = true
defer func() { showSizes = true }()
theme.ShowSizes = true
defer func() { theme.ShowSizes = true }()
tm := prepare(t)

@ -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
}

@ -6,32 +6,35 @@ import (
"os"
"os/exec"
"path"
"github.com/antonmedv/fx/internal/engine"
)
//go:embed npm/index.js
var src []byte
func reduce(fns []string) {
script := path.Join(os.TempDir(), fmt.Sprintf("fx-%v.js", version))
_, err := os.Stat(script)
if os.IsNotExist(err) {
err := os.WriteFile(script, src, 0644)
if err != nil {
panic(err)
}
}
var deno bool
deno := false
bin, err := exec.LookPath("node")
if err != nil {
bin, err = exec.LookPath("deno")
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Node.js or Deno is required to run fx with reducers.\n")
os.Exit(1)
engine.Reduce(fns)
return
}
deno = true
}
script := path.Join(os.TempDir(), fmt.Sprintf("fx-%v.js", version))
_, err = os.Stat(script)
if os.IsNotExist(err) {
err := os.WriteFile(script, src, 0644)
if err != nil {
panic(err)
}
}
env := os.Environ()
var args []string

@ -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,6 @@ 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