Add fallback engine (#300)

pull/297/head
Anton Medvedev 2 months 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 - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:
go-version: 1.18 go-version: 1.21
- uses: snapcore/action-build@v1 - uses: snapcore/action-build@v1
id: build id: build

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

@ -1,6 +1,6 @@
module github.com/antonmedv/fx module github.com/antonmedv/fx
go 1.20 go 1.21
require ( require (
github.com/antonmedv/clipboard v1.0.1 github.com/antonmedv/clipboard v1.0.1
@ -23,10 +23,10 @@ require (
github.com/aymanbagabas/go-udiff v0.1.3 // indirect github.com/aymanbagabas/go-udiff v0.1.3 // indirect
github.com/containerd/console v1.0.4 // indirect github.com/containerd/console v1.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // 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/fatih/color v1.16.0 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // 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/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-localereader v0.0.1 // 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.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.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-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 h1:O7I1iuzEA7SG+dK8ocOBSlYAA9jBUmCYl/Qa7ey7JAM=
github.com/dop251/goja v0.0.0-20240220182346-e401ed450204/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= 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 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 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 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 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 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 h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= 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 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I=
github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= 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 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-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/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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 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 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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 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-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.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 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/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=

@ -1,6 +1,7 @@
package complete package complete
import ( import (
_ "embed"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -10,6 +11,7 @@ import (
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/goccy/go-yaml" "github.com/goccy/go-yaml"
"github.com/antonmedv/fx/internal/engine"
"github.com/antonmedv/fx/internal/shlex" "github.com/antonmedv/fx/internal/shlex"
) )
@ -53,6 +55,9 @@ var globals = []string{
"skip", "skip",
} }
//go:embed prelude.js
var prelude string
func Complete() bool { func Complete() bool {
compLine, ok := os.LookupEnv("COMP_LINE") 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 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. args = args[2:] // Drop binary & file from the args.
if compWord == "" { if compWord == "" {
@ -180,21 +185,24 @@ func codeComplete(input string, args []string, compWord string) {
var code strings.Builder var code strings.Builder
code.WriteString(prelude) 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 { for _, arg := range args {
if arg == "" { if arg == "" { // After dropTail, we can have empty strings.
continue continue
} }
code.WriteString(Transform(arg)) code.WriteString(engine.Transform(arg))
} }
code.WriteString("\n__keys\n") code.WriteString("\n__keys\n")
out, err := goja.New().RunString(code.String()) vm := goja.New()
value, err := vm.RunString(code.String())
if err != nil { if err != nil {
return return
} }
if array, ok := out.Export().([]interface{}); ok { if array, ok := value.Export().([]interface{}); ok {
prefix := dropTail(compWord) prefix := dropTail(compWord)
var reply []string var reply []string
for _, key := range array { 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) { function apply(fn, ...args) {
if (typeof fn === 'function') return fn(...args) if (typeof fn === 'function') return fn(...args)
return fn return fn
@ -20,23 +7,23 @@ function len(x) {
if (Array.isArray(x)) return x.length if (Array.isArray(x)) return x.length
if (typeof x === 'string') return x.length if (typeof x === 'string') return x.length
if (typeof x === 'object' && x !== null) return Object.keys(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) { function uniq(x) {
if (Array.isArray(x)) return [...new Set(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) { function sort(x) {
if (Array.isArray(x)) return x.sort() if (Array.isArray(x)) return x.sort()
throw new Error() throw new Error(`Cannot sort ${typeof x}`)
} }
function map(fn) { function map(fn) {
return function (x) { return function (x) {
if (Array.isArray(x)) return x.map((v, i) => fn(v, i)) 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) const fb = fn(b)
return fa < fb ? -1 : fa > fb ? 1 : 0 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) { function flatten(x) {
if (Array.isArray(x)) return x.flat() if (Array.isArray(x)) return x.flat()
throw new Error() throw new Error(`Cannot flatten ${typeof x}`)
} }
function reverse(x) { function reverse(x) {
if (Array.isArray(x)) return x.reverse() if (Array.isArray(x)) return x.reverse()
throw new Error() throw new Error(`Cannot reverse ${typeof x}`)
} }
function keys(x) { function keys(x) {
if (typeof x === 'object' && x !== null) return Object.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) { function values(x) {
if (typeof x === 'object' && x !== null) return Object.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 ( import (
"fmt" "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 ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"github.com/antonmedv/fx/internal/utils"
) )
type jsonParser struct { type jsonParser struct {
@ -17,7 +19,7 @@ type jsonParser struct {
skipFirstIdent bool skipFirstIdent bool
} }
func parse(data []byte) (head *node, err error) { func Parse(data []byte) (head *Node, err error) {
p := &jsonParser{ p := &jsonParser{
data: data, data: data,
lineNumber: 1, lineNumber: 1,
@ -29,14 +31,14 @@ func parse(data []byte) (head *node, err error) {
} }
}() }()
p.next() p.next()
var next *node var next *Node
for p.lastChar != 0 { for p.lastChar != 0 {
value := p.parseValue() value := p.parseValue()
if head == nil { if head == nil {
head = value head = value
next = head next = head
} else { } else {
value.index = -1 value.Index = -1
next.adjacent(value) next.adjacent(value)
next = value next = value
} }
@ -57,10 +59,10 @@ func (p *jsonParser) next() {
p.sourceTail.writeByte(p.lastChar) p.sourceTail.writeByte(p.lastChar)
} }
func (p *jsonParser) parseValue() *node { func (p *jsonParser) parseValue() *Node {
p.skipWhitespace() p.skipWhitespace()
var l *node var l *Node
switch p.lastChar { switch p.lastChar {
case '"': case '"':
l = p.parseString() l = p.parseString()
@ -84,8 +86,8 @@ func (p *jsonParser) parseValue() *node {
return l return l
} }
func (p *jsonParser) parseString() *node { func (p *jsonParser) parseString() *Node {
str := &node{depth: p.depth} str := &Node{Depth: p.depth}
start := p.end - 1 start := p.end - 1
p.next() p.next()
escaped := false escaped := false
@ -96,7 +98,7 @@ func (p *jsonParser) parseString() *node {
var unicode string var unicode string
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
p.next() p.next()
if !isHexDigit(p.lastChar) { if !utils.IsHexDigit(p.lastChar) {
panic(fmt.Sprintf("Invalid Unicode escape sequence '\\u%s%c'", unicode, p.lastChar)) panic(fmt.Sprintf("Invalid Unicode escape sequence '\\u%s%c'", unicode, p.lastChar))
} }
unicode += string(p.lastChar) unicode += string(p.lastChar)
@ -122,19 +124,19 @@ func (p *jsonParser) parseString() *node {
p.next() p.next()
} }
str.value = p.data[start:p.end] str.Value = p.data[start:p.end]
p.next() p.next()
return str return str
} }
func (p *jsonParser) parseNumber() *node { func (p *jsonParser) parseNumber() *Node {
num := &node{depth: p.depth} num := &Node{Depth: p.depth}
start := p.end - 1 start := p.end - 1
// Handle negative numbers // Handle negative numbers
if p.lastChar == '-' { if p.lastChar == '-' {
p.next() p.next()
if !isDigit(p.lastChar) { if !utils.IsDigit(p.lastChar) {
panic(fmt.Sprintf("Invalid character %q in number", 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' { if p.lastChar == '0' {
p.next() p.next()
} else { } else {
for isDigit(p.lastChar) { for utils.IsDigit(p.lastChar) {
p.next() p.next()
} }
} }
@ -151,10 +153,10 @@ func (p *jsonParser) parseNumber() *node {
// Decimal portion // Decimal portion
if p.lastChar == '.' { if p.lastChar == '.' {
p.next() p.next()
if !isDigit(p.lastChar) { if !utils.IsDigit(p.lastChar) {
panic(fmt.Sprintf("Invalid character %q in number", p.lastChar)) panic(fmt.Sprintf("Invalid character %q in number", p.lastChar))
} }
for isDigit(p.lastChar) { for utils.IsDigit(p.lastChar) {
p.next() p.next()
} }
} }
@ -165,28 +167,28 @@ func (p *jsonParser) parseNumber() *node {
if p.lastChar == '+' || p.lastChar == '-' { if p.lastChar == '+' || p.lastChar == '-' {
p.next() p.next()
} }
if !isDigit(p.lastChar) { if !utils.IsDigit(p.lastChar) {
panic(fmt.Sprintf("Invalid character %q in number", p.lastChar)) panic(fmt.Sprintf("Invalid character %q in number", p.lastChar))
} }
for isDigit(p.lastChar) { for utils.IsDigit(p.lastChar) {
p.next() p.next()
} }
} }
num.value = p.data[start : p.end-1] num.Value = p.data[start : p.end-1]
return num return num
} }
func (p *jsonParser) parseObject() *node { func (p *jsonParser) parseObject() *Node {
object := &node{depth: p.depth} object := &Node{Depth: p.depth}
object.value = []byte{'{'} object.Value = []byte{'{'}
p.next() p.next()
p.skipWhitespace() p.skipWhitespace()
// Empty object // Empty object
if p.lastChar == '}' { if p.lastChar == '}' {
object.value = append(object.value, '}') object.Value = append(object.Value, '}')
p.next() p.next()
return object return object
} }
@ -199,8 +201,8 @@ func (p *jsonParser) parseObject() *node {
p.depth++ p.depth++
key := p.parseString() key := p.parseString()
key.key, key.value = key.value, nil key.Key, key.Value = key.Value, nil
object.size += 1 object.Size += 1
key.directParent = object key.directParent = object
p.skipWhitespace() p.skipWhitespace()
@ -216,34 +218,34 @@ func (p *jsonParser) parseObject() *node {
value := p.parseValue() value := p.parseValue()
p.depth-- p.depth--
key.value = value.value key.Value = value.Value
key.size = value.size key.Size = value.Size
key.next = value.next key.Next = value.Next
if key.next != nil { if key.Next != nil {
key.next.prev = key key.Next.Prev = key
} }
key.end = value.end key.End = value.End
value.indirectParent = key value.indirectParent = key
object.append(key) object.append(key)
p.skipWhitespace() p.skipWhitespace()
if p.lastChar == ',' { if p.lastChar == ',' {
object.end.comma = true object.End.Comma = true
p.next() p.next()
p.skipWhitespace() p.skipWhitespace()
if p.lastChar == '}' { if p.lastChar == '}' {
object.end.comma = false object.End.Comma = false
} else { } else {
continue continue
} }
} }
if p.lastChar == '}' { if p.lastChar == '}' {
closeBracket := &node{depth: p.depth} closeBracket := &Node{Depth: p.depth}
closeBracket.value = []byte{'}'} closeBracket.Value = []byte{'}'}
closeBracket.directParent = object closeBracket.directParent = object
closeBracket.index = -1 closeBracket.Index = -1
object.append(closeBracket) object.append(closeBracket)
p.next() p.next()
return object return object
@ -253,15 +255,15 @@ func (p *jsonParser) parseObject() *node {
} }
} }
func (p *jsonParser) parseArray() *node { func (p *jsonParser) parseArray() *Node {
arr := &node{depth: p.depth} arr := &Node{Depth: p.depth}
arr.value = []byte{'['} arr.Value = []byte{'['}
p.next() p.next()
p.skipWhitespace() p.skipWhitespace()
if p.lastChar == ']' { if p.lastChar == ']' {
arr.value = append(arr.value, ']') arr.Value = append(arr.Value, ']')
p.next() p.next()
return arr return arr
} }
@ -270,29 +272,29 @@ func (p *jsonParser) parseArray() *node {
p.depth++ p.depth++
value := p.parseValue() value := p.parseValue()
value.directParent = arr value.directParent = arr
arr.size += 1 arr.Size += 1
value.index = i value.Index = i
p.depth-- p.depth--
arr.append(value) arr.append(value)
p.skipWhitespace() p.skipWhitespace()
if p.lastChar == ',' { if p.lastChar == ',' {
arr.end.comma = true arr.End.Comma = true
p.next() p.next()
p.skipWhitespace() p.skipWhitespace()
if p.lastChar == ']' { if p.lastChar == ']' {
arr.end.comma = false arr.End.Comma = false
} else { } else {
continue continue
} }
} }
if p.lastChar == ']' { if p.lastChar == ']' {
closeBracket := &node{depth: p.depth} closeBracket := &Node{Depth: p.depth}
closeBracket.value = []byte{']'} closeBracket.Value = []byte{']'}
closeBracket.directParent = arr closeBracket.directParent = arr
closeBracket.index = -1 closeBracket.Index = -1
arr.append(closeBracket) arr.append(closeBracket)
p.next() p.next()
return arr 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++ { for i := 1; i < len(name); i++ {
p.next() p.next()
if p.lastChar != name[i] { 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 nextCharIsSpecial := isWhitespace(p.lastChar) || p.lastChar == ',' || p.lastChar == '}' || p.lastChar == ']' || p.lastChar == 0
if nextCharIsSpecial { if nextCharIsSpecial {
keyword := &node{depth: p.depth} keyword := &Node{Depth: p.depth}
keyword.value = []byte(name) keyword.Value = []byte(name)
return keyword 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 ( import (
"testing" "testing"
@ -8,28 +8,28 @@ import (
) )
func TestNode_paths(t *testing.T) { 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) require.NoError(t, err)
var paths []string var paths []string
var nodes []*node var nodes []*Node
n.paths("", &paths, &nodes) n.paths("", &paths, &nodes)
assert.Equal(t, []string{".a", ".b", ".b.f", ".c", ".c[0]", ".c[1]"}, paths) assert.Equal(t, []string{".a", ".b", ".b.f", ".c", ".c[0]", ".c[1]"}, paths)
} }
func TestNode_children(t *testing.T) { 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) require.NoError(t, err)
paths, _ := n.children() paths, _ := n.Children()
assert.Equal(t, []string{"a", "b", "c"}, paths) assert.Equal(t, []string{"a", "b", "c"}, paths)
} }
func TestNode_expandRecursively(t *testing.T) { 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) require.NoError(t, err)
n.collapseRecursively() n.CollapseRecursively()
n.expandRecursively(0, 3) n.ExpandRecursively(0, 3)
assert.Equal(t, `"c"`, string(n.next.next.next.key)) assert.Equal(t, `"c"`, string(n.Next.Next.Next.Key))
} }

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

@ -1,4 +1,4 @@
package main package theme
import ( import (
"encoding/json" "encoding/json"
@ -10,42 +10,44 @@ import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv" "github.com/muesli/termenv"
"github.com/antonmedv/fx/internal/utils"
) )
type theme struct { type Theme struct {
Cursor color Cursor Color
Syntax color Syntax Color
Preview color Preview Color
StatusBar color StatusBar Color
Search color Search Color
Key color Key Color
String color String Color
Null color Null Color
Boolean color Boolean Color
Number color Number Color
Size 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 { if selected {
return currentTheme.Cursor return CurrentTheme.Cursor
} else if chunk { } else if chunk {
return currentTheme.String return CurrentTheme.String
} else { } else {
switch b[0] { switch b[0] {
case '"': case '"':
return currentTheme.String return CurrentTheme.String
case 't', 'f': case 't', 'f':
return currentTheme.Boolean return CurrentTheme.Boolean
case 'n': case 'n':
return currentTheme.Null return CurrentTheme.Null
case '{', '[', '}', ']': case '{', '[', '}', ']':
return currentTheme.Syntax return CurrentTheme.Syntax
default: default:
if isDigit(b[0]) || b[0] == '-' { if utils.IsDigit(b[0]) || b[0] == '-' {
return currentTheme.Number return CurrentTheme.Number
} }
return noColor return noColor
} }
@ -53,7 +55,7 @@ func valueStyle(b []byte, selected, chunk bool) color {
} }
var ( var (
termOutput = termenv.NewOutput(os.Stderr) TermOutput = termenv.NewOutput(os.Stderr)
) )
func init() { func init() {
@ -71,51 +73,51 @@ func init() {
showSizesValue, ok := os.LookupEnv("FX_SHOW_SIZE") showSizesValue, ok := os.LookupEnv("FX_SHOW_SIZE")
if ok { if ok {
showSizesValue := strings.ToLower(showSizesValue) 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 { if !ok {
_, _ = fmt.Fprintf(os.Stderr, "fx: unknown theme %q, available themes: %v\n", themeId, themeNames) _, _ = fmt.Fprintf(os.Stderr, "fx: unknown theme %q, available themes: %v\n", themeId, themeNames)
os.Exit(1) os.Exit(1)
} }
if termOutput.ColorProfile() == termenv.Ascii { if TermOutput.ColorProfile() == termenv.Ascii {
currentTheme = themes["0"] CurrentTheme = themes["0"]
} }
colon = currentTheme.Syntax([]byte{':', ' '}) Colon = CurrentTheme.Syntax([]byte{':', ' '})
colonPreview = currentTheme.Preview([]byte{':'}) ColonPreview = CurrentTheme.Preview([]byte{':'})
comma = currentTheme.Syntax([]byte{','}) Comma = CurrentTheme.Syntax([]byte{','})
empty = currentTheme.Preview([]byte{'~'}) Empty = CurrentTheme.Preview([]byte{'~'})
dot3 = currentTheme.Preview([]byte("…")) Dot3 = CurrentTheme.Preview([]byte("…"))
closeCurlyBracket = currentTheme.Syntax([]byte{'}'}) CloseCurlyBracket = CurrentTheme.Syntax([]byte{'}'})
closeSquareBracket = currentTheme.Syntax([]byte{']'}) CloseSquareBracket = CurrentTheme.Syntax([]byte{']'})
} }
var ( var (
themeNames []string themeNames []string
currentTheme theme CurrentTheme Theme
defaultCursor = toColor(lipgloss.NewStyle().Reverse(true).Render) defaultCursor = toColor(lipgloss.NewStyle().Reverse(true).Render)
defaultPreview = toColor(lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render) defaultPreview = toColor(lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render)
defaultStatusBar = toColor(lipgloss.NewStyle().Background(lipgloss.Color("7")).Foreground(lipgloss.Color("0")).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) defaultSearch = toColor(lipgloss.NewStyle().Background(lipgloss.Color("11")).Foreground(lipgloss.Color("16")).Render)
defaultNull = fg("243") defaultNull = fg("243")
defaultSize = toColor(lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render) defaultSize = toColor(lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render)
showSizes = false ShowSizes = false
) )
var ( var (
colon []byte Colon []byte
colonPreview []byte ColonPreview []byte
comma []byte Comma []byte
empty []byte Empty []byte
dot3 []byte Dot3 []byte
closeCurlyBracket []byte CloseCurlyBracket []byte
closeSquareBracket []byte CloseSquareBracket []byte
) )
var themes = map[string]theme{ var themes = map[string]Theme{
"0": { "0": {
Cursor: defaultCursor, Cursor: defaultCursor,
Syntax: noColor, Syntax: noColor,
@ -268,21 +270,21 @@ func noColor(s []byte) []byte {
return s return s
} }
func toColor(f func(s ...string) string) color { func toColor(f func(s ...string) string) Color {
return func(s []byte) []byte { return func(s []byte) []byte {
return []byte(f(string(s))) return []byte(f(string(s)))
} }
} }
func fg(color string) color { func fg(color string) Color {
return toColor(lipgloss.NewStyle().Foreground(lipgloss.Color(color)).Render) 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) return toColor(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(color)).Render)
} }
func themeTester() { func ThemeTester() {
title := lipgloss.NewStyle().Bold(true) title := lipgloss.NewStyle().Bold(true)
for _, name := range themeNames { for _, name := range themeNames {
t := themes[name] t := themes[name]
@ -319,7 +321,7 @@ func themeTester() {
} }
} }
func exportThemes() { func ExportThemes() {
lipgloss.SetColorProfile(termenv.ANSI256) // Export in Terminal.app compatible colors lipgloss.SetColorProfile(termenv.ANSI256) // Export in Terminal.app compatible colors
placeholder := []byte{'_'} placeholder := []byte{'_'}
extract := func(b []byte) string { 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/sahilm/fuzzy"
"github.com/antonmedv/fx/internal/complete" "github.com/antonmedv/fx/internal/complete"
. "github.com/antonmedv/fx/internal/jsonx"
"github.com/antonmedv/fx/internal/theme"
jsonpath "github.com/antonmedv/fx/path" jsonpath "github.com/antonmedv/fx/path"
) )
@ -71,10 +73,10 @@ func main() {
fmt.Println(version) fmt.Println(version)
return return
case "--themes": case "--themes":
themeTester() theme.ThemeTester()
return return
case "--export-themes": case "--export-themes":
exportThemes() theme.ExportThemes()
return return
default: default:
args = append(args, arg) args = append(args, arg)
@ -143,7 +145,7 @@ func main() {
} }
} }
head, err := parse(data) head, err := Parse(data)
if err != nil { if err != nil {
fmt.Print(err.Error()) fmt.Print(err.Error())
os.Exit(1) os.Exit(1)
@ -176,7 +178,7 @@ func main() {
search: newSearch(), search: newSearch(),
} }
lipgloss.SetColorProfile(termOutput.ColorProfile()) lipgloss.SetColorProfile(theme.TermOutput.ColorProfile())
withMouse := tea.WithMouseCellMotion() withMouse := tea.WithMouseCellMotion()
if _, ok := os.LookupEnv("FX_NO_MOUSE"); ok { if _, ok := os.LookupEnv("FX_NO_MOUSE"); ok {
@ -200,7 +202,7 @@ func main() {
type model struct { type model struct {
termWidth, termHeight int termWidth, termHeight int
head, top *node head, top *Node
cursor int // cursor position [0, termHeight) cursor int // cursor position [0, termHeight)
showCursor bool showCursor bool
wrap 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.help.Height = m.termHeight - 1
m.preview.Width = m.termWidth m.preview.Width = m.termWidth
m.preview.Height = m.termHeight - 1 m.preview.Height = m.termHeight - 1
wrapAll(m.top, m.termWidth) WrapAll(m.top, m.termWidth)
m.redoSearch() m.redoSearch()
} }
@ -257,18 +259,18 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.cursor == msg.Y { if m.cursor == msg.Y {
to := m.cursorPointsTo() to := m.cursorPointsTo()
if to != nil { if to != nil {
if to.isCollapsed() { if to.IsCollapsed() {
to.expand() to.Expand()
} else { } else {
to.collapse() to.Collapse()
} }
} }
} else { } else {
to := m.at(msg.Y) to := m.at(msg.Y)
if to != nil { if to != nil {
m.cursor = msg.Y m.cursor = msg.Y
if to.isCollapsed() { if to.IsCollapsed() {
to.expand() to.Expand()
} }
} }
} }
@ -490,11 +492,11 @@ func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case key.Matches(msg, keyMap.NextSibling): case key.Matches(msg, keyMap.NextSibling):
pointsTo := m.cursorPointsTo() pointsTo := m.cursorPointsTo()
var nextSibling *node var nextSibling *Node
if pointsTo.end != nil && pointsTo.end.next != nil { if pointsTo.End != nil && pointsTo.End.Next != nil {
nextSibling = pointsTo.end.next nextSibling = pointsTo.End.Next
} else { } else {
nextSibling = pointsTo.next nextSibling = pointsTo.Next
} }
if nextSibling != nil { if nextSibling != nil {
m.selectNode(nextSibling) m.selectNode(nextSibling)
@ -502,13 +504,13 @@ func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case key.Matches(msg, keyMap.PrevSibling): case key.Matches(msg, keyMap.PrevSibling):
pointsTo := m.cursorPointsTo() pointsTo := m.cursorPointsTo()
var prevSibling *node var prevSibling *Node
if pointsTo.parent() != nil && pointsTo.parent().end == pointsTo { if pointsTo.Parent() != nil && pointsTo.Parent().End == pointsTo {
prevSibling = pointsTo.parent() prevSibling = pointsTo.Parent()
} else if pointsTo.prev != nil { } else if pointsTo.Prev != nil {
prevSibling = pointsTo.prev prevSibling = pointsTo.Prev
parent := prevSibling.parent() parent := prevSibling.Parent()
if parent != nil && parent.end == prevSibling { if parent != nil && parent.End == prevSibling {
prevSibling = parent prevSibling = parent
} }
} }
@ -518,41 +520,41 @@ func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case key.Matches(msg, keyMap.Collapse): case key.Matches(msg, keyMap.Collapse):
n := m.cursorPointsTo() n := m.cursorPointsTo()
if n.hasChildren() && !n.isCollapsed() { if n.HasChildren() && !n.IsCollapsed() {
n.collapse() n.Collapse()
} else { } else {
if n.parent() != nil { if n.Parent() != nil {
n = n.parent() n = n.Parent()
} }
} }
m.selectNode(n) m.selectNode(n)
case key.Matches(msg, keyMap.Expand): case key.Matches(msg, keyMap.Expand):
m.cursorPointsTo().expand() m.cursorPointsTo().Expand()
m.showCursor = true m.showCursor = true
case key.Matches(msg, keyMap.CollapseRecursively): case key.Matches(msg, keyMap.CollapseRecursively):
n := m.cursorPointsTo() n := m.cursorPointsTo()
if n.hasChildren() { if n.HasChildren() {
n.collapseRecursively() n.CollapseRecursively()
} }
m.showCursor = true m.showCursor = true
case key.Matches(msg, keyMap.ExpandRecursively): case key.Matches(msg, keyMap.ExpandRecursively):
n := m.cursorPointsTo() n := m.cursorPointsTo()
if n.hasChildren() { if n.HasChildren() {
n.expandRecursively(0, math.MaxInt) n.ExpandRecursively(0, math.MaxInt)
} }
m.showCursor = true m.showCursor = true
case key.Matches(msg, keyMap.CollapseAll): case key.Matches(msg, keyMap.CollapseAll):
n := m.top n := m.top
for n != nil { for n != nil {
n.collapseRecursively() n.CollapseRecursively()
if n.end == nil { if n.End == nil {
n = nil n = nil
} else { } else {
n = n.end.next n = n.End.Next
} }
} }
m.cursor = 0 m.cursor = 0
@ -563,21 +565,21 @@ func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
at := m.cursorPointsTo() at := m.cursorPointsTo()
n := m.top n := m.top
for n != nil { for n != nil {
n.expandRecursively(0, math.MaxInt) n.ExpandRecursively(0, math.MaxInt)
if n.end == nil { if n.End == nil {
n = nil n = nil
} else { } else {
n = n.end.next n = n.End.Next
} }
} }
m.selectNode(at) m.selectNode(at)
case key.Matches(msg, keyMap.CollapseLevel): case key.Matches(msg, keyMap.CollapseLevel):
at := m.cursorPointsTo() at := m.cursorPointsTo()
if at != nil && at.hasChildren() { if at != nil && at.HasChildren() {
toLevel, _ := strconv.Atoi(msg.String()) toLevel, _ := strconv.Atoi(msg.String())
at.collapseRecursively() at.CollapseRecursively()
at.expandRecursively(0, toLevel) at.ExpandRecursively(0, toLevel)
m.showCursor = true m.showCursor = true
} }
@ -585,12 +587,12 @@ func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
at := m.cursorPointsTo() at := m.cursorPointsTo()
m.wrap = !m.wrap m.wrap = !m.wrap
if m.wrap { if m.wrap {
wrapAll(m.top, m.termWidth) WrapAll(m.top, m.termWidth)
} else { } else {
dropWrapAll(m.top) DropWrapAll(m.top)
} }
if at.chunk != nil && at.value == nil { if at.Chunk != nil && at.Value == nil {
at = at.parent() at = at.Parent()
} }
m.redoSearch() m.redoSearch()
m.selectNode(at) m.selectNode(at)
@ -632,8 +634,8 @@ func (m *model) up() {
m.cursor-- m.cursor--
if m.cursor < 0 { if m.cursor < 0 {
m.cursor = 0 m.cursor = 0
if m.head.prev != nil { if m.head.Prev != nil {
m.head = m.head.prev m.head = m.head.Prev
} }
} }
} }
@ -648,8 +650,8 @@ func (m *model) down() {
} }
if m.cursor >= m.viewHeight() { if m.cursor >= m.viewHeight() {
m.cursor = m.viewHeight() - 1 m.cursor = m.viewHeight() - 1
if m.head.next != nil { if m.head.Next != nil {
m.head = m.head.next m.head = m.head.Next
} }
} }
} }
@ -659,7 +661,7 @@ func (m *model) visibleLines() int {
n := m.head n := m.head
for n != nil && visibleLines < m.viewHeight() { for n != nil && visibleLines < m.viewHeight() {
visibleLines++ visibleLines++
n = n.next n = n.Next
} }
return visibleLines return visibleLines
} }
@ -669,22 +671,22 @@ func (m *model) scrollIntoView() {
if m.cursor >= visibleLines { if m.cursor >= visibleLines {
m.cursor = visibleLines - 1 m.cursor = visibleLines - 1
} }
for visibleLines < m.viewHeight() && m.head.prev != nil { for visibleLines < m.viewHeight() && m.head.Prev != nil {
visibleLines++ visibleLines++
m.cursor++ m.cursor++
m.head = m.head.prev m.head = m.head.Prev
} }
} }
func (m *model) View() string { func (m *model) View() string {
if m.showHelp { if m.showHelp {
statusBar := flex(m.termWidth, ": press q or ? to close help", "") 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 { if m.showPreview {
statusBar := flex(m.termWidth, m.cursorPath(), m.fileName) 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 var screen []byte
@ -695,7 +697,7 @@ func (m *model) View() string {
if n == nil { if n == nil {
break break
} }
for ident := 0; ident < int(n.depth); ident++ { for ident := 0; ident < int(n.Depth); ident++ {
screen = append(screen, ' ', ' ') screen = append(screen, ' ', ' ')
} }
@ -704,47 +706,47 @@ func (m *model) View() string {
isSelected = false // don't highlight the cursor while iterating search results 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, m.prettyKey(n, isSelected)...)
screen = append(screen, colon...) screen = append(screen, theme.Colon...)
isSelected = false // don't highlight the key's value isSelected = false // don't highlight the key's value
} }
screen = append(screen, m.prettyPrint(n, isSelected)...) screen = append(screen, m.prettyPrint(n, isSelected)...)
if n.isCollapsed() { if n.IsCollapsed() {
if n.value[0] == '{' { if n.Value[0] == '{' {
if n.collapsed.key != nil { if n.Collapsed.Key != nil {
screen = append(screen, currentTheme.Preview(n.collapsed.key)...) screen = append(screen, theme.CurrentTheme.Preview(n.Collapsed.Key)...)
screen = append(screen, colonPreview...) screen = append(screen, theme.ColonPreview...)
} }
screen = append(screen, dot3...) screen = append(screen, theme.Dot3...)
screen = append(screen, closeCurlyBracket...) screen = append(screen, theme.CloseCurlyBracket...)
} else if n.value[0] == '[' { } else if n.Value[0] == '[' {
screen = append(screen, dot3...) screen = append(screen, theme.Dot3...)
screen = append(screen, closeSquareBracket...) screen = append(screen, theme.CloseSquareBracket...)
} }
if n.end != nil && n.end.comma { if n.End != nil && n.End.Comma {
screen = append(screen, comma...) screen = append(screen, theme.Comma...)
} }
} }
if n.comma { if n.Comma {
screen = append(screen, comma...) screen = append(screen, theme.Comma...)
} }
if showSizes && len(n.value) > 0 && (n.value[0] == '{' || n.value[0] == '[') { if theme.ShowSizes && len(n.Value) > 0 && (n.Value[0] == '{' || n.Value[0] == '[') {
if n.isCollapsed() || n.size > 1 { if n.IsCollapsed() || n.Size > 1 {
screen = append(screen, currentTheme.Size([]byte(fmt.Sprintf(" // %d", n.size)))...) screen = append(screen, theme.CurrentTheme.Size([]byte(fmt.Sprintf(" // %d", n.Size)))...)
} }
} }
screen = append(screen, '\n') screen = append(screen, '\n')
printedLines++ printedLines++
n = n.next n = n.Next
} }
for i := printedLines; i < m.viewHeight(); i++ { for i := printedLines; i < m.viewHeight(); i++ {
screen = append(screen, empty...) screen = append(screen, theme.Empty...)
screen = append(screen, '\n') screen = append(screen, '\n')
} }
@ -752,7 +754,7 @@ func (m *model) View() string {
screen = append(screen, m.digInput.View()...) screen = append(screen, m.digInput.View()...)
} else { } else {
statusBar := flex(m.termWidth, m.cursorPath(), m.fileName) 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 { if m.yank {
@ -781,12 +783,12 @@ func (m *model) View() string {
return string(screen) return string(screen)
} }
func (m *model) prettyKey(node *node, selected bool) []byte { func (m *model) prettyKey(node *Node, selected bool) []byte {
b := node.key b := node.Key
style := currentTheme.Key style := theme.CurrentTheme.Key
if selected { if selected {
style = currentTheme.Cursor style = theme.CurrentTheme.Cursor
} }
if indexes, ok := m.search.keys[node]; ok { 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 { if i%2 == 0 {
out = append(out, style(p.b)...) out = append(out, style(p.b)...)
} else if p.index == m.search.cursor { } else if p.index == m.search.cursor {
out = append(out, currentTheme.Cursor(p.b)...) out = append(out, theme.CurrentTheme.Cursor(p.b)...)
} else { } else {
out = append(out, currentTheme.Search(p.b)...) out = append(out, theme.CurrentTheme.Search(p.b)...)
} }
} }
return out 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 var b []byte
if node.chunk != nil { if node.Chunk != nil {
b = node.chunk b = node.Chunk
} else { } else {
b = node.value b = node.Value
} }
if len(b) == 0 { if len(b) == 0 {
return b 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 { if indexes, ok := m.search.values[node]; ok {
var out []byte var out []byte
@ -826,9 +828,9 @@ func (m *model) prettyPrint(node *node, selected bool) []byte {
if i%2 == 0 { if i%2 == 0 {
out = append(out, style(p.b)...) out = append(out, style(p.b)...)
} else if p.index == m.search.cursor { } else if p.index == m.search.cursor {
out = append(out, currentTheme.Cursor(p.b)...) out = append(out, theme.CurrentTheme.Cursor(p.b)...)
} else { } else {
out = append(out, currentTheme.Search(p.b)...) out = append(out, theme.CurrentTheme.Search(p.b)...)
} }
} }
return out return out
@ -847,34 +849,34 @@ func (m *model) viewHeight() int {
return m.termHeight - 1 return m.termHeight - 1
} }
func (m *model) cursorPointsTo() *node { func (m *model) cursorPointsTo() *Node {
return m.at(m.cursor) return m.at(m.cursor)
} }
func (m *model) at(pos int) *node { func (m *model) at(pos int) *Node {
head := m.head head := m.head
for i := 0; i < pos; i++ { for i := 0; i < pos; i++ {
if head == nil { if head == nil {
break break
} }
head = head.next head = head.Next
} }
return head return head
} }
func (m *model) findBottom() *node { func (m *model) findBottom() *Node {
n := m.head n := m.head
for n.next != nil { for n.Next != nil {
if n.end != nil { if n.End != nil {
n = n.end n = n.End
} else { } else {
n = n.next n = n.Next
} }
} }
return n return n
} }
func (m *model) nodeInsideView(n *node) bool { func (m *model) nodeInsideView(n *Node) bool {
if n == nil { if n == nil {
return false return false
} }
@ -886,12 +888,12 @@ func (m *model) nodeInsideView(n *node) bool {
if head == n { if head == n {
return true return true
} }
head = head.next head = head.Next
} }
return false return false
} }
func (m *model) selectNodeInView(n *node) { func (m *model) selectNodeInView(n *Node) {
head := m.head head := m.head
for i := 0; i < m.viewHeight(); i++ { for i := 0; i < m.viewHeight(); i++ {
if head == nil { if head == nil {
@ -901,11 +903,11 @@ func (m *model) selectNodeInView(n *node) {
m.cursor = i m.cursor = i
return return
} }
head = head.next head = head.Next
} }
} }
func (m *model) selectNode(n *node) { func (m *model) selectNode(n *Node) {
m.showCursor = true m.showCursor = true
if m.nodeInsideView(n) { if m.nodeInsideView(n) {
m.selectNodeInView(n) m.selectNodeInView(n)
@ -915,10 +917,10 @@ func (m *model) selectNode(n *node) {
m.head = n m.head = n
m.scrollIntoView() m.scrollIntoView()
} }
parent := n.parent() parent := n.Parent()
for parent != nil { for parent != nil {
parent.expand() parent.Expand()
parent = parent.parent() parent = parent.Parent()
} }
} }
@ -926,23 +928,23 @@ func (m *model) cursorPath() string {
path := "" path := ""
at := m.cursorPointsTo() at := m.cursorPointsTo()
for at != nil { for at != nil {
if at.prev != nil { if at.Prev != nil {
if at.chunk != nil && at.value == nil { if at.Chunk != nil && at.Value == nil {
at = at.parent() at = at.Parent()
} }
if at.key != nil { if at.Key != nil {
quoted := string(at.key) quoted := string(at.Key)
unquoted, err := strconv.Unquote(quoted) unquoted, err := strconv.Unquote(quoted)
if err == nil && jsonpath.Identifier.MatchString(unquoted) { if err == nil && jsonpath.Identifier.MatchString(unquoted) {
path = "." + unquoted + path path = "." + unquoted + path
} else { } else {
path = "[" + quoted + "]" + path path = "[" + quoted + "]" + path
} }
} else if at.index >= 0 { } else if at.Index >= 0 {
path = "[" + strconv.Itoa(at.index) + "]" + path path = "[" + strconv.Itoa(at.Index) + "]" + path
} }
} }
at = at.parent() at = at.Parent()
} }
return path return path
} }
@ -952,55 +954,55 @@ func (m *model) cursorValue() string {
if at == nil { if at == nil {
return "" return ""
} }
parent := at.parent() parent := at.Parent()
if parent != nil { if parent != nil {
// wrapped string part // wrapped string part
if at.chunk != nil && at.value == nil { if at.Chunk != nil && at.Value == nil {
at = parent 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 at = parent
} }
} }
if len(at.value) > 0 && at.value[0] == '"' { if len(at.Value) > 0 && at.Value[0] == '"' {
str, err := strconv.Unquote(string(at.value)) str, err := strconv.Unquote(string(at.Value))
if err == nil { if err == nil {
return str return str
} }
return string(at.value) return string(at.Value)
} }
var out strings.Builder var out strings.Builder
out.Write(at.value) out.Write(at.Value)
out.WriteString("\n") out.WriteString("\n")
if at.hasChildren() { if at.HasChildren() {
it := at.next it := at.Next
if at.isCollapsed() { if at.IsCollapsed() {
it = at.collapsed it = at.Collapsed
} }
for it != nil { for it != nil {
out.WriteString(strings.Repeat(" ", int(it.depth-at.depth))) out.WriteString(strings.Repeat(" ", int(it.Depth-at.Depth)))
if it.key != nil { if it.Key != nil {
out.Write(it.key) out.Write(it.Key)
out.WriteString(": ") out.WriteString(": ")
} }
if it.value != nil { if it.Value != nil {
out.Write(it.value) out.Write(it.Value)
} }
if it == at.end { if it == at.End {
break break
} }
if it.comma { if it.Comma {
out.WriteString(",") out.WriteString(",")
} }
out.WriteString("\n") out.WriteString("\n")
if it.chunkEnd != nil { if it.ChunkEnd != nil {
it = it.chunkEnd.next it = it.ChunkEnd.Next
} else if it.isCollapsed() { } else if it.IsCollapsed() {
it = it.collapsed it = it.Collapsed
} else { } else {
it = it.next it = it.Next
} }
} }
} }
@ -1012,16 +1014,16 @@ func (m *model) cursorKey() string {
if at == nil { if at == nil {
return "" return ""
} }
if at.key != nil { if at.Key != nil {
var v string var v string
_ = json.Unmarshal(at.key, &v) _ = json.Unmarshal(at.Key, &v)
return 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() n := m.currentTopNode()
for _, part := range path { for _, part := range path {
if n == nil { if n == nil {
@ -1029,21 +1031,21 @@ func (m *model) selectByPath(path []any) *node {
} }
switch part := part.(type) { switch part := part.(type) {
case string: case string:
n = n.findChildByKey(part) n = n.FindChildByKey(part)
case int: case int:
n = n.findChildByIndex(part) n = n.FindChildByIndex(part)
} }
} }
return n return n
} }
func (m *model) currentTopNode() *node { func (m *model) currentTopNode() *Node {
at := m.cursorPointsTo() at := m.cursorPointsTo()
if at == nil { if at == nil {
return nil return nil
} }
for at.parent() != nil { for at.Parent() != nil {
at = at.parent() at = at.Parent()
} }
return at return at
} }
@ -1069,8 +1071,8 @@ func (m *model) doSearch(s string) {
n := m.top n := m.top
searchIndex := 0 searchIndex := 0
for n != nil { for n != nil {
if n.key != nil { if n.Key != nil {
indexes := re.FindAllIndex(n.key, -1) indexes := re.FindAllIndex(n.Key, -1)
if len(indexes) > 0 { if len(indexes) > 0 {
for i, pair := range indexes { for i, pair := range indexes {
m.search.results = append(m.search.results, n) m.search.results = append(m.search.results, n)
@ -1079,24 +1081,24 @@ func (m *model) doSearch(s string) {
searchIndex += len(indexes) searchIndex += len(indexes)
} }
} }
indexes := re.FindAllIndex(n.value, -1) indexes := re.FindAllIndex(n.Value, -1)
if len(indexes) > 0 { if len(indexes) > 0 {
for range indexes { for range indexes {
m.search.results = append(m.search.results, n) 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. // String can be split into chunks, so we need to map the indexes to the chunks.
chunks := [][]byte{n.chunk} chunks := [][]byte{n.Chunk}
chunkNodes := []*node{n} chunkNodes := []*Node{n}
it := n.next it := n.Next
for it != nil { for it != nil {
chunkNodes = append(chunkNodes, it) chunkNodes = append(chunkNodes, it)
chunks = append(chunks, it.chunk) chunks = append(chunks, it.Chunk)
if it == n.chunkEnd { if it == n.ChunkEnd {
break break
} }
it = it.next it = it.Next
} }
chunkMatches := splitIndexesToChunks(chunks, indexes, searchIndex) chunkMatches := splitIndexesToChunks(chunks, indexes, searchIndex)
@ -1111,10 +1113,10 @@ func (m *model) doSearch(s string) {
searchIndex += len(indexes) searchIndex += len(indexes)
} }
if n.isCollapsed() { if n.IsCollapsed() {
n = n.collapsed n = n.Collapsed
} else { } 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) p, ok := jsonpath.Split(v)
if !ok { if !ok {
return nil return nil
@ -1167,7 +1169,7 @@ func (m *model) dig(v string) *node {
return nil return nil
} }
keys, nodes := at.children() keys, nodes := at.Children()
matches := fuzzy.Find(searchTerm, keys) matches := fuzzy.Find(searchTerm, keys)
if len(matches) == 0 { if len(matches) == 0 {

@ -13,6 +13,9 @@ import (
"github.com/charmbracelet/x/exp/teatest" "github.com/charmbracelet/x/exp/teatest"
"github.com/muesli/termenv" "github.com/muesli/termenv"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/antonmedv/fx/internal/jsonx"
"github.com/antonmedv/fx/internal/theme"
) )
func init() { func init() {
@ -26,7 +29,7 @@ func prepare(t *testing.T) *teatest.TestModel {
json, err := io.ReadAll(file) json, err := io.ReadAll(file)
require.NoError(t, err) require.NoError(t, err)
head, err := parse(json) head, err := jsonx.Parse(json)
require.NoError(t, err) require.NoError(t, err)
m := &model{ m := &model{
@ -103,8 +106,8 @@ func TestCollapseRecursive(t *testing.T) {
} }
func TestCollapseRecursiveWithSizes(t *testing.T) { func TestCollapseRecursiveWithSizes(t *testing.T) {
showSizes = true theme.ShowSizes = true
defer func() { showSizes = true }() defer func() { theme.ShowSizes = true }()
tm := prepare(t) 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"
"os/exec" "os/exec"
"path" "path"
"github.com/antonmedv/fx/internal/engine"
) )
//go:embed npm/index.js //go:embed npm/index.js
var src []byte var src []byte
func reduce(fns []string) { func reduce(fns []string) {
script := path.Join(os.TempDir(), fmt.Sprintf("fx-%v.js", version)) var deno bool
_, err := os.Stat(script)
if os.IsNotExist(err) {
err := os.WriteFile(script, src, 0644)
if err != nil {
panic(err)
}
}
deno := false
bin, err := exec.LookPath("node") bin, err := exec.LookPath("node")
if err != nil { if err != nil {
bin, err = exec.LookPath("deno") bin, err = exec.LookPath("deno")
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Node.js or Deno is required to run fx with reducers.\n") engine.Reduce(fns)
os.Exit(1) return
} }
deno = true 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() env := os.Environ()
var args []string var args []string

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

@ -4,21 +4,6 @@ import (
"strings" "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) { func regexCase(code string) (string, bool) {
if strings.HasSuffix(code, "/i") { if strings.HasSuffix(code, "/i") {
return code[:len(code)-2], true return code[:len(code)-2], true

Loading…
Cancel
Save