Start work on introducing RelPath type

Max Brunsfeld created

Change summary

Cargo.lock                            |   1 
crates/git/src/commit.rs              |   0 
crates/git/src/repository.rs          |  82 ++-----
crates/project/src/git_store.rs       |   2 
crates/proto/Cargo.toml               |   1 
crates/proto/src/typed_envelope.rs    |   7 
crates/util/src/rel_path.rs           | 171 ++++++++++++++++
crates/util/src/util.rs               |   1 
crates/worktree/src/worktree.rs       |  83 +++----
crates/worktree/src/worktree_tests.rs | 293 ++++++++++++++--------------
10 files changed, 398 insertions(+), 243 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -12871,6 +12871,7 @@ dependencies = [
  "prost-build 0.9.0",
  "serde",
  "typed-path",
+ "util",
  "workspace-hack",
 ]
 

crates/git/src/repository.rs 🔗

@@ -27,6 +27,7 @@ use std::{
 use sum_tree::MapSeekTarget;
 use thiserror::Error;
 use util::command::{new_smol_command, new_std_command};
+use util::rel_path::RelPath;
 use util::{ResultExt, paths};
 use uuid::Uuid;
 
@@ -662,14 +663,22 @@ impl GitRepository for RealGitRepository {
             for (path, status_code) in changes {
                 match status_code {
                     StatusCode::Modified => {
-                        writeln!(&mut stdin, "{commit}:{}", path.display())?;
-                        writeln!(&mut stdin, "{parent_sha}:{}", path.display())?;
+                        write!(&mut stdin, "{commit}:")?;
+                        stdin.write_all(path.as_bytes())?;
+                        stdin.write_all(b"\n")?;
+                        write!(&mut stdin, "{parent_sha}:")?;
+                        stdin.write_all(path.as_bytes())?;
+                        stdin.write_all(b"\n")?;
                     }
                     StatusCode::Added => {
-                        writeln!(&mut stdin, "{commit}:{}", path.display())?;
+                        write!(&mut stdin, "{commit}:")?;
+                        stdin.write_all(path.as_bytes())?;
+                        stdin.write_all(b"\n")?;
                     }
                     StatusCode::Deleted => {
-                        writeln!(&mut stdin, "{parent_sha}:{}", path.display())?;
+                        write!(&mut stdin, "{parent_sha}:")?;
+                        stdin.write_all(path.as_bytes())?;
+                        stdin.write_all(b"\n")?;
                     }
                     _ => continue,
                 }
@@ -1652,7 +1661,7 @@ fn git_status_args(path_prefixes: &[RepoPath]) -> Vec<OsString> {
         OsString::from("-z"),
     ];
     args.extend(path_prefixes.iter().map(|path_prefix| {
-        if path_prefix.0.as_ref() == Path::new("") {
+        if path_prefix.0.as_ref() == RelPath::new("") {
             Path::new(".").into()
         } else {
             path_prefix.as_os_str().into()
@@ -1905,64 +1914,33 @@ async fn run_askpass_command(
 }
 
 pub static WORK_DIRECTORY_REPO_PATH: LazyLock<RepoPath> =
-    LazyLock::new(|| RepoPath(Path::new("").into()));
+    LazyLock::new(|| RepoPath(RelPath::new("").into()));
 
 #[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
-pub struct RepoPath(pub Arc<Path>);
+pub struct RepoPath(pub Arc<RelPath>);
 
 impl RepoPath {
-    pub fn new(path: PathBuf) -> Self {
-        debug_assert!(path.is_relative(), "Repo paths must be relative");
-
-        RepoPath(path.into())
-    }
-
     pub fn from_str(path: &str) -> Self {
-        let path = Path::new(path);
-        debug_assert!(path.is_relative(), "Repo paths must be relative");
-
-        RepoPath(path.into())
+        RepoPath(RelPath::new(path).into())
     }
 
     pub fn to_unix_style(&self) -> Cow<'_, OsStr> {
-        #[cfg(target_os = "windows")]
-        {
-            use std::ffi::OsString;
-
-            let path = self.0.as_os_str().to_string_lossy().replace("\\", "/");
-            Cow::Owned(OsString::from(path))
-        }
-        #[cfg(not(target_os = "windows"))]
-        {
-            Cow::Borrowed(self.0.as_os_str())
-        }
+        self.0.as_os_str()
     }
 }
 
-impl std::fmt::Display for RepoPath {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.0.to_string_lossy().fmt(f)
-    }
-}
-
-impl From<&Path> for RepoPath {
-    fn from(value: &Path) -> Self {
-        RepoPath::new(value.into())
+impl From<&RelPath> for RepoPath {
+    fn from(value: &RelPath) -> Self {
+        RepoPath(value.into())
     }
 }
 
-impl From<Arc<Path>> for RepoPath {
-    fn from(value: Arc<Path>) -> Self {
+impl From<Arc<RelPath>> for RepoPath {
+    fn from(value: Arc<RelPath>) -> Self {
         RepoPath(value)
     }
 }
 
-impl From<PathBuf> for RepoPath {
-    fn from(value: PathBuf) -> Self {
-        RepoPath::new(value)
-    }
-}
-
 impl From<&str> for RepoPath {
     fn from(value: &str) -> Self {
         Self::from_str(value)
@@ -1971,32 +1949,32 @@ impl From<&str> for RepoPath {
 
 impl Default for RepoPath {
     fn default() -> Self {
-        RepoPath(Path::new("").into())
+        RepoPath(RelPath::new("").into())
     }
 }
 
-impl AsRef<Path> for RepoPath {
-    fn as_ref(&self) -> &Path {
+impl AsRef<RelPath> for RepoPath {
+    fn as_ref(&self) -> &RelPath {
         self.0.as_ref()
     }
 }
 
 impl std::ops::Deref for RepoPath {
-    type Target = Path;
+    type Target = RelPath;
 
     fn deref(&self) -> &Self::Target {
         &self.0
     }
 }
 
-impl Borrow<Path> for RepoPath {
-    fn borrow(&self) -> &Path {
+impl Borrow<RelPath> for RepoPath {
+    fn borrow(&self) -> &RelPath {
         self.0.as_ref()
     }
 }
 
 #[derive(Debug)]
-pub struct RepoPathDescendants<'a>(pub &'a Path);
+pub struct RepoPathDescendants<'a>(pub &'a RelPath);
 
 impl MapSeekTarget<RepoPath> for RepoPathDescendants<'_> {
     fn cmp_cursor(&self, key: &RepoPath) -> Ordering {

crates/project/src/git_store.rs 🔗

@@ -1663,7 +1663,7 @@ impl GitStore {
             .payload
             .paths
             .into_iter()
-            .map(PathBuf::from)
+            .map(RelPath::new)
             .map(RepoPath::new)
             .collect();
 

crates/proto/Cargo.toml 🔗

@@ -20,6 +20,7 @@ doctest = false
 anyhow.workspace = true
 prost.workspace = true
 serde.workspace = true
+util.workspace = true
 workspace-hack.workspace = true
 
 [build-dependencies]

crates/proto/src/typed_envelope.rs 🔗

@@ -9,6 +9,7 @@ use std::{
     sync::Arc,
 };
 use std::{marker::PhantomData, time::Instant};
+use util::rel_path::RelPath;
 
 pub trait EnvelopedMessage: Clone + Debug + Serialize + Sized + Send + Sync + 'static {
     const NAME: &'static str;
@@ -158,6 +159,12 @@ impl FromProto for Arc<Path> {
     }
 }
 
+impl FromProto for Arc<RelPath> {
+    fn from_proto(proto: String) -> Self {
+        RelPath::new(proto.as_bytes()).into()
+    }
+}
+
 impl ToProto for PathBuf {
     fn to_proto(self) -> String {
         to_proto_path(&self)

crates/util/src/rel_path.rs 🔗

@@ -0,0 +1,171 @@
+use std::{
+    borrow::Cow,
+    ffi::OsStr,
+    path::{Display, Path, PathBuf},
+    sync::Arc,
+};
+
+#[repr(transparent)]
+#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct RelPath([u8]);
+
+impl RelPath {
+    pub fn new<S: AsRef<[u8]> + ?Sized>(s: &S) -> &Self {
+        unsafe { &*(s.as_ref() as *const [u8] as *const Self) }
+    }
+
+    pub fn components(&self) -> RelPathComponents {
+        RelPathComponents(&self.0)
+    }
+
+    pub fn file_name(&self) -> Option<&[u8]> {
+        self.components().next_back()
+    }
+
+    pub fn parent(&self) -> Option<&Self> {
+        let mut components = self.components();
+        components.next_back()?;
+        Some(Self::new(components.0))
+    }
+
+    pub fn starts_with(&self, other: &Self) -> bool {
+        let mut components = self.components();
+        other.components().all(|other_component| {
+            components
+                .next()
+                .map_or(false, |component| component == other_component)
+        })
+    }
+
+    pub fn strip_prefix(&self, other: &Self) -> Result<&Self, ()> {
+        let mut components = self.components();
+        other
+            .components()
+            .all(|other_component| {
+                components
+                    .next()
+                    .map_or(false, |component| component == other_component)
+            })
+            .then(|| Self::new(components.0))
+            .ok_or_else(|| ())
+    }
+
+    pub fn append_to_abs_path(&self, abs_path: &Path) -> PathBuf {
+        // TODO: implement this differently
+        let mut result = abs_path.to_path_buf();
+        for component in self.components() {
+            result.push(String::from_utf8_lossy(component).as_ref());
+        }
+        result
+    }
+
+    pub fn to_proto(&self) -> String {
+        String::from_utf8_lossy(&self.0).to_string()
+    }
+
+    pub fn as_bytes(&self) -> &[u8] {
+        &self.0
+    }
+
+    pub fn as_os_str(&self) -> Cow<'_, OsStr> {
+        #[cfg(target_os = "windows")]
+        {
+            use std::ffi::OsString;
+            let path = String::from_utf8_lossy(&self.0);
+            match path {
+                Cow::Borrowed(s) => Cow::Borrowed(OsStr::new(s)),
+                Cow::Owned(s) => Cow::Owned(OsString::from(s)),
+            }
+        }
+        #[cfg(not(target_os = "windows"))]
+        {
+            Cow::Borrowed(self.0.as_os_str())
+        }
+    }
+}
+
+impl From<&RelPath> for Arc<RelPath> {
+    fn from(rel_path: &RelPath) -> Self {
+        let bytes: Arc<[u8]> = Arc::from(&rel_path.0);
+        unsafe { Arc::from_raw(Arc::into_raw(bytes) as *const RelPath) }
+    }
+}
+
+impl AsRef<RelPath> for &str {
+    fn as_ref(&self) -> &RelPath {
+        RelPath::new(self)
+    }
+}
+
+impl std::fmt::Debug for RelPath {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        if let Ok(str) = std::str::from_utf8(&self.0) {
+            write!(f, "RelPath({})", str)
+        } else {
+            write!(f, "RelPath({:?})", &self.0)
+        }
+    }
+}
+
+pub struct RelPathComponents<'a>(&'a [u8]);
+
+const SEPARATOR: u8 = b'/';
+
+impl<'a> Iterator for RelPathComponents<'a> {
+    type Item = &'a [u8];
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if let Some(sep_ix) = self.0.iter().position(|&byte| byte == SEPARATOR) {
+            let (head, tail) = self.0.split_at(sep_ix);
+            self.0 = &tail[1..];
+            Some(head)
+        } else if self.0.is_empty() {
+            None
+        } else {
+            let result = self.0;
+            self.0 = &[];
+            Some(result)
+        }
+    }
+}
+
+impl<'a> DoubleEndedIterator for RelPathComponents<'a> {
+    fn next_back(&mut self) -> Option<Self::Item> {
+        if let Some(sep_ix) = self.0.iter().rposition(|&byte| byte == SEPARATOR) {
+            let (head, tail) = self.0.split_at(sep_ix);
+            self.0 = head;
+            Some(&tail[1..])
+        } else if self.0.is_empty() {
+            None
+        } else {
+            let result = self.0;
+            self.0 = &[];
+            Some(result)
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_rel_path_components() {
+        let path = RelPath::new("foo/bar/baz");
+        let mut components = path.components();
+        assert_eq!(components.next(), Some("foo".as_bytes()));
+        assert_eq!(components.next(), Some("bar".as_bytes()));
+        assert_eq!(components.next(), Some("baz".as_bytes()));
+        assert_eq!(components.next(), None);
+    }
+
+    #[test]
+    fn test_rel_path_parent() {
+        assert_eq!(
+            RelPath::new("foo/bar/baz").parent().unwrap(),
+            RelPath::new("foo/bar")
+        );
+        assert_eq!(RelPath::new("foo").parent().unwrap(), RelPath::new(""));
+        assert_eq!(RelPath::new("").parent(), None);
+    }
+}

crates/util/src/util.rs 🔗

@@ -5,6 +5,7 @@ pub mod fs;
 pub mod markdown;
 pub mod paths;
 pub mod redact;
+pub mod rel_path;
 pub mod schemars;
 pub mod serde;
 pub mod shell_env;

crates/worktree/src/worktree.rs 🔗

@@ -67,6 +67,7 @@ use text::{LineEnding, Rope};
 use util::{
     ResultExt,
     paths::{PathMatcher, SanitizedPath, home_dir},
+    rel_path::RelPath,
 };
 pub use worktree_settings::WorktreeSettings;
 
@@ -132,12 +133,12 @@ pub struct LocalWorktree {
 }
 
 pub struct PathPrefixScanRequest {
-    path: Arc<Path>,
+    path: Arc<RelPath>,
     done: SmallVec<[barrier::Sender; 1]>,
 }
 
 struct ScanRequest {
-    relative_paths: Vec<Arc<Path>>,
+    relative_paths: Vec<Arc<RelPath>>,
     done: SmallVec<[barrier::Sender; 1]>,
 }
 
@@ -186,7 +187,7 @@ pub struct Snapshot {
 #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
 pub enum WorkDirectory {
     InProject {
-        relative_path: Arc<Path>,
+        relative_path: Arc<RelPath>,
     },
     AboveProject {
         absolute_path: Arc<Path>,
@@ -197,7 +198,7 @@ pub enum WorkDirectory {
 impl WorkDirectory {
     #[cfg(test)]
     fn in_project(path: &str) -> Self {
-        let path = Path::new(path);
+        let path = RelPath::new(path);
         Self::InProject {
             relative_path: path.into(),
         }
@@ -232,9 +233,8 @@ impl WorkDirectory {
     /// is a repository in a directory between these two paths
     /// external .git folder in a parent folder of the project root.
     #[track_caller]
-    pub fn directory_contains(&self, path: impl AsRef<Path>) -> bool {
+    pub fn directory_contains(&self, path: impl AsRef<RelPath>) -> bool {
         let path = path.as_ref();
-        debug_assert!(path.is_relative());
         match self {
             WorkDirectory::InProject { relative_path } => path.starts_with(relative_path),
             WorkDirectory::AboveProject { .. } => true,
@@ -246,9 +246,8 @@ impl WorkDirectory {
     /// If the root of the repository (and its .git folder) are located in a parent folder
     /// of the project root folder, then the returned RepoPath is relative to the root
     /// of the repository and not a valid path inside the project.
-    pub fn relativize(&self, path: &Path) -> Result<RepoPath> {
+    pub fn relativize(&self, path: &RelPath) -> Result<RepoPath> {
         // path is assumed to be relative to worktree root.
-        debug_assert!(path.is_relative());
         match self {
             WorkDirectory::InProject { relative_path } => Ok(path
                 .strip_prefix(relative_path)
@@ -842,12 +841,12 @@ impl Worktree {
 
     pub fn create_entry(
         &mut self,
-        path: impl Into<Arc<Path>>,
+        path: impl Into<Arc<RelPath>>,
         is_directory: bool,
         content: Option<Vec<u8>>,
         cx: &Context<Worktree>,
     ) -> Task<Result<CreatedEntry>> {
-        let path: Arc<Path> = path.into();
+        let path: Arc<RelPath> = path.into();
         let worktree_id = self.id();
         match self {
             Worktree::Local(this) => this.create_entry(path, is_directory, content, cx),
@@ -914,7 +913,7 @@ impl Worktree {
         Some(task)
     }
 
-    fn get_children_ids_recursive(&self, path: &Path, ids: &mut Vec<ProjectEntryId>) {
+    fn get_children_ids_recursive(&self, path: &RelPath, ids: &mut Vec<ProjectEntryId>) {
         let children_iter = self.child_entries(path);
         for child in children_iter {
             ids.push(child.id);
@@ -1575,7 +1574,7 @@ impl LocalWorktree {
 
     fn create_entry(
         &self,
-        path: impl Into<Arc<Path>>,
+        path: impl Into<Arc<RelPath>>,
         is_dir: bool,
         content: Option<Vec<u8>>,
         cx: &Context<Worktree>,
@@ -1975,7 +1974,7 @@ impl LocalWorktree {
         }))
     }
 
-    fn refresh_entries_for_paths(&self, paths: Vec<Arc<Path>>) -> barrier::Receiver {
+    fn refresh_entries_for_paths(&self, paths: Vec<Arc<RelPath>>) -> barrier::Receiver {
         let (tx, rx) = barrier::channel();
         self.scan_requests_tx
             .try_send(ScanRequest {
@@ -1987,11 +1986,14 @@ impl LocalWorktree {
     }
 
     #[cfg(feature = "test-support")]
-    pub fn manually_refresh_entries_for_paths(&self, paths: Vec<Arc<Path>>) -> barrier::Receiver {
+    pub fn manually_refresh_entries_for_paths(
+        &self,
+        paths: Vec<Arc<RelPath>>,
+    ) -> barrier::Receiver {
         self.refresh_entries_for_paths(paths)
     }
 
-    pub fn add_path_prefix_to_scan(&self, path_prefix: Arc<Path>) -> barrier::Receiver {
+    pub fn add_path_prefix_to_scan(&self, path_prefix: Arc<RelPath>) -> barrier::Receiver {
         let (tx, rx) = barrier::channel();
         self.path_prefixes_to_scan_tx
             .try_send(PathPrefixScanRequest {
@@ -2004,8 +2006,8 @@ impl LocalWorktree {
 
     fn refresh_entry(
         &self,
-        path: Arc<Path>,
-        old_path: Option<Arc<Path>>,
+        path: Arc<RelPath>,
+        old_path: Option<Arc<RelPath>>,
         cx: &Context<Worktree>,
     ) -> Task<Result<Option<Entry>>> {
         if self.settings.is_path_excluded(&path) {
@@ -2403,18 +2405,8 @@ impl Snapshot {
         }
     }
 
-    pub fn absolutize(&self, path: &Path) -> Result<PathBuf> {
-        if path
-            .components()
-            .any(|component| !matches!(component, std::path::Component::Normal(_)))
-        {
-            anyhow::bail!("invalid path");
-        }
-        if path.file_name().is_some() {
-            Ok(self.abs_path.as_path().join(path))
-        } else {
-            Ok(self.abs_path.as_path().to_path_buf())
-        }
+    pub fn absolutize(&self, path: &RelPath) -> Result<PathBuf> {
+        Ok(path.append_to_abs_path(&self.abs_path.0))
     }
 
     pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool {
@@ -2585,7 +2577,7 @@ impl Snapshot {
         include_files: bool,
         include_dirs: bool,
         include_ignored: bool,
-        path: &Path,
+        path: &RelPath,
     ) -> Traversal<'_> {
         Traversal::new(self, include_files, include_dirs, include_ignored, path)
     }
@@ -2602,15 +2594,15 @@ impl Snapshot {
         self.traverse_from_offset(true, true, include_ignored, start)
     }
 
-    pub fn paths(&self) -> impl Iterator<Item = &Arc<Path>> {
-        let empty_path = Path::new("");
+    pub fn paths(&self) -> impl Iterator<Item = &Arc<RelPath>> {
+        let empty_path = RelPath::new("");
         self.entries_by_path
             .cursor::<()>(&())
             .filter(move |entry| entry.path.as_ref() != empty_path)
             .map(|entry| &entry.path)
     }
 
-    pub fn child_entries<'a>(&'a self, parent_path: &'a Path) -> ChildEntriesIter<'a> {
+    pub fn child_entries<'a>(&'a self, parent_path: &'a RelPath) -> ChildEntriesIter<'a> {
         let options = ChildEntriesOptions {
             include_files: true,
             include_dirs: true,
@@ -2621,7 +2613,7 @@ impl Snapshot {
 
     pub fn child_entries_with_options<'a>(
         &'a self,
-        parent_path: &'a Path,
+        parent_path: &'a RelPath,
         options: ChildEntriesOptions,
     ) -> ChildEntriesIter<'a> {
         let mut cursor = self.entries_by_path.cursor(&());
@@ -2659,9 +2651,8 @@ impl Snapshot {
         self.scan_id
     }
 
-    pub fn entry_for_path(&self, path: impl AsRef<Path>) -> Option<&Entry> {
+    pub fn entry_for_path(&self, path: impl AsRef<RelPath>) -> Option<&Entry> {
         let path = path.as_ref();
-        debug_assert!(path.is_relative());
         self.traverse_from_path(true, true, true, path)
             .entry()
             .and_then(|entry| {
@@ -3436,7 +3427,7 @@ impl File {
 pub struct Entry {
     pub id: ProjectEntryId,
     pub kind: EntryKind,
-    pub path: Arc<Path>,
+    pub path: Arc<RelPath>,
     pub inode: u64,
     pub mtime: Option<MTime>,
 
@@ -3510,7 +3501,7 @@ pub struct UpdatedGitRepository {
     pub common_dir_abs_path: Option<Arc<Path>>,
 }
 
-pub type UpdatedEntriesSet = Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>;
+pub type UpdatedEntriesSet = Arc<[(Arc<RelPath>, ProjectEntryId, PathChange)]>;
 pub type UpdatedGitRepositoriesSet = Arc<[UpdatedGitRepository]>;
 
 #[derive(Clone, Debug)]
@@ -3786,7 +3777,7 @@ impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for ProjectEntryId {
 }
 
 #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
-pub struct PathKey(pub Arc<Path>);
+pub struct PathKey(pub Arc<RelPath>);
 
 impl Default for PathKey {
     fn default() -> Self {
@@ -5188,7 +5179,7 @@ impl WorktreeModelHandle for Entity<Worktree> {
 
 #[derive(Clone, Debug)]
 struct TraversalProgress<'a> {
-    max_path: &'a Path,
+    max_path: &'a RelPath,
     count: usize,
     non_ignored_count: usize,
     file_count: usize,
@@ -5250,7 +5241,7 @@ impl<'a> Traversal<'a> {
         include_files: bool,
         include_dirs: bool,
         include_ignored: bool,
-        start_path: &Path,
+        start_path: &RelPath,
     ) -> Self {
         let mut cursor = snapshot.entries_by_path.cursor(&());
         cursor.seek(&TraversalTarget::path(start_path), Bias::Left);
@@ -5343,12 +5334,12 @@ impl<'a> Iterator for Traversal<'a> {
 
 #[derive(Debug, Clone, Copy)]
 pub enum PathTarget<'a> {
-    Path(&'a Path),
-    Successor(&'a Path),
+    Path(&'a RelPath),
+    Successor(&'a RelPath),
 }
 
 impl PathTarget<'_> {
-    fn cmp_path(&self, other: &Path) -> Ordering {
+    fn cmp_path(&self, other: &RelPath) -> Ordering {
         match self {
             PathTarget::Path(path) => path.cmp(&other),
             PathTarget::Successor(path) => {
@@ -5386,11 +5377,11 @@ enum TraversalTarget<'a> {
 }
 
 impl<'a> TraversalTarget<'a> {
-    fn path(path: &'a Path) -> Self {
+    fn path(path: &'a RelPath) -> Self {
         Self::Path(PathTarget::Path(path))
     }
 
-    fn successor(path: &'a Path) -> Self {
+    fn successor(path: &'a RelPath) -> Self {
         Self::Path(PathTarget::Successor(path))
     }
 

crates/worktree/src/worktree_tests.rs 🔗

@@ -20,7 +20,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::{ResultExt, path, test::TempTree};
+use util::{ResultExt, path, rel_path::RelPath, test::TempTree};
 
 #[gpui::test]
 async fn test_traversal(cx: &mut TestAppContext) {
@@ -56,10 +56,10 @@ async fn test_traversal(cx: &mut TestAppContext) {
                 .map(|entry| entry.path.as_ref())
                 .collect::<Vec<_>>(),
             vec![
-                Path::new(""),
-                Path::new(".gitignore"),
-                Path::new("a"),
-                Path::new("a/c"),
+                RelPath::new(""),
+                RelPath::new(".gitignore"),
+                RelPath::new("a"),
+                RelPath::new("a/c"),
             ]
         );
         assert_eq!(
@@ -67,11 +67,11 @@ async fn test_traversal(cx: &mut TestAppContext) {
                 .map(|entry| entry.path.as_ref())
                 .collect::<Vec<_>>(),
             vec![
-                Path::new(""),
-                Path::new(".gitignore"),
-                Path::new("a"),
-                Path::new("a/b"),
-                Path::new("a/c"),
+                RelPath::new(""),
+                RelPath::new(".gitignore"),
+                RelPath::new("a"),
+                RelPath::new("a/b"),
+                RelPath::new("a/c"),
             ]
         );
     })
@@ -121,14 +121,14 @@ async fn test_circular_symlinks(cx: &mut TestAppContext) {
                 .map(|entry| entry.path.as_ref())
                 .collect::<Vec<_>>(),
             vec![
-                Path::new(""),
-                Path::new("lib"),
-                Path::new("lib/a"),
-                Path::new("lib/a/a.txt"),
-                Path::new("lib/a/lib"),
-                Path::new("lib/b"),
-                Path::new("lib/b/b.txt"),
-                Path::new("lib/b/lib"),
+                RelPath::new(""),
+                RelPath::new("lib"),
+                RelPath::new("lib/a"),
+                RelPath::new("lib/a/a.txt"),
+                RelPath::new("lib/a/lib"),
+                RelPath::new("lib/b"),
+                RelPath::new("lib/b/b.txt"),
+                RelPath::new("lib/b/lib"),
             ]
         );
     });
@@ -147,14 +147,14 @@ async fn test_circular_symlinks(cx: &mut TestAppContext) {
                 .map(|entry| entry.path.as_ref())
                 .collect::<Vec<_>>(),
             vec![
-                Path::new(""),
-                Path::new("lib"),
-                Path::new("lib/a"),
-                Path::new("lib/a/a.txt"),
-                Path::new("lib/a/lib-2"),
-                Path::new("lib/b"),
-                Path::new("lib/b/b.txt"),
-                Path::new("lib/b/lib"),
+                RelPath::new(""),
+                RelPath::new("lib"),
+                RelPath::new("lib/a"),
+                RelPath::new("lib/a/a.txt"),
+                RelPath::new("lib/a/lib-2"),
+                RelPath::new("lib/b"),
+                RelPath::new("lib/b/b.txt"),
+                RelPath::new("lib/b/lib"),
             ]
         );
     });
@@ -236,18 +236,20 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
                 .map(|entry| (entry.path.as_ref(), entry.is_external))
                 .collect::<Vec<_>>(),
             vec![
-                (Path::new(""), false),
-                (Path::new("deps"), false),
-                (Path::new("deps/dep-dir2"), true),
-                (Path::new("deps/dep-dir3"), true),
-                (Path::new("src"), false),
-                (Path::new("src/a.rs"), false),
-                (Path::new("src/b.rs"), false),
+                (RelPath::new(""), false),
+                (RelPath::new("deps"), false),
+                (RelPath::new("deps/dep-dir2"), true),
+                (RelPath::new("deps/dep-dir3"), true),
+                (RelPath::new("src"), false),
+                (RelPath::new("src/a.rs"), false),
+                (RelPath::new("src/b.rs"), false),
             ]
         );
 
         assert_eq!(
-            tree.entry_for_path("deps/dep-dir2").unwrap().kind,
+            tree.entry_for_path(RelPath::new("deps/dep-dir2"))
+                .unwrap()
+                .kind,
             EntryKind::UnloadedDir
         );
     });
@@ -256,7 +258,7 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
     tree.read_with(cx, |tree, _| {
         tree.as_local()
             .unwrap()
-            .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()])
+            .refresh_entries_for_paths(vec![RelPath::new("deps/dep-dir3").into()])
     })
     .recv()
     .await;
@@ -269,24 +271,27 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
                 .map(|entry| (entry.path.as_ref(), entry.is_external))
                 .collect::<Vec<_>>(),
             vec![
-                (Path::new(""), false),
-                (Path::new("deps"), false),
-                (Path::new("deps/dep-dir2"), true),
-                (Path::new("deps/dep-dir3"), true),
-                (Path::new("deps/dep-dir3/deps"), true),
-                (Path::new("deps/dep-dir3/src"), true),
-                (Path::new("src"), false),
-                (Path::new("src/a.rs"), false),
-                (Path::new("src/b.rs"), false),
+                (RelPath::new(""), false),
+                (RelPath::new("deps"), false),
+                (RelPath::new("deps/dep-dir2"), true),
+                (RelPath::new("deps/dep-dir3"), true),
+                (RelPath::new("deps/dep-dir3/deps"), true),
+                (RelPath::new("deps/dep-dir3/src"), true),
+                (RelPath::new("src"), false),
+                (RelPath::new("src/a.rs"), false),
+                (RelPath::new("src/b.rs"), false),
             ]
         );
     });
     assert_eq!(
         mem::take(&mut *tree_updates.lock()),
         &[
-            (Path::new("deps/dep-dir3").into(), PathChange::Loaded),
-            (Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded),
-            (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded)
+            (RelPath::new("deps/dep-dir3").into(), PathChange::Loaded),
+            (
+                RelPath::new("deps/dep-dir3/deps").into(),
+                PathChange::Loaded
+            ),
+            (RelPath::new("deps/dep-dir3/src").into(), PathChange::Loaded)
         ]
     );
 
@@ -294,7 +299,7 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
     tree.read_with(cx, |tree, _| {
         tree.as_local()
             .unwrap()
-            .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()])
+            .refresh_entries_for_paths(vec![RelPath::new("deps/dep-dir3/src").into()])
     })
     .recv()
     .await;
@@ -306,17 +311,17 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
                 .map(|entry| (entry.path.as_ref(), entry.is_external))
                 .collect::<Vec<_>>(),
             vec![
-                (Path::new(""), false),
-                (Path::new("deps"), false),
-                (Path::new("deps/dep-dir2"), true),
-                (Path::new("deps/dep-dir3"), true),
-                (Path::new("deps/dep-dir3/deps"), true),
-                (Path::new("deps/dep-dir3/src"), true),
-                (Path::new("deps/dep-dir3/src/e.rs"), true),
-                (Path::new("deps/dep-dir3/src/f.rs"), true),
-                (Path::new("src"), false),
-                (Path::new("src/a.rs"), false),
-                (Path::new("src/b.rs"), false),
+                (RelPath::new(""), false),
+                (RelPath::new("deps"), false),
+                (RelPath::new("deps/dep-dir2"), true),
+                (RelPath::new("deps/dep-dir3"), true),
+                (RelPath::new("deps/dep-dir3/deps"), true),
+                (RelPath::new("deps/dep-dir3/src"), true),
+                (RelPath::new("deps/dep-dir3/src/e.rs"), true),
+                (RelPath::new("deps/dep-dir3/src/f.rs"), true),
+                (RelPath::new("src"), false),
+                (RelPath::new("src/a.rs"), false),
+                (RelPath::new("src/b.rs"), false),
             ]
         );
     });
@@ -324,13 +329,13 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
     assert_eq!(
         mem::take(&mut *tree_updates.lock()),
         &[
-            (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded),
+            (RelPath::new("deps/dep-dir3/src").into(), PathChange::Loaded),
             (
-                Path::new("deps/dep-dir3/src/e.rs").into(),
+                RelPath::new("deps/dep-dir3/src/e.rs").into(),
                 PathChange::Loaded
             ),
             (
-                Path::new("deps/dep-dir3/src/f.rs").into(),
+                RelPath::new("deps/dep-dir3/src/f.rs").into(),
                 PathChange::Loaded
             )
         ]
@@ -368,7 +373,7 @@ async fn test_renaming_case_only(cx: &mut TestAppContext) {
             tree.entries(true, 0)
                 .map(|entry| entry.path.as_ref())
                 .collect::<Vec<_>>(),
-            vec![Path::new(""), Path::new(OLD_NAME)]
+            vec![RelPath::new(""), RelPath::new(OLD_NAME)]
         );
     });
 
@@ -390,7 +395,7 @@ async fn test_renaming_case_only(cx: &mut TestAppContext) {
             tree.entries(true, 0)
                 .map(|entry| entry.path.as_ref())
                 .collect::<Vec<_>>(),
-            vec![Path::new(""), Path::new(NEW_NAME)]
+            vec![RelPath::new(""), RelPath::new(NEW_NAME)]
         );
     });
 }
@@ -446,13 +451,13 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
                 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
                 .collect::<Vec<_>>(),
             vec![
-                (Path::new(""), false),
-                (Path::new(".gitignore"), false),
-                (Path::new("one"), false),
-                (Path::new("one/node_modules"), true),
-                (Path::new("two"), false),
-                (Path::new("two/x.js"), false),
-                (Path::new("two/y.js"), false),
+                (RelPath::new(""), false),
+                (RelPath::new(".gitignore"), false),
+                (RelPath::new("one"), false),
+                (RelPath::new("one/node_modules"), true),
+                (RelPath::new("two"), false),
+                (RelPath::new("two/x.js"), false),
+                (RelPath::new("two/y.js"), false),
             ]
         );
     });
@@ -473,24 +478,24 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
                 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
                 .collect::<Vec<_>>(),
             vec![
-                (Path::new(""), false),
-                (Path::new(".gitignore"), false),
-                (Path::new("one"), false),
-                (Path::new("one/node_modules"), true),
-                (Path::new("one/node_modules/a"), true),
-                (Path::new("one/node_modules/b"), true),
-                (Path::new("one/node_modules/b/b1.js"), true),
-                (Path::new("one/node_modules/b/b2.js"), true),
-                (Path::new("one/node_modules/c"), true),
-                (Path::new("two"), false),
-                (Path::new("two/x.js"), false),
-                (Path::new("two/y.js"), false),
+                (RelPath::new(""), false),
+                (RelPath::new(".gitignore"), false),
+                (RelPath::new("one"), false),
+                (RelPath::new("one/node_modules"), true),
+                (RelPath::new("one/node_modules/a"), true),
+                (RelPath::new("one/node_modules/b"), true),
+                (RelPath::new("one/node_modules/b/b1.js"), true),
+                (RelPath::new("one/node_modules/b/b2.js"), true),
+                (RelPath::new("one/node_modules/c"), true),
+                (RelPath::new("two"), false),
+                (RelPath::new("two/x.js"), false),
+                (RelPath::new("two/y.js"), false),
             ]
         );
 
         assert_eq!(
             loaded.file.path.as_ref(),
-            Path::new("one/node_modules/b/b1.js")
+            RelPath::new("one/node_modules/b/b1.js")
         );
 
         // Only the newly-expanded directories are scanned.
@@ -513,26 +518,26 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
                 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
                 .collect::<Vec<_>>(),
             vec![
-                (Path::new(""), false),
-                (Path::new(".gitignore"), false),
-                (Path::new("one"), false),
-                (Path::new("one/node_modules"), true),
-                (Path::new("one/node_modules/a"), true),
-                (Path::new("one/node_modules/a/a1.js"), true),
-                (Path::new("one/node_modules/a/a2.js"), true),
-                (Path::new("one/node_modules/b"), true),
-                (Path::new("one/node_modules/b/b1.js"), true),
-                (Path::new("one/node_modules/b/b2.js"), true),
-                (Path::new("one/node_modules/c"), true),
-                (Path::new("two"), false),
-                (Path::new("two/x.js"), false),
-                (Path::new("two/y.js"), false),
+                (RelPath::new(""), false),
+                (RelPath::new(".gitignore"), false),
+                (RelPath::new("one"), false),
+                (RelPath::new("one/node_modules"), true),
+                (RelPath::new("one/node_modules/a"), true),
+                (RelPath::new("one/node_modules/a/a1.js"), true),
+                (RelPath::new("one/node_modules/a/a2.js"), true),
+                (RelPath::new("one/node_modules/b"), true),
+                (RelPath::new("one/node_modules/b/b1.js"), true),
+                (RelPath::new("one/node_modules/b/b2.js"), true),
+                (RelPath::new("one/node_modules/c"), true),
+                (RelPath::new("two"), false),
+                (RelPath::new("two/x.js"), false),
+                (RelPath::new("two/y.js"), false),
             ]
         );
 
         assert_eq!(
             loaded.file.path.as_ref(),
-            Path::new("one/node_modules/a/a2.js")
+            RelPath::new("one/node_modules/a/a2.js")
         );
 
         // Only the newly-expanded directory is scanned.
@@ -592,7 +597,7 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
     .await;
 
     let tree = Worktree::local(
-        Path::new("/root"),
+        RelPath::new("/root"),
         true,
         fs.clone(),
         Default::default(),
@@ -610,7 +615,7 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
     tree.read_with(cx, |tree, _| {
         tree.as_local()
             .unwrap()
-            .refresh_entries_for_paths(vec![Path::new("node_modules/d/d.js").into()])
+            .refresh_entries_for_paths(vec![RelPath::new("node_modules/d/d.js").into()])
     })
     .recv()
     .await;
@@ -622,18 +627,18 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
                 .map(|e| (e.path.as_ref(), e.is_ignored))
                 .collect::<Vec<_>>(),
             &[
-                (Path::new(""), false),
-                (Path::new(".gitignore"), false),
-                (Path::new("a"), false),
-                (Path::new("a/a.js"), false),
-                (Path::new("b"), false),
-                (Path::new("b/b.js"), false),
-                (Path::new("node_modules"), true),
-                (Path::new("node_modules/c"), true),
-                (Path::new("node_modules/d"), true),
-                (Path::new("node_modules/d/d.js"), true),
-                (Path::new("node_modules/d/e"), true),
-                (Path::new("node_modules/d/f"), true),
+                (RelPath::new(""), false),
+                (RelPath::new(".gitignore"), false),
+                (RelPath::new("a"), false),
+                (RelPath::new("a/a.js"), false),
+                (RelPath::new("b"), false),
+                (RelPath::new("b/b.js"), false),
+                (RelPath::new("node_modules"), true),
+                (RelPath::new("node_modules/c"), true),
+                (RelPath::new("node_modules/d"), true),
+                (RelPath::new("node_modules/d/d.js"), true),
+                (RelPath::new("node_modules/d/e"), true),
+                (RelPath::new("node_modules/d/f"), true),
             ]
         );
     });
@@ -654,23 +659,23 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
                 .map(|e| (e.path.as_ref(), e.is_ignored))
                 .collect::<Vec<_>>(),
             &[
-                (Path::new(""), false),
-                (Path::new(".gitignore"), false),
-                (Path::new("a"), false),
-                (Path::new("a/a.js"), false),
-                (Path::new("b"), false),
-                (Path::new("b/b.js"), false),
+                (RelPath::new(""), false),
+                (RelPath::new(".gitignore"), false),
+                (RelPath::new("a"), false),
+                (RelPath::new("a/a.js"), false),
+                (RelPath::new("b"), false),
+                (RelPath::new("b/b.js"), false),
                 // This directory is no longer ignored
-                (Path::new("node_modules"), false),
-                (Path::new("node_modules/c"), false),
-                (Path::new("node_modules/c/c.js"), false),
-                (Path::new("node_modules/d"), false),
-                (Path::new("node_modules/d/d.js"), false),
+                (RelPath::new("node_modules"), false),
+                (RelPath::new("node_modules/c"), false),
+                (RelPath::new("node_modules/c/c.js"), false),
+                (RelPath::new("node_modules/d"), false),
+                (RelPath::new("node_modules/d/d.js"), false),
                 // This subdirectory is now ignored
-                (Path::new("node_modules/d/e"), true),
-                (Path::new("node_modules/d/f"), false),
-                (Path::new("node_modules/d/f/f1.js"), false),
-                (Path::new("node_modules/d/f/f2.js"), false),
+                (RelPath::new("node_modules/d/e"), true),
+                (RelPath::new("node_modules/d/f"), false),
+                (RelPath::new("node_modules/d/f/f1.js"), false),
+                (RelPath::new("node_modules/d/f/f2.js"), false),
             ]
         );
     });
@@ -711,7 +716,7 @@ async fn test_write_file(cx: &mut TestAppContext) {
     worktree
         .update(cx, |tree, cx| {
             tree.write_file(
-                Path::new("tracked-dir/file.txt"),
+                RelPath::new("tracked-dir/file.txt"),
                 "hello".into(),
                 Default::default(),
                 cx,
@@ -722,7 +727,7 @@ async fn test_write_file(cx: &mut TestAppContext) {
     worktree
         .update(cx, |tree, cx| {
             tree.write_file(
-                Path::new("ignored-dir/file.txt"),
+                RelPath::new("ignored-dir/file.txt"),
                 "world".into(),
                 Default::default(),
                 cx,
@@ -1421,7 +1426,7 @@ async fn test_random_worktree_operations_during_initial_scan(
         .map(|o| o.parse().unwrap())
         .unwrap_or(20);
 
-    let root_dir = Path::new(path!("/test"));
+    let root_dir = RelPath::new(path!("/test"));
     let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
     fs.as_fake().insert_tree(root_dir, json!({})).await;
     for _ in 0..initial_entries {
@@ -1512,7 +1517,7 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng)
         .map(|o| o.parse().unwrap())
         .unwrap_or(20);
 
-    let root_dir = Path::new(path!("/test"));
+    let root_dir = RelPath::new(path!("/test"));
     let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
     fs.as_fake().insert_tree(root_dir, json!({})).await;
     for _ in 0..initial_entries {
@@ -1702,11 +1707,11 @@ fn randomly_mutate_worktree(
     let entry = snapshot.entries(false, 0).choose(rng).unwrap();
 
     match rng.gen_range(0_u32..100) {
-        0..=33 if entry.path.as_ref() != Path::new("") => {
+        0..=33 if entry.path.as_ref() != RelPath::new("") => {
             log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
             worktree.delete_entry(entry.id, false, cx).unwrap()
         }
-        ..=66 if entry.path.as_ref() != Path::new("") => {
+        ..=66 if entry.path.as_ref() != RelPath::new("") => {
             let other_entry = snapshot.entries(false, 0).choose(rng).unwrap();
             let new_parent_path = if other_entry.is_dir() {
                 other_entry.path.clone()
@@ -1759,7 +1764,7 @@ fn randomly_mutate_worktree(
 
 async fn randomly_mutate_fs(
     fs: &Arc<dyn Fs>,
-    root_path: &Path,
+    root_path: &RelPath,
     insertion_probability: f64,
     rng: &mut impl Rng,
 ) {
@@ -1931,7 +1936,7 @@ async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
     fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
         .await;
     let tree = Worktree::local(
-        Path::new("/.env"),
+        RelPath::new("/.env"),
         true,
         fs.clone(),
         Default::default(),
@@ -1952,18 +1957,18 @@ fn test_unrelativize() {
     let work_directory = WorkDirectory::in_project("");
     pretty_assertions::assert_eq!(
         work_directory.try_unrelativize(&"crates/gpui/gpui.rs".into()),
-        Some(Path::new("crates/gpui/gpui.rs").into())
+        Some(RelPath::new("crates/gpui/gpui.rs").into())
     );
 
     let work_directory = WorkDirectory::in_project("vendor/some-submodule");
     pretty_assertions::assert_eq!(
         work_directory.try_unrelativize(&"src/thing.c".into()),
-        Some(Path::new("vendor/some-submodule/src/thing.c").into())
+        Some(RelPath::new("vendor/some-submodule/src/thing.c").into())
     );
 
     let work_directory = WorkDirectory::AboveProject {
-        absolute_path: Path::new("/projects/zed").into(),
-        location_in_repo: Path::new("crates/gpui").into(),
+        absolute_path: RelPath::new("/projects/zed").into(),
+        location_in_repo: RelPath::new("crates/gpui").into(),
     };
 
     pretty_assertions::assert_eq!(
@@ -1973,14 +1978,14 @@ fn test_unrelativize() {
 
     pretty_assertions::assert_eq!(
         work_directory.unrelativize(&"crates/util/util.rs".into()),
-        Path::new("../util/util.rs").into()
+        RelPath::new("../util/util.rs").into()
     );
 
     pretty_assertions::assert_eq!(work_directory.try_unrelativize(&"README.md".into()), None,);
 
     pretty_assertions::assert_eq!(
         work_directory.unrelativize(&"README.md".into()),
-        Path::new("../../README.md").into()
+        RelPath::new("../../README.md").into()
     );
 }
 
@@ -2023,7 +2028,7 @@ async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestA
             .map(|entry| entry.work_directory_abs_path.clone())
             .collect::<Vec<_>>()
     });
-    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
+    pretty_assertions::assert_eq!(repos, [RelPath::new(path!("/root")).into()]);
 
     eprintln!(">>>>>>>>>> touch");
     fs.touch_path(path!("/root/subproject")).await;
@@ -2043,7 +2048,7 @@ async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestA
             .map(|entry| entry.work_directory_abs_path.clone())
             .collect::<Vec<_>>()
     });
-    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
+    pretty_assertions::assert_eq!(repos, [RelPath::new(path!("/root")).into()]);
 }
 
 #[track_caller]