diff --git a/autocomplete/completion.go b/autocomplete/completion.go index 2793829..b9be60e 100644 --- a/autocomplete/completion.go +++ b/autocomplete/completion.go @@ -17,6 +17,7 @@ var ( cmdDelimiter = regexp.MustCompile(`(\|\|)|(&&)|(;)`) lastSuggestionCmd = "" lastSuggestion = []Suggestion{} + lastCmdRunes = 0 ) func getOption(token string, options []model.Option) *model.Option { @@ -59,14 +60,34 @@ func getLongName(names []string) string { return longestName } +func getShortName(names []string) string { + if len(names) == 0 { + return "" + } + shortestName := names[0] + for _, name := range names { + if len(name) < len(shortestName) { + shortestName = name + } + } + return shortestName +} + func getSubcommandDrivenRecommendation(spec model.Subcommand, persistentOptions []model.Option, partialCmd *commandToken) []Suggestion { suggestions := []Suggestion{} + allOptions := append(spec.Options, persistentOptions...) if partialCmd != nil { switch spec.FilterStrategy { case model.FilterStrategyFuzzy: - return fuzzyMatchSubcommands(partialCmd.token, spec.Subcommands) + return append( + fuzzyMatchSubcommands(partialCmd.token, spec.Subcommands), + fuzzyMatchOptions(partialCmd.token, allOptions)..., + ) case model.FilterStrategyPrefix, model.FilterStrategyEmpty: - return prefixMatchSubcommands(partialCmd.token, spec.Subcommands) + return append( + prefixMatchSubcommands(partialCmd.token, spec.Subcommands), + prefixMatchOptions(partialCmd.token, allOptions)..., + ) } } @@ -76,9 +97,9 @@ func getSubcommandDrivenRecommendation(spec model.Subcommand, persistentOptions Description: sub.Description, }) } - for _, op := range append(spec.Options, persistentOptions...) { + for _, op := range allOptions { suggestions = append(suggestions, Suggestion{ - Name: getLongName(op.Name), + Name: getShortName(op.Name), Description: op.Description, }) } @@ -164,7 +185,7 @@ func handleArg(tokens []commandToken, args []model.Arg, spec model.Subcommand, p return handleArg(tokens[1:], args[1:], spec, persistentOptions) } -func loadSuggestions(cmd string) (suggestions []Suggestion) { +func loadSuggestions(cmd string) (suggestions []Suggestion, charsInLastCmd int) { activeCmd := ParseCommand(cmd) if len(activeCmd) <= 0 { return @@ -173,17 +194,22 @@ func loadSuggestions(cmd string) (suggestions []Suggestion) { if !rootToken.complete { return } + lastCmd := activeCmd[len(activeCmd)-1] + charsInLastCmd = len(lastCmd.token) + if lastCmd.complete { + charsInLastCmd = 0 + } if spec, ok := specs.Specs[rootToken.token]; ok { - return handleSubcommand(activeCmd[1:], spec, []model.Option{}) + return handleSubcommand(activeCmd[1:], spec, []model.Option{}), charsInLastCmd } return } -func LoadSuggestions(cmd string) []Suggestion { +func LoadSuggestions(cmd string) ([]Suggestion, int) { if cmd == lastSuggestionCmd { - return lastSuggestion + return lastSuggestion, lastCmdRunes } - suggestions := loadSuggestions(cmd) - lastSuggestionCmd, lastSuggestion = cmd, suggestions - return suggestions + suggestions, lastRunes := loadSuggestions(cmd) + lastSuggestionCmd, lastSuggestion, lastCmdRunes = cmd, suggestions, lastRunes + return suggestions, lastRunes } diff --git a/autocomplete/match.go b/autocomplete/match.go index 5b6571c..6fbd32c 100644 --- a/autocomplete/match.go +++ b/autocomplete/match.go @@ -8,17 +8,22 @@ import ( "github.com/lithammer/fuzzysearch/fuzzy" ) -func fuzzyMatchSubcommands(input string, subcommands []model.Subcommand) []Suggestion { +type matchable interface { + GetName() []string + GetDescription() string +} + +func fuzzyMatch[M matchable](input string, targets []M) []Suggestion { type match struct { - name string - rank int - subcommand model.Subcommand + name string + rank int + item M } matchers := []match{} - for _, sub := range subcommands { + for _, item := range targets { bestName := "" bestNameRank := -1 - for _, n := range sub.Name { + for _, n := range item.GetName() { rank := fuzzy.RankMatch(input, n) if rank > bestNameRank { bestName = n @@ -27,8 +32,9 @@ func fuzzyMatchSubcommands(input string, subcommands []model.Subcommand) []Sugge } if bestNameRank != -1 { matchers = append(matchers, match{ - name: bestName, - subcommand: sub, + name: bestName, + item: item, + rank: bestNameRank, }) } } @@ -39,20 +45,28 @@ func fuzzyMatchSubcommands(input string, subcommands []model.Subcommand) []Sugge for _, m := range matchers { results = append(results, Suggestion{ Name: m.name, - Description: m.subcommand.Description, + Description: m.item.GetDescription(), }) } return results } -func prefixMatchSubcommands(input string, subcommands []model.Subcommand) []Suggestion { +func fuzzyMatchSubcommands(input string, subcommands []model.Subcommand) []Suggestion { + return fuzzyMatch[model.Subcommand](input, subcommands) +} + +func fuzzyMatchOptions(input string, options []model.Option) []Suggestion { + return fuzzyMatch[model.Option](input, options) +} + +func prefixMatch[M matchable](input string, subcommands []M) []Suggestion { results := []Suggestion{} for _, sub := range subcommands { - for _, n := range sub.Name { + for _, n := range sub.GetName() { if strings.HasPrefix(n, input) { results = append(results, Suggestion{ Name: n, - Description: sub.Description, + Description: sub.GetDescription(), }) break } @@ -60,3 +74,11 @@ func prefixMatchSubcommands(input string, subcommands []model.Subcommand) []Sugg } return results } + +func prefixMatchSubcommands(input string, subcommands []model.Subcommand) []Suggestion { + return prefixMatch[model.Subcommand](input, subcommands) +} + +func prefixMatchOptions(input string, options []model.Option) []Suggestion { + return prefixMatch[model.Option](input, options) +} diff --git a/autocomplete/model/model.go b/autocomplete/model/model.go index 4927688..cc66f00 100644 --- a/autocomplete/model/model.go +++ b/autocomplete/model/model.go @@ -9,6 +9,14 @@ type Subcommand struct { FilterStrategy FilterStrategy } +func (s Subcommand) GetName() []string { + return s.Name +} + +func (s Subcommand) GetDescription() string { + return s.Description +} + type Option struct { Name []string //single or array string, required Args []Arg //single or array Arg, optional @@ -17,6 +25,14 @@ type Option struct { ExclusiveOn []string } +func (o Option) GetName() []string { + return o.Name +} + +func (o Option) GetDescription() string { + return o.Description +} + type Arg struct { Name string //single, optional Description string //single, optional diff --git a/ui/suggestions/suggestions.go b/ui/suggestions/suggestions.go index 490c45c..4efdcbd 100644 --- a/ui/suggestions/suggestions.go +++ b/ui/suggestions/suggestions.go @@ -22,6 +22,7 @@ type Model struct { keyMap KeyMap windowWidth int windowHeight int + runesToRemove int } const ( @@ -71,8 +72,8 @@ func (m Model) HasActiveSuggestion() bool { return len(m.suggestions) > 0 } -func (m Model) ActiveSuggestion() string { - return m.suggestions[m.cursor].Name +func (m Model) ActiveSuggestion() (string, int) { + return m.suggestions[m.cursor].Name, m.runesToRemove } func (m Model) Update(msg tea.Msg, command string, userInputCursorLocation int) Model { @@ -93,7 +94,7 @@ func (m Model) Update(msg tea.Msg, command string, userInputCursorLocation int) m.windowWidth = msg.Width } m.userInputCursorLocation = userInputCursorLocation - m.suggestions = autocomplete.LoadSuggestions(command) + m.suggestions, m.runesToRemove = autocomplete.LoadSuggestions(command) if len(m.suggestions) == 0 { m.cursor = 0 } diff --git a/ui/ui.go b/ui/ui.go index 49d541a..3cf7e14 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -49,8 +49,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if !m.suggestions.HasActiveSuggestion() { return m, nil } - activeSuggestion := m.suggestions.ActiveSuggestion() - s := m.textInput.Value() + activeSuggestion + activeSuggestion, runesToRemove := m.suggestions.ActiveSuggestion() + currentValue := m.textInput.Value() + s := currentValue[:len(currentValue)-runesToRemove] + activeSuggestion + " " m.textInput.SetValue(s) m.textInput.SetCursor(len(s)) return m, nil