Separate completion metadata from literal choices

This commit is contained in:
2026-11-03 13:12:05 +00:00
parent f39f27b5cd
commit b83c0a0dfc
3 changed files with 53 additions and 35 deletions

View File

@@ -67,7 +67,9 @@ hints. Coordinate with finding 1 so the same context drives safe shell calls.
### 3. Separate transport control records from literal candidates
Priority: P2. Status: open.
Priority: P2. Status: complete.
- [x] Completed: Successful replies frame replacement metadata in a dedicated first line. Bash and Zsh no longer interpret ActiveHelp-like candidate text. Interactive checks pass for literal control-like choices, attached choices, and empty replies.
[CompletionReply](../../internal/cobra/completion.go#L19) emits literal choices
in a stream where generated scripts also recognize Cobra's `_activeHelp_ `

View File

@@ -28,12 +28,15 @@ func CompletionReply(result commandmeta.CompletionResult) (string, error) {
default:
return "", fmt.Errorf("unsupported completion suggestion strategy")
}
var output strings.Builder
if result.Kind != commandmeta.CompletionNone && result.Prefix != "" {
output.WriteString(completionPathPrefixMarker)
output.WriteString(result.Prefix)
output.WriteByte('\n')
// The first line is always metadata, even for a whole-word replacement.
// Remaining lines before the directive are exclusively literal candidates.
if strings.ContainsAny(result.Prefix, "\x00\r\n\t") {
return "", fmt.Errorf("unrepresentable completion prefix")
}
var output strings.Builder
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") {
@@ -145,20 +148,29 @@ func adjustBashCompletion(script, name string) (string, error) {
if err != nil {
return "", err
}
parsePrefix := `if [[ ${out} == ` + completionPathPrefixMarker + `-* ]]; then
completionPrefix=${out%%$'\n'*}
completionPrefix=${completionPrefix#` + completionPathPrefixMarker + `}
completionOriginalCur=$cur
if [[ $completionOriginalCur == "$completionPrefix"* ]]; then
if [[ $out == *$'\n'* ]]; then out=${out#*$'\n'}; else out=; fi
cur=${completionOriginalCur#"$completionPrefix"}
else
completionPrefix=""
fi
parsePrefix := `if [[ ${out} != ` + completionPathPrefixMarker + `* ]]; then
compopt +o default
return
fi
completionPrefix=${out%%$'\n'*}
completionPrefix=${completionPrefix#` + completionPathPrefixMarker + `}
completionOriginalCur=$cur
if [[ $completionOriginalCur != "$completionPrefix"* ]]; then
compopt +o default
return
fi
if [[ $out == *$'\n'* ]]; then out=${out#*$'\n'}; else out=; fi
cur=${completionOriginalCur#"$completionPrefix"}
# Separate activeHelp from normal completions`
script, err = replaceCompletionText(script, `# Separate activeHelp from normal completions`, parsePrefix)
if err != nil {
return "", err
}
script, err = replaceCompletionSection(script, " local activeHelpMarker=", "\n}\n", ` while IFS='' read -r comp; do
[[ -z $comp ]] || completions+=("$comp")
done <<<"${out}"
`)
if err != nil {
return "", err
}
@@ -196,19 +208,23 @@ func adjustZshCompletion(script string) (string, error) {
if err != nil {
return "", err
}
script, err = replaceCompletionText(script, `local activeHelpMarker="_activeHelp_ "`, `if [[ "$out" == `+completionPathPrefixMarker+`-* ]]; then
local pathPrefix="${out%%$'\n'*}"
pathPrefix="${pathPrefix#`+completionPathPrefixMarker+`}"
if [[ "$lastParam" == "$pathPrefix"* ]]; then
if [[ $out == *$'\n'* ]]; then out="${out#*$'\n'}"; else out=; fi
flagPrefix="$pathPrefix"
fi
fi
script, err = replaceCompletionSection(script, ` local activeHelpMarker=`, " if [ $((directive & shellCompDirectiveNoSpace))", ` [[ "$out" == `+completionPathPrefixMarker+`* ]] || return 1
flagPrefix="${out%%$'\n'*}"
flagPrefix="${flagPrefix#`+completionPathPrefixMarker+`}"
[[ "$lastParam" == "$flagPrefix"* ]] || return 1
if [[ $out == *$'\n'* ]]; then out="${out#*$'\n'}"; else out=; fi
if [[ -n "$flagPrefix" ]]; then
prefixArgs=(-P "$flagPrefix")
fi
local tab="$(printf '\t')"
while IFS= read -r comp; do
if [[ -n "$comp" ]]; then
comp=${comp//:/\\:}
completions+=("${comp//$tab/:}")
fi
done <<<"$out"
local activeHelpMarker="_activeHelp_ "`)
`)
if err != nil {
return "", err
}
@@ -272,15 +288,14 @@ func adjustFishCompletion(script, name string) (string, error) {
if err != nil {
return "", err
}
script, err = replaceCompletionSection(script, " # For Fish, when completing a flag", ` printf "%s\n" "$directiveLine"`, ` set -l flagPrefix ""
if set -q comps[1]; and string match -q -- '`+completionPathPrefixMarker+`-*' "$comps[1]"
set -l candidatePrefix (string replace -- '`+completionPathPrefixMarker+`' '' "$comps[1]")
if test (string sub -l (string length -- "$candidatePrefix") -- "$lastArg") = "$candidatePrefix"
set flagPrefix "$candidatePrefix"
set -e comps[1]
end
script, err = replaceCompletionSection(script, " # For Fish, when completing a flag", ` printf "%s\n" "$directiveLine"`, ` if not set -q comps[1]; or not string match -q -- '`+completionPathPrefixMarker+`*' "$comps[1]"
printf ":%s\n" `+fmt.Sprint(spfcobra.ShellCompDirectiveError)+`
return
end
printf "%s%s\n" '`+completionPathPrefixMarker+`' "$flagPrefix"
set -l flagPrefix (string replace -- '`+completionPathPrefixMarker+`' '' "$comps[1]")
set -q flagPrefix[1]; or set flagPrefix ""
printf "%s\n" "$comps[1]"
set -e comps[1]
for comp in $comps
printf "%s%s\n" "$flagPrefix" "$comp"
end

View File

@@ -82,7 +82,7 @@ sys.stdout.write(os.environ['PROBE_REPLY'])
t.Fatal(err)
}
reply := func(candidates string, directive spfcobra.ShellCompDirective) string {
return fmt.Sprintf("%s:%d\n", candidates, directive)
return fmt.Sprintf("%s\n%s:%d\n", completionPathPrefixMarker, candidates, directive)
}
pathReply := func(kind commandmeta.CompletionKind, prefix string) string {
result, err := CompletionReply(commandmeta.CompletionResult{Kind: kind, Prefix: prefix})
@@ -115,6 +115,7 @@ sys.stdout.write(os.environ['PROBE_REPLY'])
{"short equals choice", "probe -c=bl", choiceReply("-c=", "blue"), []string{"__complete", "-c=bl"}, []string{"-c=blue"}},
{"short choice with equals", "probe -cfoo=ba", choiceReply("-c", "foo=bar"), []string{"__complete", "-cfoo=ba"}, []string{"-cfoo=bar"}},
{"short no suggestions", "probe -cnomatch", pathReply(commandmeta.CompletionChoices, "-c"), []string{"__complete", "-cnomatch"}, []string{"-cnomatch"}},
{"literal active help", "probe _act", choiceReply("", "_activeHelp_ choice"), []string{"__complete", "_act"}, []string{"_activeHelp_ choice"}},
{"literal prefix marker", "probe __commandkit_path_pre", choiceReply("", completionPathPrefixMarker+"-c"), []string{"__complete", "__commandkit_path_pre"}, []string{completionPathPrefixMarker + "-c"}},
{"literal marker attached", "probe --config=__commandkit_path_pre", choiceReply("--config=", completionPathPrefixMarker+"-c"), []string{"__complete", "--config=__commandkit_path_pre"}, []string{"--config=" + completionPathPrefixMarker + "-c"}},
{"option-looking separate value", "probe --format --tag=ch", choiceReply("", "--tag=chosen"), []string{"__complete", "--format", "--tag=ch"}, []string{"--format", "--tag=chosen"}},
@@ -174,7 +175,7 @@ sys.stdout.write(os.environ['PROBE_REPLY'])
t.Fatal(err)
}
cmd := exec.Command("python3", "testdata/completion_terminal.py", shell, script, tc.line)
cmd.Env = append(os.Environ(), "PATH="+dir+string(os.PathListSeparator)+os.Getenv("PATH"), "HOME="+dir, "TERM=xterm", "PROBE_DIR="+dir, "PROBE_LOG="+log, "PROBE_TAB_ONLY=1", "PROBE_REPLY=:4\n")
cmd.Env = append(os.Environ(), "PATH="+dir+string(os.PathListSeparator)+os.Getenv("PATH"), "HOME="+dir, "TERM=xterm", "PROBE_DIR="+dir, "PROBE_LOG="+log, "PROBE_TAB_ONLY=1", "PROBE_REPLY="+reply("", spfcobra.ShellCompDirectiveNoFileComp))
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("tab-only completion: %v\n%s", err, out)
}