@@ -106,10 +106,9 @@ fn collect_affected_profiles(
106106
107107 // Read directly — if the file doesn't exist, read_to_string returns Err
108108 // which .ok().filter() handles gracefully (no redundant exists() check).
109- if let Some ( content) = std:: fs:: read_to_string ( & path)
110- . ok ( )
111- . filter ( |c| contains_vite_plus_source_line ( c, source_matcher, profile. env_file ) )
112- {
109+ if let Some ( content) = std:: fs:: read_to_string ( & path) . ok ( ) . filter ( |c| {
110+ c. lines ( ) . any ( |line| source_matcher. is_vite_plus_source_line ( line, profile. env_file ) )
111+ } ) {
113112 if matches ! ( profile. kind, ShellProfileKind :: Snippet ) {
114113 affected. push ( AffectedProfile { name, path, kind : AffectedProfileKind :: Snippet } ) ;
115114 continue ;
@@ -273,81 +272,57 @@ fn spawn_deferred_delete(trash_path: &std::path::Path) -> std::io::Result<std::p
273272 . spawn ( )
274273}
275274
275+ /// Matches shell-profile `source` lines that reference *this* install's env
276+ /// files, so a second Vite+ install's lines are left untouched.
277+ ///
278+ /// The recognized home spellings must mirror what the writers emit:
279+ /// `install.sh`/`install.ps1` (shell PATH setup) and `render_env_content` in
280+ /// `env/setup.rs`. `env/doctor.rs::check_profile_files` derives the same
281+ /// variants for its profile scan; keep them in sync.
276282struct VitePlusSourceMatcher {
283+ /// Home-dir spellings with forward-slash separators: the absolute path,
284+ /// plus `$HOME`- and `~`-relative forms when the home is under `$HOME`.
277285 roots : Vec < Str > ,
278286}
279287
280288impl VitePlusSourceMatcher {
281289 fn new ( home_dir : & AbsolutePathBuf , user_home : & AbsolutePathBuf ) -> Self {
282- Self { roots : vite_plus_home_refs ( home_dir, user_home) }
290+ let mut roots = vec ! [ normalize_path_separators( & home_dir. as_path( ) . display( ) . to_string( ) ) ] ;
291+
292+ if let Ok ( Some ( suffix) ) = home_dir. strip_prefix ( user_home) {
293+ // `RelativePathBuf` guarantees forward-slash separators.
294+ let suffix = vite_str:: format!( "{suffix}" ) ;
295+ if suffix. is_empty ( ) {
296+ roots. push ( Str :: from ( "$HOME" ) ) ;
297+ roots. push ( Str :: from ( "~" ) ) ;
298+ } else {
299+ roots. push ( vite_str:: format!( "$HOME/{suffix}" ) ) ;
300+ roots. push ( vite_str:: format!( "~/{suffix}" ) ) ;
301+ }
302+ }
303+
304+ Self { roots }
283305 }
284306
285307 fn is_vite_plus_source_line ( & self , line : & str , env_file : & str ) -> bool {
286308 let Some ( arg) = source_line_arg ( line) else {
287309 return false ;
288310 } ;
289311
290- self . roots . iter ( ) . any ( |root| {
291- let path = join_path_ref ( root, env_file) ;
292- arg == & * path || ( env_file == "env.nu" && arg == & * path_ref_with_backslashes ( & path) )
293- } )
294- }
295- }
296-
297- fn vite_plus_home_refs ( home_dir : & AbsolutePathBuf , user_home : & AbsolutePathBuf ) -> Vec < Str > {
298- let mut refs = Vec :: new ( ) ;
299- push_path_ref ( & mut refs, vite_str:: format!( "{}" , home_dir. as_path( ) . display( ) ) ) ;
300- push_path_ref ( & mut refs, normalize_path_separators ( & home_dir. as_path ( ) . display ( ) . to_string ( ) ) ) ;
301-
302- if let Ok ( Some ( suffix) ) = home_dir. strip_prefix ( user_home) {
303- let suffix = normalize_path_separators ( & vite_str:: format!( "{suffix}" ) ) ;
304- if suffix. is_empty ( ) {
305- push_path_ref ( & mut refs, Str :: from ( "$HOME" ) ) ;
306- push_path_ref ( & mut refs, Str :: from ( "~" ) ) ;
307- } else {
308- push_path_ref ( & mut refs, vite_str:: format!( "$HOME/{suffix}" ) ) ;
309- push_path_ref ( & mut refs, vite_str:: format!( "~/{suffix}" ) ) ;
310- }
311- }
312-
313- refs
314- }
315-
316- fn push_path_ref ( paths : & mut Vec < Str > , path : Str ) {
317- if !paths. iter ( ) . any ( |existing| existing == & path) {
318- paths. push ( path) ;
312+ // Windows profiles may spell the path with backslashes (e.g. Nushell's
313+ // `source '~\.vite-plus\env.nu'`); compare in forward-slash form.
314+ let arg = normalize_path_separators ( arg) ;
315+ self . roots . iter ( ) . any ( |root| arg == join_path_ref ( root, env_file) )
319316 }
320317}
321318
322319fn join_path_ref ( root : & str , env_file : & str ) -> Str {
323- let separator = if root. ends_with ( '/' ) || root . ends_with ( '\\' ) { "" } else { "/" } ;
320+ let separator = if root. ends_with ( '/' ) { "" } else { "/" } ;
324321 vite_str:: format!( "{root}{separator}{env_file}" )
325322}
326323
327- #[ expect( clippy:: disallowed_types) ]
328324fn normalize_path_separators ( path : & str ) -> Str {
329- let mut normalized = String :: with_capacity ( path. len ( ) ) ;
330- for ch in path. chars ( ) {
331- normalized. push ( if ch == '\\' { '/' } else { ch } ) ;
332- }
333- Str :: from ( normalized)
334- }
335-
336- #[ expect( clippy:: disallowed_types) ]
337- fn path_ref_with_backslashes ( path : & str ) -> Str {
338- let mut normalized = String :: with_capacity ( path. len ( ) ) ;
339- for ch in path. chars ( ) {
340- normalized. push ( if ch == '/' { '\\' } else { ch } ) ;
341- }
342- Str :: from ( normalized)
343- }
344-
345- fn contains_vite_plus_source_line (
346- content : & str ,
347- source_matcher : & VitePlusSourceMatcher ,
348- env_file : & str ,
349- ) -> bool {
350- content. lines ( ) . any ( |line| source_matcher. is_vite_plus_source_line ( line, env_file) )
325+ Str :: from ( path. replace ( '\\' , "/" ) )
351326}
352327
353328fn source_line_arg ( line : & str ) -> Option < & str > {
@@ -459,13 +434,6 @@ mod tests {
459434 VitePlusSourceMatcher :: new ( & home_dir, & user_home)
460435 }
461436
462- fn source_matcher_for (
463- home_dir : & AbsolutePathBuf ,
464- user_home : & AbsolutePathBuf ,
465- ) -> VitePlusSourceMatcher {
466- VitePlusSourceMatcher :: new ( home_dir, user_home)
467- }
468-
469437 #[ test]
470438 fn test_remove_vite_plus_lines_posix ( ) {
471439 let matcher = default_source_matcher ( ) ;
@@ -486,7 +454,7 @@ mod tests {
486454 fn test_remove_vite_plus_lines_absolute_path ( ) {
487455 let user_home = default_user_home ( ) ;
488456 let home_dir = user_home. join ( ".vite-plus" ) ;
489- let matcher = source_matcher_for ( & home_dir, & user_home) ;
457+ let matcher = VitePlusSourceMatcher :: new ( & home_dir, & user_home) ;
490458 let env_path = shell_path ( & home_dir. join ( "env" ) ) ;
491459 let content = vite_str:: format!( "# existing\n . \" {env_path}\" \n " ) ;
492460 let result = remove_vite_plus_lines ( & content, & matcher, "env" ) ;
@@ -497,7 +465,7 @@ mod tests {
497465 fn test_remove_vite_plus_lines_custom_absolute_path ( ) {
498466 let user_home = custom_user_home ( ) ;
499467 let home_dir = user_home. join ( "tools" ) . join ( "vp" ) ;
500- let matcher = source_matcher_for ( & home_dir, & user_home) ;
468+ let matcher = VitePlusSourceMatcher :: new ( & home_dir, & user_home) ;
501469 let env_path = shell_path ( & home_dir. join ( "env" ) ) ;
502470 let content = vite_str:: format!( "# existing\n . \" {env_path}\" \n " ) ;
503471 let result = remove_vite_plus_lines ( & content, & matcher, "env" ) ;
@@ -508,7 +476,7 @@ mod tests {
508476 fn test_remove_vite_plus_lines_custom_home_relative_path ( ) {
509477 let user_home = custom_user_home ( ) ;
510478 let home_dir = user_home. join ( "tools" ) . join ( "vp" ) ;
511- let matcher = source_matcher_for ( & home_dir, & user_home) ;
479+ let matcher = VitePlusSourceMatcher :: new ( & home_dir, & user_home) ;
512480 let content = "# existing\n . \" $HOME/tools/vp/env\" \n " ;
513481 let result = remove_vite_plus_lines ( content, & matcher, "env" ) ;
514482 assert_eq ! ( & * result, "# existing\n " ) ;
@@ -518,7 +486,7 @@ mod tests {
518486 fn test_remove_vite_plus_lines_custom_tilde_path ( ) {
519487 let user_home = custom_user_home ( ) ;
520488 let home_dir = user_home. join ( "tools" ) . join ( "vp" ) ;
521- let matcher = source_matcher_for ( & home_dir, & user_home) ;
489+ let matcher = VitePlusSourceMatcher :: new ( & home_dir, & user_home) ;
522490 let content = "# existing\n source '~/tools/vp/env.nu'\n " ;
523491 let result = remove_vite_plus_lines ( content, & matcher, "env.nu" ) ;
524492 assert_eq ! ( & * result, "# existing\n " ) ;
@@ -577,7 +545,7 @@ mod tests {
577545 let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
578546 let temp_path = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
579547 let home_dir = temp_path. join ( ".vite-plus" ) ;
580- let matcher = source_matcher_for ( & home_dir, & temp_path) ;
548+ let matcher = VitePlusSourceMatcher :: new ( & home_dir, & temp_path) ;
581549 let profile_path = temp_path. join ( ".zshrc" ) ;
582550 let original = "# my config\n export FOO=bar\n \n # Vite+ bin (https://viteplus.dev)\n . \" $HOME/.vite-plus/env\" \n " ;
583551 std:: fs:: write ( & profile_path, original) . unwrap ( ) ;
@@ -647,7 +615,7 @@ mod tests {
647615 let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
648616 let home = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
649617 let home_dir = home. join ( ".vite-plus" ) ;
650- let matcher = source_matcher_for ( & home_dir, & home) ;
618+ let matcher = VitePlusSourceMatcher :: new ( & home_dir, & home) ;
651619
652620 // Clear env overrides so the test environment doesn't affect results
653621 let _guard = ProfileEnvGuard :: new ( None , None , None ) ;
@@ -674,7 +642,7 @@ mod tests {
674642 let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
675643 let home = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
676644 let home_dir = home. join ( "tools/vp" ) ;
677- let matcher = source_matcher_for ( & home_dir, & home) ;
645+ let matcher = VitePlusSourceMatcher :: new ( & home_dir, & home) ;
678646
679647 let _guard = ProfileEnvGuard :: new ( None , None , None ) ;
680648
@@ -760,7 +728,7 @@ mod tests {
760728 std:: fs:: write ( zdotdir. join ( ".zshenv" ) , ". \" $HOME/.vite-plus/env\" \n " ) . unwrap ( ) ;
761729
762730 let _guard = ProfileEnvGuard :: new ( Some ( & zdotdir) , None , None ) ;
763- let matcher = source_matcher_for ( & home. join ( ".vite-plus" ) , & home) ;
731+ let matcher = VitePlusSourceMatcher :: new ( & home. join ( ".vite-plus" ) , & home) ;
764732
765733 let profiles = collect_affected_profiles ( & home, & matcher) ;
766734 let zdotdir_profiles: Vec < _ > =
@@ -784,7 +752,7 @@ mod tests {
784752 . unwrap ( ) ;
785753
786754 let _guard = ProfileEnvGuard :: new ( None , Some ( & xdg_config) , None ) ;
787- let matcher = source_matcher_for ( & home. join ( ".vite-plus" ) , & home) ;
755+ let matcher = VitePlusSourceMatcher :: new ( & home. join ( ".vite-plus" ) , & home) ;
788756
789757 let profiles = collect_affected_profiles ( & home, & matcher) ;
790758 let xdg_profiles: Vec < _ > =
@@ -807,7 +775,7 @@ mod tests {
807775 std:: fs:: write ( nushell_dir. join ( "vite-plus.nu" ) , "source '~/.vite-plus/env.nu'\n " ) . unwrap ( ) ;
808776
809777 let _guard = ProfileEnvGuard :: new ( None , None , Some ( & xdg_data) ) ;
810- let matcher = source_matcher_for ( & home. join ( ".vite-plus" ) , & home) ;
778+ let matcher = VitePlusSourceMatcher :: new ( & home. join ( ".vite-plus" ) , & home) ;
811779
812780 let profiles = collect_affected_profiles ( & home, & matcher) ;
813781 let xdg_profiles: Vec < _ > =
0 commit comments