-
Notifications
You must be signed in to change notification settings - Fork 156
feat(env): add Nushell support via env.nu wrapper #1312
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
18e5d59
fc9d457
e17d514
f339b45
babc76d
840bf1e
4e4d495
d55632e
c060095
d39efa0
aa170c4
5e0bf75
f39e459
2e8fc7b
08a8e9d
371d6ef
6fede95
5938405
3170641
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,6 +46,23 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result<ExitStatus, Error> | |
| // Create env files with PATH guard (prevents duplicate PATH entries) | ||
| create_env_files(&vite_plus_home).await?; | ||
|
|
||
| // Generate Fish completion file at ~/.vite-plus/vp.fish for use by the Nushell completer. | ||
| // Fish completions are generated statically so that Nushell can delegate to Fish at runtime. | ||
| // command_with_help() uses a deep call stack, so we spawn a thread with a larger stack. | ||
| let fish_completions_path = vite_plus_home.join("vp.fish").as_path().to_path_buf(); | ||
| std::thread::Builder::new() | ||
| .stack_size(8 * 1024 * 1024) | ||
| .spawn(move || -> std::io::Result<()> { | ||
| let mut file = std::fs::File::create(&fish_completions_path)?; | ||
| let mut cmd = crate::cli::command_with_help(); | ||
| clap_complete::generate(clap_complete::Shell::Fish, &mut cmd, "vp", &mut file); | ||
| Ok(()) | ||
| }) | ||
| .map_err(Error::CommandExecution)? | ||
| .join() | ||
| .map_err(|_| Error::Other("Fish completion generation failed".into()))? | ||
| .map_err(Error::CommandExecution)?; | ||
|
|
||
| if env_only { | ||
| println!("{}", help::render_heading("Setup")); | ||
| println!(" Updated shell environment files."); | ||
|
|
@@ -388,6 +405,7 @@ async fn cleanup_legacy_completion_dir(vite_plus_home: &vite_path::AbsolutePath) | |
| /// Creates: | ||
| /// - `~/.vite-plus/env` (POSIX shell — bash/zsh) with `vp()` wrapper function | ||
| /// - `~/.vite-plus/env.fish` (fish shell) with `vp` wrapper function | ||
| /// - `~/.vite-plus/env.nu` (Nushell) with `vp env use` wrapper function | ||
| /// - `~/.vite-plus/env.ps1` (PowerShell) with PATH setup + `vp` function | ||
| /// - `~/.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> { | ||
|
|
@@ -499,6 +517,61 @@ complete -c vpr --keep-order --exclusive --arguments "(__vpr_complete)" | |
| let env_fish_file = vite_plus_home.join("env.fish"); | ||
| tokio::fs::write(&env_fish_file, env_fish_content).await?; | ||
|
|
||
| // Nushell env file with vp wrapper function. | ||
| // Completions delegate to Fish via vp.fish because clap_complete_nushell generates | ||
| // multiple rest params (e.g. for `vp install`), which Nushell does not support. | ||
| let env_nu_content = r#"# Vite+ environment setup (https://viteplus.dev) | ||
| $env.PATH = ($env.PATH | where { $in != "__VP_BIN__" } | prepend "__VP_BIN__") | ||
|
|
||
| # Shell function wrapper: intercepts `vp env use` to parse its stdout, | ||
| # which sets/unsets VP_NODE_VERSION in the current shell session. | ||
| def --env --wrapped vp [...args: string@"nu-complete vp"] { | ||
| if ($args | length) >= 2 and $args.0 == "env" and $args.1 == "use" { | ||
| if ("-h" in $args) or ("--help" in $args) { | ||
| ^vp ...$args | ||
| return | ||
| } | ||
| let out = (with-env { VP_ENV_USE_EVAL_ENABLE: "1", VP_SHELL_NU: "1" } { | ||
| ^vp ...$args | ||
| }) | ||
| let lines = ($out | lines) | ||
| let exports = ($lines | where { $in =~ '^\$env\.' } | parse '$env.{key} = "{value}"') | ||
| let export_keys = ($exports | get key? | default []) | ||
| # Exclude keys that also appear in exports: when vp emits `hide-env X` then | ||
| # `$env.X = "v"` (e.g. `vp env use` with no args resolving from .node-version), | ||
| # the set should win. | ||
| let unsets = ($lines | where { $in =~ '^hide-env ' } | parse 'hide-env {key}' | get key? | default [] | where { $in not-in $export_keys }) | ||
| if ($exports | is-not-empty) { | ||
| load-env ($exports | reduce -f {} {|it, acc| $acc | insert $it.key $it.value}) | ||
| } | ||
| for key in $unsets { | ||
| if ($key in $env) { hide-env $key } | ||
| } | ||
| } else { | ||
| ^vp ...$args | ||
| } | ||
| } | ||
|
|
||
| # Shell completion for nushell (delegates to fish completions) | ||
| def "nu-complete vp" [context: string] { | ||
| let fish_comp_path = "__VP_HOME__/vp.fish" | ||
| let fish_cmd = $"source '($fish_comp_path)'; complete '--do-complete=($context)'" | ||
| fish --command $fish_cmd | from tsv --flexible --noheaders --no-infer | rename value description | update value {|row| | ||
| let value = $row.value | ||
| let need_quote = ['\' ',' '[' ']' '(' ')' ' ' '\t' "'" '"' "`"] | any {$in in $value} | ||
| if ($need_quote and ($value | path exists)) { | ||
| let expanded_path = if ($value starts-with ~) {$value | path expand --no-symlink} else {$value} | ||
| $'"($expanded_path | str replace --all "\"" "\\\"")"' | ||
| } else {$value} | ||
| } | ||
| } | ||
| export def --env --wrapped vpr [...args: string@"nu-complete vp"] { ^vp run ...$args } | ||
|
Collaborator
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. Since we delegate completion to Fish shell via static completion files, dynamic completion for |
||
| "# | ||
| .replace("__VP_BIN__", &bin_path_ref) | ||
| .replace("__VP_HOME__", &vite_plus_home.as_path().display().to_string()); | ||
| let env_nu_file = vite_plus_home.join("env.nu"); | ||
| tokio::fs::write(&env_nu_file, env_nu_content).await?; | ||
|
|
||
| // PowerShell env file | ||
| let env_ps1_content = r#"# Vite+ environment setup (https://viteplus.dev) | ||
| $__vp_bin = "__VP_BIN_WIN__" | ||
|
|
@@ -601,6 +674,10 @@ fn print_path_instructions(bin_dir: &vite_path::AbsolutePath) { | |
| println!(); | ||
| println!(" source \"{home_path}/env.fish\""); | ||
| println!(); | ||
| println!(" For Nushell, add to ~/.config/nushell/config.nu:"); | ||
| println!(); | ||
| println!(" source \"{home_path}/env.nu\""); | ||
|
Collaborator
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 line outputs: But Nushell seems to use Should we switch to using (BTW, I'm not familiar with nushell, so please correct me if I'm missing anything)
Collaborator
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. |
||
| println!(); | ||
| println!(" For PowerShell, add to your $PROFILE:"); | ||
| println!(); | ||
| println!(" . \"{home_path}/env.ps1\""); | ||
|
|
@@ -654,12 +731,43 @@ mod tests { | |
|
|
||
| let env_path = home.join("env"); | ||
| let env_fish_path = home.join("env.fish"); | ||
| let env_nu_path = home.join("env.nu"); | ||
| let env_ps1_path = home.join("env.ps1"); | ||
| assert!(env_path.as_path().exists(), "env file should be created"); | ||
| assert!(env_fish_path.as_path().exists(), "env.fish file should be created"); | ||
| assert!(env_nu_path.as_path().exists(), "env.nu file should be created"); | ||
| assert!(env_ps1_path.as_path().exists(), "env.ps1 file should be created"); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn test_create_env_files_nu_contains_path_guard() { | ||
| 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 nu_content = tokio::fs::read_to_string(home.join("env.nu")).await.unwrap(); | ||
| assert!( | ||
| !nu_content.contains("__VP_BIN__"), | ||
| "env.nu should not contain __VP_BIN__ placeholder" | ||
| ); | ||
| assert!(nu_content.contains("$HOME/bin"), "env.nu should reference $HOME/bin"); | ||
| assert!( | ||
| nu_content.contains("VP_ENV_USE_EVAL_ENABLE"), | ||
| "env.nu should set VP_ENV_USE_EVAL_ENABLE" | ||
| ); | ||
| assert!( | ||
| nu_content.contains("vp.fish"), | ||
| "env.nu should reference the Fish completion file for Nushell delegation" | ||
| ); | ||
| assert!( | ||
| nu_content.contains("VP_SHELL_NU"), | ||
| "env.nu should use VP_SHELL_NU explicit marker instead of inherited NU_VERSION" | ||
| ); | ||
| assert!(nu_content.contains("load-env"), "env.nu should use load-env to apply exports"); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn test_create_env_files_replaces_placeholder_with_home_relative_path() { | ||
| let temp_dir = TempDir::new().unwrap(); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ref: clap-rs/clap#5771
When a command has multiple Vec arguments (for example, when vp install has both packages and pass_through_args), clap_complete_nushell generates invalid syntax. This is because Nushell's extern only allows a single ...rest parameter
As a workaround for this problem, completion processing is delegated to Fish. Running vp env setup generates vp.fish (which is shared with the Fish shell integration), so if Fish is installed, completions can be used without any additional configuration.