Deliver shell completion query replies
This commit is contained in:
@@ -6,12 +6,57 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||
spfcobra "github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// CompletionQueryToken is the hidden request understood by generated scripts.
|
||||
const CompletionQueryToken = spfcobra.ShellCompRequestCmd
|
||||
|
||||
const completionPathPrefixMarker = "__commandkit_path_prefix__"
|
||||
|
||||
// CompletionReply translates neutral suggestions to the shell line protocol.
|
||||
func CompletionReply(result commandmeta.CompletionResult) (string, error) {
|
||||
var directive spfcobra.ShellCompDirective
|
||||
switch result.Kind {
|
||||
case commandmeta.CompletionNone, commandmeta.CompletionChoices:
|
||||
directive = spfcobra.ShellCompDirectiveNoFileComp
|
||||
case commandmeta.CompletionFiles:
|
||||
directive = spfcobra.ShellCompDirectiveDefault
|
||||
case commandmeta.CompletionDirectories:
|
||||
directive = spfcobra.ShellCompDirectiveFilterDirs
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported completion suggestion strategy")
|
||||
}
|
||||
var output strings.Builder
|
||||
if result.Kind != commandmeta.CompletionNone && result.Prefix != "" && !strings.Contains(result.Prefix, "=") {
|
||||
output.WriteString(completionPathPrefixMarker)
|
||||
output.WriteString(result.Prefix)
|
||||
output.WriteByte('\n')
|
||||
}
|
||||
if result.Kind == commandmeta.CompletionChoices {
|
||||
for _, choice := range result.Choices {
|
||||
if choice.Value == "" || strings.ContainsAny(choice.Value, "\x00\r\n\t") {
|
||||
continue
|
||||
}
|
||||
// Generated scripts restore the attached prefix when needed.
|
||||
output.WriteString(choice.Value)
|
||||
if choice.Description != "" {
|
||||
output.WriteByte('\t')
|
||||
output.WriteString(strings.NewReplacer("\x00", " ", "\r", " ", "\n", " ", "\t", " ").Replace(choice.Description))
|
||||
}
|
||||
output.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&output, ":%d\n", directive)
|
||||
return output.String(), nil
|
||||
}
|
||||
|
||||
// CompletionErrorReply is the protocol response for a malformed query.
|
||||
func CompletionErrorReply() string {
|
||||
return fmt.Sprintf(":%d\n", spfcobra.ShellCompDirectiveError)
|
||||
}
|
||||
|
||||
// CompletionScript generates a shell script for the installed executable.
|
||||
func CompletionScript(w io.Writer, executable, shell string) error {
|
||||
root := &spfcobra.Command{Use: executable}
|
||||
@@ -32,11 +77,10 @@ func CompletionScript(w io.Writer, executable, shell string) error {
|
||||
}
|
||||
|
||||
script := generated.String()
|
||||
if shell == "zsh" {
|
||||
script, err = replaceCompletionText(script, `words=("${=words[1,CURRENT]}")`, `words=("${(@)words[1,CURRENT]}")`)
|
||||
if err == nil {
|
||||
script, err = replaceCompletionText(script, `=~ '-.*='`, `=~ '-[^=]*='`)
|
||||
}
|
||||
if shell == "bash" {
|
||||
script, err = adjustBashCompletion(script, root.Name())
|
||||
} else if shell == "zsh" {
|
||||
script, err = adjustZshCompletion(script)
|
||||
} else if shell == "fish" {
|
||||
script, err = adjustFishCompletion(script, root.Name())
|
||||
}
|
||||
@@ -50,6 +94,82 @@ func CompletionScript(w io.Writer, executable, shell string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func adjustBashCompletion(script, name string) (string, error) {
|
||||
var err error
|
||||
script, err = replaceCompletionText(script, `local out directive`, `local out directive completionPrefix completionOriginalCur`)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
errorDebug := fmt.Sprintf(`__%s_debug "Received error from custom completion go code"`, name)
|
||||
script, err = replaceCompletionText(script, errorDebug, errorDebug+"\n compopt +o default")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parsePrefix := `if [[ ${out} == ` + completionPathPrefixMarker + `-* ]]; then
|
||||
completionPrefix=${out%%$'\n'*}
|
||||
completionPrefix=${completionPrefix#` + completionPathPrefixMarker + `}
|
||||
completionOriginalCur=${words[${#words[@]}-1]}
|
||||
if [[ $completionPrefix != --* && $completionOriginalCur == "$completionPrefix"* ]]; then
|
||||
out=${out#*$'\n'}
|
||||
cur=${completionOriginalCur#"$completionPrefix"}
|
||||
else
|
||||
completionPrefix=""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Separate activeHelp from normal completions`
|
||||
script, err = replaceCompletionText(script, `# Separate activeHelp from normal completions`, parsePrefix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
script, err = replaceCompletionText(script,
|
||||
`if (((directive & shellCompDirectiveFilterFileExt) != 0)); then`,
|
||||
`if [[ -n $completionPrefix ]] && (((directive & shellCompDirectiveFilterDirs) == 0)) && (((directive & shellCompDirectiveNoFileComp) == 0)); then
|
||||
compopt +o default
|
||||
_filedir
|
||||
elif (((directive & shellCompDirectiveFilterFileExt) != 0)); then`)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
old := fmt.Sprintf("__%[1]s_handle_special_char \"$cur\" :\n __%[1]s_handle_special_char \"$cur\" =", name)
|
||||
replacement := fmt.Sprintf(`if [[ -n $completionPrefix ]]; then
|
||||
compopt +o default
|
||||
local i
|
||||
for i in "${!COMPREPLY[@]}"; do COMPREPLY[$i]="${completionPrefix}${COMPREPLY[$i]}"; done
|
||||
cur=$completionOriginalCur
|
||||
fi
|
||||
__%[1]s_handle_special_char "$cur" :
|
||||
__%[1]s_handle_special_char "$cur" =`, name)
|
||||
return replaceCompletionText(script, old, replacement)
|
||||
}
|
||||
|
||||
func adjustZshCompletion(script string) (string, error) {
|
||||
var err error
|
||||
script, err = replaceCompletionText(script, `words=("${=words[1,CURRENT]}")`, `words=("${(@)words[1,CURRENT]}")`)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
script, err = replaceCompletionText(script, `=~ '-.*='`, `=~ '-[^=]*='`)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return replaceCompletionText(script, `local activeHelpMarker="_activeHelp_ "`, `if [[ "$out" == `+completionPathPrefixMarker+`-* ]]; then
|
||||
local pathPrefix="${out%%$'\n'*}"
|
||||
pathPrefix="${pathPrefix#`+completionPathPrefixMarker+`}"
|
||||
if [[ "$pathPrefix" != --* && "$lastParam" == "$pathPrefix"* ]]; then
|
||||
out="${out#*$'\n'}"
|
||||
if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
|
||||
flagPrefix="-P ${pathPrefix}"
|
||||
else
|
||||
flagPrefix=""
|
||||
compset -P "${(b)pathPrefix}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
local activeHelpMarker="_activeHelp_ "`)
|
||||
}
|
||||
|
||||
func replaceCompletionText(script, old, replacement string) (string, error) {
|
||||
if strings.Count(script, old) != 1 {
|
||||
return "", fmt.Errorf("unsupported generated completion script")
|
||||
@@ -77,10 +197,62 @@ func adjustFishCompletion(script, name string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
script, err = replaceCompletionText(script, `string match -r -- '-.*=' "$lastArg"`, `string match -r -- '-[^=]*=' "$lastArg"`)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
script, err = replaceCompletionText(script, `for comp in $comps`, `if set -q comps[1]; and string match -q -- '`+completionPathPrefixMarker+`-*' "$comps[1]"
|
||||
set -l candidatePrefix (string replace -- '`+completionPathPrefixMarker+`' '' "$comps[1]")
|
||||
if not string match -q -- '--*' "$candidatePrefix"; and test (string sub -l (string length -- "$candidatePrefix") -- "$lastArg") = "$candidatePrefix"
|
||||
set flagPrefix "$candidatePrefix"
|
||||
set -e comps[1]
|
||||
printf "%s%s\n" '`+completionPathPrefixMarker+`' "$flagPrefix"
|
||||
end
|
||||
end
|
||||
for comp in $comps`)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
variable := "__" + variableName + "_comp_results"
|
||||
errorBranch := fmt.Sprintf(`__%[1]s_debug "Received error directive: aborting."
|
||||
# Might as well do file completion, in case it helps
|
||||
return 1`, variableName)
|
||||
errorReplacement := fmt.Sprintf(`__%[1]s_debug "Received error directive: aborting."
|
||||
return 0`, variableName)
|
||||
script, err = replaceCompletionText(script, errorBranch, errorReplacement)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pathContext := fmt.Sprintf(`set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) %% 2)
|
||||
if test (count $%[1]s) -gt 0; and string match -q -- '%[2]s-*' "$%[1]s[1]"
|
||||
set -l pathPrefix (string replace -- '%[2]s' '' "$%[1]s[1]")
|
||||
set -l token (string unescape -- (commandline -ct))
|
||||
if not string match -q -- '--*' "$pathPrefix"; and test (string sub -l (string length -- "$pathPrefix") -- "$token") = "$pathPrefix"
|
||||
set -e %[1]s[1]
|
||||
set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) %% 2)
|
||||
if test $nofiles -eq 1
|
||||
return 0
|
||||
end
|
||||
set -l value (string sub -s (math (string length -- "$pathPrefix") + 1) -- "$token")
|
||||
set -q value[1]; or set value ""
|
||||
set -l escaped (string escape --no-quoted -- "$value" | string replace -a -- '=' '\\=')
|
||||
set --global %[1]s
|
||||
if test $dirfilter -eq 1
|
||||
for entry in (__fish_complete_directories "$escaped")
|
||||
set -a %[1]s "$pathPrefix$entry"
|
||||
end
|
||||
else
|
||||
for entry in (complete -C "'' $escaped")
|
||||
set -a %[1]s "$pathPrefix$entry"
|
||||
end
|
||||
end
|
||||
__%[3]s_debug "Path completions: $%[1]s"
|
||||
return 0
|
||||
end
|
||||
end`, variable, completionPathPrefixMarker, variableName)
|
||||
script, err = replaceCompletionText(script, `set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2)`, pathContext)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
directories := fmt.Sprintf(`if test $dirfilter -eq 1
|
||||
set -l flagPrefix (string match -r -- '^-[^=]*=' (commandline -ct))
|
||||
set --global %[1]s
|
||||
|
||||
Reference in New Issue
Block a user