From 6e44c7c77614e8a8be8337aad69a7af8aca1c802 Mon Sep 17 00:00:00 2001 From: cpendery Date: Mon, 11 Sep 2023 17:50:38 -0400 Subject: [PATCH] feat: first full pass (w/ broken ui) working Signed-off-by: cpendery --- autocomplete/completion.go | 179 +++++++++++++++++++++++++++++++- autocomplete/extract/extract.ts | 81 ++++++++++++++- autocomplete/model/model.go | 19 +++- autocomplete/parse.go | 3 +- 4 files changed, 272 insertions(+), 10 deletions(-) diff --git a/autocomplete/completion.go b/autocomplete/completion.go index 9833df5..59c3326 100644 --- a/autocomplete/completion.go +++ b/autocomplete/completion.go @@ -1,9 +1,10 @@ package autocomplete import ( - "log" + "log/slog" "regexp" + "github.com/cpendery/clac/autocomplete/model" "github.com/cpendery/clac/autocomplete/specs" ) @@ -18,6 +19,177 @@ var ( lastSuggestion = []Suggestion{} ) +func getOption(token string, options []model.Option) *model.Option { + for _, option := range options { + for _, optionName := range option.Name { + if token == optionName { + return &option + } + } + } + return nil +} + +func getSubcommand(token string, spec model.Subcommand) *model.Subcommand { + for _, subcommand := range spec.Subcommands { + for _, subcommandName := range subcommand.Name { + if token == subcommandName { + return &subcommand + } + } + } + return nil +} + +func argsAreOptional(args []model.Arg) bool { + allOptional := true + for _, arg := range args { + allOptional = allOptional && arg.IsOptional + } + return allOptional +} + +/* +cmd --flag arg --flag + +thus load suggestions for the base command, which would be any template, suggestions, options, subcommand, or an arg + +rules: + if there is an arg, just show the description of that arg + else, show suggestions, subcommand, template, options + in that org +*/ + +/* +generally, we want to make a recommendation based on what we finish on, so that is what matters, we can tree down until we get there + +subcommand + if isOption -> option + else + if match subcommand -> subcommand + else -> arg + + if _ -> recommend based self + rest + +option + if hasArgs + if args optional + if next is subcommand || option -> go there + if args variadic ---> arg + else -> arg + + if _ -> + if args optional -> recommend on parent subcommand + rest + args + else if has args -> recommend based on args + else -> recommend based on parent subcommand + rest + +arg + + if _ -> recommend based on self +*/ + +func getLongName(names []string) string { + longestName := "" + for _, name := range names { + if len(name) > len(longestName) { + longestName = name + } + } + return longestName +} + +func getSubcommandDrivenRecommendation(spec model.Subcommand, persistentOptions []model.Option) []Suggestion { + suggestions := []Suggestion{} + for _, sub := range spec.Subcommands { + suggestions = append(suggestions, Suggestion{ + Name: getLongName(sub.Name), + Description: sub.Description, + }) + } + for _, op := range append(spec.Options, persistentOptions...) { + suggestions = append(suggestions, Suggestion{ + Name: getLongName(op.Name), + Description: op.Description, + }) + } + return suggestions +} + +func getArgDrivenRecommendation(args []model.Arg, spec model.Subcommand, persistentOptions []model.Option) []Suggestion { + return []Suggestion{} +} + +func handleSubcommand(tokens []commandToken, spec model.Subcommand, persistentOptions []model.Option) (suggestions []Suggestion) { + if len(tokens) == 0 { + return getSubcommandDrivenRecommendation(spec, persistentOptions) + } + for _, option := range spec.Options { + if option.IsPersistent { + persistentOptions = append(persistentOptions, option) + } + } + activeCmd := tokens[0] + if activeCmd.isOption { + if option := getOption(activeCmd.token, append(spec.Options, persistentOptions...)); option != nil { + return handleOption(tokens, *option, spec, persistentOptions) + } + return + } + if subcommand := getSubcommand(activeCmd.token, spec); subcommand != nil { + return handleSubcommand(tokens[1:], *subcommand, persistentOptions) + } + + return handleArg(tokens, spec.Args, spec, persistentOptions) +} + +func handleOption(tokens []commandToken, option model.Option, spec model.Subcommand, persistentOptions []model.Option) (suggestions []Suggestion) { + if len(tokens) == 0 { + slog.Error("invalid state reached, option with no tokens") + return + } + if len(option.Args) == 0 { + return handleSubcommand(tokens[1:], spec, persistentOptions) + } + return handleArg(tokens[1:], option.Args, spec, persistentOptions) +} + +func handleArg(tokens []commandToken, args []model.Arg, spec model.Subcommand, persistentOptions []model.Option) (suggestions []Suggestion) { + if len(tokens) == 0 { + return getArgDrivenRecommendation(args, spec, persistentOptions) + } else if len(args) == 0 { + return handleSubcommand(tokens, spec, persistentOptions) + } + + activeCmd := tokens[0] + if argsAreOptional(args) { + if activeCmd.isOption { + if option := getOption(activeCmd.token, append(spec.Options, persistentOptions...)); option != nil { + return handleOption(tokens, *option, spec, persistentOptions) + } + return + } + subcommand := getSubcommand(activeCmd.token, spec) + if subcommand != nil { + return handleSubcommand(tokens[1:], *subcommand, persistentOptions) + } + } + + activeArg := args[0] + if activeArg.IsVariadic { + return []Suggestion{{Name: activeArg.Name}} + } else if activeArg.IsCommand { + if len(tokens) <= 1 { + return + } + activeCmd = tokens[1] + if subcommand := getSubcommand(activeCmd.token, spec); subcommand != nil { + return handleSubcommand(tokens[2:], *subcommand, persistentOptions) + } + return + } + return handleArg(tokens[1:], args[1:], spec, persistentOptions) +} + func loadSuggestions(cmd string) (suggestions []Suggestion) { activeCmd := ParseCommand(cmd) if len(activeCmd) <= 0 { @@ -27,8 +199,11 @@ func loadSuggestions(cmd string) (suggestions []Suggestion) { if !rootToken.complete { return } + if !activeCmd[len(activeCmd)-1].complete { + return + } if spec, ok := specs.Specs[rootToken.token]; ok { - log.Println(spec.Description) + return handleSubcommand(activeCmd[1:], spec, []model.Option{}) } return } diff --git a/autocomplete/extract/extract.ts b/autocomplete/extract/extract.ts index 6ddecc8..fb58a4f 100644 --- a/autocomplete/extract/extract.ts +++ b/autocomplete/extract/extract.ts @@ -15,7 +15,27 @@ const clearAllNewlines = (input: string): string => { }; const escapeSpecialCharacters = (input: string): string => { - return input.replaceAll("`", '"').replaceAll("\0", "\\\\0"); + const chars = [...input]; + const special = new Set(["a", "b", "f", "n", "r", "t", "v", "\\", "0", ":"]); + for (let i = 0; i < chars.length; i++) { + switch (chars[i]) { + case "\\": + if (special.has(chars.at(i + 1))) { + chars[i] = "\\\\"; + } + if (chars.at(i + 1) == "\\") { + chars[i + 1] = "\\\\"; + } + break; + case "`": + chars[i] = '"'; + break; + case "\0": + chars[i] = "\\\\0"; + break; + } + } + return chars.join(""); }; function chunk(arr: T[], chuckSize: number): T[][] { @@ -114,7 +134,12 @@ const genName = (name: Fig.SingleOrArray | undefined): string => { .filter((n) => n != null) .map((n) => `"${n}"`) .join(",")}},` - : `Name: "${name}",`; + : `Name: []string{"${name}"},`; +}; + +const genSingleName = (name: string | undefined): string => { + if (name == null) return ""; + return `Name: "${name}",`; }; const genDescription = (description: string | undefined): string => { @@ -141,6 +166,12 @@ const genArgs = (args: Fig.SingleOrArray | undefined): string => { return args != null ? generateArgs(args) : ""; }; +const genSuggestions = ( + suggestions: (string | Fig.Suggestion)[] | undefined +): string => { + return suggestions != null ? generateSuggestions(suggestions) : ""; +}; + const genSubcommands = ( subcommand: Fig.SingleOrArray | undefined ): string => { @@ -151,14 +182,56 @@ const genSubcommands = ( .join(",")}},`; }; +const genIsPersistent = (persistent: boolean): string => { + return persistent === true ? "IsPersistent: true," : ""; +}; + +const genExclusiveOn = (exclusiveOn: string[] | undefined): string => { + if (exclusiveOn == null) return ""; + return `ExclusiveOn: []string{${exclusiveOn + .map((e) => `"${e}"`) + .join(",")}},`; +}; + +const generateSuggestions = ( + suggestions: (string | Fig.Suggestion)[] +): string => { + const genName = (name: Fig.SingleOrArray) => { + return Array.isArray(name) + ? `Name: []string{${name + .map((n) => `\`${escapeSpecialCharacters(n)}\``) + .join(",")}},` + : `Name: []string{\`${escapeSpecialCharacters(name)}\`},`; + }; + + const generatedSuggestions = suggestions + .map((suggestion) => { + if (typeof suggestion == "string") { + return `{Name: []string{\`${escapeSpecialCharacters(suggestion)}\`}}`; + } + if (suggestion.name == null) { + return null; + } + return `{ + ${genName(suggestion.name)} + ${genDescription(suggestion.description)} + }`; + }) + .filter((s) => s != null) + .join(","); + + return `Suggestions: []model.Suggestion{${generatedSuggestions}},`; +}; + const generateArgs = (args: Fig.SingleOrArray): string => { const argList = Array.isArray(args) ? args : [args]; const generatedArgs = argList .map((arg) => { return `{ ${genTemplates(arg.template)} - ${genName(arg.name)} + ${genSingleName(arg.name)} ${genDescription(arg.description)} + ${genSuggestions(arg.suggestions)} }`; }) .join(","); @@ -171,6 +244,8 @@ const generateOptions = (options: Fig.Option[]): string => { ${genName(option.name)} ${genDescription(option.description)} ${genArgs(option.args)} + ${genIsPersistent(option.isPersistent)} + ${genExclusiveOn(option.exclusiveOn)} }`; }); return `Options: []model.Option{${generatedOptions}},`; diff --git a/autocomplete/model/model.go b/autocomplete/model/model.go index 85a3db8..a617591 100644 --- a/autocomplete/model/model.go +++ b/autocomplete/model/model.go @@ -1,7 +1,7 @@ package model type Subcommand struct { - Name interface{} //single or array string, required + Name []string //single or array string, required Description string Args []Arg Options []Option @@ -9,15 +9,26 @@ type Subcommand struct { } type Option struct { - Name interface{} //single or array string, required - Args interface{} //single or array Arg, optional - Description string //single, optional + Name []string //single or array string, required + Args []Arg //single or array Arg, optional + Description string //single, optional + IsPersistent bool + ExclusiveOn []string } type Arg struct { Name string //single, optional Description string //single, optional Templates []Template + Suggestions []Suggestion + IsVariadic bool + IsOptional bool + IsCommand bool +} + +type Suggestion struct { + Name []string + Description string } type Template string diff --git a/autocomplete/parse.go b/autocomplete/parse.go index d312d6a..51bd1f7 100644 --- a/autocomplete/parse.go +++ b/autocomplete/parse.go @@ -13,6 +13,7 @@ var ( type commandToken struct { token string complete bool + isOption bool } func ParseCommand(cmd string) []commandToken { @@ -48,7 +49,7 @@ func parse(cmd []rune) []commandToken { 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])}) + results = append(results, commandToken{complete: true, token: string(cmd[readingIdx:idx]), isOption: true}) } else if readingCmd && unicode.IsSpace(r) { readingCmd = false results = append(results, commandToken{complete: true, token: string(cmd[readingIdx:idx])})