diff --git a/internal/pkg/completion/completion.go b/internal/pkg/completion/completion.go index abc18a7..925d563 100644 --- a/internal/pkg/completion/completion.go +++ b/internal/pkg/completion/completion.go @@ -4,6 +4,7 @@ import ( "log/slog" "slices" "strings" + "unicode/utf8" "github.com/spf13/cobra" @@ -25,20 +26,88 @@ func ClusterID(clustersInventoryFile string, stopAfterFirst bool, filter func(li return nil, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveError } - suggestions := make([]string, 0, len(clusters)) - for _, cluster := range clusters { - if cluster.ID == "" { - continue - } - if filter != nil && !filter(cluster) { - continue - } - if cur == "" || strings.HasPrefix(cluster.ID, cur) { - suggestions = append(suggestions, cluster.ID) + return suggestClusterIDs(clusters, cur, filter), cobra.ShellCompDirectiveNoFileComp + } +} + +func suggestClusterIDs(clusters []lieutenant.Cluster, cur string, filter func(lieutenant.Cluster) bool) []string { + type rankedSuggestion struct { + suggestion string + distance int + } + + suggestions := make([]rankedSuggestion, 0, len(clusters)) + for _, cluster := range clusters { + if cluster.ID == "" { + continue + } + if filter != nil && !filter(cluster) { + continue + } + + if !match(cluster.ID, cur) && !match(strings.ToLower(cur), strings.ToLower(cluster.DisplayName)) { + continue + } + + idDistance := levenshteinDistance(cur, cluster.ID) + displayNameDistance := 100 * levenshteinDistance(strings.ToLower(cur), strings.ToLower(cluster.DisplayName)) + suggestions = append(suggestions, rankedSuggestion{suggestion: cluster.ID, distance: idDistance + displayNameDistance}) + } + + slices.SortFunc(suggestions, func(a, b rankedSuggestion) int { + return 10*(a.distance-b.distance) + strings.Compare(a.suggestion, b.suggestion) + }) + + result := make([]string, len(suggestions)) + for i, s := range suggestions { + result[i] = s.suggestion + } + return result +} + +func match(source, target string) bool { + if source == target { + return true + } + if len(target)-len(source) < 0 { + return false + } + +outer: + for _, r1 := range source { + for i, r2 := range target { + if r1 == r2 { + target = target[i+utf8.RuneLen(r2):] + continue outer } } - slices.Sort(suggestions) + return false + } + + return true +} - return suggestions, cobra.ShellCompDirectiveNoFileComp +func levenshteinDistance(s, t string) int { + r1, r2 := []rune(s), []rune(t) + column := make([]int, len(r1)+1) + + for j := range column { + column[j] = j } + + for x := 1; x <= len(r2); x++ { + column[0] = x + + for y, lastDiag := 1, x-1; y <= len(r1); y++ { + oldDiag := column[y] + cost := 0 + if r1[y-1] != r2[x-1] { + cost = 1 + } + column[y] = min(column[y]+1, column[y-1]+1, lastDiag+cost) + lastDiag = oldDiag + } + } + + return column[len(r1)] }