feat(comp): Allow fuzzy matching during completion

We had made the assumption that when doing shell completion, we only
needed choices that had for *prefix* what the user had typed.
However, the zsh and fish shells have a more advanced matching system
which first matches on prefix, but if no match is found, then does
more advanced matching attempts, such as sub-strings; fish even matches
on descriptions of completions.

For example,
  helm status nginx<tab>
would match releases such as
  ingress-nginx
  ingress-nginx-release
as long as no release had a prefix of "nginx".

Such fuzzy matching can make completion even more useful for users in
cases where identical prefixes are common.

Signed-off-by: Marc Khouzam <marc.khouzam@montreal.ca>
pull/10519/head
Marc Khouzam 3 years ago committed by Marc Khouzam
parent 66fb403804
commit b8d3535991

@ -69,14 +69,7 @@ func newDocsCmd(out io.Writer) *cobra.Command {
f.BoolVar(&o.generateHeaders, "generate-headers", false, "generate standard headers for markdown files")
cmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
types := []string{"bash", "man", "markdown"}
var comps []string
for _, t := range types {
if strings.HasPrefix(t, toComplete) {
comps = append(comps, t)
}
}
return comps, cobra.ShellCompDirectiveNoFileComp
return []string{"bash", "man", "markdown"}, cobra.ShellCompDirectiveNoFileComp
})
return cmd

@ -26,9 +26,9 @@ func TestDocsTypeFlagCompletion(t *testing.T) {
cmd: "__complete docs --type ''",
golden: "output/docs-type-comp.txt",
}, {
name: "completion for docs --type",
name: "completion for docs --type, no filter",
cmd: "__complete docs --type mar",
golden: "output/docs-type-filtered-comp.txt",
golden: "output/docs-type-comp.txt",
}}
runTestCmd(t, tests)
}

