diff --git a/autocomplete/completion.go b/autocomplete/completion.go index e67c4c9..7a1911b 100644 --- a/autocomplete/completion.go +++ b/autocomplete/completion.go @@ -1,7 +1,6 @@ package autocomplete import ( - "log" "log/slog" "regexp" @@ -74,8 +73,17 @@ func getShortName(names []string) string { return shortestName } -func getSubcommandDrivenRecommendation(spec model.Subcommand, persistentOptions []model.Option, partialCmd *commandToken, onlyRecommendSubcommands bool) []model.TermSuggestion { - log.Println("sub rec") +func getPersistentTokens(tokens []model.ProcessedToken) []model.ProcessedToken { + persistentTokens := []model.ProcessedToken{} + for _, token := range tokens { + if token.Persist { + persistentTokens = append(persistentTokens, token) + } + } + return persistentTokens +} + +func getSubcommandDrivenRecommendation(spec model.Subcommand, persistentOptions []model.Option, partialCmd *commandToken, onlyRecommendSubcommands bool, acceptedTokens []model.ProcessedToken) []model.TermSuggestion { suggestions := []model.TermSuggestion{} allOptions := append(spec.Options, persistentOptions...) @@ -91,6 +99,7 @@ func getSubcommandDrivenRecommendation(spec model.Subcommand, persistentOptions getOptionDrivenRecommendations(allOptions, &suggestions) } + removeDuplicateRecommendation(&suggestions, acceptedTokens) if partialCmd != nil { switch spec.FilterStrategy { case model.FilterStrategyFuzzy: @@ -103,8 +112,7 @@ func getSubcommandDrivenRecommendation(spec model.Subcommand, persistentOptions return suggestions } -func getArgDrivenRecommendation(args []model.Arg, spec model.Subcommand, persistentOptions []model.Option, partialCmd *commandToken) []model.TermSuggestion { - log.Println("arg rec") +func getArgDrivenRecommendation(args []model.Arg, spec model.Subcommand, persistentOptions []model.Option, partialCmd *commandToken, acceptedTokens []model.ProcessedToken) []model.TermSuggestion { suggestions := []model.TermSuggestion{} activeArg := args[0] allOptions := append(spec.Options, persistentOptions...) @@ -117,6 +125,7 @@ func getArgDrivenRecommendation(args []model.Arg, spec model.Subcommand, persist getOptionDrivenRecommendations(allOptions, &suggestions) } + removeDuplicateRecommendation(&suggestions, acceptedTokens) if partialCmd != nil { switch spec.FilterStrategy { case model.FilterStrategyFuzzy: @@ -129,11 +138,11 @@ func getArgDrivenRecommendation(args []model.Arg, spec model.Subcommand, persist return suggestions } -func handleSubcommand(tokens []commandToken, spec model.Subcommand, persistentOptions []model.Option, argsDepleted bool) (suggestions []model.TermSuggestion) { +func handleSubcommand(tokens []commandToken, spec model.Subcommand, persistentOptions []model.Option, argsDepleted bool, acceptedTokens []model.ProcessedToken) (suggestions []model.TermSuggestion) { if len(tokens) == 0 { - return getSubcommandDrivenRecommendation(spec, persistentOptions, nil, argsDepleted) + return getSubcommandDrivenRecommendation(spec, persistentOptions, nil, argsDepleted, acceptedTokens) } else if !tokens[0].complete { - return getSubcommandDrivenRecommendation(spec, persistentOptions, &tokens[0], argsDepleted) + return getSubcommandDrivenRecommendation(spec, persistentOptions, &tokens[0], argsDepleted, acceptedTokens) } for _, option := range spec.Options { if option.IsPersistent { @@ -143,65 +152,78 @@ func handleSubcommand(tokens []commandToken, spec model.Subcommand, persistentOp activeCmd := tokens[0] if activeCmd.isOption { if option := getOption(activeCmd.token, append(spec.Options, persistentOptions...)); option != nil { - return handleOption(tokens, *option, spec, persistentOptions) + return handleOption(tokens, *option, spec, persistentOptions, acceptedTokens) } return } if subcommand := getSubcommand(activeCmd.token, spec); subcommand != nil { - return handleSubcommand(tokens[1:], *subcommand, persistentOptions, false) + return handleSubcommand(tokens[1:], *subcommand, persistentOptions, false, getPersistentTokens(acceptedTokens)) } - return handleArg(tokens, spec.Args, spec, persistentOptions) + return handleArg(tokens, spec.Args, spec, persistentOptions, acceptedTokens) } -func handleOption(tokens []commandToken, option model.Option, spec model.Subcommand, persistentOptions []model.Option) (suggestions []model.TermSuggestion) { +func handleOption(tokens []commandToken, option model.Option, spec model.Subcommand, persistentOptions []model.Option, acceptedTokens []model.ProcessedToken) (suggestions []model.TermSuggestion) { 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, false) + activeOption := tokens[0] + isPersistent := false + for _, persistentOption := range persistentOptions { + for _, persistentOptionName := range persistentOption.Name { + if activeOption.token == persistentOptionName { + isPersistent = true + goto persistenceDetermined + } + } } - return handleArg(tokens[1:], option.Args, spec, persistentOptions) +persistenceDetermined: + acceptedTokens = append(acceptedTokens, model.ProcessedToken{Token: activeOption.token, Persist: isPersistent}) + if len(option.Args) == 0 { + return handleSubcommand(tokens[1:], spec, persistentOptions, false, acceptedTokens) + } + return handleArg(tokens[1:], option.Args, spec, persistentOptions, acceptedTokens) } -func handleArg(tokens []commandToken, args []model.Arg, spec model.Subcommand, persistentOptions []model.Option) (suggestions []model.TermSuggestion) { +func handleArg(tokens []commandToken, args []model.Arg, spec model.Subcommand, persistentOptions []model.Option, acceptedTokens []model.ProcessedToken) (suggestions []model.TermSuggestion) { if len(args) == 0 { - return handleSubcommand(tokens, spec, persistentOptions, true) + return handleSubcommand(tokens, spec, persistentOptions, true, acceptedTokens) } else if len(tokens) == 0 { - return getArgDrivenRecommendation(args, spec, persistentOptions, nil) + return getArgDrivenRecommendation(args, spec, persistentOptions, nil, acceptedTokens) } else if !tokens[0].complete { - return getArgDrivenRecommendation(args, spec, persistentOptions, &tokens[0]) + return getArgDrivenRecommendation(args, spec, persistentOptions, &tokens[0], acceptedTokens) } 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 handleOption(tokens, *option, spec, persistentOptions, acceptedTokens) } return } subcommand := getSubcommand(activeCmd.token, spec) if subcommand != nil { - return handleSubcommand(tokens[1:], *subcommand, persistentOptions, false) + return handleSubcommand(tokens[1:], *subcommand, persistentOptions, false, getPersistentTokens(acceptedTokens)) } } activeArg := args[0] + acceptedTokens = append(acceptedTokens, model.ProcessedToken{Token: activeCmd.token, Persist: false}) if activeArg.IsVariadic { - return handleArg(tokens[1:], args, spec, persistentOptions) + return handleArg(tokens[1:], args, spec, persistentOptions, acceptedTokens) } 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, false) + return handleSubcommand(tokens[2:], *subcommand, persistentOptions, false, []model.ProcessedToken{}) } return } - return handleArg(tokens[1:], args[1:], spec, persistentOptions) + return handleArg(tokens[1:], args[1:], spec, persistentOptions, getPersistentTokens(acceptedTokens)) } func loadSuggestions(cmd string) (suggestions []model.TermSuggestion, charsInLastCmd int) { @@ -219,7 +241,7 @@ func loadSuggestions(cmd string) (suggestions []model.TermSuggestion, charsInLas charsInLastCmd = 0 } if spec, ok := specs.Specs[rootToken.token]; ok { - return handleSubcommand(activeCmd[1:], spec, []model.Option{}, false), charsInLastCmd + return handleSubcommand(activeCmd[1:], spec, []model.Option{}, false, []model.ProcessedToken{}), charsInLastCmd } return } diff --git a/autocomplete/model/model.go b/autocomplete/model/model.go index 21c2cba..db4e7c7 100644 --- a/autocomplete/model/model.go +++ b/autocomplete/model/model.go @@ -63,6 +63,11 @@ func (t TermSuggestion) GetDescription() string { return t.Description } +type ProcessedToken struct { + Token string + Persist bool +} + type Generator struct { Script string Function func() string diff --git a/autocomplete/suggestions.go b/autocomplete/suggestions.go index 00b98dd..8cc1ddb 100644 --- a/autocomplete/suggestions.go +++ b/autocomplete/suggestions.go @@ -121,3 +121,16 @@ func getOptionDrivenRecommendations(options []model.Option, suggestions *[]model }) } } + +func removeDuplicateRecommendation(suggestions *[]model.TermSuggestion, processedTokens []model.ProcessedToken) { + dupMap := make(map[string]struct{}) + for _, processedToken := range processedTokens { + dupMap[processedToken.Token] = struct{}{} + } + for i := 0; i < len(*suggestions); i++ { + if _, includes := dupMap[(*suggestions)[i].Name]; includes { + *suggestions = append((*suggestions)[:i], (*suggestions)[i+1:]...) + i-- + } + } +}