diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 169220a3614bf2d74d24a9638f87b9613a556bd6..facb86f3b87e746d35d8b91f27550e351b10e8b6 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1423,7 +1423,7 @@ mod tests { rel_path("b/eight.txt"), ]; - let slash = PathStyle::local().separator(); + let slash = PathStyle::local().primary_separator(); let mut opened_editors = Vec::new(); for path in paths { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 1c9e3f83e383658051f7799a7e3096f532addbe1..45b15e6e9e3eaa03fc69912eab3e778335b714d4 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -3989,7 +3989,7 @@ impl AcpThreadView { let file = buffer.read(cx).file()?; let path = file.path(); let path_style = file.path_style(cx); - let separator = file.path_style(cx).separator(); + let separator = file.path_style(cx).primary_separator(); let file_path = path.parent().and_then(|parent| { if parent.is_empty() { diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 6f64dc20d0b97f1b12fb627c72209df555e6f1a7..050d7a45a1b46e94a195f88e49fd6795ce37f09f 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1060,7 +1060,7 @@ impl FileFinderDelegate { ( filename.to_string(), Vec::new(), - prefix.display(path_style).to_string() + path_style.separator(), + prefix.display(path_style).to_string() + path_style.primary_separator(), Vec::new(), ) } else { @@ -1071,7 +1071,7 @@ impl FileFinderDelegate { .map_or(String::new(), |f| f.to_string_lossy().into_owned()), Vec::new(), entry_path.absolute.parent().map_or(String::new(), |path| { - path.to_string_lossy().into_owned() + path_style.separator() + path.to_string_lossy().into_owned() + path_style.primary_separator() }), Vec::new(), ) diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index d6971da15fde8406ac4d00fb613906c91e25d8d4..aeb9d794c2b4bc014bd332ed03dc8e5c3dda709b 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -1598,7 +1598,7 @@ async fn test_history_match_positions(cx: &mut gpui::TestAppContext) { assert_eq!(file_label.highlight_indices(), &[0, 1, 2]); assert_eq!( path_label.text(), - format!("test{}", PathStyle::local().separator()) + format!("test{}", PathStyle::local().primary_separator()) ); assert_eq!(path_label.highlight_indices(), &[] as &[usize]); }); diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index 53bad3b34880d69aba169df965db71f69b2296eb..2ae0c47776acb5c58b7d0919aa7522fb64d923d0 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -559,7 +559,7 @@ impl PickerDelegate for OpenPathDelegate { parent_path, candidate.path.string, if candidate.is_dir { - path_style.separator() + path_style.primary_separator() } else { "" } @@ -569,7 +569,7 @@ impl PickerDelegate for OpenPathDelegate { parent_path, candidate.path.string, if candidate.is_dir { - path_style.separator() + path_style.primary_separator() } else { "" } @@ -826,7 +826,13 @@ impl PickerDelegate for OpenPathDelegate { } fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - Arc::from(format!("[directory{}]filename.ext", self.path_style.separator()).as_str()) + Arc::from( + format!( + "[directory{}]filename.ext", + self.path_style.primary_separator() + ) + .as_str(), + ) } fn separators_after_indices(&self) -> Vec { diff --git a/crates/fuzzy/src/paths.rs b/crates/fuzzy/src/paths.rs index b35f0c1ce6cec73995838eb82bf782d00f0129af..cce0e082840c4cd05d6e2b21eac0073d3eb7700f 100644 --- a/crates/fuzzy/src/paths.rs +++ b/crates/fuzzy/src/paths.rs @@ -107,7 +107,7 @@ pub fn match_fixed_path_set( .display(path_style) .chars() .collect::>(); - path_prefix_chars.extend(path_style.separator().chars()); + path_prefix_chars.extend(path_style.primary_separator().chars()); let lowercase_pfx = path_prefix_chars .iter() .map(|c| c.to_ascii_lowercase()) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 4a5cd56ec90fd95fe94d55edfdeb7e2114fea820..1f66d194477c64fef207e63d4c87ad4d76675f65 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -4351,8 +4351,11 @@ impl GitPanel { .when(strikethrough, Label::strikethrough), ), (true, false) => this.child( - self.entry_label(format!("{dir}{}", path_style.separator()), path_color) - .when(strikethrough, Label::strikethrough), + self.entry_label( + format!("{dir}{}", path_style.primary_separator()), + path_color, + ) + .when(strikethrough, Label::strikethrough), ), _ => this, } diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 8b83fa48e9b61a7200a001f4d42227b1c2302874..e7a69c0e81464ac74d02bc8a552089ddcd7db039 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -3222,10 +3222,8 @@ impl RepositorySnapshot { abs_path: &Path, path_style: PathStyle, ) -> Option { - abs_path - .strip_prefix(&work_directory_abs_path) - .ok() - .and_then(|path| RepoPath::from_std_path(path, path_style).ok()) + let rel_path = path_style.strip_prefix(abs_path, work_directory_abs_path)?; + Some(RepoPath::from_rel_path(&rel_path)) } pub fn had_conflict_on_last_merge_head_change(&self, repo_path: &RepoPath) -> bool { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index beebf5a1d133eb75fdd98184ddf7880b9cedc7e0..afc854bceb59f88a496b6fcb99e840184277c894 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -927,7 +927,7 @@ impl DirectoryLister { .map(|worktree| worktree.read(cx).abs_path().to_string_lossy().into_owned()) .or_else(|| std::env::home_dir().map(|dir| dir.to_string_lossy().into_owned())) .map(|mut s| { - s.push_str(path_style.separator()); + s.push_str(path_style.primary_separator()); s }) .unwrap_or_else(|| { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 6a7036fce81eee5810dfbc41f57119efd22cfdca..cde0b89bb9115476744ed606f16174039db62cf6 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -4837,7 +4837,7 @@ impl ProjectPanel { .collect::>(); let active_index = folded_ancestors.active_index(); let components_len = components.len(); - let delimiter = SharedString::new(path_style.separator()); + let delimiter = SharedString::new(path_style.primary_separator()); for (index, component) in components.iter().enumerate() { if index != 0 { let delimiter_target_index = index - 1; diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 564e78dc57b8b27398d79f861b538a9cc9dbf21c..499d6b04653b06c41ef4e302cfd4b4e77efc95c9 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -2192,7 +2192,7 @@ impl SettingsWindow { format!( "{}{}{}", directory_name, - path_style.separator(), + path_style.primary_separator(), path.display(path_style) ) } diff --git a/crates/toolchain_selector/src/toolchain_selector.rs b/crates/toolchain_selector/src/toolchain_selector.rs index c017483a32325d13e85a5db34566a3b0bf6e15a5..96f692694dcf6b1adaa6494a4c1cbf6905c57c7c 100644 --- a/crates/toolchain_selector/src/toolchain_selector.rs +++ b/crates/toolchain_selector/src/toolchain_selector.rs @@ -876,7 +876,7 @@ impl ToolchainSelectorDelegate { .strip_prefix(&worktree_root) .ok() .and_then(|suffix| suffix.to_str()) - .map(|suffix| format!(".{}{suffix}", path_style.separator()).into()) + .map(|suffix| format!(".{}{suffix}", path_style.primary_separator()).into()) .unwrap_or(path) } } diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 74929c6c831bcdb035756483ddbf9b2bc9ad444c..0834039e0e59ff4149614ad863bd7a07b4a2efd7 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -3,6 +3,7 @@ use globset::{Glob, GlobSet, GlobSetBuilder}; use itertools::Itertools; use regex::Regex; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; use std::cmp::Ordering; use std::error::Error; use std::fmt::{Display, Formatter}; @@ -331,13 +332,20 @@ impl PathStyle { } #[inline] - pub fn separator(&self) -> &'static str { + pub fn primary_separator(&self) -> &'static str { match self { PathStyle::Posix => "/", PathStyle::Windows => "\\", } } + pub fn separators(&self) -> &'static [&'static str] { + match self { + PathStyle::Posix => &["/"], + PathStyle::Windows => &["\\", "/"], + } + } + pub fn is_windows(&self) -> bool { *self == PathStyle::Windows } @@ -353,25 +361,54 @@ impl PathStyle { } else { Some(format!( "{left}{}{right}", - if left.ends_with(self.separator()) { + if left.ends_with(self.primary_separator()) { "" } else { - self.separator() + self.primary_separator() } )) } } pub fn split(self, path_like: &str) -> (Option<&str>, &str) { - let Some(pos) = path_like.rfind(self.separator()) else { + let Some(pos) = path_like.rfind(self.primary_separator()) else { return (None, path_like); }; - let filename_start = pos + self.separator().len(); + let filename_start = pos + self.primary_separator().len(); ( Some(&path_like[..filename_start]), &path_like[filename_start..], ) } + + pub fn strip_prefix<'a>( + &self, + child: &'a Path, + parent: &'a Path, + ) -> Option> { + let parent = parent.to_str()?; + if parent.is_empty() { + return RelPath::new(child, *self).ok(); + } + let parent = self + .separators() + .iter() + .find_map(|sep| parent.strip_suffix(sep)) + .unwrap_or(parent); + let child = child.to_str()?; + let stripped = child.strip_prefix(parent)?; + if let Some(relative) = self + .separators() + .iter() + .find_map(|sep| stripped.strip_prefix(sep)) + { + RelPath::new(relative.as_ref(), *self).ok() + } else if stripped.is_empty() { + Some(Cow::Borrowed(RelPath::empty())) + } else { + None + } + } } #[derive(Debug, Clone)] @@ -788,7 +825,7 @@ impl PathMatcher { fn check_with_end_separator(&self, path: &Path) -> bool { let path_str = path.to_string_lossy(); - let separator = self.path_style.separator(); + let separator = self.path_style.primary_separator(); if path_str.ends_with(separator) { false } else { @@ -1311,6 +1348,8 @@ impl WslPath { #[cfg(test)] mod tests { + use crate::rel_path::rel_path; + use super::*; use util_macros::perf; @@ -2480,6 +2519,89 @@ mod tests { assert_eq!(strip_path_suffix(base, suffix), None); } + #[test] + fn test_strip_prefix() { + let expected = [ + ( + PathStyle::Posix, + "/a/b/c", + "/a/b", + Some(rel_path("c").into_arc()), + ), + ( + PathStyle::Posix, + "/a/b/c", + "/a/b/", + Some(rel_path("c").into_arc()), + ), + ( + PathStyle::Posix, + "/a/b/c", + "/", + Some(rel_path("a/b/c").into_arc()), + ), + (PathStyle::Posix, "/a/b/c", "", None), + (PathStyle::Posix, "/a/b//c", "/a/b/", None), + (PathStyle::Posix, "/a/bc", "/a/b", None), + ( + PathStyle::Posix, + "/a/b/c", + "/a/b/c", + Some(rel_path("").into_arc()), + ), + ( + PathStyle::Windows, + "C:\\a\\b\\c", + "C:\\a\\b", + Some(rel_path("c").into_arc()), + ), + ( + PathStyle::Windows, + "C:\\a\\b\\c", + "C:\\a\\b\\", + Some(rel_path("c").into_arc()), + ), + ( + PathStyle::Windows, + "C:\\a\\b\\c", + "C:\\", + Some(rel_path("a/b/c").into_arc()), + ), + (PathStyle::Windows, "C:\\a\\b\\c", "", None), + (PathStyle::Windows, "C:\\a\\b\\\\c", "C:\\a\\b\\", None), + (PathStyle::Windows, "C:\\a\\bc", "C:\\a\\b", None), + ( + PathStyle::Windows, + "C:\\a\\b/c", + "C:\\a\\b", + Some(rel_path("c").into_arc()), + ), + ( + PathStyle::Windows, + "C:\\a\\b/c", + "C:\\a\\b\\", + Some(rel_path("c").into_arc()), + ), + ( + PathStyle::Windows, + "C:\\a\\b/c", + "C:\\a\\b/", + Some(rel_path("c").into_arc()), + ), + ]; + let actual = expected.clone().map(|(style, child, parent, _)| { + ( + style, + child, + parent, + style + .strip_prefix(child.as_ref(), parent.as_ref()) + .map(|rel_path| rel_path.into_arc()), + ) + }); + pretty_assertions::assert_eq!(actual, expected); + } + #[cfg(target_os = "windows")] #[test] fn test_wsl_path() { diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 70d0e93c5db5999878f2bb79c7fc42f16e6861a1..5bf0fca041cf274f38c84031e35903c9e339cc24 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -965,7 +965,7 @@ impl VimCommand { } }; - let rel_path = if args.ends_with(PathStyle::local().separator()) { + let rel_path = if args.ends_with(PathStyle::local().primary_separator()) { rel_path } else { rel_path @@ -998,7 +998,7 @@ impl VimCommand { .display(PathStyle::local()) .to_string(); if dir.is_dir { - path_string.push_str(PathStyle::local().separator()); + path_string.push_str(PathStyle::local().primary_separator()); } path_string }) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 1e8c1648dca98b267146211a9b36fb78f743fb82..a62f1b3cd1305a4e396a9fb0dd6b2f3212a321b6 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -999,7 +999,7 @@ impl Worktree { }; if worktree_relative_path.components().next().is_some() { - full_path_string.push_str(self.path_style.separator()); + full_path_string.push_str(self.path_style.primary_separator()); full_path_string.push_str(&worktree_relative_path.display(self.path_style)); } @@ -2108,8 +2108,8 @@ impl Snapshot { if path.file_name().is_some() { let mut abs_path = self.abs_path.to_string(); for component in path.components() { - if !abs_path.ends_with(self.path_style.separator()) { - abs_path.push_str(self.path_style.separator()); + if !abs_path.ends_with(self.path_style.primary_separator()) { + abs_path.push_str(self.path_style.primary_separator()); } abs_path.push_str(component); }