@@ -7,29 +7,17 @@ use std::{
77
88use directories:: BaseDirs ;
99use owo_colors:: OwoColorize ;
10- use vite_path:: { AbsolutePath , AbsolutePathBuf } ;
10+ use vite_path:: AbsolutePathBuf ;
1111use vite_shared:: output;
1212use vite_str:: Str ;
1313
14- use crate :: { cli:: exit_status, error:: Error } ;
15-
16- /// All shell profile paths to check, with `is_snippet` flag.
17- const SHELL_PROFILES : & [ ( & str , bool ) ] = & [
18- ( ".zshenv" , false ) ,
19- ( ".zshrc" , false ) ,
20- ( ".bash_profile" , false ) ,
21- ( ".bashrc" , false ) ,
22- ( ".profile" , false ) ,
23- ( ".config/fish/conf.d/vite-plus.fish" , true ) ,
24- ] ;
25-
26- /// Abbreviate a path for display: replace `$HOME` prefix with `~`.
27- fn abbreviate_home_path ( path : & AbsolutePath , user_home : & AbsolutePath ) -> Str {
28- match path. strip_prefix ( user_home) {
29- Ok ( Some ( suffix) ) => vite_str:: format!( "~/{suffix}" ) ,
30- _ => Str :: from ( path. to_string ( ) ) ,
31- }
32- }
14+ use crate :: {
15+ cli:: exit_status,
16+ commands:: shell:: {
17+ ALL_SHELL_PROFILES , ShellProfileKind , abbreviate_home_path, resolve_profile_path,
18+ } ,
19+ error:: Error ,
20+ } ;
3321
3422/// Comment marker written by the install script above the sourcing line.
3523const VITE_PLUS_COMMENT : & str = "# Vite+ bin" ;
@@ -106,39 +94,12 @@ enum AffectedProfileKind {
10694fn collect_affected_profiles ( user_home : & AbsolutePathBuf ) -> Vec < AffectedProfile > {
10795 let mut affected = Vec :: new ( ) ;
10896
109- // Build full list of (display_name, path, is_snippet) from the base set
110- let mut profiles: Vec < ( Str , AbsolutePathBuf , bool ) > = SHELL_PROFILES
111- . iter ( )
112- . map ( |& ( name, is_snippet) | {
113- ( vite_str:: format!( "~/{name}" ) , user_home. join ( name) , is_snippet)
114- } )
115- . collect ( ) ;
116-
117- // If ZDOTDIR is set and differs from $HOME, also check there.
118- if let Ok ( zdotdir) = std:: env:: var ( "ZDOTDIR" )
119- && let Some ( zdotdir_path) = AbsolutePathBuf :: new ( zdotdir. into ( ) )
120- && zdotdir_path != * user_home
121- {
122- for name in [ ".zshenv" , ".zshrc" ] {
123- let path = zdotdir_path. join ( name) ;
124- let display = abbreviate_home_path ( & path, user_home) ;
125- profiles. push ( ( display, path, false ) ) ;
126- }
127- }
128-
129- // If XDG_CONFIG_HOME is set and differs from $HOME/.config, also check there.
130- if let Ok ( xdg_config) = std:: env:: var ( "XDG_CONFIG_HOME" )
131- && let Some ( xdg_path) = AbsolutePathBuf :: new ( xdg_config. into ( ) )
132- && xdg_path != user_home. join ( ".config" )
133- {
134- let path = xdg_path. join ( "fish/conf.d/vite-plus.fish" ) ;
135- let display = abbreviate_home_path ( & path, user_home) ;
136- profiles. push ( ( display, path, true ) ) ;
137- }
97+ for profile in ALL_SHELL_PROFILES {
98+ let path = resolve_profile_path ( profile, user_home) ;
99+ let name = abbreviate_home_path ( & path, user_home) ;
138100
139- for ( name, path, is_snippet) in profiles {
140101 // For snippets, check if the file exists only
141- if is_snippet {
102+ if matches ! ( profile . kind , ShellProfileKind :: Snippet ) {
142103 if let Ok ( true ) = std:: fs:: exists ( & path) {
143104 affected. push ( AffectedProfile { name, path, kind : AffectedProfileKind :: Snippet } )
144105 }
@@ -147,7 +108,7 @@ fn collect_affected_profiles(user_home: &AbsolutePathBuf) -> Vec<AffectedProfile
147108 // Read directly — if the file doesn't exist, read_to_string returns Err
148109 // which .ok().filter() handles gracefully (no redundant exists() check).
149110 if let Some ( content) =
150- std:: fs:: read_to_string ( & path) . ok ( ) . filter ( |c| has_vite_plus_lines ( c ) )
111+ std:: fs:: read_to_string ( & path) . ok ( ) . filter ( |c| c . lines ( ) . any ( is_vite_plus_source_line ) )
151112 {
152113 affected. push ( AffectedProfile {
153114 name,
@@ -303,19 +264,22 @@ fn spawn_deferred_delete(trash_path: &std::path::Path) -> std::io::Result<std::p
303264}
304265
305266/// Check if file content contains Vite+ sourcing lines.
306- fn has_vite_plus_lines ( content : & str ) -> bool {
307- let pattern = ".vite-plus/env\" " ;
308- content. lines ( ) . any ( |line| line. contains ( pattern) )
267+ fn is_vite_plus_source_line ( line : & str ) -> bool {
268+ let trimmed = line. trim_start ( ) ;
269+ ( trimmed. starts_with ( ". " ) || trimmed. starts_with ( "source " ) )
270+ && [ "env" , "env.fish" , "env.nu" ] . iter ( ) . any ( |env_file| {
271+ trimmed. contains ( & format ! ( ".vite-plus/{env_file}\" " ) )
272+ || trimmed. contains ( & format ! ( ".vite-plus\\ {env_file}\" " ) )
273+ } )
309274}
310275
311276/// Remove Vite+ lines from content, returning the cleaned string.
312277fn remove_vite_plus_lines ( content : & str ) -> Str {
313- let pattern = ".vite-plus/env\" " ;
314278 let lines: Vec < & str > = content. lines ( ) . collect ( ) ;
315279 let mut remove_indices = Vec :: new ( ) ;
316280
317281 for ( i, line) in lines. iter ( ) . enumerate ( ) {
318- if line . contains ( pattern ) {
282+ if is_vite_plus_source_line ( line ) {
319283 remove_indices. push ( i) ;
320284 // Also remove the comment line above
321285 if i > 0 && lines[ i - 1 ] . contains ( VITE_PLUS_COMMENT ) {
@@ -396,6 +360,27 @@ mod tests {
396360 assert_eq ! ( & * result, "# existing\n " ) ;
397361 }
398362
363+ #[ test]
364+ fn test_remove_vite_plus_lines_fish ( ) {
365+ let content = "# existing config\n \n # Vite+ bin (https://viteplus.dev)\n source \" $HOME/.vite-plus/env.fish\" \n " ;
366+ let result = remove_vite_plus_lines ( content) ;
367+ assert_eq ! ( & * result, "# existing config\n " ) ;
368+ }
369+
370+ #[ test]
371+ fn test_remove_vite_plus_lines_nushell ( ) {
372+ let content = "# existing config\n \n # Vite+ bin (https://viteplus.dev)\n source \" ~/.vite-plus/env.nu\" \n " ;
373+ let result = remove_vite_plus_lines ( content) ;
374+ assert_eq ! ( & * result, "# existing config\n " ) ;
375+ }
376+
377+ #[ test]
378+ fn test_remove_vite_plus_lines_nushell_windows_path ( ) {
379+ let content = "# existing config\n source \" ~\\ .vite-plus\\ env.nu\" \n " ;
380+ let result = remove_vite_plus_lines ( content) ;
381+ assert_eq ! ( & * result, "# existing config\n " ) ;
382+ }
383+
399384 #[ test]
400385 fn test_remove_vite_plus_lines_preserves_surrounding ( ) {
401386 let content = "# before\n export A=1\n \n # Vite+ bin (https://viteplus.dev)\n . \" $HOME/.vite-plus/env\" \n # after\n export B=2\n " ;
@@ -476,8 +461,8 @@ mod tests {
476461 let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
477462 let home = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
478463
479- // Clear ZDOTDIR/XDG_CONFIG_HOME so the test environment doesn't affect results
480- let _guard = ProfileEnvGuard :: new ( None , None ) ;
464+ // Clear env overrides so the test environment doesn't affect results
465+ let _guard = ProfileEnvGuard :: new ( None , None , None ) ;
481466
482467 // Main profile with vite-plus line
483468 std:: fs:: write ( home. join ( ".zshrc" ) , ". \" $HOME/.vite-plus/env\" \n " ) . unwrap ( ) ;
@@ -494,19 +479,25 @@ mod tests {
494479 assert ! ( matches!( & profiles[ 1 ] . kind, AffectedProfileKind :: Snippet ) ) ;
495480 }
496481
497- /// Guard that saves and restores ZDOTDIR and XDG_CONFIG_HOME env vars.
482+ /// Guard that saves and restores profile-related env vars.
498483 #[ cfg( not( windows) ) ]
499484 struct ProfileEnvGuard {
500485 original_zdotdir : Option < std:: ffi:: OsString > ,
501486 original_xdg_config : Option < std:: ffi:: OsString > ,
487+ original_xdg_data : Option < std:: ffi:: OsString > ,
502488 }
503489
504490 #[ cfg( not( windows) ) ]
505491 impl ProfileEnvGuard {
506- fn new ( zdotdir : Option < & std:: path:: Path > , xdg_config : Option < & std:: path:: Path > ) -> Self {
492+ fn new (
493+ zdotdir : Option < & std:: path:: Path > ,
494+ xdg_config : Option < & std:: path:: Path > ,
495+ xdg_data : Option < & std:: path:: Path > ,
496+ ) -> Self {
507497 let guard = Self {
508498 original_zdotdir : std:: env:: var_os ( "ZDOTDIR" ) ,
509499 original_xdg_config : std:: env:: var_os ( "XDG_CONFIG_HOME" ) ,
500+ original_xdg_data : std:: env:: var_os ( "XDG_DATA_HOME" ) ,
510501 } ;
511502 unsafe {
512503 match zdotdir {
@@ -517,6 +508,10 @@ mod tests {
517508 Some ( v) => std:: env:: set_var ( "XDG_CONFIG_HOME" , v) ,
518509 None => std:: env:: remove_var ( "XDG_CONFIG_HOME" ) ,
519510 }
511+ match xdg_data {
512+ Some ( v) => std:: env:: set_var ( "XDG_DATA_HOME" , v) ,
513+ None => std:: env:: remove_var ( "XDG_DATA_HOME" ) ,
514+ }
520515 }
521516 guard
522517 }
@@ -534,6 +529,10 @@ mod tests {
534529 Some ( v) => std:: env:: set_var ( "XDG_CONFIG_HOME" , v) ,
535530 None => std:: env:: remove_var ( "XDG_CONFIG_HOME" ) ,
536531 }
532+ match & self . original_xdg_data {
533+ Some ( v) => std:: env:: set_var ( "XDG_DATA_HOME" , v) ,
534+ None => std:: env:: remove_var ( "XDG_DATA_HOME" ) ,
535+ }
537536 }
538537 }
539538 }
@@ -550,7 +549,7 @@ mod tests {
550549
551550 std:: fs:: write ( zdotdir. join ( ".zshenv" ) , ". \" $HOME/.vite-plus/env\" \n " ) . unwrap ( ) ;
552551
553- let _guard = ProfileEnvGuard :: new ( Some ( & zdotdir) , None ) ;
552+ let _guard = ProfileEnvGuard :: new ( Some ( & zdotdir) , None , None ) ;
554553
555554 let profiles = collect_affected_profiles ( & home) ;
556555 let zdotdir_profiles: Vec < _ > =
@@ -572,7 +571,7 @@ mod tests {
572571
573572 std:: fs:: write ( fish_dir. join ( "vite-plus.fish" ) , "" ) . unwrap ( ) ;
574573
575- let _guard = ProfileEnvGuard :: new ( None , Some ( & xdg_config) ) ;
574+ let _guard = ProfileEnvGuard :: new ( None , Some ( & xdg_config) , None ) ;
576575
577576 let profiles = collect_affected_profiles ( & home) ;
578577 let xdg_profiles: Vec < _ > =
@@ -581,6 +580,29 @@ mod tests {
581580 assert ! ( matches!( & xdg_profiles[ 0 ] . kind, AffectedProfileKind :: Snippet ) ) ;
582581 }
583582
583+ #[ test]
584+ #[ serial]
585+ #[ cfg( not( windows) ) ]
586+ fn test_collect_affected_profiles_xdg_data ( ) {
587+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
588+ let home = AbsolutePathBuf :: new ( temp_dir. path ( ) . join ( "home" ) ) . unwrap ( ) ;
589+ let xdg_data = temp_dir. path ( ) . join ( "xdg_data" ) ;
590+ let nushell_dir = xdg_data. join ( "nushell/vendor/autoload" ) ;
591+ std:: fs:: create_dir_all ( & home) . unwrap ( ) ;
592+ std:: fs:: create_dir_all ( & nushell_dir) . unwrap ( ) ;
593+
594+ std:: fs:: write ( nushell_dir. join ( "vite-plus.nu" ) , "source \" ~/.vite-plus/env.nu\" \n " )
595+ . unwrap ( ) ;
596+
597+ let _guard = ProfileEnvGuard :: new ( None , None , Some ( & xdg_data) ) ;
598+
599+ let profiles = collect_affected_profiles ( & home) ;
600+ let xdg_profiles: Vec < _ > =
601+ profiles. iter ( ) . filter ( |p| p. path . as_path ( ) . starts_with ( & xdg_data) ) . collect ( ) ;
602+ assert_eq ! ( xdg_profiles. len( ) , 1 ) ;
603+ assert ! ( matches!( & xdg_profiles[ 0 ] . kind, AffectedProfileKind :: Snippet ) ) ;
604+ }
605+
584606 #[ test]
585607 fn test_execute_not_installed ( ) {
586608 let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
0 commit comments