fix: ui tabbing for partial command completion

Signed-off-by: cpendery <cpendery@vt.edu>
This commit is contained in:
cpendery
2023-09-11 21:41:44 -04:00
parent 59ba875565
commit 89c7186ad0
5 changed files with 94 additions and 28 deletions
+37 -11
View File
@@ -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
}
+34 -12
View File
@@ -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)
}
+16
View File
@@ -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
+4 -3
View File
@@ -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
}
+3 -2
View File
@@ -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