Detailed changes
@@ -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 {
@@ -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() {
@@ -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(),
)
@@ -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]);
});
@@ -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<str> {
- 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<usize> {
@@ -107,7 +107,7 @@ pub fn match_fixed_path_set(
.display(path_style)
.chars()
.collect::<Vec<_>>();
- 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())
@@ -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,
}
@@ -3222,10 +3222,8 @@ impl RepositorySnapshot {
abs_path: &Path,
path_style: PathStyle,
) -> Option<RepoPath> {
- 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 {
@@ -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(|| {
@@ -4837,7 +4837,7 @@ impl ProjectPanel {
.collect::<Vec<_>>();
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;
@@ -2192,7 +2192,7 @@ impl SettingsWindow {
format!(
"{}{}{}",
directory_name,
- path_style.separator(),
+ path_style.primary_separator(),
path.display(path_style)
)
}
@@ -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)
}
}
@@ -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<std::borrow::Cow<'a, RelPath>> {
+ 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() {
@@ -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
})
@@ -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);
}