1717
1818use std:: process:: ExitStatus ;
1919
20+ use clap:: CommandFactory ;
2021use owo_colors:: OwoColorize ;
2122
2223use 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)
2627const 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`)
379422async 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
452519end
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