-
Notifications
You must be signed in to change notification settings - Fork 101
feat(cli): add shell completion support #974
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4245aa3
2b7f46f
5f6a9f8
9fb37ec
84ad6ae
444eabb
641a3a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,10 +17,11 @@ | |
|
|
||
| use std::process::ExitStatus; | ||
|
|
||
| use clap::CommandFactory; | ||
| use owo_colors::OwoColorize; | ||
|
|
||
| use super::config::{get_bin_dir, get_vite_plus_home}; | ||
| use crate::{error::Error, help}; | ||
| use crate::{cli::Args, error::Error, help}; | ||
|
|
||
| /// Tools to create shims for (node, npm, npx, vpx) | ||
| const SHIM_TOOLS: &[&str] = &["node", "npm", "npx", "vpx"]; | ||
|
|
@@ -40,6 +41,9 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result<ExitStatus, Error> | |
| // Ensure home directory exists (env files are written here) | ||
| tokio::fs::create_dir_all(&vite_plus_home).await?; | ||
|
|
||
| // Generate completion scripts | ||
| generate_completion_scripts(&vite_plus_home).await?; | ||
|
|
||
| // Create env files with PATH guard (prevents duplicate PATH entries) | ||
| create_env_files(&vite_plus_home).await?; | ||
|
|
||
|
|
@@ -270,6 +274,39 @@ async fn create_windows_shim( | |
| Ok(()) | ||
| } | ||
|
|
||
| /// Creates completion scripts in `~/.vite-plus/completion/`: | ||
| /// - `vp.bash` (bash) | ||
| /// - `_vp` (zsh, following zsh convention) | ||
| /// - `vp.fish` (fish shell) | ||
| /// - `vp.ps1` (PowerShell) | ||
| async fn generate_completion_scripts( | ||
| vite_plus_home: &vite_path::AbsolutePath, | ||
| ) -> Result<(), Error> { | ||
| let mut cmd = Args::command(); | ||
|
|
||
| // Create completion directory | ||
| let completion_dir = vite_plus_home.join("completion"); | ||
| tokio::fs::create_dir_all(&completion_dir).await?; | ||
|
|
||
| // Generate shell completion scripts | ||
| let completions = [ | ||
| (clap_complete::Shell::Bash, "vp.bash"), | ||
| (clap_complete::Shell::Zsh, "_vp"), | ||
| (clap_complete::Shell::Fish, "vp.fish"), | ||
| (clap_complete::Shell::PowerShell, "vp.ps1"), | ||
| ]; | ||
|
|
||
| for (shell, filename) in completions { | ||
| let path = completion_dir.join(filename); | ||
| let mut file = std::fs::File::create(&path)?; | ||
| clap_complete::generate(shell, &mut cmd, "vp", &mut file); | ||
| } | ||
|
|
||
| tracing::debug!("Generated completion scripts in {:?}", completion_dir); | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| /// Get the path to the trampoline template binary (vp-shim.exe). | ||
| /// | ||
| /// The trampoline binary is distributed alongside vp.exe in the same directory. | ||
|
|
@@ -378,23 +415,28 @@ pub(crate) async fn cleanup_legacy_windows_shim(bin_dir: &vite_path::AbsolutePat | |
| /// - `~/.vite-plus/bin/vp-use.cmd` (cmd.exe wrapper for `vp env use`) | ||
| async fn create_env_files(vite_plus_home: &vite_path::AbsolutePath) -> Result<(), Error> { | ||
| let bin_path = vite_plus_home.join("bin"); | ||
| let completion_path = vite_plus_home.join("completion"); | ||
|
|
||
| // Use $HOME-relative path if install dir is under HOME (like rustup's ~/.cargo/env) | ||
| // This makes the env file portable across sessions where HOME may differ | ||
| let bin_path_ref = if let Some(home_dir) = vite_shared::EnvConfig::get().user_home { | ||
| if let Ok(suffix) = bin_path.as_path().strip_prefix(&home_dir) { | ||
| format!("$HOME/{}", suffix.display()) | ||
| } else { | ||
| bin_path.as_path().display().to_string() | ||
| } | ||
| } else { | ||
| bin_path.as_path().display().to_string() | ||
| let home_dir = vite_shared::EnvConfig::get().user_home; | ||
| let to_ref = |path: &vite_path::AbsolutePath| -> String { | ||
| home_dir | ||
| .as_ref() | ||
| .and_then(|h| path.as_path().strip_prefix(h).ok()) | ||
| .map(|s| { | ||
| // Normalize to forward slashes for $HOME/... paths (POSIX-style) | ||
| format!("$HOME/{}", s.display().to_string().replace('\\', "/")) | ||
| }) | ||
| .unwrap_or_else(|| path.as_path().display().to_string()) | ||
| }; | ||
| let bin_path_ref = to_ref(&bin_path); | ||
|
|
||
| // POSIX env file (bash/zsh) | ||
| // When sourced multiple times, removes existing entry and re-prepends to front | ||
| // Uses parameter expansion to split PATH around the bin entry in O(1) operations | ||
| // Includes vp() shell function wrapper for `vp env use` (evals stdout) | ||
| // Includes shell completion support | ||
| let env_content = r#"#!/bin/sh | ||
| # Vite+ environment setup (https://viteplus.dev) | ||
| __vp_bin="__VP_BIN__" | ||
|
|
@@ -425,8 +467,29 @@ vp() { | |
| command vp "$@" | ||
| fi | ||
| } | ||
|
|
||
| # Shell completion for bash/zsh | ||
| # Source appropriate completion script based on current shell | ||
| # Only load completion in interactive shells with required builtins | ||
| if [ -n "$BASH_VERSION" ] && type complete >/dev/null 2>&1; then | ||
| # Bash shell with completion support | ||
| __vp_completion="__VP_COMPLETION_BASH__" | ||
| if [ -f "$__vp_completion" ]; then | ||
| . "$__vp_completion" | ||
| fi | ||
| unset __vp_completion | ||
| elif [ -n "$ZSH_VERSION" ] && type compdef >/dev/null 2>&1; then | ||
| # Zsh shell with completion support | ||
| __vp_completion="__VP_COMPLETION_ZSH__" | ||
| if [ -f "$__vp_completion" ]; then | ||
| . "$__vp_completion" | ||
|
Comment on lines
+481
to
+485
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This only sources Useful? React with 👍 / 👎.
Comment on lines
+481
to
+485
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This only sources Useful? React with 👍 / 👎. |
||
| fi | ||
| unset __vp_completion | ||
| fi | ||
| "# | ||
| .replace("__VP_BIN__", &bin_path_ref); | ||
| .replace("__VP_BIN__", &bin_path_ref) | ||
| .replace("__VP_COMPLETION_BASH__", &to_ref(&completion_path.join("vp.bash"))) | ||
| .replace("__VP_COMPLETION_ZSH__", &to_ref(&completion_path.join("_vp"))); | ||
| let env_file = vite_plus_home.join("env"); | ||
| tokio::fs::write(&env_file, env_content).await?; | ||
|
|
||
|
|
@@ -450,8 +513,15 @@ function vp | |
| command vp $argv | ||
| end | ||
| end | ||
|
|
||
| # Shell completion for fish | ||
| set -l __vp_completion "__VP_COMPLETION_FISH__" | ||
| if test -f "$__vp_completion" | ||
| source "$__vp_completion" | ||
|
Comment on lines
+517
to
+520
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎.
Comment on lines
+517
to
+520
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| end | ||
| "# | ||
| .replace("__VP_BIN__", &bin_path_ref); | ||
| .replace("__VP_BIN__", &bin_path_ref) | ||
| .replace("__VP_COMPLETION_FISH__", &to_ref(&completion_path.join("vp.fish"))); | ||
| let env_fish_file = vite_plus_home.join("env.fish"); | ||
| tokio::fs::write(&env_fish_file, env_fish_content).await?; | ||
|
|
||
|
|
@@ -485,11 +555,20 @@ function vp { | |
| & (Join-Path $__vp_bin "vp.exe") @args | ||
| } | ||
| } | ||
|
|
||
| # Shell completion for PowerShell | ||
| $__vp_completion = "__VP_COMPLETION_PS1__" | ||
| if (Test-Path $__vp_completion) { | ||
| . $__vp_completion | ||
| } | ||
| "#; | ||
|
|
||
| // For PowerShell, use the actual absolute path (not $HOME-relative) | ||
| let bin_path_win = bin_path.as_path().display().to_string(); | ||
| let env_ps1_content = env_ps1_content.replace("__VP_BIN_WIN__", &bin_path_win); | ||
| let completion_ps1_win = completion_path.join("vp.ps1").as_path().display().to_string(); | ||
| let env_ps1_content = env_ps1_content | ||
| .replace("__VP_BIN_WIN__", &bin_path_win) | ||
| .replace("__VP_COMPLETION_PS1__", &completion_ps1_win); | ||
| let env_ps1_file = vite_plus_home.join("env.ps1"); | ||
| tokio::fs::write(&env_ps1_file, env_ps1_content).await?; | ||
|
|
||
|
|
@@ -820,4 +899,76 @@ mod tests { | |
| assert!(fresh_home.join("env.fish").exists(), "env.fish file should be created"); | ||
| assert!(fresh_home.join("env.ps1").exists(), "env.ps1 file should be created"); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn test_generate_completion_scripts_creates_all_files() { | ||
| let temp_dir = TempDir::new().unwrap(); | ||
| let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); | ||
|
|
||
| generate_completion_scripts(&home).await.unwrap(); | ||
|
|
||
| let completion_dir = home.join("completion"); | ||
|
|
||
| // Verify all completion scripts are created | ||
| let bash_completion = completion_dir.join("vp.bash"); | ||
| let zsh_completion = completion_dir.join("_vp"); | ||
| let fish_completion = completion_dir.join("vp.fish"); | ||
| let ps1_completion = completion_dir.join("vp.ps1"); | ||
|
|
||
| assert!(bash_completion.as_path().exists(), "bash completion (vp.bash) should be created"); | ||
| assert!(zsh_completion.as_path().exists(), "zsh completion (_vp) should be created"); | ||
| assert!(fish_completion.as_path().exists(), "fish completion (vp.fish) should be created"); | ||
| assert!( | ||
| ps1_completion.as_path().exists(), | ||
| "PowerShell completion (vp.ps1) should be created" | ||
| ); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn test_create_env_files_contains_completion() { | ||
| let temp_dir = TempDir::new().unwrap(); | ||
| let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); | ||
| let _guard = home_guard(temp_dir.path()); | ||
|
|
||
| create_env_files(&home).await.unwrap(); | ||
|
|
||
| let env_content = tokio::fs::read_to_string(home.join("env")).await.unwrap(); | ||
| let fish_content = tokio::fs::read_to_string(home.join("env.fish")).await.unwrap(); | ||
| let ps1_content = tokio::fs::read_to_string(home.join("env.ps1")).await.unwrap(); | ||
|
|
||
| assert!( | ||
| env_content.contains("Shell completion") | ||
| && env_content.contains("/completion/vp.bash\""), | ||
| "env file should contain bash completion" | ||
| ); | ||
| assert!( | ||
| fish_content.contains("Shell completion") | ||
| && fish_content.contains("/completion/vp.fish\""), | ||
| "env.fish file should contain fish completion" | ||
| ); | ||
| assert!( | ||
| ps1_content.contains("Shell completion") | ||
| && ps1_content.contains(&format!( | ||
| "{}completion{}vp.ps1\"", | ||
| std::path::MAIN_SEPARATOR_STR, | ||
| std::path::MAIN_SEPARATOR_STR | ||
| )), | ||
| "env.ps1 file should contain PowerShell completion" | ||
| ); | ||
|
|
||
| // Verify placeholders are replaced | ||
| assert!( | ||
| !env_content.contains("__VP_COMPLETION_BASH__") | ||
| && !env_content.contains("__VP_COMPLETION_ZSH__"), | ||
| "env file should not contain __VP_COMPLETION_* placeholders" | ||
| ); | ||
| assert!( | ||
| !fish_content.contains("__VP_COMPLETION_FISH__"), | ||
| "env.fish file should not contain __VP_COMPLETION_FISH__ placeholder" | ||
| ); | ||
| assert!( | ||
| !ps1_content.contains("__VP_COMPLETION_PS1__"), | ||
| "env.ps1 file should not contain __VP_COMPLETION_PS1__ placeholder" | ||
| ); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.