@ -69,9 +69,7 @@ func bindOutputFlag(cmd *cobra.Command, varRef *output.Format) {
err := cmd.RegisterFlagCompletionFunc(outputFlag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var formatNames []string
for format, desc := range output.FormatsWithDesc() {
if strings.HasPrefix(format, toComplete) {
formatNames = append(formatNames, fmt.Sprintf("%s\t%s", format, desc))
}
formatNames = append(formatNames, fmt.Sprintf("%s\t%s", format, desc))
}
// Sort the results to get a deterministic order for the tests
@ -153,24 +151,21 @@ func compVersionFlag(chartRef string, toComplete string) ([]string, cobra.ShellC
var versions []string
if indexFile, err := repo.LoadIndexFile(path); err == nil {
for _, details := range indexFile.Entries[chartName] {
version := details.Metadata.Version
if strings.HasPrefix(version, toComplete) {
appVersion := details.Metadata.AppVersion
appVersionDesc := ""
if appVersion != "" {
appVersionDesc = fmt.Sprintf("App: %s, ", appVersion)
}
created := details.Created.Format("January 2, 2006")
createdDesc := ""
if created != "" {
createdDesc = fmt.Sprintf("Created: %s ", created)
}
deprecated := ""
if details.Metadata.Deprecated {
deprecated = "(deprecated)"
}
versions = append(versions, fmt.Sprintf("%s\t%s%s%s", version, appVersionDesc, createdDesc, deprecated))
appVersion := details.Metadata.AppVersion
appVersionDesc := ""
if appVersion != "" {
appVersionDesc = fmt.Sprintf("App: %s, ", appVersion)
}
created := details.Created.Format("January 2, 2006")
createdDesc := ""
if created != "" {
createdDesc = fmt.Sprintf("Created: %s ", created)
}
deprecated := ""
if details.Metadata.Deprecated {
deprecated = "(deprecated)"
}
versions = append(versions, fmt.Sprintf("%s\t%s%s%s", details.Metadata.Version, appVersionDesc, createdDesc, deprecated))
}
}

@ -83,6 +83,13 @@ func outputFlagCompletionTest(t *testing.T, cmdName string) {
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}, {
name: "completion for output flag, no filter",
cmd: fmt.Sprintf("__complete %s --output jso", cmdName),
golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}}
runTestCmd(t, tests)
}

@ -20,7 +20,6 @@ import (
"fmt"
"io"
"strconv"
"strings"
"time"
"github.com/gosuri/uitable"
@ -191,12 +190,9 @@ func compListRevisions(toComplete string, cfg *action.Configuration, releaseName
var revisions []string
if hist, err := client.Run(releaseName); err == nil {
for _, release := range hist {
version := strconv.Itoa(release.Version)
if strings.HasPrefix(version, toComplete) {
appVersion := fmt.Sprintf("App: %s", release.Chart.Metadata.AppVersion)
chartDesc := fmt.Sprintf("Chart: %s-%s", release.Chart.Metadata.Name, release.Chart.Metadata.Version)
revisions = append(revisions, fmt.Sprintf("%s\t%s, %s", version, appVersion, chartDesc))
}
appVersion := fmt.Sprintf("App: %s", release.Chart.Metadata.AppVersion)
chartDesc := fmt.Sprintf("Chart: %s-%s", release.Chart.Metadata.Name, release.Chart.Metadata.Version)
revisions = append(revisions, fmt.Sprintf("%s\t%s, %s", strconv.Itoa(release.Version), appVersion, chartDesc))
}
return revisions, cobra.ShellCompDirectiveNoFileComp
}

@ -95,6 +95,11 @@ func revisionFlagCompletionTest(t *testing.T, cmdName string) {
cmd: fmt.Sprintf("__complete %s musketeers --revision ''", cmdName),
rels: releases,
golden: "output/revision-comp.txt",
}, {
name: "completion for revision flag, no filter",
cmd: fmt.Sprintf("__complete %s musketeers --revision 1", cmdName),
rels: releases,
golden: "output/revision-comp.txt",
}, {
name: "completion for revision flag with too few args",
cmd: fmt.Sprintf("__complete %s --revision ''", cmdName),

@ -275,6 +275,10 @@ func TestInstallVersionCompletion(t *testing.T) {
name: "completion for install version flag with generate-name",
cmd: fmt.Sprintf("%s __complete install --generate-name testing/alpine --version ''", repoSetup),
golden: "output/version-comp.txt",
}, {
name: "completion for install version flag, no filter",
cmd: fmt.Sprintf("%s __complete install releasename testing/alpine --version 0.3", repoSetup),
golden: "output/version-comp.txt",
}, {
name: "completion for install version flag too few args",
cmd: fmt.Sprintf("%s __complete install testing/alpine --version ''", repoSetup),

@ -224,7 +224,14 @@ func compListReleases(toComplete string, ignoredReleaseNames []string, cfg *acti
client := action.NewList(cfg)
client.All = true
client.Limit = 0
client.Filter = fmt.Sprintf("^%s", toComplete)
// Do not filter so as to get the entire list of releases.
// This will allow zsh and fish to match completion choices
// on other criteria then prefix. For example:
// helm status ingress<TAB>
// can match
// helm status nginx-ingress
//
// client.Filter = fmt.Sprintf("^%s", toComplete)
client.SetStateMask()
releases, err := client.Run()

@ -18,7 +18,6 @@ package main
import (
"fmt"
"io"
"strings"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
@ -82,9 +81,7 @@ func compListPlugins(toComplete string, ignoredPluginNames []string) []string {
if err == nil && len(plugins) > 0 {
filteredPlugins := filterPlugins(plugins, ignoredPluginNames)
for _, p := range filteredPlugins {
if strings.HasPrefix(p.Metadata.Name, toComplete) {
pNames = append(pNames, fmt.Sprintf("%s\t%s", p.Metadata.Name, p.Metadata.Usage))
}
pNames = append(pNames, fmt.Sprintf("%s\t%s", p.Metadata.Name, p.Metadata.Usage))
}
}
return pNames

@ -307,6 +307,11 @@ func TestPluginCmdsCompletion(t *testing.T) {
cmd: "__complete plugin update ''",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin update, no filter",
cmd: "__complete plugin update full",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin update repetition",
cmd: "__complete plugin update args ''",
@ -317,6 +322,11 @@ func TestPluginCmdsCompletion(t *testing.T) {
cmd: "__complete plugin uninstall ''",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin uninstall, no filter",
cmd: "__complete plugin uninstall full",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin uninstall repetition",
cmd: "__complete plugin uninstall args ''",

@ -371,6 +371,10 @@ func TestPullVersionCompletion(t *testing.T) {
name: "completion for pull version flag",
cmd: fmt.Sprintf("%s __complete pull testing/alpine --version ''", repoSetup),
golden: "output/version-comp.txt",
}, {
name: "completion for pull version flag, no filter",
cmd: fmt.Sprintf("%s __complete pull testing/alpine --version 0.3", repoSetup),
golden: "output/version-comp.txt",
}, {
name: "completion for pull version flag too few args",
cmd: fmt.Sprintf("%s __complete pull --version ''", repoSetup),

@ -19,7 +19,6 @@ package main
import (
"fmt"
"io"
"strings"
"github.com/gosuri/uitable"
"github.com/pkg/errors"
@ -131,9 +130,7 @@ func compListRepos(prefix string, ignoredRepoNames []string) []string {
if err == nil && len(f.Repositories) > 0 {
filteredRepos := filterRepos(f.Repositories, ignoredRepoNames)
for _, repo := range filteredRepos {
if strings.HasPrefix(repo.Name, prefix) {
rNames = append(rNames, fmt.Sprintf("%s\t%s", repo.Name, repo.URL))
}
rNames = append(rNames, fmt.Sprintf("%s\t%s", repo.Name, repo.URL))
}
}
return rNames

@ -197,6 +197,10 @@ func TestRepoRemoveCompletion(t *testing.T) {
name: "completion for repo remove",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove ''", repoSetup),
golden: "output/repo_list_comp.txt",
}, {
name: "completion for repo remove, no filter",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove fo", repoSetup),
golden: "output/repo_list_comp.txt",
}, {
name: "completion for repo remove repetition",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove foo ''", repoSetup),

@ -106,9 +106,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
nsNames := []string{}
if namespaces, err := client.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{TimeoutSeconds: &to}); err == nil {
for _, ns := range namespaces.Items {
if strings.HasPrefix(ns.Name, toComplete) {
nsNames = append(nsNames, ns.Name)
}
nsNames = append(nsNames, ns.Name)
}
return nsNames, cobra.ShellCompDirectiveNoFileComp
}
@ -133,9 +131,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
&clientcmd.ConfigOverrides{}).RawConfig(); err == nil {
comps := []string{}
for name, context := range config.Contexts {
if strings.HasPrefix(name, toComplete) {
comps = append(comps, fmt.Sprintf("%s\t%s", name, context.Cluster))
}
comps = append(comps, fmt.Sprintf("%s\t%s", name, context.Cluster))
}
return comps, cobra.ShellCompDirectiveNoFileComp
}

@ -312,8 +312,9 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
}
repoWithSlash := fmt.Sprintf("%s/", repo)
if strings.HasPrefix(toComplete, repoWithSlash) {
// Must complete with charts within the specified repo
completions = append(completions, compListChartsOfRepo(repo, toComplete)...)
// Must complete with charts within the specified repo.
// Don't filter on toComplete to allow for shell fuzzy matching
completions = append(completions, compListChartsOfRepo(repo, "")...)
noSpace = false
break
} else if strings.HasPrefix(repo, toComplete) {

@ -98,6 +98,10 @@ func TestShowVersionCompletion(t *testing.T) {
name: "completion for show version flag",
cmd: fmt.Sprintf("%s __complete show chart testing/alpine --version ''", repoSetup),
golden: "output/version-comp.txt",
}, {
name: "completion for show version flag, no filter",
cmd: fmt.Sprintf("%s __complete show chart testing/alpine --version 0.3", repoSetup),
golden: "output/version-comp.txt",
}, {
name: "completion for show version flag too few args",
cmd: fmt.Sprintf("%s __complete show chart --version ''", repoSetup),

@ -1,3 +0,0 @@
markdown
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -1,4 +1,5 @@
aramis Aramis-chart-0.0.0 -> uninstalled
athos Athos-chart-1.2.3 -> deployed
porthos Porthos-chart-111.222.333 -> failed
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -406,6 +406,10 @@ func TestUpgradeVersionCompletion(t *testing.T) {
name: "completion for upgrade version flag",
cmd: fmt.Sprintf("%s __complete upgrade releasename testing/alpine --version ''", repoSetup),
golden: "output/version-comp.txt",
}, {
name: "completion for upgrade version flag, no filter",
cmd: fmt.Sprintf("%s __complete upgrade releasename testing/alpine --version 0.3", repoSetup),
golden: "output/version-comp.txt",
}, {
name: "completion for upgrade version flag too few args",
cmd: fmt.Sprintf("%s __complete upgrade releasename --version ''", repoSetup),

Loading…
Cancel
Save