feat: first full pass (w/ broken ui) working

Signed-off-by: cpendery <cpendery@vt.edu>
This commit is contained in:
cpendery
2023-09-11 17:50:38 -04:00
parent 9d55b11e3f
commit 6e44c7c776
4 changed files with 272 additions and 10 deletions
+177 -2
View File
@@ -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
}
/* <has a> <no a>
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
<NEEDS_WORK>
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
}
+78 -3
View File
@@ -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<T>(arr: T[], chuckSize: number): T[][] {
@@ -114,7 +134,12 @@ const genName = (name: Fig.SingleOrArray<string> | 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<Fig.Arg> | 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<Fig.Subcommand> | 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<string>) => {
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<Fig.Arg>): 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}},`;
+15 -4
View File
@@ -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
+2 -1
View File
@@ -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])})