mirror of https://github.com/antonmedv/fx
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
269 lines
4.6 KiB
Go
269 lines
4.6 KiB
Go
package complete
|
|
|
|
import (
|
|
_ "embed"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/dop251/goja"
|
|
"github.com/goccy/go-yaml"
|
|
|
|
"github.com/antonmedv/fx/internal/engine"
|
|
"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",
|
|
"console.log",
|
|
"len",
|
|
"uniq",
|
|
"sort",
|
|
"map",
|
|
"sortBy",
|
|
"groupBy",
|
|
"chunk",
|
|
"zip",
|
|
"flatten",
|
|
"reverse",
|
|
"keys",
|
|
"values",
|
|
"skip",
|
|
}
|
|
|
|
//go:embed prelude.js
|
|
var prelude string
|
|
|
|
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
|
|
}
|
|
|
|
compFish, ok := os.LookupEnv("COMP_FISH")
|
|
if ok {
|
|
doComplete(compFish, lastWord(compFish))
|
|
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 globalsComplete(compWord) {
|
|
return
|
|
}
|
|
|
|
if isSecondArgIsFile {
|
|
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 100MB, skip completion.
|
|
if len(input) > 100*1024*1024 {
|
|
return
|
|
}
|
|
|
|
if flagYaml {
|
|
input, err = yaml.YAMLToJSON(input)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
codeComplete(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 []byte, 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(engine.Stdlib)
|
|
code.WriteString("let json = ")
|
|
code.Write(input)
|
|
for _, arg := range args {
|
|
if arg == "" { // After dropTail, we can have empty strings.
|
|
continue
|
|
}
|
|
code.WriteString(engine.Transform(arg))
|
|
}
|
|
code.WriteString("\n__keys\n")
|
|
|
|
vm := goja.New()
|
|
value, err := vm.RunString(code.String())
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if array, ok := value.Export().([]interface{}); ok {
|
|
prefix := dropTail(compWord)
|
|
var reply []string
|
|
for _, key := range array {
|
|
reply = append(reply, join(prefix, key.(string)))
|
|
}
|
|
compReply(filterReply(reply, compWord))
|
|
}
|
|
}
|
|
|
|
var alphaRe = regexp.MustCompile(`^\w+$`)
|
|
|
|
func join(prefix, key string) string {
|
|
if alphaRe.MatchString(key) {
|
|
return prefix + "." + key
|
|
} else {
|
|
if prefix == "" {
|
|
return fmt.Sprintf(".[%q]", key)
|
|
}
|
|
return fmt.Sprintf("%s[%q]", prefix, key)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|