diff --git a/autocomplete/completion.go b/autocomplete/completion.go index 5d01a5c..a8128b6 100644 --- a/autocomplete/completion.go +++ b/autocomplete/completion.go @@ -1,17 +1,29 @@ package autocomplete -import "strings" +import ( + "log" + "regexp" + + "github.com/cpendery/clac/autocomplete/specs" +) type Suggestion struct { Name string Description string } +var ( + cmdDelimiter = regexp.MustCompile(`(\|\|)|(&&)|(;)`) +) + func LoadSuggestions(cmd string) []Suggestion { - params := strings.Split(cmd, " ") - lastParam := params[len(params)-1] - if lastParam == "git" { - return []Suggestion{{"go", "the only way to go"}, {"get", "the right way to get"}, {"gone", "here today, bye tomorrow"}} + activeCmd := ParseCommand(cmd) + if len(activeCmd) <= 0 { + return []Suggestion{} + } + log.Println(activeCmd, activeCmd[0].token) + if spec, ok := specs.Specs[activeCmd[0].token]; ok { + log.Println(spec.Description) } return []Suggestion{} } diff --git a/autocomplete/extract/extract.ts b/autocomplete/extract/extract.ts index f39ce5a..b6bd2a1 100644 --- a/autocomplete/extract/extract.ts +++ b/autocomplete/extract/extract.ts @@ -2,6 +2,7 @@ import * as fsAsync from "fs/promises"; import * as fs from "fs"; import * as path from "path"; import * as process from "process"; +import * as child_process from "child_process"; const main = async () => { const basePath = path.join(process.cwd(), ".fig"); @@ -28,12 +29,73 @@ const main = async () => { return; } + const filenameWithoutExtension = path.parse(directoryItem.name).name; const subcommand = spec as unknown as Fig.Subcommand; - console.log(subcommand?.description); + const generatedCode = generateGolang( + subcommand, + filenameWithoutExtension + ); + await fsAsync.writeFile( + path.join( + process.cwd(), + "..", + "specs", + `${filenameWithoutExtension}.go` + ), + generatedCode + ); }) ); + + const generatedFilesPath = path.join(process.cwd(), "..", "specs"); + child_process.exec(`gofmt -w ${generatedFilesPath}`); }; -const generateGolang = (subcommand: Fig.Subcommand) => {}; +const generateArgs = (args: Fig.SingleOrArray): string => { + return ""; +}; + +const generateOptions = (options: Fig.Option[]): string => { + return ""; +}; + +const generateSubcommand = (subcommand: Fig.Subcommand): string => { + const name = Array.isArray(subcommand.name) + ? `Name: []string{${subcommand.name.map((n) => `"${n}"`).join(",")}},` + : `Name: "${subcommand.name}",`; + + const description = + subcommand.description != null + ? `Description: "${subcommand.description}",` + : ""; + + const args = subcommand.args != null ? generateArgs(subcommand.args) : ""; + const options = + subcommand.options != null ? generateOptions(subcommand.options) : ""; + + return `model.Subcommand{ + ${name} + ${description} + ${args} + ${options} + }`.replaceAll(/\s/g, ""); +}; + +const generateGolang = ( + subcommand: Fig.Subcommand, + filenameWithoutExtension: string +) => { + return `// Code generated by autocomplete/extract/extract.ts. DO NOT EDIT. + + package specs + + import ( + "github.com/cpendery/clac/autocomplete/model" + ) + + func init() { + Specs["${filenameWithoutExtension}"] = ${generateSubcommand(subcommand)} + }`; +}; main(); diff --git a/autocomplete/model/model.go b/autocomplete/model/model.go new file mode 100644 index 0000000..04bd63c --- /dev/null +++ b/autocomplete/model/model.go @@ -0,0 +1,32 @@ +package model + +type Subcommand struct { + Name interface{} //single or array string, required + Description string + Args interface{} //single or array Arg, optional + Options []Option +} + +type Option struct { + Name interface{} //single or array string, required + Args interface{} //single or array Arg, optional + Description string //single, optional +} + +type Arg struct { + Name string //single, optional + Description string //single, optional + Template Template //single, optional +} + +type Template string + +const ( + TemplateFilepaths Template = "filepaths" + TemplateFolders Template = "folders" + TemplateHistory Template = "history" +) + +var ( + Templates = []Template{TemplateFilepaths, TemplateFolders, TemplateHistory} +) diff --git a/autocomplete/parse.go b/autocomplete/parse.go new file mode 100644 index 0000000..d312d6a --- /dev/null +++ b/autocomplete/parse.go @@ -0,0 +1,62 @@ +package autocomplete + +import ( + "regexp" + "strings" + "unicode" +) + +var ( + quoteRegex = regexp.MustCompile(`['"]`) +) + +type commandToken struct { + token string + complete bool +} + +func ParseCommand(cmd string) []commandToken { + commands := cmdDelimiter.Split(cmd, -1) + command := commands[len(commands)-1] + cleanCommand := strings.TrimLeftFunc(command, unicode.IsSpace) + return parse([]rune(cleanCommand)) +} + +func parse(cmd []rune) []commandToken { + results := []commandToken{} + readingQuotedStr := false + readingFlag := false + readingCmd := false + readingIdx := 0 + var reading bool + + for idx, r := range cmd { + reading = readingQuotedStr || readingFlag || readingCmd + if !reading && quoteRegex.MatchString(string(r)) { + readingQuotedStr, readingIdx = true, idx + continue + } else if !reading && r == '-' { + readingFlag, readingIdx = true, idx + continue + } else if !reading && !unicode.IsSpace(r) { + readingCmd, readingIdx = true, idx + continue + } + if readingQuotedStr && quoteRegex.MatchString(string(r)) { + readingQuotedStr = false + complete := idx+1 < len(cmd) && unicode.IsSpace(cmd[idx+1]) + results = append(results, commandToken{complete: complete, token: string(cmd[readingIdx : idx+1])}) + } else if readingFlag && (unicode.IsSpace(r) || r == '=') { + readingFlag = false + results = append(results, commandToken{complete: true, token: string(cmd[readingIdx:idx])}) + } else if readingCmd && unicode.IsSpace(r) { + readingCmd = false + results = append(results, commandToken{complete: true, token: string(cmd[readingIdx:idx])}) + } + } + reading = readingQuotedStr || readingFlag || readingCmd + if reading { + results = append(results, commandToken{complete: false, token: string(cmd[readingIdx:])}) + } + return results +} diff --git a/autocomplete/parse_test.go b/autocomplete/parse_test.go new file mode 100644 index 0000000..43f86f8 --- /dev/null +++ b/autocomplete/parse_test.go @@ -0,0 +1,40 @@ +package autocomplete + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseCommand(t *testing.T) { + tests := []struct { + cmd string + expected []commandToken + }{ + {"cmd --flag value ", []commandToken{{"cmd", true}, {"--flag", true}, {"value", true}}}, + {"cmd --flag 'value' ", []commandToken{{"cmd", true}, {"--flag", true}, {"'value'", true}}}, + {"cmd --flag=value ", []commandToken{{"cmd", true}, {"--flag", true}, {"value", true}}}, + {"cmd --flag='value' ", []commandToken{{"cmd", true}, {"--flag", true}, {"'value'", true}}}, + {"cmd 'value' ", []commandToken{{"cmd", true}, {"'value'", true}}}, + {"cmd value ", []commandToken{{"cmd", true}, {"value", true}}}, + {"cmd -f", []commandToken{{"cmd", true}, {"-f", false}}}, + {"cmd -f=value ", []commandToken{{"cmd", true}, {"-f", true}, {"value", true}}}, + {"cmd -f value ", []commandToken{{"cmd", true}, {"-f", true}, {"value", true}}}, + {"cmd -f 'value' ", []commandToken{{"cmd", true}, {"-f", true}, {"'value'", true}}}, + {"cmd -f='value' ", []commandToken{{"cmd", true}, {"-f", true}, {"'value'", true}}}, + {"cmd -f='val", []commandToken{{"cmd", true}, {"-f", true}, {"'val", false}}}, + {"cmd -f", []commandToken{{"cmd", true}, {"-f", false}}}, + {"cmd -f=", []commandToken{{"cmd", true}, {"-f", true}}}, + {"cmd -f ", []commandToken{{"cmd", true}, {"-f", true}}}, + {"cmd", []commandToken{{"cmd", false}}}, + {"cmd ", []commandToken{{"cmd", true}}}, + } + + for _, test := range tests { + t.Run(test.cmd, func(tc *testing.T) { + result := ParseCommand(test.cmd) + assert.Equal(tc, test.expected, result) + }) + + } +} diff --git a/autocomplete/specs/afplay.go b/autocomplete/specs/afplay.go new file mode 100644 index 0000000..b5a9b9e --- /dev/null +++ b/autocomplete/specs/afplay.go @@ -0,0 +1,11 @@ +// Code generated by autocomplete/extract/extract.ts. DO NOT EDIT. + +package specs + +import ( + "github.com/cpendery/clac/autocomplete/model" +) + +func init() { + Specs["afplay"] = model.Subcommand{Name: "afplay", Description: "AudioFilePlay"} +} diff --git a/autocomplete/specs/specs_.go b/autocomplete/specs/specs_.go new file mode 100644 index 0000000..e1d82b1 --- /dev/null +++ b/autocomplete/specs/specs_.go @@ -0,0 +1,7 @@ +package specs + +import "github.com/cpendery/clac/autocomplete/model" + +var ( + Specs = make(map[string]model.Subcommand) +) diff --git a/go.mod b/go.mod index edc6cd8..03e8335 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/charmbracelet/lipgloss v0.8.0 github.com/mattn/go-runewidth v0.0.14 github.com/muesli/reflow v0.3.0 + github.com/stretchr/testify v1.8.4 golang.org/x/term v0.6.0 ) @@ -15,14 +16,17 @@ require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.15.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.7.0 // indirect golang.org/x/text v0.3.8 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6868739..15f57e1 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/charmbracelet/lipgloss v0.8.0 h1:IS00fk4XAHcf8uZKc3eHeMUTCxUH6NkaTrdy github.com/charmbracelet/lipgloss v0.8.0/go.mod h1:p4eYUZZJ/0oXTuCQKFF8mqyKCz0ja6y+7DniDDw5KKU= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +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/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= @@ -27,9 +29,13 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -40,3 +46,7 @@ golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=