From 4664a71411968696fa8e63d0b5337ba56b3521ab Mon Sep 17 00:00:00 2001 From: nekomoyi Date: Thu, 5 Nov 2026 23:34:39 +0800 Subject: [PATCH 1/2] fix(completion): handle -C chdir and vpr -C translation in shell completion --- Cargo.lock | 1 + .../completion_chdir/assert_completion.bash | 44 +++++++++++++++++++ .../fixtures/completion_chdir/snapshots.toml | 6 +++ .../snapshots/completion_chdir.md | 7 +++ crates/vp_global_cli/Cargo.toml | 1 + crates/vp_global_cli/completion-register.bash | 32 ++++++++------ crates/vp_global_cli/src/cli.rs | 2 +- crates/vp_global_cli/src/main.rs | 15 ++++++- 8 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/completion_chdir/assert_completion.bash create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/completion_chdir/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/completion_chdir/snapshots/completion_chdir.md diff --git a/Cargo.lock b/Cargo.lock index 276cb653f8..8bcf509f59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8487,6 +8487,7 @@ dependencies = [ "same-file", "serde", "serde_json", + "shlex", "serial_test", "tar", "temp-env", diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/completion_chdir/assert_completion.bash b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/completion_chdir/assert_completion.bash new file mode 100644 index 0000000000..08a09c2b35 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/completion_chdir/assert_completion.bash @@ -0,0 +1,44 @@ +set -eu +vp_bin=$(command -v vp) +VP_COMPLETE=bash "$vp_bin" > completion.bash +source completion.bash +COMP_WORDBREAKS=' :=' +COMP_TYPE=9 +vp() { + local IFS=" " + [[ $_CLAP_COMPLETE_INDEX == "$expected_index" && "$*" == "$expected_args" ]] || { + printf 'unexpected completion argv: index=%s %s\n' "$_CLAP_COMPLETE_INDEX" "$*" >&2 + return 1 + } + printf 'build:prod' +} +for invocation in vp vpr /path/vpr; do + for style in separate attached equals; do + case $style in + separate) COMP_WORDS=("$invocation" -C 'apps\ web'); expected_index=4 ;; + attached) COMP_WORDS=("$invocation" '-Capps\ web'); expected_index=3 ;; + equals) COMP_WORDS=("$invocation" -C = 'apps\ web'); expected_index=3 ;; + esac + expected_args="-- $invocation" + case $style in + separate) expected_args+=' -C apps\ web' ;; + attached) expected_args+=' -Capps\ web' ;; + equals) expected_args+=' -C=apps\ web' ;; + esac + [[ $invocation != vp ]] || COMP_WORDS+=(run) + expected_args+=' run build:pr' + COMP_WORDS+=(build : pr) + COMP_CWORD=$((${#COMP_WORDS[@]} - 1)) + COMP_LINE="${COMP_WORDS[*]}" + COMP_LINE=${COMP_LINE//build : pr/build:pr} + [[ $style != equals ]] || COMP_LINE=${COMP_LINE//-C = /-C=} + _clap_complete_vp || exit 1 + [[ ${COMPREPLY[*]} == prod ]] + done +done +mkdir -p 'apps web/unique-child' +for dir in 'apps\ web' "'apps web'" '"apps web"'; do + result=$(VP_COMPLETE=bash _CLAP_COMPLETE_INDEX=4 "$vp_bin" -- vp -C "$dir" -C unique) + [[ $result == unique-child* ]] +done +printf 'completion argv, colon trimming, and escaped cwd: ok\n' diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/completion_chdir/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/completion_chdir/snapshots.toml new file mode 100644 index 0000000000..bd8d9d7102 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/completion_chdir/snapshots.toml @@ -0,0 +1,6 @@ +[[case]] +name = "completion_chdir" +vp = "global" +skip-platforms = ["windows"] +requires = ["bash"] +steps = [["bash", "--noprofile", "--norc", "assert_completion.bash"]] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/completion_chdir/snapshots/completion_chdir.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/completion_chdir/snapshots/completion_chdir.md new file mode 100644 index 0000000000..96d507b4e0 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/completion_chdir/snapshots/completion_chdir.md @@ -0,0 +1,7 @@ +# completion_chdir + +## `bash --noprofile --norc assert_completion.bash` + +``` +completion argv, colon trimming, and escaped cwd: ok +``` diff --git a/crates/vp_global_cli/Cargo.toml b/crates/vp_global_cli/Cargo.toml index e62a984395..6003c74283 100644 --- a/crates/vp_global_cli/Cargo.toml +++ b/crates/vp_global_cli/Cargo.toml @@ -12,6 +12,7 @@ name = "vp" path = "src/main.rs" [dependencies] +shlex = "2.0.1" chrono = { workspace = true } clap = { workspace = true, features = ["derive", "string"] } clap_complete = { workspace = true, features = ["unstable-dynamic"] } diff --git a/crates/vp_global_cli/completion-register.bash b/crates/vp_global_cli/completion-register.bash index 6812418591..a3f810a221 100644 --- a/crates/vp_global_cli/completion-register.bash +++ b/crates/vp_global_cli/completion-register.bash @@ -1,12 +1,12 @@ _clap_reassemble_words() { - if [[ "$COMP_WORDBREAKS" != *:* ]]; then + if [[ "$COMP_WORDBREAKS" != *:* && "$COMP_WORDBREAKS" != *=* ]]; then return fi local i j=0 line=$COMP_LINE words=() _CLAP_COMPLETE_INDEX=0 for ((i = 0; i < ${#COMP_WORDS[@]}; i++)); do - if ((i > 0 && j > 0)) && [[ "${COMP_WORDS[i]}" == :* || "${words[j-1]}" == *: ]] && [[ "$line" != [[:blank:]]* ]]; then + if ((i > 0 && j > 0)) && [[ "${COMP_WORDS[i]}" == [:=]* || "${words[j-1]}" == *[:=] ]] && [[ "$line" != [[:blank:]]* ]]; then words[j-1]="${words[j-1]}${COMP_WORDS[i]}" else words[j]="${COMP_WORDS[i]}" @@ -21,10 +21,11 @@ _clap_reassemble_words() { _clap_trim_completions() { local cur="${words[_CLAP_COMPLETE_INDEX]}" - if [[ "$cur" != *:* || "$COMP_WORDBREAKS" != *:* ]]; then + local breaks=${COMP_WORDBREAKS//[^:=]} + if [[ ! $breaks || "$cur" != *["$breaks"]* ]]; then return fi - local colon_word=${cur%"${cur##*:}"} + local colon_word=${cur%"${cur##*["$breaks"]}"} local i=${#COMPREPLY[*]} while [[ $((--i)) -ge 0 ]]; do COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"} @@ -32,7 +33,6 @@ _clap_trim_completions() { } _clap_complete_vp() { - local IFS=$'\013' local _CLAP_COMPLETE_INDEX=${COMP_CWORD} local _CLAP_COMPLETE_COMP_TYPE=${COMP_TYPE} if compopt +o nospace 2> /dev/null; then @@ -42,6 +42,17 @@ _clap_complete_vp() { fi local words=("${COMP_WORDS[@]}") _clap_reassemble_words + local insert=1 + if [[ ${words[0]##*/} == vpr ]]; then + if [[ ${words[1]-} == -C ]]; then insert=3 + elif [[ ${words[1]-} == -C?* ]]; then insert=2 + fi + if (( _CLAP_COMPLETE_INDEX >= insert )); then + words=("${words[@]:0:insert}" run "${words[@]:insert}") + ((_CLAP_COMPLETE_INDEX++)) + fi + fi + local IFS=$'\013' COMPREPLY=( $( \ _CLAP_IFS="$IFS" \ _CLAP_COMPLETE_INDEX="$_CLAP_COMPLETE_INDEX" \ @@ -58,17 +69,10 @@ _clap_complete_vp() { _clap_trim_completions } -_clap_complete_vpr() { - local COMP_WORDS=("vp" "run" "${COMP_WORDS[@]:1}") - local COMP_CWORD=$((COMP_CWORD + 1)) - local COMP_LINE="vp run ${COMP_LINE#vpr}" - _clap_complete_vp -} - if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then complete -o nospace -o bashdefault -o nosort -F _clap_complete_vp vp - complete -o nospace -o bashdefault -o nosort -F _clap_complete_vpr vpr + complete -o nospace -o bashdefault -o nosort -F _clap_complete_vp vpr else complete -o nospace -o bashdefault -F _clap_complete_vp vp - complete -o nospace -o bashdefault -F _clap_complete_vpr vpr + complete -o nospace -o bashdefault -F _clap_complete_vp vpr fi diff --git a/crates/vp_global_cli/src/cli.rs b/crates/vp_global_cli/src/cli.rs index d2bed7a410..eb370d020d 100644 --- a/crates/vp_global_cli/src/cli.rs +++ b/crates/vp_global_cli/src/cli.rs @@ -54,7 +54,7 @@ pub struct Args { pub version: bool, /// Run as if vp was started in instead of the current working directory - #[arg(short = 'C', value_name = "DIR")] + #[arg(short = 'C', value_name = "DIR", value_hint = clap::ValueHint::DirPath)] pub chdir: Option, #[clap(subcommand)] diff --git a/crates/vp_global_cli/src/main.rs b/crates/vp_global_cli/src/main.rs index 3e29141c0b..91c2c92632 100644 --- a/crates/vp_global_cli/src/main.rs +++ b/crates/vp_global_cli/src/main.rs @@ -408,10 +408,21 @@ async fn main() -> ExitCode { // the target directory here too: cwd-sensitive completers (run tasks) // should suggest from , where the completed command will run. if env::var_os("VP_COMPLETE").is_some() - && let Some((dir, _)) = parse_leading_chdir(&args[1..]) + && let Some(sep) = args.iter().position(|a| a == "--") + && let Some(user_args) = args.get(sep + 2..) + && let Some((mut dir, consumed)) = parse_leading_chdir(user_args) + && env::var("_CLAP_COMPLETE_INDEX") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(user_args.len()) + > consumed && let Ok(current) = vt_path::current_dir() { - // Best-effort and silent: mid-typing values are often not (yet) dirs. + if env::var_os("VP_COMPLETE").is_some_and(|s| s == "bash") { + let Some(parts) = shlex::split(&dir) else { return ExitCode::SUCCESS }; + let [value] = parts.as_slice() else { return ExitCode::SUCCESS }; + dir = value.clone(); + } let _ = apply_chdir(¤t, &dir); } CompleteEnv::with_factory(command_for_completion).var("VP_COMPLETE").complete(); From d48f83dc15642037d321774ab65c6a5ed8c67b6f Mon Sep 17 00:00:00 2001 From: nekomoyi Date: Tue, 10 Nov 2026 13:12:49 +0800 Subject: [PATCH 2/2] chore(deps): sort shlex in workspace dependencies --- Cargo.lock | 2 +- Cargo.toml | 1 + crates/vp_global_cli/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c38e6a9a6c..03fc3d0bb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8486,8 +8486,8 @@ dependencies = [ "same-file", "serde", "serde_json", - "shlex", "serial_test", + "shlex", "tar", "temp-env", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 82c8c592fc..007264f5ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -273,6 +273,7 @@ serial_test = "4.0.0" sha1 = "0.11.0" sha2 = "0.11.0" shell-escape = "0.1.5" +shlex = "2.0.1" simd-utf16-len = "0.3.0" simdutf8 = "0.1.5" smallvec = { version = "1.16.0", features = ["union"] } diff --git a/crates/vp_global_cli/Cargo.toml b/crates/vp_global_cli/Cargo.toml index 54756c09c7..dac7eebad7 100644 --- a/crates/vp_global_cli/Cargo.toml +++ b/crates/vp_global_cli/Cargo.toml @@ -12,7 +12,6 @@ name = "vp" path = "src/main.rs" [dependencies] -shlex = "2.0.1" chrono = { workspace = true } clap = { workspace = true, features = ["derive", "string"] } clap_complete = { workspace = true, features = ["unstable-dynamic"] } @@ -30,6 +29,7 @@ tokio = { workspace = true, features = ["full"] } tracing = { workspace = true } console = { workspace = true } same-file = { workspace = true } +shlex = { workspace = true } oxc_resolver = { workspace = true } pathdiff = { workspace = true } vt_pty_terminal_test_client = { workspace = true, features = ["testing"] }