@@ -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- }
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) ;
128100
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- }
138-
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,26 @@ 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+ [
270+ ( ". " , ".vite-plus/env\" " ) ,
271+ ( "source " , ".vite-plus/env\" " ) ,
272+ ( "source " , ".vite-plus/env.fish\" " ) ,
273+ ( "source " , ".vite-plus/env.nu'" ) ,
274+ ( "source " , ".vite-plus\\ env.nu'" ) ,
275+ ]
276+ . iter ( )
277+ . any ( |( prefix, suffix) | trimmed. starts_with ( prefix) && trimmed. contains ( suffix) )
309278}
310279
311280/// Remove Vite+ lines from content, returning the cleaned string.
312281fn remove_vite_plus_lines ( content : & str ) -> Str {
313- let pattern = ".vite-plus/env\" " ;
314282 let lines: Vec < & str > = content. lines ( ) . collect ( ) ;
315283 let mut remove_indices = Vec :: new ( ) ;
316284
317285 for ( i, line) in lines. iter ( ) . enumerate ( ) {
318- if line . contains ( pattern ) {
286+ if is_vite_plus_source_line ( line ) {
319287 remove_indices. push ( i) ;
320288 // Also remove the comment line above
321289 if i > 0 && lines[ i - 1 ] . contains ( VITE_PLUS_COMMENT ) {
@@ -396,6 +364,27 @@ mod tests {
396364 assert_eq ! ( & * result, "# existing\n " ) ;
397365 }
398366
367+ #[ test]
368+ fn test_remove_vite_plus_lines_fish ( ) {
369+ let content = "# existing config\n \n # Vite+ bin (https://viteplus.dev)\n source \" $HOME/.vite-plus/env.fish\" \n " ;
370+ let result = remove_vite_plus_lines ( content) ;
371+ assert_eq ! ( & * result, "# existing config\n " ) ;
372+ }
373+
374+ #[ test]
375+ fn test_remove_vite_plus_lines_nushell ( ) {
376+ let content = "# existing config\n \n # Vite+ bin (https://viteplus.dev)\n source '~/.vite-plus/env.nu'\n " ;
377+ let result = remove_vite_plus_lines ( content) ;
378+ assert_eq ! ( & * result, "# existing config\n " ) ;
379+ }
380+
381+ #[ test]
382+ fn test_remove_vite_plus_lines_nushell_windows_path ( ) {
383+ let content = "# existing config\n source '~\\ .vite-plus\\ env.nu'\n " ;
384+ let result = remove_vite_plus_lines ( content) ;
385+ assert_eq ! ( & * result, "# existing config\n " ) ;
386+ }
387+
399388 #[ test]
400389 fn test_remove_vite_plus_lines_preserves_surrounding ( ) {
401390 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 +465,8 @@ mod tests {
476465 let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
477466 let home = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
478467
479- // Clear ZDOTDIR/XDG_CONFIG_HOME so the test environment doesn't affect results
480- let _guard = ProfileEnvGuard :: new ( None , None ) ;
468+ // Clear env overrides so the test environment doesn't affect results
469+ let _guard = ProfileEnvGuard :: new ( None , None , None ) ;
481470
482471 // Main profile with vite-plus line
483472 std:: fs:: write ( home. join ( ".zshrc" ) , ". \" $HOME/.vite-plus/env\" \n " ) . unwrap ( ) ;
@@ -494,19 +483,25 @@ mod tests {
494483 assert ! ( matches!( & profiles[ 1 ] . kind, AffectedProfileKind :: Snippet ) ) ;
495484 }
496485
497- /// Guard that saves and restores ZDOTDIR and XDG_CONFIG_HOME env vars.
486+ /// Guard that saves and restores profile-related env vars.
498487 #[ cfg( not( windows) ) ]
499488 struct ProfileEnvGuard {
500489 original_zdotdir : Option < std:: ffi:: OsString > ,
501490 original_xdg_config : Option < std:: ffi:: OsString > ,
491+ original_xdg_data : Option < std:: ffi:: OsString > ,
502492 }
503493
504494 #[ cfg( not( windows) ) ]
505495 impl ProfileEnvGuard {
506- fn new ( zdotdir : Option < & std:: path:: Path > , xdg_config : Option < & std:: path:: Path > ) -> Self {
496+ fn new (
497+ zdotdir : Option < & std:: path:: Path > ,
498+ xdg_config : Option < & std:: path:: Path > ,
499+ xdg_data : Option < & std:: path:: Path > ,
500+ ) -> Self {
507501 let guard = Self {
508502 original_zdotdir : std:: env:: var_os ( "ZDOTDIR" ) ,
509503 original_xdg_config : std:: env:: var_os ( "XDG_CONFIG_HOME" ) ,
504+ original_xdg_data : std:: env:: var_os ( "XDG_DATA_HOME" ) ,
510505 } ;
511506 unsafe {
512507 match zdotdir {
@@ -517,6 +512,10 @@ mod tests {
517512 Some ( v) => std:: env:: set_var ( "XDG_CONFIG_HOME" , v) ,
518513 None => std:: env:: remove_var ( "XDG_CONFIG_HOME" ) ,
519514 }
515+ match xdg_data {
516+ Some ( v) => std:: env:: set_var ( "XDG_DATA_HOME" , v) ,
517+ None => std:: env:: remove_var ( "XDG_DATA_HOME" ) ,
518+ }
520519 }
521520 guard
522521 }
@@ -534,6 +533,10 @@ mod tests {
534533 Some ( v) => std:: env:: set_var ( "XDG_CONFIG_HOME" , v) ,
535534 None => std:: env:: remove_var ( "XDG_CONFIG_HOME" ) ,
536535 }
536+ match & self . original_xdg_data {
537+ Some ( v) => std:: env:: set_var ( "XDG_DATA_HOME" , v) ,
538+ None => std:: env:: remove_var ( "XDG_DATA_HOME" ) ,
539+ }
537540 }
538541 }
539542 }
@@ -550,7 +553,7 @@ mod tests {
550553
551554 std:: fs:: write ( zdotdir. join ( ".zshenv" ) , ". \" $HOME/.vite-plus/env\" \n " ) . unwrap ( ) ;
552555
553- let _guard = ProfileEnvGuard :: new ( Some ( & zdotdir) , None ) ;
556+ let _guard = ProfileEnvGuard :: new ( Some ( & zdotdir) , None , None ) ;
554557
555558 let profiles = collect_affected_profiles ( & home) ;
556559 let zdotdir_profiles: Vec < _ > =
@@ -572,7 +575,7 @@ mod tests {
572575
573576 std:: fs:: write ( fish_dir. join ( "vite-plus.fish" ) , "" ) . unwrap ( ) ;
574577
575- let _guard = ProfileEnvGuard :: new ( None , Some ( & xdg_config) ) ;
578+ let _guard = ProfileEnvGuard :: new ( None , Some ( & xdg_config) , None ) ;
576579
577580 let profiles = collect_affected_profiles ( & home) ;
578581 let xdg_profiles: Vec < _ > =
@@ -581,6 +584,28 @@ mod tests {
581584 assert ! ( matches!( & xdg_profiles[ 0 ] . kind, AffectedProfileKind :: Snippet ) ) ;
582585 }
583586
587+ #[ test]
588+ #[ serial]
589+ #[ cfg( not( windows) ) ]
590+ fn test_collect_affected_profiles_xdg_data ( ) {
591+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
592+ let home = AbsolutePathBuf :: new ( temp_dir. path ( ) . join ( "home" ) ) . unwrap ( ) ;
593+ let xdg_data = temp_dir. path ( ) . join ( "xdg_data" ) ;
594+ let nushell_dir = xdg_data. join ( "nushell/vendor/autoload" ) ;
595+ std:: fs:: create_dir_all ( & home) . unwrap ( ) ;
596+ std:: fs:: create_dir_all ( & nushell_dir) . unwrap ( ) ;
597+
598+ std:: fs:: write ( nushell_dir. join ( "vite-plus.nu" ) , "source '~/.vite-plus/env.nu'\n " ) . unwrap ( ) ;
599+
600+ let _guard = ProfileEnvGuard :: new ( None , None , Some ( & xdg_data) ) ;
601+
602+ let profiles = collect_affected_profiles ( & home) ;
603+ let xdg_profiles: Vec < _ > =
604+ profiles. iter ( ) . filter ( |p| p. path . as_path ( ) . starts_with ( & xdg_data) ) . collect ( ) ;
605+ assert_eq ! ( xdg_profiles. len( ) , 1 ) ;
606+ assert ! ( matches!( & xdg_profiles[ 0 ] . kind, AffectedProfileKind :: Snippet ) ) ;
607+ }
608+
584609 #[ test]
585610 fn test_execute_not_installed ( ) {
586611 let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
0 commit comments