Skip to content

Commit 4245aa3

Browse files
committed
feat(cli): add shell completion support
1 parent fb7cc34 commit 4245aa3

4 files changed

Lines changed: 107 additions & 12 deletions

File tree

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ brush-parser = "0.3.0"
7171
blake3 = "1.8.2"
7272
chrono = { version = "0.4", features = ["serde"] }
7373
clap = "4.5.40"
74+
clap_complete = "4.6.0"
7475
commondir = "1.0.0"
7576
cow-utils = "0.1.3"
7677
criterion = { version = "0.7", features = ["html_reports"] }

crates/vite_global_cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +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 }
1819
directories = { workspace = true }
1920
flate2 = { workspace = true }
2021
serde = { workspace = true }

crates/vite_global_cli/src/commands/env/setup.rs

Lines changed: 95 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
1818
use std::process::ExitStatus;
1919

20+
use clap::CommandFactory;
2021
use owo_colors::OwoColorize;
2122

2223
use super::config::{get_bin_dir, get_vite_plus_home};
23-
use crate::{error::Error, help};
24+
use crate::{cli::Args, error::Error, help};
2425

2526
/// Tools to create shims for (node, npm, npx, vpx)
2627
const SHIM_TOOLS: &[&str] = &["node", "npm", "npx", "vpx"];
@@ -40,6 +41,9 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result<ExitStatus, Error>
4041
// Ensure home directory exists (env files are written here)
4142
tokio::fs::create_dir_all(&vite_plus_home).await?;
4243

44+
// Generate completion scripts
45+
generate_completion_scripts(&vite_plus_home).await?;
46+
4347
// Create env files with PATH guard (prevents duplicate PATH entries)
4448
create_env_files(&vite_plus_home).await?;
4549

@@ -270,6 +274,45 @@ async fn create_windows_shim(
270274
Ok(())
271275
}
272276

