Represent relative paths using a dedicated, separator-agnostic type (#38744)
Closes https://github.com/zed-industries/zed/issues/38690 Closes #37353 ### Background On Windows, paths are normally separated by `\`, unlike mac and linux where they are separated by `/`. When editing code in a project that uses a different path style than your local system (e.g. remoting from Windows to Linux, using WSL, and collaboration between windows and unix users), the correct separator for a path may differ from the "native" separator. Previously, to work around this, Zed converted paths' separators in numerous places. This was applied to both absolute and relative paths, leading to incorrect conversions in some cases. ### Solution Many code paths in Zed use paths that are *relative* to either a worktree root or a git repository. This PR introduces a dedicated type for these paths called `RelPath`, which stores the path in the same way regardless of host platform, and offers `Path`-like manipulation APIs. RelPath supports *displaying* the path using either separator, so that we can display paths in a style that is determined at runtime based on the current project. The representation of absolute paths is left untouched, for now. Absolute paths are different from relative paths because (except in contexts where we know that the path refers to the local filesystem) they should generally be treated as opaque strings. Currently we use a mix of types for these paths (std::path::Path, String, SanitizedPath). Release Notes: - N/A --------- Co-authored-by: Cole Miller <cole@zed.dev> Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Co-authored-by: Peter Tripp <petertripp@gmail.com> Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com> Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
This commit is contained in:
co-authored by
Cole Miller
Piotr Osiewicz
Peter Tripp
Smit Barmase
Lukas Wirth
parent
3c626f3758
commit
03f9cf4414
+440
-721
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,11 @@ use std::path::Path;
|
||||
use anyhow::Context as _;
|
||||
use gpui::App;
|
||||
use settings::{Settings, SettingsContent};
|
||||
use util::{ResultExt, paths::PathMatcher};
|
||||
use util::{
|
||||
ResultExt,
|
||||
paths::{PathMatcher, PathStyle},
|
||||
rel_path::RelPath,
|
||||
};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct WorktreeSettings {
|
||||
@@ -14,19 +18,19 @@ pub struct WorktreeSettings {
|
||||
}
|
||||
|
||||
impl WorktreeSettings {
|
||||
pub fn is_path_private(&self, path: &Path) -> bool {
|
||||
pub fn is_path_private(&self, path: &RelPath) -> bool {
|
||||
path.ancestors()
|
||||
.any(|ancestor| self.private_files.is_match(ancestor))
|
||||
.any(|ancestor| self.private_files.is_match(ancestor.as_std_path()))
|
||||
}
|
||||
|
||||
pub fn is_path_excluded(&self, path: &Path) -> bool {
|
||||
pub fn is_path_excluded(&self, path: &RelPath) -> bool {
|
||||
path.ancestors()
|
||||
.any(|ancestor| self.file_scan_exclusions.is_match(&ancestor))
|
||||
.any(|ancestor| self.file_scan_exclusions.is_match(ancestor.as_std_path()))
|
||||
}
|
||||
|
||||
pub fn is_path_always_included(&self, path: &Path) -> bool {
|
||||
pub fn is_path_always_included(&self, path: &RelPath) -> bool {
|
||||
path.ancestors()
|
||||
.any(|ancestor| self.file_scan_inclusions.is_match(&ancestor))
|
||||
.any(|ancestor| self.file_scan_inclusions.is_match(ancestor.as_std_path()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,5 +94,6 @@ impl Settings for WorktreeSettings {
|
||||
|
||||
fn path_matchers(mut values: Vec<String>, context: &'static str) -> anyhow::Result<PathMatcher> {
|
||||
values.sort();
|
||||
PathMatcher::new(values).with_context(|| format!("Failed to parse globs from {}", context))
|
||||
PathMatcher::new(values, PathStyle::local())
|
||||
.with_context(|| format!("Failed to parse globs from {}", context))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
Entry, EntryKind, Event, PathChange, WorkDirectory, Worktree, WorktreeModelHandle,
|
||||
Entry, EntryKind, Event, PathChange, Worktree, WorktreeModelHandle,
|
||||
worktree_settings::WorktreeSettings,
|
||||
};
|
||||
use anyhow::Result;
|
||||
@@ -20,7 +20,11 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::{ResultExt, path, test::TempTree};
|
||||
use util::{
|
||||
ResultExt, path,
|
||||
rel_path::{RelPath, rel_path},
|
||||
test::TempTree,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_traversal(cx: &mut TestAppContext) {
|
||||
@@ -56,10 +60,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"),
|
||||
rel_path(""),
|
||||
rel_path(".gitignore"),
|
||||
rel_path("a"),
|
||||
rel_path("a/c"),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -67,11 +71,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"),
|
||||
rel_path(""),
|
||||
rel_path(".gitignore"),
|
||||
rel_path("a"),
|
||||
rel_path("a/b"),
|
||||
rel_path("a/c"),
|
||||
]
|
||||
);
|
||||
})
|
||||
@@ -121,14 +125,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"),
|
||||
rel_path(""),
|
||||
rel_path("lib"),
|
||||
rel_path("lib/a"),
|
||||
rel_path("lib/a/a.txt"),
|
||||
rel_path("lib/a/lib"),
|
||||
rel_path("lib/b"),
|
||||
rel_path("lib/b/b.txt"),
|
||||
rel_path("lib/b/lib"),
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -147,14 +151,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"),
|
||||
rel_path(""),
|
||||
rel_path("lib"),
|
||||
rel_path("lib/a"),
|
||||
rel_path("lib/a/a.txt"),
|
||||
rel_path("lib/a/lib-2"),
|
||||
rel_path("lib/b"),
|
||||
rel_path("lib/b/b.txt"),
|
||||
rel_path("lib/b/lib"),
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -236,18 +240,18 @@ 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),
|
||||
(rel_path(""), false),
|
||||
(rel_path("deps"), false),
|
||||
(rel_path("deps/dep-dir2"), true),
|
||||
(rel_path("deps/dep-dir3"), true),
|
||||
(rel_path("src"), false),
|
||||
(rel_path("src/a.rs"), false),
|
||||
(rel_path("src/b.rs"), false),
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
tree.entry_for_path("deps/dep-dir2").unwrap().kind,
|
||||
tree.entry_for_path(rel_path("deps/dep-dir2")).unwrap().kind,
|
||||
EntryKind::UnloadedDir
|
||||
);
|
||||
});
|
||||
@@ -256,7 +260,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![rel_path("deps/dep-dir3").into()])
|
||||
})
|
||||
.recv()
|
||||
.await;
|
||||
@@ -269,24 +273,24 @@ 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),
|
||||
(rel_path(""), false),
|
||||
(rel_path("deps"), false),
|
||||
(rel_path("deps/dep-dir2"), true),
|
||||
(rel_path("deps/dep-dir3"), true),
|
||||
(rel_path("deps/dep-dir3/deps"), true),
|
||||
(rel_path("deps/dep-dir3/src"), true),
|
||||
(rel_path("src"), false),
|
||||
(rel_path("src/a.rs"), false),
|
||||
(rel_path("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)
|
||||
(rel_path("deps/dep-dir3").into(), PathChange::Loaded),
|
||||
(rel_path("deps/dep-dir3/deps").into(), PathChange::Loaded),
|
||||
(rel_path("deps/dep-dir3/src").into(), PathChange::Loaded)
|
||||
]
|
||||
);
|
||||
|
||||
@@ -294,7 +298,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![rel_path("deps/dep-dir3/src").into()])
|
||||
})
|
||||
.recv()
|
||||
.await;
|
||||
@@ -306,17 +310,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),
|
||||
(rel_path(""), false),
|
||||
(rel_path("deps"), false),
|
||||
(rel_path("deps/dep-dir2"), true),
|
||||
(rel_path("deps/dep-dir3"), true),
|
||||
(rel_path("deps/dep-dir3/deps"), true),
|
||||
(rel_path("deps/dep-dir3/src"), true),
|
||||
(rel_path("deps/dep-dir3/src/e.rs"), true),
|
||||
(rel_path("deps/dep-dir3/src/f.rs"), true),
|
||||
(rel_path("src"), false),
|
||||
(rel_path("src/a.rs"), false),
|
||||
(rel_path("src/b.rs"), false),
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -324,13 +328,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),
|
||||
(rel_path("deps/dep-dir3/src").into(), PathChange::Loaded),
|
||||
(
|
||||
Path::new("deps/dep-dir3/src/e.rs").into(),
|
||||
rel_path("deps/dep-dir3/src/e.rs").into(),
|
||||
PathChange::Loaded
|
||||
),
|
||||
(
|
||||
Path::new("deps/dep-dir3/src/f.rs").into(),
|
||||
rel_path("deps/dep-dir3/src/f.rs").into(),
|
||||
PathChange::Loaded
|
||||
)
|
||||
]
|
||||
@@ -368,7 +372,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![rel_path(""), rel_path(OLD_NAME)]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -390,7 +394,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![rel_path(""), rel_path(NEW_NAME)]
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -446,13 +450,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),
|
||||
(rel_path(""), false),
|
||||
(rel_path(".gitignore"), false),
|
||||
(rel_path("one"), false),
|
||||
(rel_path("one/node_modules"), true),
|
||||
(rel_path("two"), false),
|
||||
(rel_path("two/x.js"), false),
|
||||
(rel_path("two/y.js"), false),
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -462,7 +466,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
|
||||
let prev_read_dir_count = fs.read_dir_call_count();
|
||||
let loaded = tree
|
||||
.update(cx, |tree, cx| {
|
||||
tree.load_file("one/node_modules/b/b1.js".as_ref(), cx)
|
||||
tree.load_file(rel_path("one/node_modules/b/b1.js"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -473,24 +477,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),
|
||||
(rel_path(""), false),
|
||||
(rel_path(".gitignore"), false),
|
||||
(rel_path("one"), false),
|
||||
(rel_path("one/node_modules"), true),
|
||||
(rel_path("one/node_modules/a"), true),
|
||||
(rel_path("one/node_modules/b"), true),
|
||||
(rel_path("one/node_modules/b/b1.js"), true),
|
||||
(rel_path("one/node_modules/b/b2.js"), true),
|
||||
(rel_path("one/node_modules/c"), true),
|
||||
(rel_path("two"), false),
|
||||
(rel_path("two/x.js"), false),
|
||||
(rel_path("two/y.js"), false),
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
loaded.file.path.as_ref(),
|
||||
Path::new("one/node_modules/b/b1.js")
|
||||
rel_path("one/node_modules/b/b1.js")
|
||||
);
|
||||
|
||||
// Only the newly-expanded directories are scanned.
|
||||
@@ -502,7 +506,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
|
||||
let prev_read_dir_count = fs.read_dir_call_count();
|
||||
let loaded = tree
|
||||
.update(cx, |tree, cx| {
|
||||
tree.load_file("one/node_modules/a/a2.js".as_ref(), cx)
|
||||
tree.load_file(rel_path("one/node_modules/a/a2.js"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -513,26 +517,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),
|
||||
(rel_path(""), false),
|
||||
(rel_path(".gitignore"), false),
|
||||
(rel_path("one"), false),
|
||||
(rel_path("one/node_modules"), true),
|
||||
(rel_path("one/node_modules/a"), true),
|
||||
(rel_path("one/node_modules/a/a1.js"), true),
|
||||
(rel_path("one/node_modules/a/a2.js"), true),
|
||||
(rel_path("one/node_modules/b"), true),
|
||||
(rel_path("one/node_modules/b/b1.js"), true),
|
||||
(rel_path("one/node_modules/b/b2.js"), true),
|
||||
(rel_path("one/node_modules/c"), true),
|
||||
(rel_path("two"), false),
|
||||
(rel_path("two/x.js"), false),
|
||||
(rel_path("two/y.js"), false),
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
loaded.file.path.as_ref(),
|
||||
Path::new("one/node_modules/a/a2.js")
|
||||
rel_path("one/node_modules/a/a2.js")
|
||||
);
|
||||
|
||||
// Only the newly-expanded directory is scanned.
|
||||
@@ -610,7 +614,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![rel_path("node_modules/d/d.js").into()])
|
||||
})
|
||||
.recv()
|
||||
.await;
|
||||
@@ -622,18 +626,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),
|
||||
(rel_path(""), false),
|
||||
(rel_path(".gitignore"), false),
|
||||
(rel_path("a"), false),
|
||||
(rel_path("a/a.js"), false),
|
||||
(rel_path("b"), false),
|
||||
(rel_path("b/b.js"), false),
|
||||
(rel_path("node_modules"), true),
|
||||
(rel_path("node_modules/c"), true),
|
||||
(rel_path("node_modules/d"), true),
|
||||
(rel_path("node_modules/d/d.js"), true),
|
||||
(rel_path("node_modules/d/e"), true),
|
||||
(rel_path("node_modules/d/f"), true),
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -654,23 +658,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),
|
||||
(rel_path(""), false),
|
||||
(rel_path(".gitignore"), false),
|
||||
(rel_path("a"), false),
|
||||
(rel_path("a/a.js"), false),
|
||||
(rel_path("b"), false),
|
||||
(rel_path("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),
|
||||
(rel_path("node_modules"), false),
|
||||
(rel_path("node_modules/c"), false),
|
||||
(rel_path("node_modules/c/c.js"), false),
|
||||
(rel_path("node_modules/d"), false),
|
||||
(rel_path("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),
|
||||
(rel_path("node_modules/d/e"), true),
|
||||
(rel_path("node_modules/d/f"), false),
|
||||
(rel_path("node_modules/d/f/f1.js"), false),
|
||||
(rel_path("node_modules/d/f/f2.js"), false),
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -711,7 +715,7 @@ async fn test_write_file(cx: &mut TestAppContext) {
|
||||
worktree
|
||||
.update(cx, |tree, cx| {
|
||||
tree.write_file(
|
||||
Path::new("tracked-dir/file.txt"),
|
||||
rel_path("tracked-dir/file.txt").into(),
|
||||
"hello".into(),
|
||||
Default::default(),
|
||||
cx,
|
||||
@@ -722,7 +726,7 @@ async fn test_write_file(cx: &mut TestAppContext) {
|
||||
worktree
|
||||
.update(cx, |tree, cx| {
|
||||
tree.write_file(
|
||||
Path::new("ignored-dir/file.txt"),
|
||||
rel_path("ignored-dir/file.txt").into(),
|
||||
"world".into(),
|
||||
Default::default(),
|
||||
cx,
|
||||
@@ -732,8 +736,12 @@ async fn test_write_file(cx: &mut TestAppContext) {
|
||||
.unwrap();
|
||||
|
||||
worktree.read_with(cx, |tree, _| {
|
||||
let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
|
||||
let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
|
||||
let tracked = tree
|
||||
.entry_for_path(rel_path("tracked-dir/file.txt"))
|
||||
.unwrap();
|
||||
let ignored = tree
|
||||
.entry_for_path(rel_path("ignored-dir/file.txt"))
|
||||
.unwrap();
|
||||
assert!(!tracked.is_ignored);
|
||||
assert!(ignored.is_ignored);
|
||||
});
|
||||
@@ -918,11 +926,11 @@ async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppC
|
||||
|
||||
tree.read_with(cx, |tree, _| {
|
||||
assert!(
|
||||
tree.entry_for_path("node_modules")
|
||||
tree.entry_for_path(rel_path("node_modules"))
|
||||
.is_some_and(|f| f.is_always_included)
|
||||
);
|
||||
assert!(
|
||||
tree.entry_for_path("node_modules/prettier/package.json")
|
||||
tree.entry_for_path(rel_path("node_modules/prettier/package.json"))
|
||||
.is_some_and(|f| f.is_always_included)
|
||||
);
|
||||
});
|
||||
@@ -941,11 +949,11 @@ async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppC
|
||||
|
||||
tree.read_with(cx, |tree, _| {
|
||||
assert!(
|
||||
tree.entry_for_path("node_modules")
|
||||
tree.entry_for_path(rel_path("node_modules"))
|
||||
.is_some_and(|f| !f.is_always_included)
|
||||
);
|
||||
assert!(
|
||||
tree.entry_for_path("node_modules/prettier/package.json")
|
||||
tree.entry_for_path(rel_path("node_modules/prettier/package.json"))
|
||||
.is_some_and(|f| !f.is_always_included)
|
||||
);
|
||||
});
|
||||
@@ -1272,7 +1280,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
||||
.update(cx, |tree, cx| {
|
||||
tree.as_local_mut()
|
||||
.unwrap()
|
||||
.create_entry("a/e".as_ref(), true, None, cx)
|
||||
.create_entry(rel_path("a/e").into(), true, None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -1282,7 +1290,10 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
tree.read_with(cx, |tree, _| {
|
||||
assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
|
||||
assert_eq!(
|
||||
tree.entry_for_path(rel_path("a/e")).unwrap().kind,
|
||||
EntryKind::Dir
|
||||
);
|
||||
});
|
||||
|
||||
let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
|
||||
@@ -1319,9 +1330,12 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
||||
|
||||
let entry = tree_fake
|
||||
.update(cx, |tree, cx| {
|
||||
tree.as_local_mut()
|
||||
.unwrap()
|
||||
.create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
|
||||
tree.as_local_mut().unwrap().create_entry(
|
||||
rel_path("a/b/c/d.txt").into(),
|
||||
false,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -1331,9 +1345,13 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
tree_fake.read_with(cx, |tree, _| {
|
||||
assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
|
||||
assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
|
||||
assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
|
||||
assert!(
|
||||
tree.entry_for_path(rel_path("a/b/c/d.txt"))
|
||||
.unwrap()
|
||||
.is_file()
|
||||
);
|
||||
assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir());
|
||||
assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir());
|
||||
});
|
||||
|
||||
let fs_real = Arc::new(RealFs::new(None, cx.executor()));
|
||||
@@ -1353,9 +1371,12 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
||||
|
||||
let entry = tree_real
|
||||
.update(cx, |tree, cx| {
|
||||
tree.as_local_mut()
|
||||
.unwrap()
|
||||
.create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
|
||||
tree.as_local_mut().unwrap().create_entry(
|
||||
rel_path("a/b/c/d.txt").into(),
|
||||
false,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -1365,17 +1386,24 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
tree_real.read_with(cx, |tree, _| {
|
||||
assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
|
||||
assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
|
||||
assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
|
||||
assert!(
|
||||
tree.entry_for_path(rel_path("a/b/c/d.txt"))
|
||||
.unwrap()
|
||||
.is_file()
|
||||
);
|
||||
assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir());
|
||||
assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir());
|
||||
});
|
||||
|
||||
// Test smallest change
|
||||
let entry = tree_real
|
||||
.update(cx, |tree, cx| {
|
||||
tree.as_local_mut()
|
||||
.unwrap()
|
||||
.create_entry("a/b/c/e.txt".as_ref(), false, None, cx)
|
||||
tree.as_local_mut().unwrap().create_entry(
|
||||
rel_path("a/b/c/e.txt").into(),
|
||||
false,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -1385,15 +1413,22 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
tree_real.read_with(cx, |tree, _| {
|
||||
assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
|
||||
assert!(
|
||||
tree.entry_for_path(rel_path("a/b/c/e.txt"))
|
||||
.unwrap()
|
||||
.is_file()
|
||||
);
|
||||
});
|
||||
|
||||
// Test largest change
|
||||
let entry = tree_real
|
||||
.update(cx, |tree, cx| {
|
||||
tree.as_local_mut()
|
||||
.unwrap()
|
||||
.create_entry("d/e/f/g.txt".as_ref(), false, None, cx)
|
||||
tree.as_local_mut().unwrap().create_entry(
|
||||
rel_path("d/e/f/g.txt").into(),
|
||||
false,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -1403,10 +1438,14 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
tree_real.read_with(cx, |tree, _| {
|
||||
assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
|
||||
assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
|
||||
assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
|
||||
assert!(tree.entry_for_path("d/").unwrap().is_dir());
|
||||
assert!(
|
||||
tree.entry_for_path(rel_path("d/e/f/g.txt"))
|
||||
.unwrap()
|
||||
.is_file()
|
||||
);
|
||||
assert!(tree.entry_for_path(rel_path("d/e/f")).unwrap().is_dir());
|
||||
assert!(tree.entry_for_path(rel_path("d/e")).unwrap().is_dir());
|
||||
assert!(tree.entry_for_path(rel_path("d")).unwrap().is_dir());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1701,37 +1740,13 @@ fn randomly_mutate_worktree(
|
||||
let entry = snapshot.entries(false, 0).choose(rng).unwrap();
|
||||
|
||||
match rng.random_range(0_u32..100) {
|
||||
0..=33 if entry.path.as_ref() != Path::new("") => {
|
||||
0..=33 if entry.path.as_ref() != RelPath::empty() => {
|
||||
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("") => {
|
||||
let other_entry = snapshot.entries(false, 0).choose(rng).unwrap();
|
||||
let new_parent_path = if other_entry.is_dir() {
|
||||
other_entry.path.clone()
|
||||
} else {
|
||||
other_entry.path.parent().unwrap().into()
|
||||
};
|
||||
let mut new_path = new_parent_path.join(random_filename(rng));
|
||||
if new_path.starts_with(&entry.path) {
|
||||
new_path = random_filename(rng).into();
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"renaming entry {:?} ({}) to {:?}",
|
||||
entry.path,
|
||||
entry.id.0,
|
||||
new_path
|
||||
);
|
||||
let task = worktree.rename_entry(entry.id, new_path, cx);
|
||||
cx.background_spawn(async move {
|
||||
task.await?.into_included().unwrap();
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
if entry.is_dir() {
|
||||
let child_path = entry.path.join(random_filename(rng));
|
||||
let child_path = entry.path.join(rel_path(&random_filename(rng)));
|
||||
let is_dir = rng.random_bool(0.3);
|
||||
log::info!(
|
||||
"creating {} at {:?}",
|
||||
@@ -1744,7 +1759,7 @@ fn randomly_mutate_worktree(
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
|
||||
log::info!("overwriting file {:?} ({})", &entry.path, entry.id.0);
|
||||
let task =
|
||||
worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
|
||||
cx.background_spawn(async move {
|
||||
@@ -1794,7 +1809,7 @@ async fn randomly_mutate_fs(
|
||||
}
|
||||
} else if rng.random_bool(0.05) {
|
||||
let ignore_dir_path = dirs.choose(rng).unwrap();
|
||||
let ignore_path = ignore_dir_path.join(*GITIGNORE);
|
||||
let ignore_path = ignore_dir_path.join(GITIGNORE);
|
||||
|
||||
let subdirs = dirs
|
||||
.iter()
|
||||
@@ -1923,101 +1938,6 @@ fn random_filename(rng: &mut impl Rng) -> String {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rename_file_to_new_directory(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
let expected_contents = "content";
|
||||
fs.as_fake()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"test.txt": expected_contents
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let worktree = Worktree::local(
|
||||
Path::new("/root"),
|
||||
true,
|
||||
fs.clone(),
|
||||
Arc::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
let entry_id = worktree.read_with(cx, |worktree, _| {
|
||||
worktree.entry_for_path("test.txt").unwrap().id
|
||||
});
|
||||
let _result = worktree
|
||||
.update(cx, |worktree, cx| {
|
||||
worktree.rename_entry(entry_id, Path::new("dir1/dir2/dir3/test.txt"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
worktree.read_with(cx, |worktree, _| {
|
||||
assert!(
|
||||
worktree.entry_for_path("test.txt").is_none(),
|
||||
"Old file should have been removed"
|
||||
);
|
||||
assert!(
|
||||
worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_some(),
|
||||
"Whole directory hierarchy and the new file should have been created"
|
||||
);
|
||||
});
|
||||
assert_eq!(
|
||||
worktree
|
||||
.update(cx, |worktree, cx| {
|
||||
worktree.load_file("dir1/dir2/dir3/test.txt".as_ref(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.text,
|
||||
expected_contents,
|
||||
"Moved file's contents should be preserved"
|
||||
);
|
||||
|
||||
let entry_id = worktree.read_with(cx, |worktree, _| {
|
||||
worktree
|
||||
.entry_for_path("dir1/dir2/dir3/test.txt")
|
||||
.unwrap()
|
||||
.id
|
||||
});
|
||||
let _result = worktree
|
||||
.update(cx, |worktree, cx| {
|
||||
worktree.rename_entry(entry_id, Path::new("dir1/dir2/test.txt"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
worktree.read_with(cx, |worktree, _| {
|
||||
assert!(
|
||||
worktree.entry_for_path("test.txt").is_none(),
|
||||
"First file should not reappear"
|
||||
);
|
||||
assert!(
|
||||
worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_none(),
|
||||
"Old file should have been removed"
|
||||
);
|
||||
assert!(
|
||||
worktree.entry_for_path("dir1/dir2/test.txt").is_some(),
|
||||
"No error should have occurred after moving into existing directory"
|
||||
);
|
||||
});
|
||||
assert_eq!(
|
||||
worktree
|
||||
.update(cx, |worktree, cx| {
|
||||
worktree.load_file("dir1/dir2/test.txt".as_ref(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.text,
|
||||
expected_contents,
|
||||
"Moved file's contents should be preserved"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -2036,48 +1956,11 @@ async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
tree.read_with(cx, |tree, _| {
|
||||
let entry = tree.entry_for_path("").unwrap();
|
||||
let entry = tree.entry_for_path(rel_path("")).unwrap();
|
||||
assert!(entry.is_private);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
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())
|
||||
);
|
||||
|
||||
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())
|
||||
);
|
||||
|
||||
let work_directory = WorkDirectory::AboveProject {
|
||||
absolute_path: Path::new("/projects/zed").into(),
|
||||
location_in_repo: Path::new("crates/gpui").into(),
|
||||
};
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
work_directory.try_unrelativize(&"crates/util/util.rs".into()),
|
||||
None,
|
||||
);
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
work_directory.unrelativize(&"crates/util/util.rs".into()),
|
||||
Path::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()
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -2259,7 +2142,7 @@ fn check_worktree_entries(
|
||||
expected_included_paths: &[&str],
|
||||
) {
|
||||
for path in expected_excluded_paths {
|
||||
let entry = tree.entry_for_path(path);
|
||||
let entry = tree.entry_for_path(rel_path(path));
|
||||
assert!(
|
||||
entry.is_none(),
|
||||
"expected path '{path}' to be excluded, but got entry: {entry:?}",
|
||||
@@ -2267,7 +2150,7 @@ fn check_worktree_entries(
|
||||
}
|
||||
for path in expected_ignored_paths {
|
||||
let entry = tree
|
||||
.entry_for_path(path)
|
||||
.entry_for_path(rel_path(path))
|
||||
.unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
|
||||
assert!(
|
||||
entry.is_ignored,
|
||||
@@ -2276,7 +2159,7 @@ fn check_worktree_entries(
|
||||
}
|
||||
for path in expected_tracked_paths {
|
||||
let entry = tree
|
||||
.entry_for_path(path)
|
||||
.entry_for_path(rel_path(path))
|
||||
.unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
|
||||
assert!(
|
||||
!entry.is_ignored || entry.is_always_included,
|
||||
@@ -2285,7 +2168,7 @@ fn check_worktree_entries(
|
||||
}
|
||||
for path in expected_included_paths {
|
||||
let entry = tree
|
||||
.entry_for_path(path)
|
||||
.entry_for_path(rel_path(path))
|
||||
.unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
|
||||
assert!(
|
||||
entry.is_always_included,
|
||||
|
||||
Reference in New Issue
Block a user