Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
474634a
feat(completion): switch to dynamic shell completion
nekomoyi Mar 27, 2026
607d912
feat(delegate): add output handling for local cli execution
nekomoyi Mar 28, 2026
80a707e
feat(completion): implement dynamic task completion for vp run
nekomoyi Mar 28, 2026
b64280f
chore(completion): update shell completion variable to VP_COMPLETE
nekomoyi Mar 28, 2026
e00859b
feat(completion): support hashtags and quotes in task completion
nekomoyi Mar 28, 2026
96e9491
lint
nekomoyi Mar 28, 2026
b8a3d62
fix(completion): update PowerShell completion variable to VP_COMPLETE
nekomoyi Mar 28, 2026
f45c026
feat(cli): validate vite-plus installation before task completions
nekomoyi Mar 28, 2026
3ec4723
fix(completion): replace bash completion script to handle colons in t…
nekomoyi Mar 28, 2026
aa10bc5
chore(js_executor): restore code structure and comments
nekomoyi Mar 28, 2026
ac7a667
Merge branch 'main' into feat/dynamic-completion
nekomoyi Mar 28, 2026
f203c62
fix(test): update PowerShell completion assertion to check for VP_COM…
nekomoyi Mar 29, 2026
6109711
Merge branch 'main' into feat/dynamic-completion
nekomoyi Mar 29, 2026
f78eaab
Merge branch 'main' into feat/dynamic-completion
nekomoyi Mar 29, 2026
01bcb18
feat(env): add cleanup for legacy completion directory
nekomoyi Mar 29, 2026
62b6e8d
fix(cli): use has_vite_plus_dependency for dependency check
nekomoyi Mar 29, 2026
9ae77d1
Merge branch 'main' into feat/dynamic-completion
fengmk2 Mar 30, 2026
ef5a63b
Merge branch 'main' into feat/dynamic-completion
fengmk2 Mar 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 87 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/vite_global_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ path = "src/main.rs"
base64-simd = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true, features = ["derive"] }
clap_complete = { workspace = true }
clap_complete = { workspace = true, features = ["unstable-dynamic"] }
directories = { workspace = true }
flate2 = { workspace = true }
serde = { workspace = true }
Expand Down
64 changes: 64 additions & 0 deletions crates/vite_global_cli/completion-register.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
_clap_reassemble_words() {
if [[ "$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
words[j-1]="${words[j-1]}${COMP_WORDS[i]}"
else
words[j]="${COMP_WORDS[i]}"
((j++))
fi
if ((i == COMP_CWORD)); then
_CLAP_COMPLETE_INDEX=$((j - 1))
fi
line=${line#*"${COMP_WORDS[i]}"}
done
}

_clap_trim_completions() {
local cur="${words[_CLAP_COMPLETE_INDEX]}"
if [[ "$cur" != *:* || "$COMP_WORDBREAKS" != *:* ]]; then
return
fi
local colon_word=${cur%"${cur##*:}"}
local i=${#COMPREPLY[*]}
while [[ $((--i)) -ge 0 ]]; do
COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"}
done
}

_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
local _CLAP_COMPLETE_SPACE=false
else
local _CLAP_COMPLETE_SPACE=true
fi
local words=("${COMP_WORDS[@]}")
_clap_reassemble_words
COMPREPLY=( $( \
_CLAP_IFS="$IFS" \
_CLAP_COMPLETE_INDEX="$_CLAP_COMPLETE_INDEX" \
_CLAP_COMPLETE_COMP_TYPE="$_CLAP_COMPLETE_COMP_TYPE" \
_CLAP_COMPLETE_SPACE="$_CLAP_COMPLETE_SPACE" \
VP_COMPLETE="bash" \
"vp" -- "${words[@]}" \
) )
if [[ $? != 0 ]]; then
unset COMPREPLY
elif [[ $_CLAP_COMPLETE_SPACE == false ]] && [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then
compopt -o nospace
fi
_clap_trim_completions
}
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
else
complete -o nospace -o bashdefault -F _clap_complete_vp vp
fi
46 changes: 44 additions & 2 deletions crates/vite_global_cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
//! This module defines the CLI structure using clap and routes commands
//! to their appropriate handlers.

use std::process::ExitStatus;
use std::{ffi::OsStr, process::ExitStatus};

use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
use clap_complete::ArgValueCompleter;
use tokio::runtime::Runtime;
use vite_install::commands::{
add::SaveDependencyType, install::InstallCommandOptions, outdated::Format,
};
Expand Down Expand Up @@ -615,7 +617,7 @@ pub enum Commands {
#[command(disable_help_flag = true)]
Run {
/// Additional arguments
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
#[arg(trailing_var_arg = true, allow_hyphen_values = true, add = ArgValueCompleter::new(run_tasks_completions))]
args: Vec<String>,
},

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

/// Get available tasks for shell completion.
///
/// Delegates to the local vite-plus CLI to run `vp run` without arguments,
/// which returns a list of available tasks in the format "task_name: description".
fn run_tasks_completions(current: &OsStr) -> Vec<clap_complete::CompletionCandidate> {
let Some(cwd) = std::env::current_dir()
.ok()
.and_then(AbsolutePathBuf::new)
.filter(|p| commands::has_vite_plus_dependency(p))
else {
return vec![];
};

// Unescape hashtag and trim quotes for better matching
let current = current
.to_string_lossy()
.replace("\\#", "#")
.trim_matches(|c| c == '"' || c == '\'')
.to_string();

let output = tokio::task::block_in_place(|| {
Runtime::new().ok().and_then(|rt| {
rt.block_on(async { commands::delegate::execute_output(cwd, "run", &[]).await.ok() })
})
});

output
.filter(|o| o.status.success())
.map(|output| {
String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|line| line.split_once(": ").map(|(name, _)| name.trim()))
.filter(|name| !name.is_empty())
.filter(|name| name.starts_with(&current) || current.is_empty())
.map(|name| clap_complete::CompletionCandidate::new(name.to_string()))
.collect()
})
.unwrap_or_default()
}

/// Run the CLI command.
pub async fn run_command(cwd: AbsolutePathBuf, args: Args) -> Result<ExitStatus, Error> {
run_command_with_options(cwd, args, RenderOptions::default()).await
Expand Down
14 changes: 13 additions & 1 deletion crates/vite_global_cli/src/commands/delegate.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! JavaScript command delegation — resolves local vite-plus first, falls back to global.

use std::process::ExitStatus;
use std::process::{ExitStatus, Output};

use vite_path::AbsolutePathBuf;

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

/// Execute a command by delegating to the local `vite-plus` CLI, capturing output.
pub async fn execute_output(
cwd: AbsolutePathBuf,
command: &str,
args: &[String],
) -> Result<Output, Error> {
let mut executor = JsExecutor::new(None);
let mut full_args = vec![command.to_string()];
full_args.extend(args.iter().cloned());
executor.delegate_to_local_cli_output(&cwd, &full_args).await
}

/// Execute a command by delegating to the global `vite-plus` CLI.
pub async fn execute_global(
cwd: AbsolutePathBuf,
Expand Down
Loading
Loading