277+
/// Creates completion scripts in `~/.vite-plus/completion/`:
278+
/// - `vp.bash` (bash)
279+
/// - `_vp` (zsh, following zsh convention)
280+
/// - `vp.fish` (fish shell)
281+
/// - `vp.ps1` (PowerShell)
282+
async fn generate_completion_scripts(
283+
vite_plus_home: &vite_path::AbsolutePath,
284+
) -> Result<(), Error> {
285+
let mut cmd = Args::command();
286+
287+
// Create completion directory
288+
let completion_dir = vite_plus_home.join("completion");
289+
tokio::fs::create_dir_all(&completion_dir).await?;
290+
291+
// Bash completion
292+
let bash_completion = completion_dir.join("vp.bash");
293+
let mut bash_file = std::fs::File::create(&bash_completion)?;
294+
clap_complete::generate(clap_complete::Shell::Bash, &mut cmd, "vp", &mut bash_file);
295+
296+
// Zsh completion (following zsh convention: _vp)
297+
let zsh_completion = completion_dir.join("_vp");
298+
let mut zsh_file = std::fs::File::create(&zsh_completion)?;
299+
clap_complete::generate(clap_complete::Shell::Zsh, &mut cmd, "vp", &mut zsh_file);
300+
301+
// Fish completion
302+
let fish_completion = completion_dir.join("vp.fish");
303+
let mut fish_file = std::fs::File::create(&fish_completion)?;
304+
clap_complete::generate(clap_complete::Shell::Fish, &mut cmd, "vp", &mut fish_file);
305+
306+
// PowerShell completion
307+
let ps1_completion = completion_dir.join("vp.ps1");
308+
let mut ps1_file = std::fs::File::create(&ps1_completion)?;
309+
clap_complete::generate(clap_complete::Shell::PowerShell, &mut cmd, "vp", &mut ps1_file);
310+
311+
tracing::debug!("Generated completion scripts in {:?}", completion_dir);
312+
313+
Ok(())
314+
}
315+
273316
/// Get the path to the trampoline template binary (vp-shim.exe).
274317
///
275318
/// The trampoline binary is distributed alongside vp.exe in the same directory.
@@ -378,23 +421,26 @@ pub(crate) async fn cleanup_legacy_windows_shim(bin_dir: &vite_path::AbsolutePat
378421
/// - `~/.vite-plus/bin/vp-use.cmd` (cmd.exe wrapper for `vp env use`)
379422
async fn create_env_files(vite_plus_home: &vite_path::AbsolutePath) -> Result<(), Error> {
380423
let bin_path = vite_plus_home.join("bin");
424+
let completion_path = vite_plus_home.join("completion");
381425

382426
// Use $HOME-relative path if install dir is under HOME (like rustup's ~/.cargo/env)
383427
// This makes the env file portable across sessions where HOME may differ
384-
let bin_path_ref = if let Some(home_dir) = vite_shared::EnvConfig::get().user_home {
385-
if let Ok(suffix) = bin_path.as_path().strip_prefix(&home_dir) {
386-
format!("$HOME/{}", suffix.display())
387-
} else {
388-
bin_path.as_path().display().to_string()
389-
}
390-
} else {
391-
bin_path.as_path().display().to_string()
428+
let home_dir = vite_shared::EnvConfig::get().user_home;
429+
let to_ref = |path: &vite_path::AbsolutePath| -> String {
430+
home_dir
431+
.as_ref()
432+
.and_then(|h| path.as_path().strip_prefix(h).ok())
433+
.map(|s| format!("$HOME/{}", s.display()))
434+
.unwrap_or_else(|| path.as_path().display().to_string())
392435
};
436+
let bin_path_ref = to_ref(&bin_path);
437+
let completion_path_ref = to_ref(&completion_path);
393438

394439
// POSIX env file (bash/zsh)
395440
// When sourced multiple times, removes existing entry and re-prepends to front
396441
// Uses parameter expansion to split PATH around the bin entry in O(1) operations
397442
// Includes vp() shell function wrapper for `vp env use` (evals stdout)
443+
// Includes shell completion support
398444
let env_content = r#"#!/bin/sh
399445
# Vite+ environment setup (https://viteplus.dev)
400446
__vp_bin="__VP_BIN__"
@@ -425,8 +471,29 @@ vp() {
425471
command vp "$@"
426472
fi
427473
}
474+
475+
# Shell completion for bash/zsh
476+
# Source appropriate completion script based on current shell
477+
# Only load completion in interactive shells with required builtins
478+
if [ -n "$BASH_VERSION" ] && type complete >/dev/null 2>&1; then
479+
# Bash shell with completion support
480+
__vp_completion="__VP_COMPLETION_BASH__"
481+
if [ -f "$__vp_completion" ]; then
482+
. "$__vp_completion"
483+
fi
484+
unset __vp_completion
485+
elif [ -n "$ZSH_VERSION" ] && type compdef >/dev/null 2>&1; then
486+
# Zsh shell with completion support
487+
__vp_completion="__VP_COMPLETION_ZSH__"
488+
if [ -f "$__vp_completion" ]; then
489+
. "$__vp_completion"
490+
fi
491+
unset __vp_completion
492+
fi
428493
"#
429-
.replace("__VP_BIN__", &bin_path_ref);
494+
.replace("__VP_BIN__", &bin_path_ref)
495+
.replace("__VP_COMPLETION_BASH__", &format!("{}/vp.bash", completion_path_ref))
496+
.replace("__VP_COMPLETION_ZSH__", &format!("{}/_vp", completion_path_ref));
430497
let env_file = vite_plus_home.join("env");
431498
tokio::fs::write(&env_file, env_content).await?;
432499

@@ -450,8 +517,15 @@ function vp
450517
command vp $argv
451518
end
452519
end
520+
521+
# Shell completion for fish
522+
set -l __vp_completion "__VP_COMPLETION_FISH__"
523+
if test -f "$__vp_completion"
524+
source "$__vp_completion"
525+
end
453526
"#
454-
.replace("__VP_BIN__", &bin_path_ref);
527+
.replace("__VP_BIN__", &bin_path_ref)
528+
.replace("__VP_COMPLETION_FISH__", &format!("{}/vp.fish", completion_path_ref));
455529
let env_fish_file = vite_plus_home.join("env.fish");
456530
tokio::fs::write(&env_fish_file, env_fish_content).await?;
457531

@@ -485,11 +559,20 @@ function vp {
485559
& (Join-Path $__vp_bin "vp.exe") @args
486560
}
487561
}
562+
563+
# Shell completion for PowerShell
564+
$__vp_completion = "__VP_COMPLETION_PS1__"
565+
if (Test-Path $__vp_completion) {
566+
. $__vp_completion
567+
}
488568
"#;
489569

490570
// For PowerShell, use the actual absolute path (not $HOME-relative)
491571
let bin_path_win = bin_path.as_path().display().to_string();
492-
let env_ps1_content = env_ps1_content.replace("__VP_BIN_WIN__", &bin_path_win);
572+
let completion_path_win = completion_path.as_path().display().to_string();
573+
let env_ps1_content = env_ps1_content
574+
.replace("__VP_BIN_WIN__", &bin_path_win)
575+
.replace("__VP_COMPLETION_PS1__", &format!("{}/vp.ps1", completion_path_win));
493576
let env_ps1_file = vite_plus_home.join("env.ps1");
494577
tokio::fs::write(&env_ps1_file, env_ps1_content).await?;
495578

0 commit comments

Comments
 (0)