diff --git a/complete.go b/complete.go deleted file mode 100644 index f3d9a2c..0000000 --- a/complete.go +++ /dev/null @@ -1,69 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strings" - - "github.com/antonmedv/fx/internal/shlex" -) - -var flags = []string{ - "--help", - "--raw", - "--slurp", - "--themes", - "--version", - "--yaml", - "-h", - "-r", - "-s", - "-v", -} - -func complete() { - compLine, ok := os.LookupEnv("COMP_LINE") - - if !ok || len(os.Args) < 3 { - return - } - - // Get the current partial word to be completed - partial := os.Args[2] - - var reply []string - - if strings.HasPrefix(partial, "-") { - // Filter the flags that match the partial word - for _, flag := range flags { - if strings.HasPrefix(flag, partial) { - reply = append(reply, flag) - } - } - } - - args, err := shlex.Split(compLine) - if err != nil { - return - } - - if len(args) <= 2 { - reply = files(partial) - } - - for _, word := range reply { - fmt.Println(word) - } - - os.Exit(0) -} - -// log appends the given arguments to the log file. -func log(args ...interface{}) { - file, err := os.OpenFile("complete.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - return - } - fmt.Fprintln(file, args...) - file.Close() -} diff --git a/go.mod b/go.mod index 524f78e..6b40041 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/charmbracelet/bubbletea v0.25.0 github.com/charmbracelet/lipgloss v0.10.0 github.com/charmbracelet/x/exp/teatest v0.0.0-20231025135604-4a717d4fb812 + github.com/dop251/goja v0.0.0-20240220182346-e401ed450204 github.com/goccy/go-yaml v1.11.3 github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-runewidth v0.0.15 @@ -22,7 +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/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/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 diff --git a/go.sum b/go.sum index c13b636..c25296c 100644 --- a/go.sum +++ b/go.sum @@ -14,18 +14,43 @@ github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMt github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= github.com/charmbracelet/x/exp/teatest v0.0.0-20231025135604-4a717d4fb812 h1:W/hU7Z+y+QsZo2qg0hwjv56qSMP12Z72DJR8k+ULbA4= github.com/charmbracelet/x/exp/teatest v0.0.0-20231025135604-4a717d4fb812/go.mod h1:TckAxPtan3aJ5wbTgBkySpc50SZhXJRZ8PtYICnZJEw= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/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= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= 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/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +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/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +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= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +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/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -54,25 +79,58 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +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/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= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/help.go b/help.go index 1122996..a8593b3 100644 --- a/help.go +++ b/help.go @@ -25,9 +25,10 @@ func usage(keyMap KeyMap) string { -h, --help print help -v, --version print version --themes print themes - --yaml parse input as YAML + --comp print completion script -r, --raw treat input as a raw string -s, --slurp read all inputs into an array + --yaml parse input as YAML %v %v diff --git a/internal/complete/bash.go b/internal/complete/bash.go new file mode 100644 index 0000000..d58e915 --- /dev/null +++ b/internal/complete/bash.go @@ -0,0 +1,6 @@ +package complete + +func Bash() string { + return `complete -o filenames -C fx fx +` +} diff --git a/internal/complete/complete.go b/internal/complete/complete.go new file mode 100644 index 0000000..030d5b0 --- /dev/null +++ b/internal/complete/complete.go @@ -0,0 +1,239 @@ +package complete + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/dop251/goja" + "github.com/goccy/go-yaml" + + "github.com/antonmedv/fx/internal/shlex" +) + +var flags = []string{ + "--help", + "--raw", + "--slurp", + "--themes", + "--version", + "--yaml", + "-h", + "-r", + "-s", + "-v", +} + +var globals = []string{ + "JSON.stringify", + "JSON.parse", + "YAML.stringify", + "YAML.parse", + "Object.keys", + "Object.values", + "Object.entries", + "Object.fromEntries", + "Array.isArray", + "Array.from", + "len", + "uniq", + "sort", + "map", + "sortBy", + "groupBy", + "chunk", + "zip", + "flatten", + "reverse", + "keys", + "values", +} + +func Complete() bool { + compLine, ok := os.LookupEnv("COMP_LINE") + + if ok && len(os.Args) == 3 { + doComplete(compLine, os.Args[2]) + return true + } + + compZsh, ok := os.LookupEnv("COMP_ZSH") + if ok { + doComplete(compZsh, lastWord(compZsh)) + return true + } + + return false +} + +func doComplete(compLine string, compWord string) { + if strings.HasPrefix(compWord, "-") { + compReply(filterReply(flags, compWord)) + return + } + + args, err := shlex.Split(compLine) + if err != nil { + return + } + + compWord = shlex.Parse(compWord) + + var flagYaml bool + for _, arg := range args { + if arg == "--yaml" { + flagYaml = true + break + } + } + + // Remove flags from args. + args = filterArgs(args) + + isSecondArgIsFile := false + if len(args) == 0 { + return + } else if len(args) == 1 { + fileComplete(compWord) + return + } else if len(args) == 2 { + isSecondArgIsFile = isFile(args[1]) + if !isSecondArgIsFile { + fileComplete(compWord) + return + } + } else { + isSecondArgIsFile = isFile(args[1]) + } + + if isSecondArgIsFile { + if globalsComplete(compWord) { + return + } + + file := args[1] + + hasYamlExt, _ := regexp.MatchString(`(?i)\.ya?ml$`, file) + if !flagYaml && hasYamlExt { + flagYaml = true + } + + input, err := os.ReadFile(file) + if err != nil { + return + } + + // If input is bigger than 1MB, skip completion. + if len(input) > 1024*1024 { + return + } + + if flagYaml { + input, err = yaml.YAMLToJSON(input) + if err != nil { + return + } + } + + codeComplete(string(input), args, compWord) + } +} + +func globalsComplete(compWord string) bool { + if compWord == "" { + // We will not complete globals if compWord is empty, + // as we want to show only object keys. + return false + } + reply := filterReply(globals, compWord) + if len(reply) > 0 { + compReply(reply) + return true + } + return false +} + +func codeComplete(input string, args []string, compWord string) { + args = args[2:] // Drop binary & file from the args. + + if compWord == "" { + args = append(args, ".__keys()") + } else { + if len(args) > 0 { + last := args[len(args)-1] + last = dropTail(args[len(args)-1]) + last = last + ".__keys()" + last = balanceBrackets(last) + args[len(args)-1] = last + } + } + + var code strings.Builder + code.WriteString(prelude) + code.WriteString(fmt.Sprintf("let json = %s\n", input)) + for _, arg := range args { + if arg == "" { + continue + } + code.WriteString(Transform(arg)) + } + code.WriteString("\n__keys\n") + + out, err := goja.New().RunString(code.String()) + if err != nil { + return + } + + if array, ok := out.Export().([]interface{}); ok { + prefix := dropTail(compWord) + var reply []string + for _, key := range array { + reply = append(reply, prefix+key.(string)) + } + compReply(filterReply(reply, compWord)) + } +} + +func filterArgs(args []string) []string { + filtered := make([]string, 0, len(args)) + for _, arg := range args { + found := false + for _, flag := range flags { + if arg == flag { + found = true + break + } + } + if !found { + filtered = append(filtered, arg) + } + } + return filtered +} + +func fileComplete(compWord string) { + var matches []string + + dir, filePrefix := filepath.Split(compWord) + if dir == "" { + dir = "." + } + pattern := filepath.Join(dir, filePrefix+"*") + files, err := filepath.Glob(pattern) + if err != nil { + fmt.Println("Error reading directory:", err) + return + } + + for _, match := range files { + relativePath, err := filepath.Rel(".", match) + if err != nil { + continue + } + matches = append(matches, relativePath) + } + + compReply(matches) +} diff --git a/internal/complete/prelude.go b/internal/complete/prelude.go new file mode 100644 index 0000000..8ea50a8 --- /dev/null +++ b/internal/complete/prelude.go @@ -0,0 +1,107 @@ +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) + .filter(x => /^\w+$/.test(x)) + .forEach(x => __keys.add('.' + x)) +} + +function apply(fn, ...args) { + if (typeof fn === 'function') return fn(...args) + return fn +} + +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() +} + +function uniq(x) { + if (Array.isArray(x)) return [...new Set(x)] + throw new Error() +} + +function sort(x) { + if (Array.isArray(x)) return x.sort() + throw new Error() +} + +function map(fn) { + return function (x) { + if (Array.isArray(x)) return x.map((v, i) => fn(v, i)) + throw new Error() + } +} + +function sortBy(fn) { + return function (x) { + if (Array.isArray(x)) return x.sort((a, b) => { + const fa = fn(a) + const fb = fn(b) + return fa < fb ? -1 : fa > fb ? 1 : 0 + }) + throw new Error() + } +} + +function groupBy(keyFn) { + return function (x) { + const grouped = {} + for (const item of x) { + const key = typeof keyFn === 'function' ? keyFn(item) : item[keyFn] + if (!grouped.hasOwnProperty(key)) grouped[key] = [] + grouped[key].push(item) + } + return grouped + } +} + +function chunk(size) { + return function (x) { + const res = [] + let i = 0 + while (i < x.length) { + res.push(x.slice(i, i += size)) + } + return res + } +} + +function zip(...x) { + const length = Math.min(...x.map(a => a.length)) + const res = [] + for (let i = 0; i < length; i++) { + res.push(x.map(a => a[i])) + } + return res +} + +function flatten(x) { + if (Array.isArray(x)) return x.flat() + throw new Error() +} + +function reverse(x) { + if (Array.isArray(x)) return x.reverse() + throw new Error() +} + +function keys(x) { + if (typeof x === 'object' && x !== null) return Object.keys(x) + throw new Error() +} + +function values(x) { + if (typeof x === 'object' && x !== null) return Object.values(x) + throw new Error() +} +` diff --git a/internal/complete/transpile.go b/internal/complete/transpile.go new file mode 100644 index 0000000..b82780b --- /dev/null +++ b/internal/complete/transpile.go @@ -0,0 +1,70 @@ +package complete + +import ( + "fmt" + "regexp" + "strings" +) + +func Transform(code string) string { + return fmt.Sprintf(`json = apply((function () { + const x = this + return %s + }).call(json), json) +`, transpile(code)) +} + +var ( + reBracket = regexp.MustCompile(`^(\.\w*)+\[]`) + reBracketStart = regexp.MustCompile(`^\.\[`) + reDotStart = regexp.MustCompile(`^\.`) + reMap = regexp.MustCompile(`^map\(.+?\)$`) + reAt = regexp.MustCompile(`^@`) +) + +func transpile(code string) string { + if code == "." { + return "x" + } + + if reBracket.MatchString(code) { + return fmt.Sprintf("(%s)(x)", fold(strings.Split(code, "[]"))) + } + + if reBracketStart.MatchString(code) { + return "x" + code[1:] + } + + if reDotStart.MatchString(code) { + return "x" + code + } + + if reMap.MatchString(code) { + s := code[4 : len(code)-1] + if s[0] == '.' { + s = "x" + s + } + return fmt.Sprintf(`x.map((x, i) => apply(%s, x, i))`, s) + } + + if reAt.MatchString(code) { + jsCode := transpile(code[1:]) + return fmt.Sprintf(`x.map((x, i) => apply(%s, x, i))`, jsCode) + } + + return code +} + +func fold(s []string) string { + if len(s) == 1 { + return "x => x" + s[0] + } + obj := s[0] + s = s[1:] + if obj == "." { + obj = "x" + } else { + obj = "x" + obj + } + return fmt.Sprintf(`x => %s.flatMap(%s)`, obj, fold(s)) +} diff --git a/internal/complete/utils.go b/internal/complete/utils.go new file mode 100644 index 0000000..277ca0e --- /dev/null +++ b/internal/complete/utils.go @@ -0,0 +1,79 @@ +package complete + +import ( + "fmt" + "os" + "regexp" + "strings" +) + +func compReply(reply []string) { + for _, word := range reply { + fmt.Println(word) + } +} + +func filterReply(reply []string, compWord string) []string { + var filtered []string + for _, word := range reply { + if strings.HasPrefix(word, compWord) { + filtered = append(filtered, word) + } + } + return filtered +} + +func isFile(path string) bool { + info, err := os.Stat(path) + if err != nil { + return false + } + return !info.IsDir() +} + +var tailRe = regexp.MustCompile(`\.?\w*$`) + +func dropTail(s string) string { + return tailRe.ReplaceAllString(s, "") +} + +func balanceBrackets(code string) string { + var stack []rune + brackets := map[rune]rune{')': '(', '}': '{', ']': '['} + reverseBrackets := map[rune]rune{'(': ')', '{': '}', '[': ']'} + + for _, char := range code { + switch char { + case '(', '{', '[': + stack = append(stack, char) + case ')', '}', ']': + if len(stack) > 0 && brackets[char] == stack[len(stack)-1] { + stack = stack[:len(stack)-1] // Pop + } + } + } + + for i := len(stack) - 1; i >= 0; i-- { + code += string(reverseBrackets[stack[i]]) + } + + return code +} + +func lastWord(line string) string { + words := strings.Split(line, " ") + var s string + if len(words) > 0 { + s = words[len(words)-1] + } + return s +} + +func debug(args ...interface{}) { + file, err := os.OpenFile("complete.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return + } + _, _ = fmt.Fprintln(file, args...) + _ = file.Close() +} diff --git a/internal/complete/zsh.go b/internal/complete/zsh.go new file mode 100644 index 0000000..414399c --- /dev/null +++ b/internal/complete/zsh.go @@ -0,0 +1,14 @@ +package complete + +func Zsh() string { + return `_fx_complete() { + if [[ $CURRENT -eq 2 ]]; then + _files + else + compadd $(COMP_ZSH="${LBUFFER}" fx) + fi +} + +compdef _fx_complete fx +` +} diff --git a/internal/shlex/shlex.go b/internal/shlex/shlex.go index e0971a5..2db3bfd 100644 --- a/internal/shlex/shlex.go +++ b/internal/shlex/shlex.go @@ -408,3 +408,19 @@ func Split(s string) ([]string, error) { subStrings = append(subStrings, word) } } + +// Parse removes the shell-style quoting from a string. +func Parse(s string) string { + l := NewLexer(strings.NewReader(s)) + var b strings.Builder + for { + word, err := l.Next() + if err != nil { + if err == io.EOF { + return b.String() + } + return "" + } + b.WriteString(word) + } +} diff --git a/main.go b/main.go index f7b79e4..437bcd4 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "errors" + "flag" "fmt" "io" "io/fs" @@ -24,13 +25,13 @@ import ( "github.com/mattn/go-isatty" "github.com/sahilm/fuzzy" + "github.com/antonmedv/fx/internal/complete" jsonpath "github.com/antonmedv/fx/path" ) var ( - flagHelp bool - flagVersion bool - flagYaml bool + flagYaml bool + flagComp bool ) func main() { @@ -51,36 +52,46 @@ func main() { defer pprof.WriteHeapProfile(memProf) } - complete() + if complete.Complete() { + os.Exit(0) + return + } var args []string for _, arg := range os.Args[1:] { + if strings.HasPrefix(arg, "--comp") { + flagComp = true + continue + } switch arg { case "-h", "--help": - flagHelp = true + fmt.Println(usage(keyMap)) + return case "-v", "-V", "--version": - flagVersion = true + fmt.Println(version) + return case "--themes": themeTester() return case "--export-themes": exportThemes() return - case "--yaml": - flagYaml = true default: args = append(args, arg) } - } - if flagHelp { - fmt.Println(usage(keyMap)) - return - } - - if flagVersion { - fmt.Println(version) + if flagComp { + shell := flag.String("comp", "", "") + flag.Parse() + switch *shell { + case "bash": + fmt.Print(complete.Bash()) + case "zsh": + fmt.Print(complete.Zsh()) + default: + fmt.Println("unknown shell type") + } return } @@ -112,7 +123,7 @@ func main() { } else if !stdinIsTty && len(args) == 0 { src = os.Stdin } else { - reduce(args) + reduce(os.Args[1:]) return }