Skip to content

Commit 0860129

Browse files
authored
feat(completion): implement dynamic shell completion with vp run task support (#1181)
## Summary - Switch to dynamic shell completion system - Add task name completion for `vp run` command, supporting workspace packages and tasks ## Details ### Dynamic Completion System Replaced the static completion files with a dynamic completion system that can provide more flexible suggestions based on the current workspace state. ### Task Name Completion for `vp run` Implemented dynamic task completion for the `vp run` command, supporting task format like `task` or `package#task` ## Known Limitations - Only Category A command arguments are supported in completions. Category B and C subcommand parameters are pending implementation. - `vp run` only completes tasks; parameters like `-r`, `-t` are not yet supported Awaiting future work to sync local CLI parameters to global CLI close #1131
1 parent 7dfc509 commit 0860129

8 files changed

Lines changed: 297 additions & 139 deletions

File tree

Cargo.lock

Lines changed: 87 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vite_global_cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ path = "src/main.rs"
1515
base64-simd = { workspace = true }
1616
chrono = { workspace = true }
1717
clap = { workspace = true, features = ["derive"] }
18-
clap_complete = { workspace = true }
18+
clap_complete = { workspace = true, features = ["unstable-dynamic"] }
1919
directories = { workspace = true }
2020
flate2 = { workspace = true }
2121
serde = { workspace = true }
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
_clap_reassemble_words() {
2+
if [[ "$COMP_WORDBREAKS" != *:* ]]; then
3+
return
4+
fi
5+
local i j=0 line=$COMP_LINE
6+
words=()
7+
_CLAP_COMPLETE_INDEX=0
8+
for ((i = 0; i < ${#COMP_WORDS[@]}; i++)); do
9+
if ((i > 0 && j > 0)) && [[ "${COMP_WORDS[i]}" == :* || "${words[j-1]}" == *: ]] && [[ "$line" != [[:blank:]]* ]]; then
10+
words[j-1]="${words[j-1]}${COMP_WORDS[i]}"
11+
else
12+
words[j]="${COMP_WORDS[i]}"
13+
((j++))
14+
fi
15+
if ((i == COMP_CWORD)); then
16+
_CLAP_COMPLETE_INDEX=$((j - 1))
17+
fi
18+
line=${line#*"${COMP_WORDS[i]}"}
19+
done
20+
}
21+
22+
_clap_trim_completions() {
23+
local cur="${words[_CLAP_COMPLETE_INDEX]}"
24+
if [[ "$cur" != *:* || "$COMP_WORDBREAKS" != *:* ]]; then
25+
return
26+
fi
27+
local colon_word=${cur%"${cur##*:}"}
28+
local i=${#COMPREPLY[*]}
29+
while [[ $((--i)) -ge 0 ]]; do
30+
COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"}
31+
done
32+
}
33+
34+
_clap_complete_vp() {
35+
local IFS=$'\013'
36+
local _CLAP_COMPLETE_INDEX=${COMP_CWORD}
37+
local _CLAP_COMPLETE_COMP_TYPE=${COMP_TYPE}
38+
if compopt +o nospace 2> /dev/null; then
39+
local _CLAP_COMPLETE_SPACE=false
40+
else
41+
local _CLAP_COMPLETE_SPACE=true
42+
fi
43+
local words=("${COMP_WORDS[@]}")
44+
_clap_reassemble_words
45+
COMPREPLY=( $( \
46+
_CLAP_IFS="$IFS" \
47+
_CLAP_COMPLETE_INDEX="$_CLAP_COMPLETE_INDEX" \
48+
_CLAP_COMPLETE_COMP_TYPE="$_CLAP_COMPLETE_COMP_TYPE" \
49+
_CLAP_COMPLETE_SPACE="$_CLAP_COMPLETE_SPACE" \
50+
VP_COMPLETE="bash" \
51+
"vp" -- "${words[@]}" \
52+
) )
53+
if [[ $? != 0 ]]; then
54+
unset COMPREPLY
55+
elif [[ $_CLAP_COMPLETE_SPACE == false ]] && [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then
56+
compopt -o nospace
57+
fi
58+
_clap_trim_completions
59+
}
60+
if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
61+
complete -o nospace -o bashdefault -o nosort -F _clap_complete_vp vp
62+
else
63+
complete -o nospace -o bashdefault -F _clap_complete_vp vp
64+
fi

crates/vite_global_cli/src/cli.rs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
//! This module defines the CLI structure using clap and routes commands
44
//! to their appropriate handlers.
55
6-
use std::process::ExitStatus;
6+
use std::{ffi::OsStr, process::ExitStatus};
77

88
use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
9+
use clap_complete::ArgValueCompleter;
10+
use tokio::runtime::Runtime;
911
use vite_install::commands::{
1012
add::SaveDependencyType, install::InstallCommandOptions, outdated::Format,
1113
};
@@ -615,7 +617,7 @@ pub enum Commands {
615617
#[command(disable_help_flag = true)]
616618
Run {
617619
/// Additional arguments
618-
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
620+
#[arg(trailing_var_arg = true, allow_hyphen_values = true, add = ArgValueCompleter::new(run_tasks_completions))]
619621
args: Vec<String>,
620622
},
621623

@@ -1480,6 +1482,46 @@ fn should_force_global_delegate(command: &str, args: &[String]) -> bool {
14801482
}
14811483
}
14821484

1485+
/// Get available tasks for shell completion.
1486+
///
1487+
/// Delegates to the local vite-plus CLI to run `vp run` without arguments,
1488+
/// which returns a list of available tasks in the format "task_name: description".
1489+
fn run_tasks_completions(current: &OsStr) -> Vec<clap_complete::CompletionCandidate> {
1490+
let Some(cwd) = std::env::current_dir()
1491+
.ok()
1492+
.and_then(AbsolutePathBuf::new)
1493+
.filter(|p| commands::has_vite_plus_dependency(p))
1494+
else {
1495+
return vec![];
1496+
};
1497+
1498+
// Unescape hashtag and trim quotes for better matching
1499+
let current = current
1500+
.to_string_lossy()
1501+
.replace("\\#", "#")
1502+
.trim_matches(|c| c == '"' || c == '\'')
1503+
.to_string();
1504+
1505+
let output = tokio::task::block_in_place(|| {
1506+
Runtime::new().ok().and_then(|rt| {
1507+
rt.block_on(async { commands::delegate::execute_output(cwd, "run", &[]).await.ok() })
1508+
})
1509+
});
1510+
1511+
output
1512+
.filter(|o| o.status.success())
1513+
.map(|output| {
1514+
String::from_utf8_lossy(&output.stdout)
1515+
.lines()
1516+
.filter_map(|line| line.split_once(": ").map(|(name, _)| name.trim()))
1517+
.filter(|name| !name.is_empty())
1518+
.filter(|name| name.starts_with(&current) || current.is_empty())
1519+
.map(|name| clap_complete::CompletionCandidate::new(name.to_string()))
1520+
.collect()
1521+
})
1522+
.unwrap_or_default()
1523+
}
1524+
14831525
/// Run the CLI command.
14841526
pub async fn run_command(cwd: AbsolutePathBuf, args: Args) -> Result<ExitStatus, Error> {
14851527
run_command_with_options(cwd, args, RenderOptions::default()).await

crates/vite_global_cli/src/commands/delegate.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! JavaScript command delegation — resolves local vite-plus first, falls back to global.
22
3-
use std::process::ExitStatus;
3+
use std::process::{ExitStatus, Output};
44

55
use vite_path::AbsolutePathBuf;
66

@@ -18,6 +18,18 @@ pub async fn execute(
1818
executor.delegate_to_local_cli(&cwd, &full_args).await
1919
}
2020

21+
/// Execute a command by delegating to the local `vite-plus` CLI, capturing output.
22+
pub async fn execute_output(
23+
cwd: AbsolutePathBuf,
24+
command: &str,
25+
args: &[String],
26+
) -> Result<Output, Error> {
27+
let mut executor = JsExecutor::new(None);
28+
let mut full_args = vec![command.to_string()];
29+
full_args.extend(args.iter().cloned());
30+
executor.delegate_to_local_cli_output(&cwd, &full_args).await
31+
}
32+
2133
/// Execute a command by delegating to the global `vite-plus` CLI.
2234
pub async fn execute_global(
2335
cwd: AbsolutePathBuf,

0 commit comments

Comments
 (0)