Use parsed replacement prefixes in shell completion
This commit is contained in:
@@ -41,7 +41,9 @@ Enter and verifies no side effect, with quoted and escaped input.
|
|||||||
|
|
||||||
### 2. Use parser-derived replacement context in every shell
|
### 2. Use parser-derived replacement context in every shell
|
||||||
|
|
||||||
Priority: P2. Status: open.
|
Priority: P2. Status: complete.
|
||||||
|
|
||||||
|
- [x] Completed: All three scripts use the reply’s parsed attachment prefix. Interactive checks cover literal option-looking values, operands after the terminator, and existing attached/static/path cases. The root suite and full adapter shell matrix passed.
|
||||||
|
|
||||||
[Core interpretation](../../completion_flags.go) correctly prioritizes a
|
[Core interpretation](../../completion_flags.go) correctly prioritizes a
|
||||||
pending value and honors `--`. But [reply encoding and script
|
pending value and honors `--`. But [reply encoding and script
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func CompletionReply(result commandmeta.CompletionResult) (string, error) {
|
|||||||
return "", fmt.Errorf("unsupported completion suggestion strategy")
|
return "", fmt.Errorf("unsupported completion suggestion strategy")
|
||||||
}
|
}
|
||||||
var output strings.Builder
|
var output strings.Builder
|
||||||
if result.Kind != commandmeta.CompletionNone && result.Prefix != "" && !strings.Contains(result.Prefix, "=") {
|
if result.Kind != commandmeta.CompletionNone && result.Prefix != "" {
|
||||||
output.WriteString(completionPathPrefixMarker)
|
output.WriteString(completionPathPrefixMarker)
|
||||||
output.WriteString(result.Prefix)
|
output.WriteString(result.Prefix)
|
||||||
output.WriteByte('\n')
|
output.WriteByte('\n')
|
||||||
@@ -129,7 +129,7 @@ func adjustBashCompletion(script, name string) (string, error) {
|
|||||||
# The Bash helpers may omit the empty word at a trailing space.
|
# The Bash helpers may omit the empty word at a trailing space.
|
||||||
if ((${#requestWords[@]} <= cword)); then requestWords+=(""); fi
|
if ((${#requestWords[@]} <= cword)); then requestWords+=(""); fi
|
||||||
fi
|
fi
|
||||||
if [[ ${cur} == -*=* ]]; then cur="${cur#*=}"; fi
|
cur=${requestWords[${#requestWords[@]}-1]}
|
||||||
out=$(COBRA_ACTIVE_HELP=0 "${requestWords[0]}" __complete "${requestWords[@]:1}" 2>/dev/null)
|
out=$(COBRA_ACTIVE_HELP=0 "${requestWords[0]}" __complete "${requestWords[@]:1}" 2>/dev/null)
|
||||||
|
|
||||||
`)
|
`)
|
||||||
@@ -148,9 +148,9 @@ func adjustBashCompletion(script, name string) (string, error) {
|
|||||||
parsePrefix := `if [[ ${out} == ` + completionPathPrefixMarker + `-* ]]; then
|
parsePrefix := `if [[ ${out} == ` + completionPathPrefixMarker + `-* ]]; then
|
||||||
completionPrefix=${out%%$'\n'*}
|
completionPrefix=${out%%$'\n'*}
|
||||||
completionPrefix=${completionPrefix#` + completionPathPrefixMarker + `}
|
completionPrefix=${completionPrefix#` + completionPathPrefixMarker + `}
|
||||||
completionOriginalCur=${words[${#words[@]}-1]}
|
completionOriginalCur=$cur
|
||||||
if [[ $completionPrefix != --* && $completionOriginalCur == "$completionPrefix"* ]]; then
|
if [[ $completionOriginalCur == "$completionPrefix"* ]]; then
|
||||||
out=${out#*$'\n'}
|
if [[ $out == *$'\n'* ]]; then out=${out#*$'\n'}; else out=; fi
|
||||||
cur=${completionOriginalCur#"$completionPrefix"}
|
cur=${completionOriginalCur#"$completionPrefix"}
|
||||||
else
|
else
|
||||||
completionPrefix=""
|
completionPrefix=""
|
||||||
@@ -164,7 +164,7 @@ func adjustBashCompletion(script, name string) (string, error) {
|
|||||||
}
|
}
|
||||||
script, err = replaceCompletionText(script,
|
script, err = replaceCompletionText(script,
|
||||||
`if (((directive & shellCompDirectiveFilterFileExt) != 0)); then`,
|
`if (((directive & shellCompDirectiveFilterFileExt) != 0)); then`,
|
||||||
`if [[ -n $completionPrefix ]] && (((directive & shellCompDirectiveFilterDirs) == 0)) && (((directive & shellCompDirectiveNoFileComp) == 0)); then
|
`if ((directive == 0)); then
|
||||||
compopt +o default
|
compopt +o default
|
||||||
_filedir
|
_filedir
|
||||||
elif (((directive & shellCompDirectiveFilterFileExt) != 0)); then`)
|
elif (((directive & shellCompDirectiveFilterFileExt) != 0)); then`)
|
||||||
@@ -190,9 +190,6 @@ func adjustZshCompletion(script string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
script, err = replaceCompletionSection(script, " # For zsh, when completing a flag", " # Extract the directive", ` local -a prefixArgs orderArgs spaceArgs
|
script, err = replaceCompletionSection(script, " # For zsh, when completing a flag", " # Extract the directive", ` local -a prefixArgs orderArgs spaceArgs
|
||||||
if [[ "$lastParam" == -*=* ]]; then
|
|
||||||
flagPrefix="${lastParam%%=*}="
|
|
||||||
fi
|
|
||||||
out=$(COBRA_ACTIVE_HELP=0 "${words[1]}" __complete "${words[@]:1}" 2>/dev/null)
|
out=$(COBRA_ACTIVE_HELP=0 "${words[1]}" __complete "${words[@]:1}" 2>/dev/null)
|
||||||
|
|
||||||
`)
|
`)
|
||||||
@@ -202,8 +199,8 @@ func adjustZshCompletion(script string) (string, error) {
|
|||||||
script, err = replaceCompletionText(script, `local activeHelpMarker="_activeHelp_ "`, `if [[ "$out" == `+completionPathPrefixMarker+`-* ]]; then
|
script, err = replaceCompletionText(script, `local activeHelpMarker="_activeHelp_ "`, `if [[ "$out" == `+completionPathPrefixMarker+`-* ]]; then
|
||||||
local pathPrefix="${out%%$'\n'*}"
|
local pathPrefix="${out%%$'\n'*}"
|
||||||
pathPrefix="${pathPrefix#`+completionPathPrefixMarker+`}"
|
pathPrefix="${pathPrefix#`+completionPathPrefixMarker+`}"
|
||||||
if [[ "$pathPrefix" != --* && "$lastParam" == "$pathPrefix"* ]]; then
|
if [[ "$lastParam" == "$pathPrefix"* ]]; then
|
||||||
out="${out#*$'\n'}"
|
if [[ $out == *$'\n'* ]]; then out="${out#*$'\n'}"; else out=; fi
|
||||||
flagPrefix="$pathPrefix"
|
flagPrefix="$pathPrefix"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -275,19 +272,20 @@ func adjustFishCompletion(script, name string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
script, err = replaceCompletionText(script, `string match -r -- '-.*=' "$lastArg"`, `string match -r -- '-[^=]*=' "$lastArg"`)
|
script, err = replaceCompletionSection(script, " # For Fish, when completing a flag", ` printf "%s\n" "$directiveLine"`, ` set -l flagPrefix ""
|
||||||
if err != nil {
|
if set -q comps[1]; and string match -q -- '`+completionPathPrefixMarker+`-*' "$comps[1]"
|
||||||
return "", err
|
set -l candidatePrefix (string replace -- '`+completionPathPrefixMarker+`' '' "$comps[1]")
|
||||||
}
|
if test (string sub -l (string length -- "$candidatePrefix") -- "$lastArg") = "$candidatePrefix"
|
||||||
script, err = replaceCompletionText(script, `for comp in $comps`, `if set -q comps[1]; and string match -q -- '`+completionPathPrefixMarker+`-*' "$comps[1]"
|
set flagPrefix "$candidatePrefix"
|
||||||
set -l candidatePrefix (string replace -- '`+completionPathPrefixMarker+`' '' "$comps[1]")
|
set -e comps[1]
|
||||||
if not string match -q -- '--*' "$candidatePrefix"; and test (string sub -l (string length -- "$candidatePrefix") -- "$lastArg") = "$candidatePrefix"
|
end
|
||||||
set flagPrefix "$candidatePrefix"
|
|
||||||
set -e comps[1]
|
|
||||||
printf "%s%s\n" '`+completionPathPrefixMarker+`' "$flagPrefix"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
for comp in $comps`)
|
printf "%s%s\n" '`+completionPathPrefixMarker+`' "$flagPrefix"
|
||||||
|
for comp in $comps
|
||||||
|
printf "%s%s\n" "$flagPrefix" "$comp"
|
||||||
|
end
|
||||||
|
|
||||||
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -302,10 +300,9 @@ func adjustFishCompletion(script, name string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
pathContext := fmt.Sprintf(`set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) %% 2)
|
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]"
|
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 pathPrefix (string replace -- '%[2]s' '' "$%[1]s[1]")
|
||||||
set -l token (string unescape -- (commandline -ct))
|
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 -e %[1]s[1]
|
||||||
set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) %% 2)
|
set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) %% 2)
|
||||||
if test $nofiles -eq 1
|
if test $nofiles -eq 1
|
||||||
@@ -326,20 +323,10 @@ func adjustFishCompletion(script, name string) (string, error) {
|
|||||||
end
|
end
|
||||||
__%[3]s_debug "Path completions: $%[1]s"
|
__%[3]s_debug "Path completions: $%[1]s"
|
||||||
return 0
|
return 0
|
||||||
end
|
|
||||||
end`, variable, completionPathPrefixMarker, variableName)
|
end`, variable, completionPathPrefixMarker, variableName)
|
||||||
script, err = replaceCompletionText(script, `set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2)`, pathContext)
|
script, err = replaceCompletionText(script, `set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2)`, pathContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
directories := fmt.Sprintf(`if test $dirfilter -eq 1
|
return script, nil
|
||||||
set -l flagPrefix (string match -r -- '^-[^=]*=' (commandline -ct))
|
|
||||||
set --global %[1]s
|
|
||||||
for dir in (__fish_complete_directories)
|
|
||||||
set -a %[1]s "$flagPrefix$dir"
|
|
||||||
end
|
|
||||||
return 0
|
|
||||||
end
|
|
||||||
if test $filefilter -eq 1`, variable)
|
|
||||||
return replaceCompletionText(script, `if test $filefilter -eq 1; or test $dirfilter -eq 1`, directories)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,12 +102,12 @@ sys.stdout.write(os.environ['PROBE_REPLY'])
|
|||||||
name, line, reply string
|
name, line, reply string
|
||||||
query, executed []string
|
query, executed []string
|
||||||
}{
|
}{
|
||||||
{"attached choice", "probe --config=./par", reply("./partial file\tdescription\n", spfcobra.ShellCompDirectiveNoFileComp), []string{"__complete", "--config=./par"}, []string{"--config=./partial file"}},
|
{"attached choice", "probe --config=./par", choiceReply("--config=", "./partial file"), []string{"__complete", "--config=./par"}, []string{"--config=./partial file"}},
|
||||||
{"choice with equals", "probe --config=foo=ba", reply("foo=bar\tdescription\n", spfcobra.ShellCompDirectiveNoFileComp), []string{"__complete", "--config=foo=ba"}, []string{"--config=foo=bar"}},
|
{"choice with equals", "probe --config=foo=ba", choiceReply("--config=", "foo=bar"), []string{"__complete", "--config=foo=ba"}, []string{"--config=foo=bar"}},
|
||||||
{"separated file", `probe --config ./partial\ f`, reply("", spfcobra.ShellCompDirectiveDefault), []string{"__complete", "--config", "./partial f"}, []string{"--config", "./partial file"}},
|
{"separated file", `probe --config ./partial\ f`, reply("", spfcobra.ShellCompDirectiveDefault), []string{"__complete", "--config", "./partial f"}, []string{"--config", "./partial file"}},
|
||||||
{"attached file", `probe --config=./partial\ f`, reply("", spfcobra.ShellCompDirectiveDefault), []string{"__complete", "--config=./partial f"}, []string{"--config=./partial file"}},
|
{"attached file", `probe --config=./partial\ f`, pathReply(commandmeta.CompletionFiles, "--config="), []string{"__complete", "--config=./partial f"}, []string{"--config=./partial file"}},
|
||||||
{"directory", "probe ./partial", reply("", spfcobra.ShellCompDirectiveFilterDirs), []string{"__complete", "./partial"}, []string{"./partial dir"}},
|
{"directory", "probe ./partial", reply("", spfcobra.ShellCompDirectiveFilterDirs), []string{"__complete", "./partial"}, []string{"./partial dir"}},
|
||||||
{"attached directory", "probe --config=./partial", reply("", spfcobra.ShellCompDirectiveFilterDirs), []string{"__complete", "--config=./partial"}, []string{"--config=./partial dir"}},
|
{"attached directory", "probe --config=./partial", pathReply(commandmeta.CompletionDirectories, "--config="), []string{"__complete", "--config=./partial"}, []string{"--config=./partial dir"}},
|
||||||
{"short attached file", `probe -c./partial\ f`, pathReply(commandmeta.CompletionFiles, "-c"), []string{"__complete", "-c./partial f"}, []string{"-c./partial file"}},
|
{"short attached file", `probe -c./partial\ f`, pathReply(commandmeta.CompletionFiles, "-c"), []string{"__complete", "-c./partial f"}, []string{"-c./partial file"}},
|
||||||
{"short cluster directory", "probe -sc./partial", pathReply(commandmeta.CompletionDirectories, "-sc"), []string{"__complete", "-sc./partial"}, []string{"-sc./partial dir"}},
|
{"short cluster directory", "probe -sc./partial", pathReply(commandmeta.CompletionDirectories, "-sc"), []string{"__complete", "-sc./partial"}, []string{"-sc./partial dir"}},
|
||||||
{"short path with equals", "probe -cfoo=ba", pathReply(commandmeta.CompletionFiles, "-c"), []string{"__complete", "-cfoo=ba"}, []string{"-cfoo=bar"}},
|
{"short path with equals", "probe -cfoo=ba", pathReply(commandmeta.CompletionFiles, "-c"), []string{"__complete", "-cfoo=ba"}, []string{"-cfoo=bar"}},
|
||||||
@@ -117,11 +117,13 @@ sys.stdout.write(os.environ['PROBE_REPLY'])
|
|||||||
{"short no suggestions", "probe -cnomatch", pathReply(commandmeta.CompletionChoices, "-c"), []string{"__complete", "-cnomatch"}, []string{"-cnomatch"}},
|
{"short no suggestions", "probe -cnomatch", pathReply(commandmeta.CompletionChoices, "-c"), []string{"__complete", "-cnomatch"}, []string{"-cnomatch"}},
|
||||||
{"literal prefix marker", "probe __commandkit_path_pre", choiceReply("", completionPathPrefixMarker+"-c"), []string{"__complete", "__commandkit_path_pre"}, []string{completionPathPrefixMarker + "-c"}},
|
{"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"}},
|
{"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"}},
|
||||||
|
{"option-looking operand after terminator", "probe -- --tag=ch", choiceReply("", "--tag=chosen"), []string{"__complete", "--", "--tag=ch"}, []string{"--", "--tag=chosen"}},
|
||||||
{"empty", "probe ", reply("", spfcobra.ShellCompDirectiveNoFileComp), []string{"__complete", ""}, []string{}},
|
{"empty", "probe ", reply("", spfcobra.ShellCompDirectiveNoFileComp), []string{"__complete", ""}, []string{}},
|
||||||
{"no file fallback", `probe ./partial\ f`, reply("", spfcobra.ShellCompDirectiveNoFileComp), []string{"__complete", "./partial f"}, []string{"./partial f"}},
|
{"no file fallback", `probe ./partial\ f`, reply("", spfcobra.ShellCompDirectiveNoFileComp), []string{"__complete", "./partial f"}, []string{"./partial f"}},
|
||||||
{"error no fallback", `probe ./partial\ f`, reply("", spfcobra.ShellCompDirectiveError), []string{"__complete", "./partial f"}, []string{"./partial f"}},
|
{"error no fallback", `probe ./partial\ f`, reply("", spfcobra.ShellCompDirectiveError), []string{"__complete", "./partial f"}, []string{"./partial f"}},
|
||||||
{"cluster", "probe -ab", reply("-abc\tflag\n", spfcobra.ShellCompDirectiveNoFileComp), []string{"__complete", "-ab"}, []string{"-abc"}},
|
{"cluster", "probe -ab", reply("-abc\tflag\n", spfcobra.ShellCompDirectiveNoFileComp), []string{"__complete", "-ab"}, []string{"-abc"}},
|
||||||
{"quoted argument", "probe 'alpha beta' --config=./par", reply("./partial file\tdescription\n", spfcobra.ShellCompDirectiveNoFileComp), []string{"__complete", "alpha beta", "--config=./par"}, []string{"alpha beta", "--config=./partial file"}},
|
{"quoted argument", "probe 'alpha beta' --config=./par", choiceReply("--config=", "./partial file"), []string{"__complete", "alpha beta", "--config=./par"}, []string{"alpha beta", "--config=./partial file"}},
|
||||||
}
|
}
|
||||||
for _, shell := range []string{"bash", "zsh", "fish"} {
|
for _, shell := range []string{"bash", "zsh", "fish"} {
|
||||||
script := filepath.Join(dir, shell+".completion")
|
script := filepath.Join(dir, shell+".completion")
|
||||||
@@ -201,7 +203,7 @@ sys.stdout.write(os.environ['PROBE_REPLY'])
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
cmd := exec.Command("python3", "testdata/completion_terminal.py", shell, script, executable+" --config=./par")
|
cmd := exec.Command("python3", "testdata/completion_terminal.py", shell, script, executable+" --config=./par")
|
||||||
cmd.Env = append(os.Environ(), "PATH="+dir+string(os.PathListSeparator)+os.Getenv("PATH"), "HOME="+dir, "TERM=xterm", "PROBE_LOG="+log, "PROBE_REPLY="+reply("./partial file\tdescription\n", spfcobra.ShellCompDirectiveNoFileComp), "PROBE_DIR="+dir)
|
cmd.Env = append(os.Environ(), "PATH="+dir+string(os.PathListSeparator)+os.Getenv("PATH"), "HOME="+dir, "TERM=xterm", "PROBE_LOG="+log, "PROBE_REPLY="+choiceReply("--config=", "./partial file"), "PROBE_DIR="+dir)
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
t.Fatalf("shell completion: %v\n%s", err, out)
|
t.Fatalf("shell completion: %v\n%s", err, out)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user