Split out RelPath into a separate crate (#61029)

This is necessary to remove some `util` dependencies from crates, as
well as better sharing for our projects. This also includes the WIP
AbsPath abstraction as well as some bug fixes from internal tooling.


Release Notes:

- N/A or Added/Fixed/Improved ...
This commit is contained in:
Lukas Wirth 2026-07-15 10:33:25 +02:00 • committed by GitHub
parent febe242c0b
commit f181a2f47b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
93 changed files with 1458 additions and 839 deletions

16
Cargo.lock generated
View file

@ -6320,6 +6320,7 @@ dependencies = [
"log",
"lsp",
"parking_lot",
"path",
"pretty_assertions",
"proto",
"semver",
@ -6909,6 +6910,7 @@ dependencies = [
"log",
"notify 9.0.0-rc.4",
"parking_lot",
"path",
"paths",
"proto",
"rope",
@ -9755,13 +9757,13 @@ dependencies = [
"log",
"lsp-types",
"parking_lot",
"path",
"regex",
"schemars 1.0.4",
"serde",
"serde_json",
"toml 0.8.23",
"tree-sitter",
"util",
]
[[package]]
@ -12706,6 +12708,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]]
name = "path"
version = "0.1.0"
dependencies = [
"anyhow",
"dunce",
"serde",
"tempfile",
]
[[package]]
name = "pathdiff"
version = "0.2.3"
@ -14041,6 +14053,7 @@ dependencies = [
"markdown",
"node_runtime",
"parking_lot",
"path",
"paths",
"percent-encoding",
"postage",
@ -19955,6 +19968,7 @@ dependencies = [
"log",
"mach2 0.5.0",
"nix 0.29.0",
"path",
"percent-encoding",
"pretty_assertions",
"rand 0.9.4",

View file

@ -151,6 +151,7 @@ members = [
"crates/outline",
"crates/outline_panel",
"crates/panel",
"crates/path",
"crates/paths",
"crates/picker",
"crates/picker_preview",
@ -414,6 +415,7 @@ outline = { path = "crates/outline" }
outline_panel = { path = "crates/outline_panel" }
panel = { path = "crates/panel" }
paths = { path = "crates/paths" }
path = { path = "crates/path" }
perf = { path = "tooling/perf" }
picker = { path = "crates/picker" }
picker_preview = { path = "crates/picker_preview" }

View file

@ -1095,8 +1095,7 @@ mod tests {
#[test]
fn test_posix_paths_are_not_rewritten_as_windows_drives() {
let parsed =
MentionUri::parse_hyperlink("/c/Projects/AGENTS.md", PathStyle::Posix).unwrap();
let parsed = MentionUri::parse_hyperlink("/c/Projects/AGENTS.md", PathStyle::Unix).unwrap();
match parsed {
MentionUri::File { abs_path } => {
assert_eq!(abs_path, PathBuf::from("/c/Projects/AGENTS.md"));
@ -1107,7 +1106,7 @@ mod tests {
#[test]
fn test_hyperlink_percent_escapes_are_decoded() {
let parsed = MentionUri::parse_hyperlink("/tmp/a%20b.rs", PathStyle::Posix).unwrap();
let parsed = MentionUri::parse_hyperlink("/tmp/a%20b.rs", PathStyle::Unix).unwrap();
assert_eq!(
parsed,
MentionUri::File {
@ -1126,15 +1125,14 @@ mod tests {
);
// Separator escapes stay encoded (no introduced path traversal).
let parsed = MentionUri::parse_hyperlink("/tmp/a%2Fb.rs", PathStyle::Posix).unwrap();
let parsed = MentionUri::parse_hyperlink("/tmp/a%2Fb.rs", PathStyle::Unix).unwrap();
assert_eq!(
parsed,
MentionUri::File {
abs_path: PathBuf::from("/tmp/a%2Fb.rs")
}
);
let parsed =
MentionUri::parse_hyperlink("/tmp/..%2F..%2Fsecret", PathStyle::Posix).unwrap();
let parsed = MentionUri::parse_hyperlink("/tmp/..%2F..%2Fsecret", PathStyle::Unix).unwrap();
assert_eq!(
parsed,
MentionUri::File {
@ -1145,7 +1143,7 @@ mod tests {
#[test]
fn test_parse_keeps_bare_path_targets_verbatim() {
let parsed = MentionUri::parse("/tmp/a%20b.rs", PathStyle::Posix).unwrap();
let parsed = MentionUri::parse("/tmp/a%20b.rs", PathStyle::Unix).unwrap();
assert_eq!(
parsed,
MentionUri::File {
@ -1165,7 +1163,7 @@ mod tests {
#[test]
fn test_parse_hyperlink_literal_keeps_percent_escapes() {
let literal =
MentionUri::parse_hyperlink_literal("/tmp/a%20b.rs", PathStyle::Posix).unwrap();
MentionUri::parse_hyperlink_literal("/tmp/a%20b.rs", PathStyle::Unix).unwrap();
assert_eq!(
literal,
MentionUri::File {
@ -1175,7 +1173,7 @@ mod tests {
// Line suffixes still parse.
let literal =
MentionUri::parse_hyperlink_literal("/tmp/a%20b.rs:42", PathStyle::Posix).unwrap();
MentionUri::parse_hyperlink_literal("/tmp/a%20b.rs:42", PathStyle::Unix).unwrap();
assert_eq!(
literal,
MentionUri::Selection {
@ -1200,27 +1198,27 @@ mod tests {
fn test_parse_hyperlink_literal_returns_none_when_unambiguous() {
// No percent escapes: identical to `parse_hyperlink`.
assert_eq!(
MentionUri::parse_hyperlink_literal("/tmp/a b.rs", PathStyle::Posix),
MentionUri::parse_hyperlink_literal("/tmp/a b.rs", PathStyle::Unix),
None
);
// Invalid escape sequences are also left alone by `parse_hyperlink`.
assert_eq!(
MentionUri::parse_hyperlink_literal("/tmp/100%_done.txt", PathStyle::Posix),
MentionUri::parse_hyperlink_literal("/tmp/100%_done.txt", PathStyle::Unix),
None
);
// Separator escapes are never decoded, so they're not ambiguous.
assert_eq!(
MentionUri::parse_hyperlink_literal("/tmp/a%2Fb.rs", PathStyle::Posix),
MentionUri::parse_hyperlink_literal("/tmp/a%2Fb.rs", PathStyle::Unix),
None
);
// URLs are spec-encoded, not ambiguous.
assert_eq!(
MentionUri::parse_hyperlink_literal("file:///tmp/a%20b.rs", PathStyle::Posix),
MentionUri::parse_hyperlink_literal("file:///tmp/a%20b.rs", PathStyle::Unix),
None
);
// Relative paths are not bare-path mentions.
assert_eq!(
MentionUri::parse_hyperlink_literal("tmp/a%20b.rs", PathStyle::Posix),
MentionUri::parse_hyperlink_literal("tmp/a%20b.rs", PathStyle::Unix),
None
);
}
@ -1476,7 +1474,7 @@ mod tests {
#[test]
fn test_parse_absolute_file_path_with_row() {
let file_path = "/path/to/file.rs:42";
let parsed = MentionUri::parse(file_path, PathStyle::Posix).unwrap();
let parsed = MentionUri::parse(file_path, PathStyle::Unix).unwrap();
match &parsed {
MentionUri::Selection {
abs_path: path,
@ -1494,7 +1492,7 @@ mod tests {
#[test]
fn test_parse_absolute_file_path_with_row_and_column() {
let file_path = "/path/to/file.rs:42:5";
let parsed = MentionUri::parse(file_path, PathStyle::Posix).unwrap();
let parsed = MentionUri::parse(file_path, PathStyle::Unix).unwrap();
match &parsed {
MentionUri::Selection {
abs_path: path,
@ -1506,7 +1504,7 @@ mod tests {
assert_eq!(line_range.end(), &41);
assert_eq!(column, &Some(4));
let parsed_again = MentionUri::parse(parsed.to_uri().as_ref(), PathStyle::Posix)
let parsed_again = MentionUri::parse(parsed.to_uri().as_ref(), PathStyle::Unix)
.expect("selection URI with column should parse");
assert_eq!(parsed_again, parsed.clone());
}
@ -1517,7 +1515,7 @@ mod tests {
#[test]
fn test_parse_absolute_file_path_with_fragment_line() {
let file_path = "/path/to/file.rs#L42";
let parsed = MentionUri::parse(file_path, PathStyle::Posix).unwrap();
let parsed = MentionUri::parse(file_path, PathStyle::Unix).unwrap();
match &parsed {
MentionUri::Selection {
abs_path: path,
@ -1589,7 +1587,7 @@ mod tests {
#[test]
fn test_parse_backticked_absolute_file_path() {
let file_path = "`/path/to/file.rs`";
let parsed = MentionUri::parse(file_path, PathStyle::Posix).unwrap();
let parsed = MentionUri::parse(file_path, PathStyle::Unix).unwrap();
match &parsed {
MentionUri::File { abs_path } => {
assert_eq!(abs_path, Path::new("/path/to/file.rs"));
@ -1601,7 +1599,7 @@ mod tests {
#[test]
fn test_parse_backticked_absolute_file_path_with_fragment_line() {
let file_path = "`/path/to/file.rs#L42`";
let parsed = MentionUri::parse(file_path, PathStyle::Posix).unwrap();
let parsed = MentionUri::parse(file_path, PathStyle::Unix).unwrap();
match &parsed {
MentionUri::Selection {
abs_path: path,

View file

@ -446,18 +446,22 @@ impl gpui::EventEmitter<SkillLoadingIssuesUpdated> for NativeAgent {}
static RULES_FILE_REL_PATHS: LazyLock<Vec<Arc<RelPath>>> = LazyLock::new(|| {
RULES_FILE_NAMES
.iter()
.filter_map(|name| RelPath::unix(name).ok().map(|path| path.into_arc()))
.filter_map(|name| {
RelPath::from_unix_str(name)
.ok()
.map(|path| path.into_arc())
})
.collect()
});
static AGENTS_PREFIX: LazyLock<Option<Arc<RelPath>>> = LazyLock::new(|| {
RelPath::unix(AGENTS_DIR_NAME)
RelPath::from_unix_str(AGENTS_DIR_NAME)
.ok()
.map(|path| path.into_arc())
});
static SKILLS_PREFIX: LazyLock<Option<Arc<RelPath>>> = LazyLock::new(|| {
RelPath::unix(project_skills_relative_path())
RelPath::from_unix_str(project_skills_relative_path())
.ok()
.map(|path| path.into_arc())
});
@ -492,7 +496,7 @@ async fn expand_project_skills_directories(
worktree: &Entity<Worktree>,
cx: &mut AsyncApp,
) -> Result<()> {
let agents_dir = RelPath::unix(AGENTS_DIR_NAME)?;
let agents_dir = RelPath::from_unix_str(AGENTS_DIR_NAME)?;
let Some(skills_prefix) = SKILLS_PREFIX.as_ref() else {
return Ok(());
};
@ -518,7 +522,7 @@ fn project_skill_files_from_worktree(worktree: &Worktree) -> Vec<ProjectSkillFil
let Some(skills_prefix) = SKILLS_PREFIX.as_ref() else {
return Vec::new();
};
let Ok(skill_file_name) = RelPath::unix(SKILL_FILE_NAME) else {
let Ok(skill_file_name) = RelPath::from_unix_str(SKILL_FILE_NAME) else {
return Vec::new();
};
@ -538,7 +542,7 @@ fn project_skill_files_from_worktree(worktree: &Worktree) -> Vec<ProjectSkillFil
skill_files.push(ProjectSkillFile {
display_path: worktree.absolutize(&relative_path),
relative_path,
relative_path: relative_path.into(),
size: skill_file.size,
});
}

View file

@ -122,7 +122,7 @@ mod tests {
root_name: "my-project".to_string(),
abs_path: std::path::Path::new("/tmp/my-project").into(),
rules_file: Some(RulesFileContext {
path_in_worktree: RelPath::unix("AGENTS.md").unwrap().into(),
path_in_worktree: RelPath::from_unix_str("AGENTS.md").unwrap().into(),
text: "project-specific guidance".to_string(),
project_entry_id: 1,
}),

View file

@ -1245,11 +1245,11 @@ fn resolve_path(
let file_name = path
.file_name()
.and_then(|file_name| file_name.to_str())
.and_then(|file_name| RelPath::unix(file_name).ok())
.and_then(|file_name| RelPath::from_unix_str(file_name).ok())
.ok_or_else(|| "Can't create file: invalid filename".to_string())?;
let new_file_path = parent_project_path.map(|parent| ProjectPath {
path: parent.path.join(file_name),
path: parent.path.join(file_name).into(),
..parent
});

View file

@ -238,7 +238,7 @@ fn project_agents_md_path(
require_existing_file: bool,
cx: &App,
) -> Option<PathBuf> {
let rel_path = util::rel_path::RelPath::unix("AGENTS.md").ok()?;
let rel_path = util::rel_path::RelPath::from_unix_str("AGENTS.md").ok()?;
project
.read(cx)
.visible_worktrees(cx)

View file

@ -1248,8 +1248,7 @@ fn full_mention_for_directory(
|(worktree_path, full_path): (Arc<RelPath>, String)| {
let rel_path = worktree_path
.strip_prefix(&directory_path)
.log_err()
.map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
.map_or_else(|_| worktree_path.clone(), |rel_path| rel_path.into());
let open_task = project.update(cx, |project, cx| {
project.buffer_store().update(cx, |buffer_store, cx| {

View file

@ -1077,7 +1077,7 @@ mod tests {
.enumerate()
.filter_map(|(i, path)| {
Some((
Arc::from(RelPath::unix(path).ok()?),
Arc::from(RelPath::from_unix_str(path).ok()?),
ProjectEntryId::from_proto(i as u64 + 1),
PathChange::Added,
))

View file

@ -961,7 +961,7 @@ impl Database {
let path_style = if project.windows_paths {
PathStyle::Windows
} else {
PathStyle::Posix
PathStyle::Unix
};
let features: Vec<String> = serde_json::from_str(&project.features).unwrap_or_default();

View file

@ -448,7 +448,7 @@ impl RandomizedTest for ProjectCollaborationTest {
.choose(rng)
.unwrap();
if entry.path.as_ref().is_empty() {
worktree.root_name().into()
worktree.root_name().to_rel_path_buf()
} else {
worktree.root_name().join(&entry.path)
}
@ -1524,7 +1524,7 @@ fn buffer_for_full_path(
else {
return false;
};
worktree.read(cx).root_name().join(&file.path()).as_ref() == full_path
worktree.read(cx).root_name().join(&file.path()) == *full_path
})
})
.cloned()

View file

@ -1090,14 +1090,14 @@ impl DebugPanel {
directory_in_worktree: dir,
..
} => {
let relative_path = if dir.ends_with(RelPath::unix(".vscode").unwrap()) {
dir.join(RelPath::unix("launch.json").unwrap())
let relative_path = if dir.ends_with(RelPath::from_unix_str(".vscode").unwrap()) {
dir.join(RelPath::from_unix_str("launch.json").unwrap())
} else {
dir.join(RelPath::unix("debug.json").unwrap())
dir.join(RelPath::from_unix_str("debug.json").unwrap())
};
ProjectPath {
worktree_id: id,
path: relative_path,
path: relative_path.into(),
}
}
_ => return self.save_scenario(scenario, worktree_id, window, cx),

View file

@ -1069,10 +1069,10 @@ impl DebugDelegate {
match path.components().next_back() {
Some(".zed") => {
path.push(RelPath::unix("debug.json").unwrap());
path.push(RelPath::from_unix_str("debug.json").unwrap());
}
Some(".vscode") => {
path.push(RelPath::unix("launch.json").unwrap());
path.push(RelPath::from_unix_str("launch.json").unwrap());
}
_ => {}
}
@ -1165,7 +1165,7 @@ impl DebugDelegate {
id: _,
directory_in_worktree: dir,
id_base: _,
} => dir.ends_with(RelPath::unix(".zed").unwrap()),
} => dir.ends_with(RelPath::from_unix_str(".zed").unwrap()),
_ => false,
});
@ -1186,7 +1186,8 @@ impl DebugDelegate {
id_base: _,
} => {
!(hide_vscode
&& dir.ends_with(RelPath::unix(".vscode").unwrap()))
&& dir
.ends_with(RelPath::from_unix_str(".vscode").unwrap()))
}
_ => true,
})

View file

@ -524,7 +524,7 @@ impl StackFrameList {
.filter(|path| {
// Since we do not know if we are debugging on the host or (a remote/WSL) target,
// we need to check if either the path is absolute as Posix or Windows.
is_absolute(path, PathStyle::Posix) || is_absolute(path, PathStyle::Windows)
is_absolute(path, PathStyle::Unix) || is_absolute(path, PathStyle::Windows)
})
.map(|path| Arc::<Path>::from(Path::new(path)))
})

View file

@ -172,13 +172,13 @@ pub fn find_devcontainer_configs(workspace: &Workspace, cx: &gpui::App) -> Vec<D
pub fn find_configs_in_snapshot(snapshot: &Snapshot) -> Vec<DevContainerConfig> {
let mut configs = Vec::new();
let devcontainer_dir_path = RelPath::unix(".devcontainer").expect("valid path");
let devcontainer_dir_path = RelPath::from_unix_str(".devcontainer").expect("valid path");
if let Some(devcontainer_entry) = snapshot.entry_for_path(devcontainer_dir_path) {
if devcontainer_entry.is_dir() {
log::debug!("find_configs_in_snapshot: Scanning .devcontainer directory");
let devcontainer_json_path =
RelPath::unix(".devcontainer/devcontainer.json").expect("valid path");
RelPath::from_unix_str(".devcontainer/devcontainer.json").expect("valid path");
for entry in snapshot.child_entries(devcontainer_dir_path) {
log::debug!(
"find_configs_in_snapshot: Found entry: {:?}, is_file: {}, is_dir: {}",
@ -199,7 +199,7 @@ pub fn find_configs_in_snapshot(snapshot: &Snapshot) -> Vec<DevContainerConfig>
let config_json_path =
format!("{}/devcontainer.json", entry.path.as_unix_str());
if let Ok(rel_config_path) = RelPath::unix(&config_json_path) {
if let Ok(rel_config_path) = RelPath::from_unix_str(&config_json_path) {
if snapshot.entry_for_path(rel_config_path).is_some() {
log::debug!(
"find_configs_in_snapshot: Found config in subfolder: {}",
@ -223,7 +223,7 @@ pub fn find_configs_in_snapshot(snapshot: &Snapshot) -> Vec<DevContainerConfig>
// Always include `.devcontainer.json` so the user can pick it from the UI
// even when `.devcontainer/devcontainer.json` also exists.
let root_config_path = RelPath::unix(".devcontainer.json").expect("valid path");
let root_config_path = RelPath::from_unix_str(".devcontainer.json").expect("valid path");
if snapshot
.entry_for_path(root_config_path)
.is_some_and(|entry| entry.is_file())
@ -400,7 +400,7 @@ pub(crate) async fn apply_devcontainer_template(
log::error!("Can't create relative path: {e}");
DevContainerError::FilesystemError
})?;
let rel_path = RelPath::unix(relative_path)
let rel_path = RelPath::from_unix_str(relative_path)
.map_err(|e| {
log::error!("Can't create relative path: {e}");
DevContainerError::FilesystemError

View file

@ -1579,11 +1579,12 @@ fn dispatch_apply_templates(
};
if files.project_files.contains(&Arc::from(
RelPath::unix(".devcontainer/devcontainer.json").unwrap(),
RelPath::from_unix_str(".devcontainer/devcontainer.json").unwrap(),
)) {
let Some(workspace_task) = workspace
.update_in(cx, |workspace, window, cx| {
let Ok(path) = RelPath::unix(".devcontainer/devcontainer.json") else {
let Ok(path) = RelPath::from_unix_str(".devcontainer/devcontainer.json")
else {
return Task::ready(Err(anyhow!(
"Couldn't create path for .devcontainer/devcontainer.json"
)));

View file

@ -309,12 +309,12 @@ pub async fn refresh_worktree_entries(
) -> Result<()> {
let mut rel_paths = Vec::new();
for path in paths {
if let Ok(rel_path) = RelPath::new(path, PathStyle::Posix) {
if let Ok(rel_path) = RelPath::new(path, PathStyle::Unix) {
rel_paths.push(rel_path.into_arc());
}
let path_without_root: PathBuf = path.components().skip(1).collect();
if let Ok(rel_path) = RelPath::new(&path_without_root, PathStyle::Posix) {
if let Ok(rel_path) = RelPath::new(&path_without_root, PathStyle::Unix) {
rel_paths.push(rel_path.into_arc());
}
}

View file

@ -178,7 +178,7 @@ impl RelatedExcerptStore {
.path
.strip_prefix(worktree.root_name().as_unix_str())
.ok()?;
let relative_path = RelPath::new(relative_path, PathStyle::Posix).ok()?;
let relative_path = RelPath::new(relative_path, PathStyle::Unix).ok()?;
let project_path = ProjectPath {
worktree_id: worktree.id(),
path: relative_path.into_owned().into(),

View file

@ -652,7 +652,7 @@ async fn collect_git_log_context(
.into_iter()
.enumerate()
{
let Ok(related_path) = RelPath::new(&related_path, PathStyle::Posix) else {
let Ok(related_path) = RelPath::new(&related_path, PathStyle::Unix) else {
continue;
};
let project_path = ProjectPath {

View file

@ -740,9 +740,9 @@ fn unused_image_path(
let mut filename = format!("image.{extension}");
let mut counter = 1u32;
loop {
let candidate = dir_rel_path.join(RelPath::unix(&filename).ok()?);
let candidate = dir_rel_path.join(RelPath::from_unix_str(&filename).ok()?);
if !exists(&candidate) {
return Some((filename, candidate));
return Some((filename, candidate.into()));
}
filename = format!("image_{counter}.{extension}");
counter += 1;

View file

@ -38855,7 +38855,7 @@ fn test_hunk_key(file_path: &str) -> DiffHunkKey {
file_path: if file_path.is_empty() {
Arc::from(util::rel_path::RelPath::empty())
} else {
Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
Arc::from(util::rel_path::RelPath::from_unix_str(file_path).unwrap())
},
hunk_start_anchor: Anchor::Min,
}
@ -38867,7 +38867,7 @@ fn test_hunk_key_with_anchor(file_path: &str, anchor: Anchor) -> DiffHunkKey {
file_path: if file_path.is_empty() {
Arc::from(util::rel_path::RelPath::empty())
} else {
Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
Arc::from(util::rel_path::RelPath::from_unix_str(file_path).unwrap())
},
hunk_start_anchor: anchor,
}
@ -39306,11 +39306,11 @@ fn test_comments_stored_for_multiple_hunks(cx: &mut TestAppContext) {
// Create two different hunk keys (simulating two different files)
let anchor = snapshot.anchor_before(Point::new(0, 0));
let key1 = DiffHunkKey {
file_path: Arc::from(util::rel_path::RelPath::unix("file1.rs").unwrap()),
file_path: Arc::from(util::rel_path::RelPath::from_unix_str("file1.rs").unwrap()),
hunk_start_anchor: anchor,
};
let key2 = DiffHunkKey {
file_path: Arc::from(util::rel_path::RelPath::unix("file2.rs").unwrap()),
file_path: Arc::from(util::rel_path::RelPath::from_unix_str("file2.rs").unwrap()),
hunk_start_anchor: anchor,
};
@ -39378,11 +39378,11 @@ fn test_same_hunk_detected_by_matching_keys(cx: &mut TestAppContext) {
// Create two keys with the same file path and anchor
let key1 = DiffHunkKey {
file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
file_path: Arc::from(util::rel_path::RelPath::from_unix_str("file.rs").unwrap()),
hunk_start_anchor: anchor,
};
let key2 = DiffHunkKey {
file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
file_path: Arc::from(util::rel_path::RelPath::from_unix_str("file.rs").unwrap()),
hunk_start_anchor: anchor,
};
@ -39399,7 +39399,7 @@ fn test_same_hunk_detected_by_matching_keys(cx: &mut TestAppContext) {
// Create a key with different file path
let different_file_key = DiffHunkKey {
file_path: Arc::from(util::rel_path::RelPath::unix("other.rs").unwrap()),
file_path: Arc::from(util::rel_path::RelPath::from_unix_str("other.rs").unwrap()),
hunk_start_anchor: anchor,
};

View file

@ -2221,14 +2221,14 @@ fn restore_serialized_buffer_contents(
fn serialize_path_key(path_key: &PathKey) -> proto::PathKey {
proto::PathKey {
sort_prefix: path_key.sort_prefix,
path: path_key.path.to_proto(),
path: path_key.path.as_unix_str().to_owned(),
}
}
fn deserialize_path_key(path_key: proto::PathKey) -> Option<PathKey> {
Some(PathKey {
sort_prefix: path_key.sort_prefix,
path: RelPath::from_proto(&path_key.path).ok()?,
path: RelPath::from_unix_str(&path_key.path).ok()?.into(),
})
}

View file

@ -26,6 +26,7 @@ language.workspace = true
log.workspace = true
lsp.workspace = true
parking_lot.workspace = true
path.workspace = true
proto.workspace = true
semver.workspace = true
serde.workspace = true

View file

@ -56,7 +56,7 @@ pub trait Extension: Send + Sync + 'static {
/// Returns a path relative to this extension's working directory.
fn path_from_extension(&self, path: &Path) -> PathBuf {
util::normalize_path(&self.work_dir().join(path))
path::normalize_path(&self.work_dir().join(path))
}
async fn language_server_command(

View file

@ -11,6 +11,7 @@ use futures::{
use heck::ToSnakeCase;
use http_client::{self, AsyncBody, HttpClient};
use language::LanguageConfig;
use path::PathExt;
use semver::Version;
use serde::Deserialize;
use std::{
@ -20,7 +21,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::{ResultExt, command::Stdio, rel_path::PathExt};
use util::{ResultExt, command::Stdio};
use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _};
use wasmparser::Parser;

View file

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::ffi::OsStr;
use std::fmt;
use std::path::{Path, PathBuf};
@ -11,7 +12,8 @@ use language::LanguageName;
use lsp::LanguageServerName;
use semver::Version;
use serde::{Deserialize, Serialize};
use util::rel_path::{PathExt, RelPathBuf};
use util::paths::PathStyle;
use util::rel_path::{RelPath, RelPathBuf};
use crate::ExtensionCapability;
@ -211,9 +213,12 @@ pub fn build_debug_adapter_schema_path(
) -> anyhow::Result<RelPathBuf> {
match &meta.schema_path {
Some(path) => Ok(path.clone()),
None => Path::new("debug_adapter_schemas")
.join(Path::new(adapter_name.as_ref()).with_extension("json"))
.to_rel_path_buf(),
None => RelPath::new(
&Path::new("debug_adapter_schemas")
.join(Path::new(adapter_name.as_ref()).with_extension("json")),
PathStyle::local(),
)
.map(Cow::into_owned),
}
}

View file

@ -57,7 +57,7 @@ use std::{
};
use task::TaskTemplates;
use url::Url;
use util::{ResultExt, paths::RemotePathBuf, rel_path::PathExt};
use util::{PathExt, ResultExt, paths::RemotePathBuf};
use wasm_host::{
WasmExtension, WasmHost,
wit::{is_supported_wasm_api_version, wasm_api_version_range},

View file

@ -432,7 +432,7 @@ impl ExtensionImports for WasmState {
self.on_main_thread(|cx| {
async move {
let path = location.as_ref().and_then(|location| {
RelPath::new(Path::new(&location.path), PathStyle::Posix).ok()
RelPath::new(Path::new(&location.path), PathStyle::Unix).ok()
});
let location = path
.as_ref()

View file

@ -595,7 +595,7 @@ impl HostWorktree for WasmState {
) -> wasmtime::Result<Result<String, String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.read_text_file(&RelPath::new(Path::new(&path), PathStyle::Posix)?)
.read_text_file(&RelPath::new(Path::new(&path), PathStyle::Unix)?)
.await
.map_err(|error| error.to_string()))
}
@ -948,7 +948,7 @@ impl ExtensionImports for WasmState {
self.on_main_thread(|cx| {
async move {
let path = location.as_ref().and_then(|location| {
RelPath::new(Path::new(&location.path), PathStyle::Posix).ok()
RelPath::new(Path::new(&location.path), PathStyle::Unix).ok()
});
let location = path
.as_ref()

View file

@ -1272,7 +1272,11 @@ impl FileFinderDelegate {
let full_path = if should_hide_root_in_entry_path(&worktree_store, cx) {
entry_path.project.path.clone()
} else {
worktree.read(cx).root_name().join(&entry_path.project.path)
worktree
.read(cx)
.root_name()
.join(&entry_path.project.path)
.into()
};
let mut components = full_path.components();
let filename = components.next_back().unwrap_or("");
@ -1841,7 +1845,7 @@ impl PickerDelegate for FileFinderDelegate {
.all(|worktree| {
worktree
.read(cx)
.entry_for_path(RelPath::unix(prefix.split_at(1).0).unwrap())
.entry_for_path(RelPath::from_unix_str(prefix.split_at(1).0).unwrap())
.is_none_or(|entry| !entry.is_dir())
})
{

View file

@ -4584,7 +4584,7 @@ fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries
if let Some(path_match) = path_match.as_ref() {
search_entries
.history
.push(path_match.0.path_prefix.join(&path_match.0.path));
.push(path_match.0.path_prefix.join(&path_match.0.path).into());
} else {
// This occurs when the query is empty and we show history matches
// that are outside the project.
@ -4597,7 +4597,7 @@ fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries
Match::Search(path_match) => {
search_entries
.search
.push(path_match.0.path_prefix.join(&path_match.0.path));
.push(path_match.0.path_prefix.join(&path_match.0.path).into());
search_entries.search_matches.push(path_match.0.clone());
}
Match::CreateNew(_) => {}

View file

@ -42,6 +42,7 @@ tempfile.workspace = true
text.workspace = true
time.workspace = true
util.workspace = true
path.workspace = true
is_executable = "1.0.5"
notify = "9.0.0-rc.4"
trash = { git = "https://github.com/zed-industries/trash-rs", rev = "47761739192828a66b11a94ba5420b82d63c03c5" }

View file

@ -61,7 +61,7 @@ use git::{
status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus},
};
#[cfg(feature = "test-support")]
use util::normalize_path;
use path::normalize_path;
#[cfg(feature = "test-support")]
use smol::io::AsyncReadExt;

View file

@ -233,7 +233,11 @@ fn generate_nucleo_path_candidates(
paths
.iter()
.map(|path| {
fuzzy_nucleo::PathMatchCandidate::new(RelPath::unix(path).unwrap(), false, None)
fuzzy_nucleo::PathMatchCandidate::new(
RelPath::from_unix_str(path).unwrap(),
false,
None,
)
})
.collect()
}
@ -245,7 +249,7 @@ fn generate_fuzzy_path_candidates(
.iter()
.map(|path| fuzzy::PathMatchCandidate {
is_dir: false,
path: RelPath::unix(path).unwrap(),
path: RelPath::from_unix_str(path).unwrap(),
char_bag: CharBag::from(path.as_str()),
})
.collect()
@ -302,7 +306,7 @@ fn bench_path_matching(criterion: &mut Criterion) {
query,
case,
size,
PathStyle::Posix,
PathStyle::Unix,
)
},
BatchSize::SmallInput,
@ -325,7 +329,7 @@ fn bench_path_matching(criterion: &mut Criterion) {
query,
false,
size,
PathStyle::Posix,
PathStyle::Unix,
)
},
BatchSize::SmallInput,

View file

@ -1517,7 +1517,7 @@ impl GitRepository for RealGitRepository {
let change = change?;
let path = change.path;
// git-show outputs `/`-delimited paths even on Windows.
let Some(rel_path) = RelPath::unix(path).log_err() else {
let Some(rel_path) = RelPath::from_unix_str(path).log_err() else {
continue;
};
@ -3803,7 +3803,7 @@ impl std::fmt::Debug for RepoPath {
impl RepoPath {
pub fn new<S: AsRef<str> + ?Sized>(s: &S) -> Result<Self> {
let rel_path = RelPath::unix(s.as_ref())?;
let rel_path = RelPath::from_unix_str(s.as_ref())?;
Ok(Self::from_rel_path(rel_path))
}
@ -3813,7 +3813,7 @@ impl RepoPath {
}
pub fn from_proto(proto: &str) -> Result<Self> {
let rel_path = RelPath::from_proto(proto)?;
let rel_path = RelPath::from_unix_str(proto)?.into();
Ok(Self(rel_path))
}
@ -3832,7 +3832,7 @@ impl RepoPath {
#[cfg(any(test, feature = "test-support"))]
pub fn repo_path<S: AsRef<str> + ?Sized>(s: &S) -> RepoPath {
RepoPath(RelPath::unix(s.as_ref()).unwrap().into())
RepoPath(RelPath::from_unix_str(s.as_ref()).unwrap().into())
}
impl AsRef<Arc<RelPath>> for RepoPath {

View file

@ -454,7 +454,7 @@ impl FromStr for GitStatus {
let status = entry.as_bytes()[0..2].try_into().unwrap();
let status = FileStatus::from_bytes(status).log_err()?;
// git-status outputs `/`-delimited repo paths, even on Windows.
let path = RepoPath::from_rel_path(RelPath::unix(path).log_err()?);
let path = RepoPath::from_rel_path(RelPath::from_unix_str(path).log_err()?);
Some((path, status))
})
.collect::<Vec<_>>();
@ -544,7 +544,7 @@ impl FromStr for TreeDiff {
let mut fields = s.split('\0');
let mut parsed = HashMap::default();
while let Some((status, path)) = fields.next().zip(fields.next()) {
let path = RepoPath::from_rel_path(RelPath::unix(path)?);
let path = RepoPath::from_rel_path(RelPath::from_unix_str(path)?);
let mut fields = status.split(" ").skip(2);
let old_sha = fields

View file

@ -1007,7 +1007,7 @@ fn name_sort_path(repo_path: &RelPath) -> Arc<RelPath> {
return repo_path.into_arc();
};
let synthetic = format!("{}/{}", file_name, repo_path.as_unix_str());
RelPath::unix(&synthetic)
RelPath::from_unix_str(&synthetic)
.map(|path| path.into_arc())
.unwrap_or_else(|_| repo_path.into_arc())
}
@ -1032,7 +1032,7 @@ fn tree_sort_path(repo_path: &RelPath) -> Arc<RelPath> {
}
synthetic.push_str(component);
}
RelPath::unix(&synthetic)
RelPath::from_unix_str(&synthetic)
.map(|path| path.into_arc())
.unwrap_or_else(|_| repo_path.into_arc())
}

View file

@ -3232,7 +3232,7 @@ impl GitPanel {
let worktree_snapshot = worktree.read(cx).snapshot();
for rules_name in RULES_FILE_NAMES {
if let Ok(rel_path) = RelPath::unix(rules_name) {
if let Ok(rel_path) = RelPath::from_unix_str(rules_name) {
if let Some(entry) = worktree_snapshot.entry_for_path(rel_path) {
if entry.is_file() {
return Some(ProjectPath {
@ -11485,7 +11485,7 @@ mod tests {
cx.read(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
let project_path = ProjectPath {
worktree_id,
path: RelPath::unix("src/a/foo.rs").unwrap().into_arc(),
path: RelPath::from_unix_str("src/a/foo.rs").unwrap().into_arc(),
};
panel.update_in(cx, |panel, window, cx| {

View file

@ -94,7 +94,7 @@ fn register_entry(
RelPath::new(rel, PathStyle::local())
.map(|r| r.into_owned().into())
.unwrap_or_else(|_| {
RelPath::new(Path::new(MultiBuffer::DEFAULT_TITLE), PathStyle::Posix)
RelPath::new(Path::new(MultiBuffer::DEFAULT_TITLE), PathStyle::Unix)
.unwrap()
.into_owned()
.into()
@ -105,10 +105,10 @@ fn register_entry(
.new_path
.file_name()
.and_then(|n| n.to_str())
.and_then(|s| RelPath::new(Path::new(s), PathStyle::Posix).ok())
.and_then(|s| RelPath::new(Path::new(s), PathStyle::Unix).ok())
.map(|r| r.into_owned().into())
.unwrap_or_else(|| {
RelPath::new(Path::new(MultiBuffer::DEFAULT_TITLE), PathStyle::Posix)
RelPath::new(Path::new(MultiBuffer::DEFAULT_TITLE), PathStyle::Unix)
.unwrap()
.into_owned()
.into()

View file

@ -586,7 +586,7 @@ impl Item for ImageView {
}
fn breadcrumbs_text_for_image(project: &Project, image: &ImageItem, cx: &App) -> String {
let mut path = image.file.path().clone();
let mut path = image.file.path().to_rel_path_buf();
if project.visible_worktrees(cx).count() > 1
&& let Some(worktree) = project.worktree_for_id(image.project_path(cx).worktree_id, cx)
{

View file

@ -50,8 +50,8 @@ pub use language_core::{
LanguageId, LanguageMatcher, OrderedListConfig, OutlineConfig, Override, OverrideConfig,
OverrideEntry, PromptResponseContext, RedactionConfig, RunnableCapture, RunnableConfig,
SoftWrap, Symbol, TaskListConfig, TextObject, TextObjectConfig, ToLspPosition,
WrapCharactersConfig, auto_indent_using_last_non_empty_line_default, deserialize_regex,
deserialize_regex_vec, regex_json_schema, regex_vec_json_schema, serialize_regex,
WrapCharactersConfig, default_true, deserialize_regex, deserialize_regex_vec,
regex_json_schema, regex_vec_json_schema, serialize_regex,
};
pub use language_registry::{
LanguageName, LanguageServerStatusUpdate, LoadedLanguage, ServerHealth,

View file

@ -11,17 +11,17 @@ path = "src/language_core.rs"
anyhow.workspace = true
collections.workspace = true
gpui_shared_string.workspace = true
gpui_util.workspace = true
log.workspace = true
lsp-types.workspace = true
parking_lot.workspace = true
path.workspace = true
regex.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
toml.workspace = true
tree-sitter.workspace = true
util.workspace = true
gpui_util.workspace = true
lsp-types.workspace = true
[features]
test-support = []

View file

@ -5,7 +5,6 @@ use regex::Regex;
use schemars::{JsonSchema, SchemaGenerator, json_schema};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use std::{num::NonZeroU32, path::Path, sync::Arc};
use util::serde::default_true;
/// Controls the soft-wrapping behavior in the editor.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@ -46,7 +45,7 @@ pub struct LanguageConfig {
pub brackets: BracketPairConfig,
/// If set to true, auto indentation uses last non empty line to determine
/// the indentation level for a new line.
#[serde(default = "auto_indent_using_last_non_empty_line_default")]
#[serde(default = "default_true")]
pub auto_indent_using_last_non_empty_line: bool,
// Whether indentation of pasted content should be adjusted based on the context.
#[serde(default)]
@ -166,7 +165,7 @@ impl Default for LanguageConfig {
grammar: None,
matcher: LanguageMatcher::default(),
brackets: Default::default(),
auto_indent_using_last_non_empty_line: auto_indent_using_last_non_empty_line_default(),
auto_indent_using_last_non_empty_line: default_true(),
auto_indent_on_paste: None,
increase_indent_pattern: Default::default(),
decrease_indent_pattern: Default::default(),
@ -480,7 +479,7 @@ pub struct WrapCharactersConfig {
pub end_suffix: String,
}
pub fn auto_indent_using_last_non_empty_line_default() -> bool {
pub fn default_true() -> bool {
true
}

View file

@ -17,9 +17,9 @@ pub use highlight_map::{HighlightId, HighlightMap};
pub use language_config::{
BlockCommentConfig, BracketPair, BracketPairConfig, BracketPairContent, DecreaseIndentConfig,
JsxTagAutoCloseConfig, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
OrderedListConfig, Override, SoftWrap, TaskListConfig, WrapCharactersConfig,
auto_indent_using_last_non_empty_line_default, deserialize_regex, deserialize_regex_vec,
regex_json_schema, regex_vec_json_schema, serialize_regex,
OrderedListConfig, Override, SoftWrap, TaskListConfig, WrapCharactersConfig, default_true,
deserialize_regex, deserialize_regex_vec, regex_json_schema, regex_vec_json_schema,
serialize_regex,
};
pub mod code_label;

View file

@ -7,7 +7,7 @@
use std::{path::Path, sync::Arc};
use gpui_shared_string::SharedString;
use util::rel_path::RelPath;
use path::rel_path::RelPath;
use crate::{LanguageName, ManifestName};

View file

@ -286,7 +286,7 @@ impl LspAdapter for EsLintLspAdapter {
let file_path = requested_file_path
.as_ref()
.and_then(|abs_path| abs_path.strip_prefix(worktree_root).ok())
.and_then(|p| RelPath::unix(&p).ok().map(ToOwned::to_owned))
.and_then(|p| RelPath::from_unix_str(&p).ok().map(ToOwned::to_owned))
.unwrap_or_else(|| RelPath::empty().to_owned());
let override_options = cx.update(|cx| {
language_server_settings_for(

View file

@ -52,8 +52,12 @@ impl ContextProvider for JsonTaskProvider {
let Some(file) = project::File::from_dyn(file).cloned() else {
return Task::ready(None);
};
let is_package_json = file.path.ends_with(RelPath::unix("package.json").unwrap());
let is_composer_json = file.path.ends_with(RelPath::unix("composer.json").unwrap());
let is_package_json = file
.path
.ends_with(RelPath::from_unix_str("package.json").unwrap());
let is_composer_json = file
.path
.ends_with(RelPath::from_unix_str("composer.json").unwrap());
if !is_package_json && !is_composer_json {
return Task::ready(None);
}

View file

@ -90,14 +90,14 @@ impl ManifestProvider for PyprojectTomlManifestProvider {
let mut outermost_workspace_root = None;
for path in path.ancestors().take(depth) {
let pyproject_path = path.join(RelPath::unix("pyproject.toml").unwrap());
let pyproject_path = path.join(RelPath::from_unix_str("pyproject.toml").unwrap());
if delegate.exists(&pyproject_path, Some(false)) {
if innermost_pyproject.is_none() {
innermost_pyproject = Some(Arc::from(path));
}
let has_lockfile = WORKSPACE_LOCKFILES.iter().any(|lockfile| {
let lockfile_path = path.join(RelPath::unix(lockfile).unwrap());
let lockfile_path = path.join(RelPath::from_unix_str(lockfile).unwrap());
delegate.exists(&lockfile_path, Some(false))
});
if has_lockfile {
@ -1114,7 +1114,7 @@ impl PythonContextProvider {
fn python_module_name_from_relative_path(relative_path: &str) -> Option<String> {
let rel_path = RelPath::new(relative_path.as_ref(), PathStyle::local()).ok()?;
let path_with_dots = rel_path.display(PathStyle::Posix).replace('/', ".");
let path_with_dots = rel_path.display(PathStyle::Unix).replace('/', ".");
Some(
path_with_dots
.strip_suffix(".py")
@ -3265,7 +3265,7 @@ mod tests {
});
let provider = PyprojectTomlManifestProvider;
provider.search(ManifestQuery {
path: RelPath::unix(query_path).unwrap().into(),
path: RelPath::from_unix_str(query_path).unwrap().into(),
depth: 10,
delegate,
})
@ -3274,7 +3274,7 @@ mod tests {
#[test]
fn test_simple_project_no_lockfile() {
let result = search(&["project/pyproject.toml"], "project/src/main.py");
assert_eq!(result.as_deref(), RelPath::unix("project").ok());
assert_eq!(result.as_deref(), RelPath::from_unix_str("project").ok());
}
#[test]
@ -3287,7 +3287,7 @@ mod tests {
],
"packages/subproject/src/main.py",
);
assert_eq!(result.as_deref(), RelPath::unix("").ok());
assert_eq!(result.as_deref(), RelPath::from_unix_str("").ok());
}
#[test]
@ -3296,7 +3296,7 @@ mod tests {
&["pyproject.toml", "poetry.lock", "libs/mylib/pyproject.toml"],
"libs/mylib/src/main.py",
);
assert_eq!(result.as_deref(), RelPath::unix("").ok());
assert_eq!(result.as_deref(), RelPath::from_unix_str("").ok());
}
#[test]
@ -3309,7 +3309,7 @@ mod tests {
],
"packages/mypackage/src/main.py",
);
assert_eq!(result.as_deref(), RelPath::unix("").ok());
assert_eq!(result.as_deref(), RelPath::from_unix_str("").ok());
}
#[test]
@ -3318,13 +3318,19 @@ mod tests {
&["project-a/pyproject.toml", "project-b/pyproject.toml"],
"project-a/src/main.py",
);
assert_eq!(result_a.as_deref(), RelPath::unix("project-a").ok());
assert_eq!(
result_a.as_deref(),
RelPath::from_unix_str("project-a").ok()
);
let result_b = search(
&["project-a/pyproject.toml", "project-b/pyproject.toml"],
"project-b/src/main.py",
);
assert_eq!(result_b.as_deref(), RelPath::unix("project-b").ok());
assert_eq!(
result_b.as_deref(),
RelPath::from_unix_str("project-b").ok()
);
}
#[test]
@ -3345,7 +3351,7 @@ mod tests {
],
"packages/sub/src/main.py",
);
assert_eq!(result.as_deref(), RelPath::unix("").ok());
assert_eq!(result.as_deref(), RelPath::from_unix_str("").ok());
}
#[test]
@ -3360,11 +3366,16 @@ mod tests {
// "deep/nested/src/main.py", "deep/nested/src", and "deep/nested"
// It won't reach "deep" or root ""
let result = provider.search(ManifestQuery {
path: RelPath::unix("deep/nested/src/main.py").unwrap().into(),
path: RelPath::from_unix_str("deep/nested/src/main.py")
.unwrap()
.into(),
depth: 3,
delegate,
});
assert_eq!(result.as_deref(), RelPath::unix("deep/nested").ok());
assert_eq!(
result.as_deref(),
RelPath::from_unix_str("deep/nested").ok()
);
}
}
}

View file

@ -292,7 +292,7 @@ impl ManifestProvider for CargoManifestProvider {
) -> Option<Arc<RelPath>> {
let mut outermost_cargo_toml = None;
for path in path.ancestors().take(depth) {
let p = path.join(RelPath::unix("Cargo.toml").unwrap());
let p = path.join(RelPath::from_unix_str("Cargo.toml").unwrap());
if delegate.exists(&p, Some(false)) {
outermost_cargo_toml = Some(Arc::from(path));
}

View file

@ -625,7 +625,9 @@ impl TypeScriptLspAdapter {
async fn tsdk_path(&self, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
let is_yarn = adapter
.read_text_file(RelPath::unix(".yarn/sdks/typescript/lib/typescript.js").unwrap())
.read_text_file(
RelPath::from_unix_str(".yarn/sdks/typescript/lib/typescript.js").unwrap(),
)
.await
.is_ok();

View file

@ -49,7 +49,7 @@ impl PathKey {
} else {
Self {
sort_prefix: None,
path: RelPath::unix(&buffer.entity_id().to_string())
path: RelPath::from_unix_str(&buffer.entity_id().to_string())
.unwrap()
.into_arc(),
}

View file

@ -68,7 +68,7 @@ impl OpenPathDelegate {
cancel_flag: Arc::new(AtomicBool::new(false)),
should_dismiss: true,
prompt_root: match path_style {
PathStyle::Posix => "/".to_string(),
PathStyle::Unix => "/".to_string(),
PathStyle::Windows => "C:\\".to_string(),
},
path_style,
@ -158,7 +158,7 @@ impl OpenPathDelegate {
fn current_dir(&self) -> &'static str {
match self.path_style {
PathStyle::Posix => "./",
PathStyle::Unix => "./",
PathStyle::Windows => ".\\",
}
}
@ -929,7 +929,7 @@ fn path_candidates(
fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
match path_style {
PathStyle::Posix => {
PathStyle::Unix => {
let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
(query[..index].to_string(), query[index + 1..].to_string())
} else {
@ -1028,39 +1028,39 @@ mod tests {
#[test]
fn test_get_dir_and_suffix_with_posix_style() {
let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Unix);
assert_eq!(dir, "/");
assert_eq!(suffix, "");
let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Unix);
assert_eq!(dir, "/");
assert_eq!(suffix, "");
let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Unix);
assert_eq!(dir, "/");
assert_eq!(suffix, "Use");
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Unix);
assert_eq!(dir, "/Users/Junkui/");
assert_eq!(suffix, "Docum");
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Unix);
assert_eq!(dir, "/Users/Junkui/");
assert_eq!(suffix, "Documents");
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Unix);
assert_eq!(dir, "/Users/Junkui/Documents/");
assert_eq!(suffix, "");
let (dir, suffix) = get_dir_and_suffix("/root/.".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("/root/.".into(), PathStyle::Unix);
assert_eq!(dir, "/root/");
assert_eq!(suffix, ".");
let (dir, suffix) = get_dir_and_suffix("/root/..".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("/root/..".into(), PathStyle::Unix);
assert_eq!(dir, "/root/");
assert_eq!(suffix, "..");
let (dir, suffix) = get_dir_and_suffix("/root/.hidden".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("/root/.hidden".into(), PathStyle::Unix);
assert_eq!(dir, "/root/");
assert_eq!(suffix, ".hidden");
}

23
crates/path/Cargo.toml Normal file
View file

@ -0,0 +1,23 @@
[package]
name = "path"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lib]
path = "src/path.rs"
[features]
test-support = []
[dependencies]
anyhow.workspace = true
dunce.workspace = true
serde = { workspace = true, optional = true }
[dev-dependencies]
tempfile.workspace = true
[lints]
workspace = true

1
crates/path/LICENSE-APACHE Symbolic link
View file

@ -0,0 +1 @@
../../LICENSE-APACHE

286
crates/path/src/abs_path.rs Normal file
View file

@ -0,0 +1,286 @@
use std::{
borrow::{Borrow, Cow},
fmt, io,
ops::Deref,
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};
use anyhow::Context;
use crate::{PathStyle, rel_path::RelPath};
// An absolute path on the user's local filesystem.
// Requires paths to be valid utf-8
#[derive(PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
#[repr(transparent)]
pub struct AbsPath(Path);
impl AbsPath {
pub fn new(path: &Path) -> anyhow::Result<&Self> {
if !path.is_absolute() {
return Err(anyhow::anyhow!("Path is not absolute: {:?}", path));
}
if path.to_str().is_none() {
return Err(anyhow::anyhow!("Path is not valid utf-8: {:?}", path));
}
Ok(Self::new_unchecked(path))
}
fn new_unchecked(path: &Path) -> &Self {
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
unsafe { &*(path as *const Path as *const Self) }
}
pub fn to_abs_path_buf(&self) -> AbsPathBuf {
AbsPathBuf(self.0.to_owned())
}
pub fn join(&self, name: impl AsRef<str>) -> AbsPathBuf {
AbsPathBuf(self.0.join(name.as_ref()))
}
pub fn join_rel_path(&self, relative_path: &RelPath) -> AbsPathBuf {
AbsPathBuf(self.0.join(relative_path.as_std_path()))
}
pub fn parent(&self) -> Option<&AbsPath> {
let parent = self.0.parent()?;
Some(AbsPath::new_unchecked(parent))
}
pub fn starts_with(&self, other: &AbsPath) -> bool {
self.0.starts_with(&other.0)
}
pub fn ends_with(&self, other: &RelPath) -> bool {
self.0.ends_with(other.as_std_path())
}
pub fn is_descendant_of(&self, ancestor: &Self) -> bool {
if self == ancestor {
return false;
}
self.starts_with(ancestor)
}
pub fn file_name(&self) -> Option<&str> {
self.0.file_name()?.to_str()
}
pub fn display(&self) -> impl fmt::Display + '_ {
self.0.display()
}
pub fn as_std_path(&self) -> &Path {
&self.0
}
pub fn as_str(&self) -> &str {
self.0
.to_str()
.expect("valid UTF-8 enforced in constructor")
}
pub fn ancestors(&self) -> impl Iterator<Item = &AbsPath> {
self.0.ancestors().map(|p| AbsPath::new_unchecked(p))
}
pub fn strip_prefix<'a>(&'a self, prefix: &AbsPath) -> Option<Cow<'a, RelPath>> {
let prefix = self.0.strip_prefix(&prefix.0).ok()?;
RelPath::new(prefix, PathStyle::local()).ok()
}
}
impl ToOwned for AbsPath {
type Owned = AbsPathBuf;
fn to_owned(&self) -> Self::Owned {
self.to_abs_path_buf()
}
}
impl AsRef<Path> for AbsPath {
fn as_ref(&self) -> &Path {
&self.0
}
}
impl AsRef<AbsPath> for AbsPath {
fn as_ref(&self) -> &AbsPath {
self
}
}
impl From<&AbsPath> for Arc<AbsPath> {
fn from(path: &AbsPath) -> Self {
let arc: Arc<Path> = Arc::from(&path.0);
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
unsafe { Arc::from_raw(Arc::into_raw(arc) as *const AbsPath) }
}
}
impl From<&AbsPath> for Rc<AbsPath> {
fn from(path: &AbsPath) -> Self {
let arc: Rc<Path> = Rc::from(&path.0);
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
unsafe { Rc::from_raw(Rc::into_raw(arc) as *const AbsPath) }
}
}
// An absolute path on the user's local filesystem.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct AbsPathBuf(PathBuf);
impl AbsPathBuf {
pub fn new(path: impl Into<PathBuf>) -> anyhow::Result<Self> {
let path = path.into();
if let Err(e) = AbsPath::new(&path) {
return Err(e);
}
Ok(Self(path))
}
pub fn home_dir() -> anyhow::Result<Self> {
let home = std::env::home_dir().context("no home dir available")?;
Self::new(home)
}
/// Resolves `path` to its canonical on-disk spelling: symlinks and `..`
/// are resolved, relative input is anchored on the current directory, and
/// on case-insensitive filesystems each component takes the casing stored
/// on disk. Unlike [`std::fs::canonicalize`], the result never uses
/// Windows extended-length (`\\?\`) syntax, which chokes tools the path
/// is later handed to (e.g. `git`).
///
/// Paths act as identity in several places (lock keys, watch-target
/// comparisons, persisted repository records), so canonicalize a path
/// where it enters the system whenever it may have been spelled by a user
/// or an external tool.
pub fn canonicalize(path: impl AsRef<Path>) -> io::Result<Self> {
let canonical = dunce::canonicalize(path.as_ref())?;
Self::new(canonical).map_err(io::Error::other)
}
pub fn push(&mut self, name: &str) {
self.0.push(name);
}
#[cfg(any(test, feature = "test-support"))]
pub fn new_test(path: &'static str) -> Self {
if cfg!(windows) {
Self::new(format!("C:{path}")).unwrap()
} else {
Self::new(path).unwrap()
}
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn abs_path(path: &str) -> AbsPathBuf {
if cfg!(windows) {
AbsPathBuf::new(format!("C:{path}")).unwrap()
} else {
AbsPathBuf::new(path).unwrap()
}
}
impl Deref for AbsPathBuf {
type Target = AbsPath;
fn deref(&self) -> &Self::Target {
AbsPath::new_unchecked(&self.0)
}
}
impl Borrow<AbsPath> for AbsPathBuf {
fn borrow(&self) -> &AbsPath {
self
}
}
impl AsRef<Path> for AbsPathBuf {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
impl AsRef<AbsPath> for AbsPathBuf {
fn as_ref(&self) -> &AbsPath {
self
}
}
impl From<AbsPathBuf> for PathBuf {
fn from(path: AbsPathBuf) -> PathBuf {
path.0
}
}
impl fmt::Display for AbsPathBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_str().fmt(f)
}
}
impl PartialEq<AbsPath> for AbsPathBuf {
fn eq(&self, other: &AbsPath) -> bool {
**self == *other
}
}
impl PartialEq<AbsPathBuf> for AbsPath {
fn eq(&self, other: &AbsPathBuf) -> bool {
*self == **other
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_canonicalize_restores_on_disk_spelling() {
let temp = tempfile::tempdir().unwrap();
let root = dunce::canonicalize(temp.path()).unwrap();
let dir = root.join("CamelCase");
std::fs::create_dir(&dir).unwrap();
let canonical = AbsPathBuf::canonicalize(&dir).unwrap();
assert_eq!(canonical.as_std_path(), dir);
assert!(
!canonical.as_str().starts_with(r"\\?\"),
"canonical paths must not use Windows extended-length syntax: {canonical}"
);
// A differently-cased spelling addresses the same directory only on a
// case-insensitive filesystem; when it does, canonicalization must
// restore the stored casing.
let lowercased = root.join("camelcase");
if std::fs::metadata(&lowercased).is_ok() {
assert_eq!(
AbsPathBuf::canonicalize(&lowercased).unwrap().as_std_path(),
dir,
"canonicalization should restore the on-disk casing"
);
}
}
#[test]
fn test_new_test_normalizes_rooted_paths() {
if cfg!(windows) {
assert_eq!(AbsPathBuf::new_test("/").as_str(), "C:/");
assert_eq!(
AbsPathBuf::new_test("/test/project").as_str(),
"C:/test/project"
);
} else {
assert_eq!(AbsPathBuf::new_test("/").as_str(), "/");
assert_eq!(
AbsPathBuf::new_test("/test/project").as_str(),
"/test/project"
);
}
}
}

263
crates/path/src/path.rs Normal file
View file

@ -0,0 +1,263 @@
//! Relative path types for deltadb.
//!
//! Provides [`RelPath`] and [`RelPathBuf`] — path types that are guaranteed to be
//! relative, normalized, and valid unicode. Internally stored in POSIX (`/`-delimited)
//! format regardless of host platform.
//!
//! Adapted from Zed's `util::rel_path` module.
use std::{
borrow::Cow,
path::{Path, PathBuf},
};
use crate::rel_path::RelPath;
pub mod abs_path;
pub mod rel_path;
pub trait PathExt {
fn to_rel_path_buf(&self) -> anyhow::Result<rel_path::RelPathBuf>;
}
impl<T: AsRef<Path> + ?Sized> PathExt for T {
fn to_rel_path_buf(&self) -> anyhow::Result<rel_path::RelPathBuf> {
Ok(RelPath::new(self.as_ref(), PathStyle::local())?.into_owned())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PathStyle {
Unix,
Windows,
}
impl PathStyle {
#[cfg(target_os = "windows")]
pub const fn local() -> Self {
PathStyle::Windows
}
#[cfg(not(target_os = "windows"))]
pub const fn local() -> Self {
PathStyle::Unix
}
#[inline]
pub fn primary_separator(&self) -> &'static str {
match self {
PathStyle::Unix => "/",
PathStyle::Windows => "\\",
}
}
pub fn separators(&self) -> &'static [&'static str] {
match self {
PathStyle::Unix => &["/"],
PathStyle::Windows => &["\\", "/"],
}
}
pub fn separators_ch(&self) -> &'static [char] {
match self {
PathStyle::Unix => &['/'],
PathStyle::Windows => &['\\', '/'],
}
}
pub fn is_absolute(&self, path_like: &str) -> bool {
path_like.starts_with('/')
|| *self == PathStyle::Windows
&& (path_like.starts_with('\\')
|| path_like
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic())
&& path_like[1..]
.strip_prefix(':')
.is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
}
pub fn is_windows(&self) -> bool {
*self == PathStyle::Windows
}
pub fn is_posix(&self) -> bool {
*self == PathStyle::Unix
}
pub fn join(self, left: impl AsRef<Path>, right: impl AsRef<Path>) -> Option<String> {
let right = right.as_ref().to_str()?;
if is_absolute(right, self) {
return None;
}
let left = left.as_ref().to_str()?;
if left.is_empty() {
Some(right.into())
} else {
Some(format!(
"{left}{}{right}",
if left.ends_with(self.primary_separator()) {
""
} else {
self.primary_separator()
}
))
}
}
pub fn join_path(
self,
left: impl AsRef<Path>,
right: impl AsRef<Path>,
) -> anyhow::Result<PathBuf> {
let left = left
.as_ref()
.to_str()
.ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
let right = right.as_ref();
let right_string = right
.to_str()
.ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
let joined = self
.join(left, right_string)
.ok_or_else(|| anyhow::anyhow!("Path must be relative: {right:?}"))?;
Ok(PathBuf::from(self.normalize(&joined)))
}
pub fn normalize(self, path_like: &str) -> String {
match self {
PathStyle::Windows => crate::normalize_path(Path::new(path_like))
.to_string_lossy()
.into_owned(),
PathStyle::Unix => {
let is_absolute = path_like.starts_with('/');
let remainder = if is_absolute {
path_like.trim_start_matches('/')
} else {
path_like
};
let mut components = Vec::new();
for component in remainder.split(self.separators_ch()) {
match component {
"" | "." => {}
".." => {
if components
.last()
.is_some_and(|component| *component != "..")
{
components.pop();
} else if !is_absolute {
components.push(component);
}
}
component => components.push(component),
}
}
let normalized = components.join(self.primary_separator());
if is_absolute && normalized.is_empty() {
"/".to_string()
} else if is_absolute {
format!("/{normalized}")
} else {
normalized
}
}
}
}
pub fn split(self, path_like: &str) -> (Option<&str>, &str) {
let Some(pos) = path_like.rfind(self.primary_separator()) else {
return (None, path_like);
};
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()?;
// Match behavior of std::path::Path, which is case-insensitive for drive letters (e.g., "C:" == "c:")
let stripped = if self.is_windows()
&& child.as_bytes().get(1) == Some(&b':')
&& parent.as_bytes().get(1) == Some(&b':')
&& child.as_bytes()[0].eq_ignore_ascii_case(&parent.as_bytes()[0])
{
child[2..].strip_prefix(&parent[2..])?
} else {
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
}
}
}
fn is_absolute(path_like: &str, path_style: PathStyle) -> bool {
path_like.starts_with('/')
|| path_style == PathStyle::Windows
&& (path_like.starts_with('\\')
|| path_like
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic())
&& path_like[1..]
.strip_prefix(':')
.is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
}
/// Normalizes a path by resolving `.` and `..` components without
/// requiring the path to exist on disk (unlike `canonicalize`).
pub fn normalize_path(path: &Path) -> PathBuf {
use std::path::Component;
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}

View file

@ -1,6 +1,4 @@
use crate::paths::{PathStyle, is_absolute};
use anyhow::{Context as _, Result, anyhow};
use serde::{Deserialize, Serialize};
use std::{
borrow::{Borrow, Cow},
fmt,
@ -9,6 +7,12 @@ use std::{
sync::Arc,
};
use crate::{
PathStyle,
abs_path::{AbsPath, AbsPathBuf},
is_absolute,
};
/// A file system path that is guaranteed to be relative and normalized.
///
/// This type can be used to represent paths in a uniform way, regardless of
@ -20,20 +24,22 @@ use std::{
///
/// Relative paths are also guaranteed to be valid unicode.
#[repr(transparent)]
#[derive(PartialEq, Eq, Hash, Serialize)]
#[derive(PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct RelPath(str);
/// An owned representation of a file system path that is guaranteed to be
/// relative and normalized.
///
/// This type is to [`RelPath`] as [`std::path::PathBuf`] is to [`std::path::Path`]
#[derive(PartialEq, Eq, Clone, Ord, PartialOrd, Serialize)]
#[derive(PartialEq, Eq, Clone, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct RelPathBuf(String);
impl RelPath {
/// Creates an empty [`RelPath`].
pub fn empty() -> &'static Self {
Self::new_unchecked("")
Self::from_str("")
}
/// Creates an empty [`RelPath`].
@ -54,7 +60,7 @@ impl RelPath {
let mut path = path.to_str().context("non utf-8 path")?;
let (prefixes, suffixes): (&[_], &[_]) = match path_style {
PathStyle::Posix => (&["./"], &['/']),
PathStyle::Unix => (&["./"], &['/']),
PathStyle::Windows => (&["./", ".\\"], &['/', '\\']),
};
@ -77,7 +83,7 @@ impl RelPath {
}
let mut result = match string {
Cow::Borrowed(string) => Cow::Borrowed(Self::new_unchecked(string)),
Cow::Borrowed(string) => Cow::Borrowed(Self::from_str(string)),
Cow::Owned(string) => Cow::Owned(RelPathBuf(string)),
};
@ -95,7 +101,7 @@ impl RelPath {
return Err(anyhow!("path is not relative: {result:?}"));
}
}
other => normalized.push(RelPath::new_unchecked(other)),
other => normalized.push(RelPath::from_str(other)),
}
}
result = Cow::Owned(normalized)
@ -104,20 +110,25 @@ impl RelPath {
Ok(result)
}
#[track_caller]
pub fn new_test<'a>(path: &'a str) -> Cow<'a, Self> {
Self::new(Path::new(path), PathStyle::Unix).unwrap()
}
/// Converts a path that is already normalized and uses '/' separators
/// into a [`RelPath`] .
///
/// Returns an error if the path is not already in the correct format.
#[track_caller]
pub fn unix<S: AsRef<Path> + ?Sized>(path: &S) -> anyhow::Result<&Self> {
pub fn from_unix_str<S: AsRef<Path> + ?Sized>(path: &S) -> anyhow::Result<&Self> {
let path = path.as_ref();
match Self::new(path, PathStyle::Posix)? {
match Self::new(path, PathStyle::Unix)? {
Cow::Borrowed(path) => Ok(path),
Cow::Owned(_) => Err(anyhow!("invalid relative path {path:?}")),
}
}
fn new_unchecked(s: &str) -> &Self {
fn from_str(s: &str) -> &Self {
// Safety: `RelPath` is a transparent wrapper around `str`.
unsafe { &*(s as *const str as *const Self) }
}
@ -131,7 +142,12 @@ impl RelPath {
}
pub fn ancestors(&self) -> RelPathAncestors<'_> {
RelPathAncestors(Some(&self.0))
RelPathAncestors {
full: &self.0,
front: self.0.len(),
back: 0,
done: false,
}
}
pub fn file_name(&self) -> Option<&str> {
@ -156,7 +172,22 @@ impl RelPath {
self.strip_prefix(other).is_ok()
}
/// Returns true if this path is a strict descendant of `ancestor`.
///
/// Unlike `starts_with`, this returns false when `self == ancestor`
/// and false when `ancestor` is empty (since every path trivially
/// starts with the empty prefix).
pub fn is_descendant_of(&self, ancestor: &Self) -> bool {
if ancestor.is_empty() || self == ancestor {
return false;
}
self.starts_with(ancestor)
}
pub fn ends_with(&self, other: &Self) -> bool {
if other.is_empty() {
return true;
}
if let Some(suffix) = self.0.strip_suffix(&other.0) {
if suffix.ends_with('/') {
return true;
@ -173,7 +204,7 @@ impl RelPath {
}
if let Some(suffix) = self.0.strip_prefix(&other.0) {
if let Some(suffix) = suffix.strip_prefix('/') {
return Ok(Self::new_unchecked(suffix));
return Ok(Self::from_str(suffix));
} else if suffix.is_empty() {
return Ok(Self::empty());
}
@ -182,7 +213,11 @@ impl RelPath {
}
pub fn len(&self) -> usize {
self.0.matches('/').count() + 1
if self.0.is_empty() {
0
} else {
self.0.matches('/').count() + 1
}
}
pub fn last_n_components(&self, count: usize) -> Option<&Self> {
@ -198,15 +233,14 @@ impl RelPath {
}
}
pub fn join(&self, other: &Self) -> Arc<Self> {
let result = if self.0.is_empty() {
Cow::Borrowed(&other.0)
pub fn join(&self, other: &Self) -> RelPathBuf {
if self.0.is_empty() {
other.to_rel_path_buf()
} else if other.0.is_empty() {
Cow::Borrowed(&self.0)
self.to_rel_path_buf()
} else {
Cow::Owned(format!("{}/{}", &self.0, &other.0))
};
Arc::from(Self::new_unchecked(result.as_ref()))
RelPathBuf(format!("{}/{}", &self.0, &other.0))
}
}
pub fn to_rel_path_buf(&self) -> RelPathBuf {
@ -217,23 +251,13 @@ impl RelPath {
Arc::from(self)
}
/// Convert the path into the wire representation.
pub fn to_proto(&self) -> String {
self.as_unix_str().to_owned()
}
/// Load the path from its wire representation.
pub fn from_proto(path: &str) -> Result<Arc<Self>> {
Ok(Arc::from(Self::unix(path)?))
}
/// Convert the path into a string with the given path style.
///
/// Whenever a path is presented to the user, it should be converted to
/// a string via this method.
pub fn display(&self, style: PathStyle) -> Cow<'_, str> {
match style {
PathStyle::Posix => Cow::Borrowed(&self.0),
PathStyle::Unix => Cow::Borrowed(&self.0),
PathStyle::Windows if self.0.contains('/') => Cow::Owned(self.0.replace('/', "\\")),
PathStyle::Windows => Cow::Borrowed(&self.0),
}
@ -255,19 +279,16 @@ impl RelPath {
pub fn as_std_path(&self) -> &Path {
Path::new(&self.0)
}
/// Resolves this relative path against an absolute base path.
pub fn absolutize(&self, base: impl AsRef<AbsPath>) -> AbsPathBuf {
base.as_ref().join(self.as_unix_str())
}
}
#[derive(Debug)]
pub struct StripPrefixError;
impl std::fmt::Display for StripPrefixError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("prefix not found")
}
}
impl std::error::Error for StripPrefixError {}
impl ToOwned for RelPath {
type Owned = RelPathBuf;
@ -324,14 +345,33 @@ impl RelPathBuf {
}
pub fn push(&mut self, path: &RelPath) {
if path.is_empty() {
return;
}
if !self.is_empty() {
self.0.push('/');
}
self.0.push_str(&path.0);
}
pub fn push_component(&mut self, component: &str) -> Result<()> {
anyhow::ensure!(
!component.is_empty()
&& !component.contains('/')
&& component != "."
&& component != "..",
"invalid relative path component: {component:?}"
);
if !self.is_empty() {
self.0.push('/');
}
self.0.push_str(component);
Ok(())
}
pub fn as_rel_path(&self) -> &RelPath {
RelPath::new_unchecked(self.0.as_str())
RelPath::from_str(self.0.as_str())
}
pub fn set_extension(&mut self, extension: &str) -> bool {
@ -339,7 +379,7 @@ impl RelPathBuf {
let mut filename = PathBuf::from(filename);
filename.set_extension(extension);
self.pop();
self.0.push_str(filename.to_str().unwrap());
self.push(RelPath::from_str(filename.to_str().unwrap()));
true
} else {
false
@ -347,33 +387,21 @@ impl RelPathBuf {
}
}
impl<'de> Deserialize<'de> for RelPathBuf {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let path = String::deserialize(deserializer)?;
let rel_path =
RelPath::new(Path::new(&path), PathStyle::local()).map_err(serde::de::Error::custom)?;
Ok(rel_path.into_owned())
impl PartialOrd for RelPathBuf {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Into<Arc<RelPath>> for RelPathBuf {
fn into(self) -> Arc<RelPath> {
Arc::from(self.as_rel_path())
impl Ord for RelPathBuf {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.as_rel_path().cmp(other.as_rel_path())
}
}
impl AsRef<Path> for RelPathBuf {
fn as_ref(&self) -> &Path {
self.as_std_path()
}
}
impl AsRef<Path> for RelPath {
fn as_ref(&self) -> &Path {
self.as_std_path()
impl From<RelPathBuf> for Arc<RelPath> {
fn from(value: RelPathBuf) -> Self {
Arc::from(value.as_rel_path())
}
}
@ -383,12 +411,24 @@ impl AsRef<RelPath> for RelPathBuf {
}
}
impl AsRef<std::path::Path> for RelPathBuf {
fn as_ref(&self) -> &Path {
self.as_std_path()
}
}
impl AsRef<RelPath> for RelPath {
fn as_ref(&self) -> &RelPath {
self
}
}
impl AsRef<std::path::Path> for RelPath {
fn as_ref(&self) -> &Path {
self.as_std_path()
}
}
impl Deref for RelPathBuf {
type Target = RelPath;
@ -406,20 +446,37 @@ impl<'a> From<&'a RelPath> for Cow<'a, RelPath> {
impl From<&RelPath> for Arc<RelPath> {
fn from(rel_path: &RelPath) -> Self {
let bytes: Arc<str> = Arc::from(&rel_path.0);
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
unsafe { Arc::from_raw(Arc::into_raw(bytes) as *const RelPath) }
}
}
impl<'a> TryFrom<&'a str> for &'a RelPath {
type Error = anyhow::Error;
fn try_from(s: &'a str) -> Result<Self> {
RelPath::from_unix_str(s)
}
}
impl TryFrom<&str> for RelPathBuf {
type Error = anyhow::Error;
fn try_from(s: &str) -> Result<Self> {
RelPath::new(Path::new(s), PathStyle::Unix).map(|cow| cow.into_owned())
}
}
#[cfg(any(test, feature = "test-support"))]
#[track_caller]
pub fn rel_path(path: &str) -> &RelPath {
RelPath::unix(path).unwrap()
RelPath::from_unix_str(path).unwrap()
}
#[cfg(any(test, feature = "test-support"))]
#[track_caller]
pub fn rel_path_buf(path: &str) -> RelPathBuf {
RelPath::unix(path).unwrap().to_rel_path_buf()
rel_path(path).to_owned()
}
impl PartialEq<str> for RelPath {
@ -428,26 +485,45 @@ impl PartialEq<str> for RelPath {
}
}
pub trait PathExt {
fn to_rel_path_buf(&self) -> Result<RelPathBuf>;
impl PartialEq<RelPath> for RelPathBuf {
fn eq(&self, other: &RelPath) -> bool {
self.as_rel_path() == other
}
}
impl<T: AsRef<Path> + ?Sized> PathExt for T {
fn to_rel_path_buf(&self) -> Result<RelPathBuf> {
Ok(RelPath::new(self.as_ref(), PathStyle::local())?.into_owned())
impl PartialEq<RelPathBuf> for RelPath {
fn eq(&self, other: &RelPathBuf) -> bool {
other.as_rel_path() == self
}
}
impl fmt::Display for RelPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl fmt::Display for RelPathBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Default)]
pub struct RelPathComponents<'a>(&'a str);
pub struct RelPathAncestors<'a>(Option<&'a str>);
pub struct RelPathAncestors<'a> {
full: &'a str,
front: usize,
back: usize,
done: bool,
}
const SEPARATOR: char = '/';
impl<'a> RelPathComponents<'a> {
pub fn rest(&self) -> &'a RelPath {
RelPath::new_unchecked(self.0)
RelPath::from_str(self.0)
}
}
@ -473,15 +549,35 @@ impl<'a> Iterator for RelPathAncestors<'a> {
type Item = &'a RelPath;
fn next(&mut self) -> Option<Self::Item> {
let result = self.0?;
if let Some(sep_ix) = result.rfind(SEPARATOR) {
self.0 = Some(&result[..sep_ix]);
} else if !result.is_empty() {
self.0 = Some("");
} else {
self.0 = None;
if self.done {
return None;
}
Some(RelPath::new_unchecked(result))
let result = &self.full[..self.front];
if self.front == self.back {
self.done = true;
} else {
self.front = result.rfind(SEPARATOR).unwrap_or_default();
}
Some(RelPath::from_str(result))
}
}
impl<'a> DoubleEndedIterator for RelPathAncestors<'a> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
let result = &self.full[..self.back];
if self.front == self.back {
self.done = true;
} else {
let search_start = if self.back == 0 { 0 } else { self.back + 1 };
self.back = match self.full[search_start..].find(SEPARATOR) {
Some(sep_ix) => search_start + sep_ix,
None => self.full.len(),
};
}
Some(RelPath::from_str(result))
}
}
@ -501,11 +597,21 @@ impl<'a> DoubleEndedIterator for RelPathComponents<'a> {
}
}
impl<'de> serde::Deserialize<'de> for RelPathBuf {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let path = String::deserialize(deserializer)?;
let rel_path =
RelPath::new(Path::new(&path), PathStyle::local()).map_err(serde::de::Error::custom)?;
Ok(rel_path.into_owned())
}
}
#[cfg(test)]
mod tests {
use super::*;
use itertools::Itertools;
use pretty_assertions::assert_matches;
#[test]
fn test_rel_path_new() {
@ -515,11 +621,11 @@ mod tests {
let path = RelPath::new("foo/".as_ref(), PathStyle::local()).unwrap();
assert_eq!(path, rel_path("foo").into());
assert_matches!(path, Cow::Borrowed(_));
assert!(matches!(path, Cow::Borrowed(_)));
let path = RelPath::new("foo\\".as_ref(), PathStyle::Windows).unwrap();
assert_eq!(path, rel_path("foo").into());
assert_matches!(path, Cow::Borrowed(_));
assert!(matches!(path, Cow::Borrowed(_)));
assert_eq!(
RelPath::new("foo/bar/../baz/./quux/".as_ref(), PathStyle::local())
@ -528,29 +634,29 @@ mod tests {
rel_path("foo/baz/quux")
);
let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Posix).unwrap();
let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Unix).unwrap();
assert_eq!(path.as_ref(), rel_path("foo/bar"));
assert_matches!(path, Cow::Borrowed(_));
assert!(matches!(path, Cow::Borrowed(_)));
let path = RelPath::new(".\\foo".as_ref(), PathStyle::Windows).unwrap();
assert_eq!(path, rel_path("foo").into());
assert_matches!(path, Cow::Borrowed(_));
assert!(matches!(path, Cow::Borrowed(_)));
let path = RelPath::new("./.\\./foo/\\/".as_ref(), PathStyle::Windows).unwrap();
assert_eq!(path, rel_path("foo").into());
assert_matches!(path, Cow::Borrowed(_));
assert!(matches!(path, Cow::Borrowed(_)));
let path = RelPath::new("foo/./bar".as_ref(), PathStyle::Posix).unwrap();
let path = RelPath::new("foo/./bar".as_ref(), PathStyle::Unix).unwrap();
assert_eq!(path.as_ref(), rel_path("foo/bar"));
assert_matches!(path, Cow::Owned(_));
assert!(matches!(path, Cow::Owned(_)));
let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Windows).unwrap();
assert_eq!(path.as_ref(), rel_path("foo/bar"));
assert_matches!(path, Cow::Borrowed(_));
assert!(matches!(path, Cow::Borrowed(_)));
let path = RelPath::new(".\\foo\\bar".as_ref(), PathStyle::Windows).unwrap();
assert_eq!(path.as_ref(), rel_path("foo/bar"));
assert_matches!(path, Cow::Owned(_));
assert!(matches!(path, Cow::Owned(_)));
}
#[test]
@ -590,6 +696,41 @@ mod tests {
let mut ancestors = path.ancestors();
assert_eq!(ancestors.next(), Some(RelPath::empty()));
assert_eq!(ancestors.next(), None);
let path = rel_path("foo/bar/baz");
let mut ancestors = path.ancestors();
assert_eq!(ancestors.next_back(), Some(rel_path("")));
assert_eq!(ancestors.next_back(), Some(rel_path("foo")));
assert_eq!(ancestors.next_back(), Some(rel_path("foo/bar")));
assert_eq!(ancestors.next_back(), Some(rel_path("foo/bar/baz")));
assert_eq!(ancestors.next_back(), None);
let path = rel_path("foo/bar/baz");
let mut ancestors = path.ancestors();
assert_eq!(ancestors.next(), Some(rel_path("foo/bar/baz")));
assert_eq!(ancestors.next_back(), Some(rel_path("")));
assert_eq!(ancestors.next(), Some(rel_path("foo/bar")));
assert_eq!(ancestors.next_back(), Some(rel_path("foo")));
assert_eq!(ancestors.next(), None);
assert_eq!(ancestors.next_back(), None);
let path = rel_path("foo");
let mut ancestors = path.ancestors();
assert_eq!(ancestors.next_back(), Some(RelPath::empty()));
assert_eq!(ancestors.next_back(), Some(rel_path("foo")));
assert_eq!(ancestors.next_back(), None);
let path = RelPath::empty();
let mut ancestors = path.ancestors();
assert_eq!(ancestors.next_back(), Some(RelPath::empty()));
assert_eq!(ancestors.next_back(), None);
let path = rel_path("über/x");
let mut ancestors = path.ancestors();
assert_eq!(ancestors.next_back(), Some(RelPath::empty()));
assert_eq!(ancestors.next_back(), Some(rel_path("über")));
assert_eq!(ancestors.next_back(), Some(rel_path("über/x")));
assert_eq!(ancestors.next_back(), None);
}
#[test]
@ -602,13 +743,18 @@ mod tests {
#[test]
fn test_rel_path_partial_ord_is_compatible_with_std() {
let test_cases = ["a/b/c", "relative/path/with/dot.", "relative/path/with.dot"];
for [lhs, rhs] in test_cases.iter().array_combinations::<2>() {
assert_eq!(
Path::new(lhs).cmp(Path::new(rhs)),
RelPath::unix(lhs)
.unwrap()
.cmp(&RelPath::unix(rhs).unwrap())
);
for (i, lhs) in test_cases.iter().enumerate() {
for rhs in &test_cases[i + 1..] {
assert_eq!(
Path::new(lhs).cmp(Path::new(rhs)),
RelPath::from_unix_str(lhs)
.unwrap()
.cmp(RelPath::from_unix_str(rhs).unwrap()),
"ordering mismatch for {:?} vs {:?}",
lhs,
rhs,
);
}
}
}
@ -621,19 +767,28 @@ mod tests {
assert_eq!(child.strip_prefix(parent).unwrap(), child);
}
#[test]
fn test_ends_with() {
assert!(rel_path("foo/bar").ends_with(rel_path("bar")));
assert!(rel_path("foo/bar").ends_with(rel_path("foo/bar")));
assert!(rel_path("foo/bar").ends_with(RelPath::empty()));
assert!(RelPath::empty().ends_with(RelPath::empty()));
assert!(!rel_path("foobar").ends_with(rel_path("bar")));
}
#[test]
fn test_rel_path_constructors_absolute_path() {
assert!(RelPath::new(Path::new("/a/b"), PathStyle::Windows).is_err());
assert!(RelPath::new(Path::new("\\a\\b"), PathStyle::Windows).is_err());
assert!(RelPath::new(Path::new("/a/b"), PathStyle::Posix).is_err());
assert!(RelPath::new(Path::new("/a/b"), PathStyle::Unix).is_err());
assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Windows).is_err());
assert!(RelPath::new(Path::new("C:\\a\\b"), PathStyle::Windows).is_err());
assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Posix).is_ok());
assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Unix).is_ok());
}
#[test]
fn test_pop() {
let mut path = rel_path("a/b").to_rel_path_buf();
let mut path = rel_path_buf("a/b");
path.pop();
assert_eq!(path.as_rel_path().as_unix_str(), "a");
path.pop();
@ -641,4 +796,27 @@ mod tests {
path.pop();
assert_eq!(path.as_rel_path().as_unix_str(), "");
}
#[test]
fn test_len() {
assert_eq!(RelPath::empty().len(), 0);
assert_eq!(rel_path("a").len(), 1);
assert_eq!(rel_path("a/b").len(), 2);
assert_eq!(rel_path("a/b/c").len(), 3);
}
#[test]
fn test_set_extension() {
let mut path = rel_path_buf("a/b/c.txt");
assert!(path.set_extension("rs"));
assert_eq!(path.as_rel_path().as_unix_str(), "a/b/c.rs");
let mut single = rel_path_buf("file.txt");
assert!(single.set_extension("md"));
assert_eq!(single.as_rel_path().as_unix_str(), "file.md");
let mut no_ext = rel_path_buf("a/b/c");
assert!(no_ext.set_extension("rs"));
assert_eq!(no_ext.as_rel_path().as_unix_str(), "a/b/c.rs");
}
}

View file

@ -68,7 +68,7 @@ static CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
/// Returns the relative path to the zed_server directory on the ssh host.
pub fn remote_server_dir_relative() -> &'static RelPath {
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".zed_server").unwrap());
LazyLock::new(|| RelPath::from_unix_str(".zed_server").unwrap());
*CACHED
}
@ -76,7 +76,7 @@ pub fn remote_server_dir_relative() -> &'static RelPath {
/// Returns the relative path to the zed_wsl_server directory on the wsl host.
pub fn remote_wsl_server_dir_relative() -> &'static RelPath {
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".zed_wsl_server").unwrap());
LazyLock::new(|| RelPath::from_unix_str(".zed_wsl_server").unwrap());
*CACHED
}
@ -496,21 +496,21 @@ pub fn local_vscode_folder_name() -> &'static str {
/// Returns the relative path to a `settings.json` file within a project.
pub fn local_settings_file_relative_path() -> &'static RelPath {
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".zed/settings.json").unwrap());
LazyLock::new(|| RelPath::from_unix_str(".zed/settings.json").unwrap());
*CACHED
}
/// Returns the relative path to a `tasks.json` file within a project.
pub fn local_tasks_file_relative_path() -> &'static RelPath {
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".zed/tasks.json").unwrap());
LazyLock::new(|| RelPath::from_unix_str(".zed/tasks.json").unwrap());
*CACHED
}
/// Returns the relative path to a `.vscode/tasks.json` file within a project.
pub fn local_vscode_tasks_file_relative_path() -> &'static RelPath {
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".vscode/tasks.json").unwrap());
LazyLock::new(|| RelPath::from_unix_str(".vscode/tasks.json").unwrap());
*CACHED
}
@ -526,14 +526,14 @@ pub fn task_file_name() -> &'static str {
/// .zed/debug.json
pub fn local_debug_file_relative_path() -> &'static RelPath {
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".zed/debug.json").unwrap());
LazyLock::new(|| RelPath::from_unix_str(".zed/debug.json").unwrap());
*CACHED
}
/// Returns the relative path to a `.vscode/launch.json` file within a project.
pub fn local_vscode_launch_file_relative_path() -> &'static RelPath {
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".vscode/launch.json").unwrap());
LazyLock::new(|| RelPath::from_unix_str(".vscode/launch.json").unwrap());
*CACHED
}

View file

@ -37,8 +37,8 @@ test-support = [
aho-corasick.workspace = true
anyhow.workspace = true
askpass.workspace = true
async-trait.workspace = true
async-channel.workspace = true
async-trait.workspace = true
base64.workspace = true
buffer_diff.workspace = true
circular-buffer.workspace = true
@ -60,15 +60,17 @@ globset.workspace = true
gpui.workspace = true
http_client = { workspace = true, features = ["github-download"] }
image.workspace = true
itertools.workspace = true
indexmap.workspace = true
itertools.workspace = true
language.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true
node_runtime.workspace = true
parking_lot.workspace = true
path.workspace = true
paths.workspace = true
percent-encoding.workspace = true
postage.workspace = true
prettier.workspace = true
rand.workspace = true
@ -93,8 +95,8 @@ tempfile.workspace = true
terminal.workspace = true
text.workspace = true
toml.workspace = true
tracing.workspace = true
url.workspace = true
percent-encoding.workspace = true
util.workspace = true
watch.workspace = true
wax.workspace = true
@ -104,7 +106,6 @@ zed_credentials_provider.workspace = true
zeroize.workspace = true
zlog.workspace = true
ztracing.workspace = true
tracing.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }

View file

@ -312,7 +312,7 @@ impl RemoteBufferStore {
.request(proto::OpenBufferByPath {
project_id,
worktree_id,
path: path.to_proto(),
path: path.as_unix_str().to_owned(),
})
.await?;
let buffer_id = BufferId::new(response.buffer_id)?;

View file

@ -254,7 +254,7 @@ impl language::File for IndexTextFile {
rpc::proto::File {
worktree_id: self.worktree_id.to_proto(),
entry_id: None,
path: self.path.as_ref().to_proto(),
path: self.path.as_ref().as_unix_str().to_owned(),
mtime: None,
is_deleted: false,
is_historic: true,
@ -336,7 +336,7 @@ impl StatusEntry {
};
proto::StatusEntry {
repo_path: self.repo_path.to_proto(),
repo_path: self.repo_path.as_unix_str().to_owned(),
simple_status,
status: Some(status_to_proto(self.status)),
diff_stat_added: self.diff_stat.map(|ds| ds.added),
@ -3825,7 +3825,7 @@ impl GitStore {
.files
.into_iter()
.map(|file| proto::CommitFile {
path: file.path.to_proto(),
path: file.path.as_unix_str().to_owned(),
old_text: file.old_text,
new_text: file.new_text,
is_binary: file.is_binary,
@ -4023,7 +4023,7 @@ impl GitStore {
.entries
.into_iter()
.map(|(path, status)| proto::TreeDiffStatus {
path: path.as_ref().to_proto(),
path: path.as_ref().as_unix_str().to_owned(),
status: match status {
TreeDiffStatus::Added {} => proto::tree_diff_status::Status::Added.into(),
TreeDiffStatus::Modified { .. } => {
@ -5109,7 +5109,7 @@ impl RepositorySnapshot {
.merge
.merge_heads_by_conflicted_path
.iter()
.map(|(repo_path, _)| repo_path.to_proto())
.map(|(repo_path, _)| repo_path.as_unix_str().to_owned())
.collect(),
merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()),
project_id,
@ -5165,13 +5165,13 @@ impl RepositorySnapshot {
current_new_entry = new_statuses.next();
}
Ordering::Greater => {
removed_statuses.push(old_entry.repo_path.to_proto());
removed_statuses.push(old_entry.repo_path.as_unix_str().to_owned());
current_old_entry = old_statuses.next();
}
}
}
(None, Some(old_entry)) => {
removed_statuses.push(old_entry.repo_path.to_proto());
removed_statuses.push(old_entry.repo_path.as_unix_str().to_owned());
current_old_entry = old_statuses.next();
}
(Some(new_entry), None) => {
@ -5196,7 +5196,7 @@ impl RepositorySnapshot {
.merge
.merge_heads_by_conflicted_path
.iter()
.map(|(path, _)| path.to_proto())
.map(|(path, _)| path.as_unix_str().to_owned())
.collect(),
merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()),
project_id,
@ -6051,7 +6051,7 @@ impl Repository {
commit,
paths: paths
.into_iter()
.map(|p| p.to_proto())
.map(|p| p.as_unix_str().to_owned())
.collect(),
})
.await?;
@ -7013,7 +7013,9 @@ impl Repository {
repository_id: id.to_proto(),
paths: entries
.into_iter()
.map(|repo_path| repo_path.to_proto())
.map(|repo_path| {
repo_path.as_unix_str().to_owned()
})
.collect(),
})
.await
@ -7026,7 +7028,9 @@ impl Repository {
repository_id: id.to_proto(),
paths: entries
.into_iter()
.map(|repo_path| repo_path.to_proto())
.map(|repo_path| {
repo_path.as_unix_str().to_owned()
})
.collect(),
})
.await
@ -7157,7 +7161,7 @@ impl Repository {
repository_id: id.to_proto(),
paths: entries
.into_iter()
.map(|repo_path| repo_path.to_proto())
.map(|repo_path| repo_path.as_unix_str().to_owned())
.collect(),
})
.await?;
@ -7245,7 +7249,7 @@ impl Repository {
is_dir: bool,
) -> oneshot::Receiver<Result<()>> {
let work_dir = self.snapshot.work_directory_abs_path.clone();
let path_display = repo_path.as_ref().display(PathStyle::Posix);
let path_display = repo_path.as_ref().display(PathStyle::Unix);
let file_path_str = if is_dir {
format!("{}/", path_display)
} else {
@ -7279,7 +7283,7 @@ impl Repository {
is_dir: bool,
) -> oneshot::Receiver<Result<()>> {
let repository_dir = self.snapshot.repository_dir_abs_path.clone();
let path_display = repo_path.as_ref().display(PathStyle::Posix);
let path_display = repo_path.as_ref().display(PathStyle::Unix);
let file_path_str = if is_dir {
format!("{}/", path_display)
} else {
@ -7722,7 +7726,7 @@ impl Repository {
.request(proto::SetIndexText {
project_id: project_id.0,
repository_id: id.to_proto(),
path: path.to_proto(),
path: path.as_unix_str().to_owned(),
text: content,
})
.await?;
@ -8406,7 +8410,7 @@ impl Repository {
};
Some((
RepoPath::from_rel_path(
&RelPath::from_proto(&entry.path).log_err()?,
RelPath::from_unix_str(&entry.path).log_err()?,
),
status,
))
@ -8735,7 +8739,7 @@ impl Repository {
.into_iter()
.filter_map(|path| {
Some(sum_tree::Edit::Remove(PathKey(
RelPath::from_proto(&path).log_err()?,
RelPath::from_unix_str(&path).log_err()?.into(),
)))
})
.chain(
@ -9326,7 +9330,7 @@ fn format_job_key(key: &GitJobKey) -> SharedString {
.iter()
.map(|p| {
let rel: &RelPath = p;
format!("{}", AsRef::<Path>::as_ref(rel).display())
rel.display(PathStyle::local())
})
.collect();
format!("WriteIndex({})", paths_str.join(", ")).into()
@ -9405,7 +9409,7 @@ pub fn worktrees_directory_for_repo(
let resolved = if path_style.is_posix() {
joined
} else {
util::normalize_path(&joined)
path::normalize_path(&joined)
};
let resolved = if resolved.starts_with(repository_anchor_path) {
resolved
@ -9662,7 +9666,9 @@ fn log_source_to_proto(log_source: &LogSource) -> proto::GitLogSource {
LogSource::All => proto::git_log_source::Source::All(proto::GitLogSourceAll {}),
LogSource::Branch(branch) => proto::git_log_source::Source::Branch(branch.to_string()),
LogSource::Sha(sha) => proto::git_log_source::Source::Sha(sha.to_string()),
LogSource::Path(path) => proto::git_log_source::Source::Path(path.to_proto()),
LogSource::Path(path) => {
proto::git_log_source::Source::Path(path.as_unix_str().to_owned())
}
}),
}
}
@ -10066,11 +10072,9 @@ mod tests {
fn test_new_worktree_path_uses_posix_style_for_remote_paths() {
let work_dir = Path::new("/home/user/dev/lsp-tests");
let directory =
worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Posix).unwrap();
let directory = PathStyle::Posix
.join_path(&directory, "nimble-sky")
.unwrap();
let path = PathStyle::Posix.join_path(&directory, "lsp-tests").unwrap();
worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Unix).unwrap();
let directory = PathStyle::Unix.join_path(&directory, "nimble-sky").unwrap();
let path = PathStyle::Unix.join_path(&directory, "lsp-tests").unwrap();
assert_eq!(
path,

View file

@ -698,7 +698,7 @@ impl ImageStoreImpl for Entity<RemoteImageStore> {
.request(rpc::proto::OpenImageByPath {
project_id,
worktree_id,
path: path.to_proto(),
path: path.as_unix_str().to_owned(),
})
.await?;

View file

@ -1614,7 +1614,7 @@ fn edit_prediction_definitions_to_proto(
.into_iter()
.map(|definition| proto::EditPredictionDefinition {
worktree_id: definition.path.worktree_id.to_proto(),
path: definition.path.path.as_ref().to_proto(),
path: definition.path.path.as_ref().as_unix_str().to_owned(),
start: Some(proto::PointUtf16 {
row: definition.range.start.0.row,
column: definition.range.start.0.column,
@ -1638,7 +1638,9 @@ fn edit_prediction_definitions_from_proto(
Ok(EditPredictionDefinition {
path: ProjectPath {
worktree_id: worktree::WorktreeId::from_proto(definition.worktree_id),
path: RelPath::from_proto(&definition.path).context("invalid path")?,
path: RelPath::from_unix_str(&definition.path)
.context("invalid path")?
.into(),
},
range: Unclipped(PointUtf16::new(start.row, start.column))
..Unclipped(PointUtf16::new(end.row, end.column)),

View file

@ -8755,7 +8755,7 @@ impl LspStore {
project_id: *project_id,
worktree_id: worktree_id.to_proto(),
summary: Some(proto::DiagnosticSummary {
path: path.as_ref().to_proto(),
path: path.as_ref().as_unix_str().to_owned(),
language_server_id: server_id.0 as u64,
error_count: 0,
warning_count: 0,
@ -9046,7 +9046,7 @@ impl LspStore {
diagnostics_summary
.more_summaries
.push(proto::DiagnosticSummary {
path: project_path.path.as_ref().to_proto(),
path: project_path.path.as_ref().as_unix_str().to_owned(),
language_server_id: server_id.0 as u64,
error_count: new_summary.error_count,
warning_count: new_summary.warning_count,
@ -9057,7 +9057,7 @@ impl LspStore {
project_id,
worktree_id: worktree_id.to_proto(),
summary: Some(proto::DiagnosticSummary {
path: project_path.path.as_ref().to_proto(),
path: project_path.path.as_ref().as_unix_str().to_owned(),
language_server_id: server_id.0 as u64,
error_count: new_summary.error_count,
warning_count: new_summary.warning_count,
@ -9141,7 +9141,7 @@ impl LspStore {
Ok(ControlFlow::Continue(Some((
*project_id,
proto::DiagnosticSummary {
path: path_in_worktree.to_proto(),
path: path_in_worktree.as_unix_str().to_owned(),
language_server_id: server_id.0 as u64,
error_count: new_summary.error_count as u32,
warning_count: new_summary.warning_count as u32,
@ -9946,7 +9946,7 @@ impl LspStore {
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
let new_worktree_id = WorktreeId::from_proto(envelope.payload.new_worktree_id);
let new_path =
RelPath::from_proto(&envelope.payload.new_path).context("invalid relative path")?;
RelPath::from_unix_str(&envelope.payload.new_path).context("invalid relative path")?;
let (worktree_store, old_worktree, new_worktree, old_entry) = this
.update(&mut cx, |this, cx| {
@ -10015,7 +10015,9 @@ impl LspStore {
{
let project_path = ProjectPath {
worktree_id,
path: RelPath::from_proto(&message_summary.path).context("invalid path")?,
path: RelPath::from_unix_str(&message_summary.path)
.context("invalid path")?
.into(),
};
let path = project_path.path.clone();
let server_id = LanguageServerId(message_summary.language_server_id as usize);
@ -10050,7 +10052,7 @@ impl LspStore {
diagnostics_summary
.more_summaries
.push(proto::DiagnosticSummary {
path: project_path.path.as_ref().to_proto(),
path: project_path.path.as_ref().as_unix_str().to_owned(),
language_server_id: server_id.0 as u64,
error_count: summary.error_count as u32,
warning_count: summary.warning_count as u32,
@ -10061,7 +10063,7 @@ impl LspStore {
project_id: *project_id,
worktree_id: worktree_id.to_proto(),
summary: Some(proto::DiagnosticSummary {
path: project_path.path.as_ref().to_proto(),
path: project_path.path.as_ref().as_unix_str().to_owned(),
language_server_id: server_id.0 as u64,
error_count: summary.error_count as u32,
warning_count: summary.warning_count as u32,
@ -11539,7 +11541,7 @@ impl LspStore {
project_id,
worktree_id: worktree_id.to_proto(),
summary: Some(proto::DiagnosticSummary {
path: path.as_ref().to_proto(),
path: path.as_ref().as_unix_str().to_owned(),
language_server_id: server_id.0 as u64,
error_count: 0,
warning_count: 0,
@ -12571,7 +12573,7 @@ impl LspStore {
match &symbol.path {
SymbolLocation::InProject(path) => {
result.worktree_id = path.worktree_id.to_proto();
result.path = path.path.to_proto();
result.path = path.path.as_unix_str().to_owned();
}
SymbolLocation::OutsideProject {
abs_path,
@ -12592,8 +12594,9 @@ impl LspStore {
let path = if serialized_symbol.signature.is_empty() {
SymbolLocation::InProject(ProjectPath {
worktree_id,
path: RelPath::from_proto(&serialized_symbol.path)
.context("invalid symbol path")?,
path: RelPath::from_unix_str(&serialized_symbol.path)
.context("invalid symbol path")?
.into(),
})
} else {
SymbolLocation::OutsideProject {
@ -14840,7 +14843,7 @@ impl DiagnosticSummary {
path: &RelPath,
) -> proto::DiagnosticSummary {
proto::DiagnosticSummary {
path: path.to_proto(),
path: path.as_unix_str().to_owned(),
language_server_id: language_server_id.0 as u64,
error_count: self.error_count as u32,
warning_count: self.warning_count as u32,

View file

@ -4,6 +4,7 @@ use std::{
sync::Arc,
};
use path::rel_path::RelPathBuf;
use util::rel_path::RelPath;
/// [RootPathTrie] is a workhorse of [super::ManifestTree]. It is responsible for determining the closest known entry for a given path.
@ -59,12 +60,12 @@ impl<Label: Ord + Clone> RootPathTrie<Label> {
) -> &mut Self {
let mut current = self;
let mut path_so_far = <Arc<RelPath>>::from(RelPath::empty());
let mut path_so_far = RelPathBuf::new();
for key in path.0.iter() {
path_so_far = path_so_far.join(RelPath::unix(key.as_ref()).unwrap());
path_so_far = path_so_far.join(RelPath::from_unix_str(key.as_ref()).unwrap());
current = match current.children.entry(key.clone()) {
Entry::Vacant(vacant_entry) => {
vacant_entry.insert(RootPathTrie::new_with_key(path_so_far.clone()))
vacant_entry.insert(RootPathTrie::new_with_key(path_so_far.clone().into()))
}
Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
};

View file

@ -460,7 +460,7 @@ impl PrettierStore {
) {
let prettier_config_files = Prettier::CONFIG_FILE_NAMES
.iter()
.map(|name| RelPath::unix(name).unwrap())
.map(|name| RelPath::from_unix_str(name).unwrap())
.collect::<HashSet<_>>();
let prettier_config_file_changed = changes

View file

@ -440,14 +440,14 @@ impl ProjectPath {
pub fn from_proto(p: proto::ProjectPath) -> Option<Self> {
Some(Self {
worktree_id: WorktreeId::from_proto(p.worktree_id),
path: RelPath::from_proto(&p.path).log_err()?,
path: RelPath::from_unix_str(&p.path).log_err()?.into(),
})
}
pub fn to_proto(&self) -> proto::ProjectPath {
proto::ProjectPath {
worktree_id: self.worktree_id.to_proto(),
path: self.path.as_ref().to_proto(),
path: self.path.as_ref().as_unix_str().to_owned(),
}
}
@ -1725,7 +1725,7 @@ impl Project {
let path_style = if response.payload.windows_paths {
PathStyle::Windows
} else {
PathStyle::Posix
PathStyle::Unix
};
let worktree_store = cx.new(|cx| {
@ -2152,7 +2152,7 @@ impl Project {
root_repo_is_linked_worktree: false,
},
client,
PathStyle::Posix,
PathStyle::Unix,
cx,
);
self.worktree_store
@ -3152,7 +3152,7 @@ impl Project {
// because SSH projects have client_state: Local but still need to communicate with remote server
let project_id = self.remote_id().unwrap_or(REMOTE_SERVER_PROJECT_ID);
let downloading_files = self.downloading_files.clone();
let path_str = path.to_proto();
let path_str = path.as_unix_str().to_owned();
static NEXT_FILE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
let file_id = NEXT_FILE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
@ -5766,7 +5766,7 @@ impl Project {
) -> Result<proto::OpenBufferResponse> {
let peer_id = envelope.original_sender_id()?;
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let path = RelPath::from_proto(&envelope.payload.path)?;
let path = RelPath::from_unix_str(&envelope.payload.path)?.into();
let open_buffer = this
.update(&mut cx, |this, cx| {
this.open_buffer(ProjectPath { worktree_id, path }, cx)

View file

@ -1023,7 +1023,7 @@ impl SettingsObserver {
.send(proto::UpdateWorktreeSettings {
project_id,
worktree_id,
path: path.to_proto(),
path: path.as_unix_str().to_owned(),
content: Some(content),
kind: Some(
local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
@ -1206,7 +1206,7 @@ impl SettingsObserver {
.unwrap()
.into();
(settings_dir, LocalSettingsKind::Debug)
} else if path.ends_with(RelPath::unix(EDITORCONFIG_NAME).unwrap()) {
} else if path.ends_with(RelPath::from_unix_str(EDITORCONFIG_NAME).unwrap()) {
let Some(settings_dir) = path.parent().map(Arc::from) else {
continue;
};

View file

@ -263,7 +263,7 @@ impl ToolchainStore {
};
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let path = if let Some(path) = envelope.payload.path {
RelPath::from_proto(&path)?
RelPath::from_unix_str(&path)?.into()
} else {
RelPath::empty_arc()
};
@ -277,7 +277,7 @@ impl ToolchainStore {
envelope: TypedEnvelope<proto::ActiveToolchain>,
mut cx: AsyncApp,
) -> Result<proto::ActiveToolchainResponse> {
let path = RelPath::unix(envelope.payload.path.as_deref().unwrap_or(""))?;
let path = RelPath::from_unix_str(envelope.payload.path.as_deref().unwrap_or(""))?;
let toolchain = this
.update(&mut cx, |this, cx| {
let language_name = LanguageName::from_proto(envelope.payload.language_name);
@ -314,7 +314,8 @@ impl ToolchainStore {
.update(&mut cx, |this, cx| {
let language_name = LanguageName::from_proto(envelope.payload.language_name);
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let path = RelPath::from_proto(envelope.payload.path.as_deref().unwrap_or(""))?;
let path =
RelPath::from_unix_str(envelope.payload.path.as_deref().unwrap_or(""))?.into();
anyhow::Ok(this.list_toolchains(
ProjectPath { worktree_id, path },
language_name,
@ -364,7 +365,7 @@ impl ToolchainStore {
has_values,
toolchains,
groups,
relative_worktree_path: Some(relative_path.to_proto()),
relative_worktree_path: Some(relative_path.as_unix_str().to_owned()),
})
}
@ -637,7 +638,7 @@ impl RemoteToolchainStore {
path: path.to_string_lossy().into_owned(),
raw_json: toolchain.as_json.to_string(),
}),
path: Some(project_path.path.to_proto()),
path: Some(project_path.path.as_unix_str().to_owned()),
})
.await
.log_err()?;
@ -667,7 +668,7 @@ impl RemoteToolchainStore {
project_id,
worktree_id: path.worktree_id.to_proto(),
language_name: language_name.clone().into(),
path: Some(path.path.to_proto()),
path: Some(path.path.as_unix_str().to_owned()),
})
.await
.log_err()?;
@ -693,7 +694,7 @@ impl RemoteToolchainStore {
Some((usize::try_from(group.start_index).ok()?, group.name.into()))
})
.collect();
let relative_path = RelPath::from_proto(
let relative_path = RelPath::from_unix_str(
response
.relative_worktree_path
.as_deref()
@ -706,7 +707,7 @@ impl RemoteToolchainStore {
default: None,
groups,
},
relative_path,
relative_path.into(),
))
})
}
@ -724,7 +725,7 @@ impl RemoteToolchainStore {
project_id,
worktree_id: path.worktree_id.to_proto(),
language_name: language_name.clone().into(),
path: Some(path.path.to_proto()),
path: Some(path.path.as_unix_str().to_owned()),
})
.await
.log_err()?;

View file

@ -513,7 +513,7 @@ impl WorktreeStore {
let response = upstream_client.request(proto::CopyProjectEntry {
project_id: *upstream_project_id,
entry_id: entry_id.to_proto(),
new_path: new_project_path.path.to_proto(),
new_path: new_project_path.path.as_unix_str().to_owned(),
new_worktree_id: new_project_path.worktree_id.to_proto(),
});
cx.spawn(async move |_, cx| {
@ -668,7 +668,7 @@ impl WorktreeStore {
let response = upstream_client.request(proto::RenameProjectEntry {
project_id: *upstream_project_id,
entry_id: entry_id.to_proto(),
new_path: new_project_path.path.to_proto(),
new_path: new_project_path.path.as_unix_str().to_owned(),
new_worktree_id: new_project_path.worktree_id.to_proto(),
});
cx.spawn(async move |_, cx| {
@ -1231,7 +1231,7 @@ impl WorktreeStore {
let new_worktree_id = WorktreeId::from_proto(envelope.payload.new_worktree_id);
let new_project_path = (
new_worktree_id,
RelPath::from_proto(&envelope.payload.new_path)?,
RelPath::from_unix_str(&envelope.payload.new_path)?,
);
let (scan_id, entry) = this.update(&mut cx, |this, cx| {
let Some((_, project_id)) = this.downstream_client else {
@ -1289,7 +1289,7 @@ impl WorktreeStore {
) -> Result<proto::ProjectEntryResponse> {
let entry_id = ProjectEntryId::from_proto(request.entry_id);
let new_worktree_id = WorktreeId::from_proto(request.new_worktree_id);
let rel_path = RelPath::from_proto(&request.new_path)
let rel_path = RelPath::from_unix_str(&request.new_path)
.with_context(|| format!("received invalid relative path {:?}", &request.new_path))?;
let (scan_id, task) = this.update(&mut cx, |this, cx| {

View file

@ -721,7 +721,12 @@ mod git_traversal {
let traversal = GitTraversal::new(
&repo_snapshots,
worktree_snapshot.traverse_from_path(true, false, true, RelPath::unix("x").unwrap()),
worktree_snapshot.traverse_from_path(
true,
false,
true,
RelPath::from_unix_str("x").unwrap(),
),
);
let entries = traversal
.map(|entry| (entry.path.clone(), entry.git_summary))
@ -1204,45 +1209,44 @@ mod git_worktrees {
let work_dir = Path::new("/code/my-project");
// Valid: sibling
assert!(worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Posix).is_ok());
assert!(worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Unix).is_ok());
// Valid: subdirectory
assert!(
worktrees_directory_for_repo(work_dir, ".git/zed-worktrees", PathStyle::Posix).is_ok()
worktrees_directory_for_repo(work_dir, ".git/zed-worktrees", PathStyle::Unix).is_ok()
);
assert!(worktrees_directory_for_repo(work_dir, "my-worktrees", PathStyle::Posix).is_ok());
assert!(worktrees_directory_for_repo(work_dir, "my-worktrees", PathStyle::Unix).is_ok());
// Invalid: just ".." would resolve back to the working directory itself
let err = worktrees_directory_for_repo(work_dir, "..", PathStyle::Posix).unwrap_err();
let err = worktrees_directory_for_repo(work_dir, "..", PathStyle::Unix).unwrap_err();
assert!(err.to_string().contains("must not be \"..\""));
// Invalid: ".." with trailing separators
let err = worktrees_directory_for_repo(work_dir, "..\\", PathStyle::Posix).unwrap_err();
let err = worktrees_directory_for_repo(work_dir, "..\\", PathStyle::Unix).unwrap_err();
assert!(err.to_string().contains("must not be \"..\""));
let err = worktrees_directory_for_repo(work_dir, "../", PathStyle::Posix).unwrap_err();
let err = worktrees_directory_for_repo(work_dir, "../", PathStyle::Unix).unwrap_err();
assert!(err.to_string().contains("must not be \"..\""));
// Invalid: empty string would resolve to the working directory itself
let err = worktrees_directory_for_repo(work_dir, "", PathStyle::Posix).unwrap_err();
let err = worktrees_directory_for_repo(work_dir, "", PathStyle::Unix).unwrap_err();
assert!(err.to_string().contains("must not be empty"));
// Invalid: absolute path
let err =
worktrees_directory_for_repo(work_dir, "/tmp/worktrees", PathStyle::Posix).unwrap_err();
worktrees_directory_for_repo(work_dir, "/tmp/worktrees", PathStyle::Unix).unwrap_err();
assert!(err.to_string().contains("relative path"));
// Invalid: "/" is absolute on Unix
let err = worktrees_directory_for_repo(work_dir, "/", PathStyle::Posix).unwrap_err();
let err = worktrees_directory_for_repo(work_dir, "/", PathStyle::Unix).unwrap_err();
assert!(err.to_string().contains("relative path"));
// Invalid: "///" is absolute
let err = worktrees_directory_for_repo(work_dir, "///", PathStyle::Posix).unwrap_err();
let err = worktrees_directory_for_repo(work_dir, "///", PathStyle::Unix).unwrap_err();
assert!(err.to_string().contains("relative path"));
// Invalid: escapes too far up
let err =
worktrees_directory_for_repo(work_dir, "../../other-project/wt", PathStyle::Posix)
.unwrap_err();
let err = worktrees_directory_for_repo(work_dir, "../../other-project/wt", PathStyle::Unix)
.unwrap_err();
assert!(err.to_string().contains("outside"));
}
@ -1251,7 +1255,7 @@ mod git_worktrees {
let work_dir = Path::new("/home/user/dev/lsp-tests");
let directory =
worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Posix).unwrap();
worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Unix).unwrap();
assert_eq!(
directory,

View file

@ -48,7 +48,7 @@ async fn test_path_inclusion_matcher(cx: &mut gpui::TestAppContext) {
let entry = Entry {
id: ProjectEntryId::from_proto(1),
kind: EntryKind::UnloadedDir,
path: Arc::from(RelPath::unix(Path::new("src/data")).unwrap()),
path: Arc::from(RelPath::from_unix_str(Path::new("src/data")).unwrap()),
inode: 0,
mtime: None,
canonical_path: None,
@ -90,7 +90,7 @@ async fn test_path_inclusion_matcher(cx: &mut gpui::TestAppContext) {
// 2. Test searching for `field`, including ignored files but updating
// `files_to_include` to only include files under `src/lib`.
let include_ignored = true;
let files_to_include = PathMatcher::new(vec!["src/lib"], PathStyle::Posix).unwrap();
let files_to_include = PathMatcher::new(vec!["src/lib"], PathStyle::Unix).unwrap();
let files_to_exclude = PathMatcher::default();
let match_full_paths = false;
let search_query = SearchQuery::text(

View file

@ -15719,7 +15719,7 @@ mod disable_ai_settings_tests {
let worktree_id = WorktreeId::from_usize(1);
let rel_path = |path: &str| -> std::sync::Arc<util::rel_path::RelPath> {
std::sync::Arc::from(util::rel_path::RelPath::unix(path).unwrap())
std::sync::Arc::from(util::rel_path::RelPath::from_unix_str(path).unwrap())
};
let project_path = rel_path("project");
let settings_location = SettingsLocation {

View file

@ -21,7 +21,7 @@ fn load_linux_repo_snapshot() -> Vec<GitEntry> {
let entry = Entry {
kind,
path: Arc::from(RelPath::unix(&(line.trim_end()[2..])).unwrap()),
path: Arc::from(RelPath::from_unix_str(&(line.trim_end()[2..])).unwrap()),
id: ProjectEntryId::default(),
size: 0,
inode: 0,

View file

@ -1841,7 +1841,7 @@ impl ProjectPanel {
}
let trimmed_filename = trimmed_filename.trim_start_matches('/');
let Ok(filename) = RelPath::unix(trimmed_filename) else {
let Ok(filename) = RelPath::from_unix_str(trimmed_filename) else {
edit_state.validation_state = ValidationState::Warning(
"File or directory name contains leading or trailing whitespace.".to_string(),
);
@ -1865,7 +1865,7 @@ impl ProjectPanel {
let new_path = if let Some(parent) = entry.path.clone().parent() {
parent.join(&filename)
} else {
filename.into()
filename.to_owned()
};
if let Some(existing) = worktree.read(cx).entry_for_path(&new_path)
&& existing.id != entry.id
@ -1950,7 +1950,7 @@ impl ProjectPanel {
});
} else {
let new_path = if let Some(parent) = entry.path.parent() {
parent.join(&filename)
parent.join(&filename).into()
} else {
filename.clone()
};
@ -3303,7 +3303,7 @@ impl ProjectPanel {
let source_entry = source_worktree.read(cx).entry_for_id(source.entry_id)?;
let clipboard_entry_file_name = source_entry.path.file_name()?.to_string();
new_path.push(RelPath::unix(&clipboard_entry_file_name).unwrap());
new_path.push(RelPath::from_unix_str(&clipboard_entry_file_name).unwrap());
let (extension, file_name_without_extension) = if source_entry.is_file() {
(
@ -3339,7 +3339,7 @@ impl ProjectPanel {
new_file_name.push_str(extension);
}
new_path.push(RelPath::unix(&new_file_name).unwrap());
new_path.push(RelPath::from_unix_str(&new_file_name).unwrap());
disambiguation_range = Some(0..(file_name_len + disambiguation_len));
ix += 1;
@ -3858,7 +3858,7 @@ impl ProjectPanel {
let dir_path = if include_root {
worktree.read(cx).root_name().join(&dir_path)
} else {
dir_path
dir_path.to_rel_path_buf()
};
self.workspace
@ -3923,7 +3923,7 @@ impl ProjectPanel {
let Some(source_name) = source_path.path.file_name() else {
return (None, None);
};
let Ok(source_name) = RelPath::unix(source_name) else {
let Ok(source_name) = RelPath::from_unix_str(source_name) else {
return (None, None);
};
@ -4183,7 +4183,10 @@ impl ProjectPanel {
entry: Entry {
id: NEW_ENTRY_ID,
kind: new_entry_kind,
path: parent_entry.path.join(RelPath::unix("\0").unwrap()),
path: parent_entry
.path
.join(RelPath::from_unix_str("\0").unwrap())
.into(),
inode: 0,
mtime: parent_entry.mtime,
size: parent_entry.size,
@ -4390,19 +4393,21 @@ impl ProjectPanel {
entry.path.strip_prefix(root_folded_entry).ok().and_then(
|suffix| {
Some(
RelPath::unix(root_folded_entry.file_name()?)
.unwrap()
.join(suffix),
RelPath::from_unix_str(
root_folded_entry.file_name()?,
)
.unwrap()
.join(suffix),
)
},
)
})
.or_else(|| {
entry.path.file_name().map(|file_name| {
RelPath::unix(file_name).unwrap().into()
RelPath::from_unix_str(file_name).unwrap().to_owned()
})
})
.unwrap_or_else(|| entry.path.clone());
.unwrap_or_else(|| entry.path.to_rel_path_buf());
let depth = path.components().count();
(depth, path.as_unix_str().chars().count())
};
@ -4588,7 +4593,7 @@ impl ProjectPanel {
if let Some(name) = path.file_name()
&& let Some(name) = name.to_str()
{
let target_path = target_directory.join(RelPath::unix(name).unwrap());
let target_path = target_directory.join(RelPath::from_unix_str(name).unwrap());
if worktree.read(cx).entry_for_path(&target_path).is_some() {
paths_to_replace.push((name.to_string(), path.clone()));
}
@ -4946,7 +4951,7 @@ impl ProjectPanel {
move_results.iter().find(|(id, _)| id == resolved_id)?;
let worktree = project.worktree_for_entry(new_entry.id, cx)?;
let leaf_path = new_entry.path.join(suffix);
Some((worktree, leaf_path))
Some((worktree, leaf_path.into()))
})
.collect()
})

View file

@ -262,7 +262,7 @@ impl PickerDelegate for ProjectSymbolsDelegate {
let path = match &symbol.path {
SymbolLocation::InProject(project_path) => {
let project = self.project.read(cx);
let mut path = project_path.path.clone();
let mut path = project_path.path.to_rel_path_buf();
if self.show_worktree_root_name
&& let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
{

View file

@ -17,13 +17,13 @@ const DEV_CONTAINER_SUGGEST_KEY: &str = "dev_container_suggest_dismissed";
fn devcontainer_dir_path() -> &'static RelPath {
static PATH: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".devcontainer").expect("valid path"));
LazyLock::new(|| RelPath::from_unix_str(".devcontainer").expect("valid path"));
*PATH
}
fn devcontainer_json_path() -> &'static RelPath {
static PATH: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".devcontainer.json").expect("valid path"));
LazyLock::new(|| RelPath::from_unix_str(".devcontainer.json").expect("valid path"));
*PATH
}

View file

@ -1845,7 +1845,7 @@ impl RemoteServerProjects {
.and_then(|path| path.into_abs_path())
.map(|path| RemotePathBuf::new(path, path_style))
.unwrap_or_else(|| match path_style {
PathStyle::Posix => RemotePathBuf::from_str("/", PathStyle::Posix),
PathStyle::Unix => RemotePathBuf::from_str("/", PathStyle::Unix),
PathStyle::Windows => {
RemotePathBuf::from_str("C:\\", PathStyle::Windows)
}
@ -2164,7 +2164,7 @@ impl RemoteServerProjects {
if let Some(worktree) = worktree {
let tree_id = worktree.read(cx).id();
let devcontainer_path =
match RelPath::new(&config_path, util::paths::PathStyle::Posix) {
match RelPath::new(&config_path, util::paths::PathStyle::Unix) {
Ok(path) => path.into_owned(),
Err(error) => {
log::error!(

View file

@ -90,7 +90,7 @@ impl DockerExecConnection {
this.path_style = match remote_platform.os {
RemoteOs::Windows => Some(PathStyle::Windows),
_ => Some(PathStyle::Posix),
_ => Some(PathStyle::Unix),
};
this.remote_platform = Some(remote_platform);
@ -219,7 +219,7 @@ impl DockerExecConnection {
version_str
);
let dst_path =
paths::remote_server_dir_relative().join(RelPath::unix(&binary_name).unwrap());
paths::remote_server_dir_relative().join(RelPath::from_unix_str(&binary_name).unwrap());
let binary_exists_on_server = self
.run_docker_exec(
@ -240,7 +240,7 @@ impl DockerExecConnection {
.await?
{
let tmp_path = paths::remote_server_dir_relative().join(
RelPath::unix(&format!(
RelPath::from_unix_str(&format!(
"download-{}-{}",
std::process::id(),
remote_server_path.file_name().unwrap().to_string_lossy()
@ -257,11 +257,11 @@ impl DockerExecConnection {
.await?;
self.extract_server_binary(&dst_path, &tmp_path, &remote_dir_for_server, delegate, cx)
.await?;
return Ok(dst_path);
return Ok(dst_path.into());
}
if binary_exists_on_server {
return Ok(dst_path);
return Ok(dst_path.into());
}
let wanted_version = cx.update(|cx| match release_channel {
@ -276,7 +276,7 @@ impl DockerExecConnection {
})?;
let tmp_path_gz = paths::remote_server_dir_relative().join(
RelPath::unix(&format!(
RelPath::from_unix_str(&format!(
"{}-download-{}.gz",
binary_name,
std::process::id()
@ -302,7 +302,7 @@ impl DockerExecConnection {
)
.await
.context("extracting server binary")?;
return Ok(dst_path);
return Ok(dst_path.into());
}
Err(e) => {
log::error!(
@ -334,7 +334,7 @@ impl DockerExecConnection {
)
.await
.context("extracting server binary")?;
Ok(dst_path)
Ok(dst_path.into())
}
async fn docker_user_home_dir(&self) -> Result<String> {
@ -850,7 +850,7 @@ impl RemoteConnection for DockerExecConnection {
}
fn path_style(&self) -> PathStyle {
self.path_style.unwrap_or(PathStyle::Posix)
self.path_style.unwrap_or(PathStyle::Unix)
}
fn remote_platform(&self) -> RemotePlatform {

View file

@ -773,7 +773,7 @@ impl SshRemoteConnection {
let (ssh_path_style, ssh_default_system_shell) = match ssh_platform.os {
RemoteOs::Windows => (PathStyle::Windows, ssh_shell.clone()),
_ => (PathStyle::Posix, String::from("/bin/sh")),
_ => (PathStyle::Unix, String::from("/bin/sh")),
};
let mut this = Self {
@ -822,7 +822,7 @@ impl SshRemoteConnection {
}
);
let dst_path =
paths::remote_server_dir_relative().join(RelPath::unix(&binary_name).unwrap());
paths::remote_server_dir_relative().join(RelPath::from_unix_str(&binary_name).unwrap());
let binary_exists_on_server = self
.socket
@ -845,7 +845,7 @@ impl SshRemoteConnection {
.await?
{
let tmp_path = paths::remote_server_dir_relative().join(
RelPath::unix(&format!(
RelPath::from_unix_str(&format!(
"download-{}-{}",
std::process::id(),
remote_server_path.file_name().unwrap().to_string_lossy()
@ -856,11 +856,11 @@ impl SshRemoteConnection {
.await?;
self.extract_server_binary(&dst_path, &tmp_path, delegate, cx)
.await?;
return Ok(dst_path);
return Ok(dst_path.into());
}
if binary_exists_on_server {
return Ok(dst_path);
return Ok(dst_path.into());
}
let wanted_version = cx.update(|cx| match release_channel {
@ -875,7 +875,7 @@ impl SshRemoteConnection {
})?;
let tmp_path_compressed = remote_server_dir_relative().join(
RelPath::unix(&format!(
RelPath::from_unix_str(&format!(
"{}-download-{}.{}",
binary_name,
std::process::id(),
@ -905,7 +905,7 @@ impl SshRemoteConnection {
self.extract_server_binary(&dst_path, &tmp_path_compressed, delegate, cx)
.await
.context("extracting server binary")?;
return Ok(dst_path);
return Ok(dst_path.into());
}
Err(e) => {
log::error!(
@ -930,7 +930,7 @@ impl SshRemoteConnection {
self.extract_server_binary(&dst_path, &tmp_path_compressed, delegate, cx)
.await
.context("extracting server binary")?;
Ok(dst_path)
Ok(dst_path.into())
}
async fn download_binary_on_server(
@ -2041,7 +2041,7 @@ mod tests {
Some("~/work".to_string()),
None,
env.clone(),
PathStyle::Posix,
PathStyle::Unix,
"/bin/bash",
ShellKind::Posix,
vec!["-o".to_string(), "ControlMaster=auto".to_string()],
@ -2061,7 +2061,7 @@ mod tests {
Some("~/work".to_string()),
None,
env.clone(),
PathStyle::Posix,
PathStyle::Unix,
"/bin/fish",
ShellKind::Fish,
vec!["-p".to_string(), "2222".to_string()],
@ -2096,7 +2096,7 @@ mod tests {
None,
Some((1, "foo".to_owned(), 2)),
env.clone(),
PathStyle::Posix,
PathStyle::Unix,
"/bin/fish",
ShellKind::Fish,
vec!["-p".to_string(), "2222".to_string()],
@ -2136,7 +2136,7 @@ mod tests {
None,
None,
HashMap::default(),
PathStyle::Posix,
PathStyle::Unix,
"/bin/bash",
ShellKind::Posix,
vec![],
@ -2291,7 +2291,7 @@ mod tests {
None,
Some((8080, "::1".to_owned(), 80)),
HashMap::default(),
PathStyle::Posix,
PathStyle::Unix,
"/bin/bash",
ShellKind::Posix,
vec![],

View file

@ -203,10 +203,10 @@ impl WslRemoteConnection {
);
let dst_path =
paths::remote_server_dir_relative().join(RelPath::unix(&binary_name).unwrap());
paths::remote_server_dir_relative().join(RelPath::from_unix_str(&binary_name).unwrap());
if let Some(parent) = dst_path.parent() {
let parent = parent.display(PathStyle::Posix);
let parent = parent.display(PathStyle::Unix);
let mkdir = self.shell_kind.prepend_command_prefix("mkdir");
self.run_wsl_command(&mkdir, &["-p", &parent])
.await
@ -214,7 +214,7 @@ impl WslRemoteConnection {
}
let binary_exists_on_server = self
.run_wsl_command(&dst_path.display(PathStyle::Posix), &["version"])
.run_wsl_command(&dst_path.display(PathStyle::Unix), &["version"])
.await
.is_ok();
@ -228,7 +228,7 @@ impl WslRemoteConnection {
.await?
{
let tmp_path = paths::remote_server_dir_relative().join(
&RelPath::unix(&format!(
&RelPath::from_unix_str(&format!(
"download-{}-{}",
std::process::id(),
remote_server_path.file_name().unwrap().to_string_lossy()
@ -239,11 +239,11 @@ impl WslRemoteConnection {
.await?;
self.extract_and_install(&tmp_path, &dst_path, delegate, cx)
.await?;
return Ok(dst_path);
return Ok(dst_path.into());
}
if binary_exists_on_server {
return Ok(dst_path);
return Ok(dst_path.into());
}
let wanted_version = match release_channel {
@ -257,16 +257,16 @@ impl WslRemoteConnection {
let tmp_path = format!(
"{}.{}.gz",
dst_path.display(PathStyle::Posix),
dst_path.display(PathStyle::Unix),
std::process::id()
);
let tmp_path = RelPath::unix(&tmp_path).unwrap();
let tmp_path = RelPath::from_unix_str(&tmp_path).unwrap();
self.upload_file(&src_path, &tmp_path, delegate, cx).await?;
self.extract_and_install(&tmp_path, &dst_path, delegate, cx)
.await?;
Ok(dst_path)
Ok(dst_path.into())
}
async fn upload_file(
@ -279,7 +279,7 @@ impl WslRemoteConnection {
delegate.set_status(Some("Uploading remote server"), cx);
if let Some(parent) = dst_path.parent() {
let parent = parent.display(PathStyle::Posix);
let parent = parent.display(PathStyle::Unix);
let mkdir = self.shell_kind.prepend_command_prefix("mkdir");
self.run_wsl_command(&mkdir, &["-p", &parent])
.await
@ -301,7 +301,7 @@ impl WslRemoteConnection {
let cp = self.shell_kind.prepend_command_prefix("cp");
self.run_wsl_command(
&cp,
&["-f", &src_path_in_wsl, &dst_path.display(PathStyle::Posix)],
&["-f", &src_path_in_wsl, &dst_path.display(PathStyle::Unix)],
)
.await
.map_err(|e| {
@ -327,8 +327,8 @@ impl WslRemoteConnection {
) -> Result<()> {
delegate.set_status(Some("Extracting remote server"), cx);
let tmp_path_str = tmp_path.display(PathStyle::Posix);
let dst_path_str = dst_path.display(PathStyle::Posix);
let tmp_path_str = tmp_path.display(PathStyle::Unix);
let dst_path_str = dst_path.display(PathStyle::Unix);
// Build extraction script with proper error handling
let script = if tmp_path_str.ends_with(".gz") {
@ -379,7 +379,7 @@ impl RemoteConnection for WslRemoteConnection {
}
}
proxy_args.push(remote_binary_path.display(PathStyle::Posix).into_owned());
proxy_args.push(remote_binary_path.display(PathStyle::Unix).into_owned());
proxy_args.push("proxy".to_owned());
proxy_args.push("--identifier".to_owned());
proxy_args.push(unique_identifier);
@ -467,7 +467,7 @@ impl RemoteConnection for WslRemoteConnection {
let shell_kind = self.shell_kind;
let working_dir = working_dir
.map(|working_dir| RemotePathBuf::new(working_dir, PathStyle::Posix).to_string())
.map(|working_dir| RemotePathBuf::new(working_dir, PathStyle::Unix).to_string())
.unwrap_or("~".to_string());
let mut exec = String::from("exec env ");
@ -538,7 +538,7 @@ impl RemoteConnection for WslRemoteConnection {
}
fn path_style(&self) -> PathStyle {
PathStyle::Posix
PathStyle::Unix
}
fn remote_platform(&self) -> RemotePlatform {

View file

@ -585,7 +585,7 @@ impl HeadlessProject {
mut cx: AsyncApp,
) -> Result<proto::OpenBufferResponse> {
let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
let path = RelPath::from_proto(&message.payload.path)?;
let path = RelPath::from_unix_str(&message.payload.path)?.into();
let (buffer_store, buffer) = this.update(&mut cx, |this, cx| {
let buffer_store = this.buffer_store.clone();
let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
@ -614,7 +614,7 @@ impl HeadlessProject {
) -> Result<proto::OpenImageResponse> {
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
let path = RelPath::from_proto(&message.payload.path)?;
let path = RelPath::from_unix_str(&message.payload.path)?;
let project_id = message.payload.project_id;
use proto::create_image_for_peer::Variant;
@ -729,7 +729,7 @@ impl HeadlessProject {
);
let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
let path = RelPath::from_proto(&message.payload.path)?;
let path = RelPath::from_unix_str(&message.payload.path)?;
let project_id = message.payload.project_id;
let file_id = message.payload.file_id;
log::debug!(

View file

@ -100,7 +100,9 @@ impl EditorconfigStore {
return Err(InvalidSettingsError::Editorconfig {
message: e.to_string(),
path: LocalSettingsPath::InWorktree(
rel_path.join(RelPath::unix(EDITORCONFIG_NAME).unwrap()),
rel_path
.join(RelPath::from_unix_str(EDITORCONFIG_NAME).unwrap())
.into(),
),
});
}

View file

@ -228,7 +228,7 @@ impl LocalSettingsPath {
pub fn to_proto(&self) -> String {
match self {
Self::InWorktree(path) => path.to_proto(),
Self::InWorktree(path) => path.as_unix_str().to_owned(),
Self::OutsideWorktree(path) => path.to_string_lossy().to_string(),
}
}
@ -237,7 +237,7 @@ impl LocalSettingsPath {
if is_outside_worktree {
Ok(Self::OutsideWorktree(PathBuf::from(path).into()))
} else {
Ok(Self::InWorktree(RelPath::from_proto(path)?))
Ok(Self::InWorktree(RelPath::from_unix_str(path)?.into()))
}
}
}
@ -1048,7 +1048,7 @@ impl SettingsStore {
return Err(InvalidSettingsError::Tasks {
message: "Attempted to submit tasks into the settings store".to_string(),
path: directory_path
.join(RelPath::unix(task_file_name()).unwrap())
.join(RelPath::from_unix_str(task_file_name()).unwrap())
.as_std_path()
.to_path_buf(),
});
@ -1058,7 +1058,7 @@ impl SettingsStore {
message: "Attempted to submit debugger config into the settings store"
.to_string(),
path: directory_path
.join(RelPath::unix(task_file_name()).unwrap())
.join(RelPath::from_unix_str(task_file_name()).unwrap())
.as_std_path()
.to_path_buf(),
});
@ -1085,7 +1085,9 @@ impl SettingsStore {
ParseStatus::Success => Ok(()),
ParseStatus::Unchanged => Ok(()),
ParseStatus::Failed { error } => Err(InvalidSettingsError::LocalSettings {
path: directory_path.join(local_settings_file_relative_path()),
path: directory_path
.join(local_settings_file_relative_path())
.into(),
message: error,
}),
}?;
@ -2772,23 +2774,17 @@ mod tests {
let local_1_child = (
WorktreeId::from_usize(0),
RelPath::new(
std::path::Path::new("child1"),
util::paths::PathStyle::Posix,
)
.unwrap()
.into_arc(),
RelPath::new(std::path::Path::new("child1"), util::paths::PathStyle::Unix)
.unwrap()
.into_arc(),
);
let local_2 = (WorktreeId::from_usize(1), RelPath::empty_arc());
let local_2_child = (
WorktreeId::from_usize(1),
RelPath::new(
std::path::Path::new("child2"),
util::paths::PathStyle::Posix,
)
.unwrap()
.into_arc(),
RelPath::new(std::path::Path::new("child2"), util::paths::PathStyle::Unix)
.unwrap()
.into_arc(),
);
fn get(content: &SettingsContent) -> Option<&u32> {

View file

@ -4113,7 +4113,7 @@ impl SettingsWindow {
} else {
Some(worktree.update(cx, |tree, cx| {
tree.create_entry(
settings_path.clone(),
settings_path.clone().into(),
false,
Some(initial_project_settings_content().as_bytes().to_vec()),
cx,
@ -4647,7 +4647,7 @@ fn update_settings_file(
anyhow::bail!("No settings window found");
};
update_project_setting_file(worktree_id, rel_path, update, settings_window, cx)
update_project_setting_file(worktree_id, rel_path.into(), update, settings_window, cx)
}
SettingsUiFile::User => {
// todo(settings_ui) error?
@ -6477,7 +6477,7 @@ mod project_settings_update_tests {
(worktree.read(cx).id(), worktree.downgrade())
});
let rel_path: Arc<RelPath> = RelPath::unix(".zed/settings.json")
let rel_path: Arc<RelPath> = RelPath::from_unix_str(".zed/settings.json")
.expect("valid path")
.into_arc();
let project_path = ProjectPath {

View file

@ -15,7 +15,7 @@ path = "src/util.rs"
doctest = true
[features]
test-support = ["rand", "util_macros"]
test-support = ["rand", "util_macros", "path/test-support"]
[dependencies]
anyhow.workspace = true
@ -42,6 +42,7 @@ url.workspace = true
percent-encoding.workspace = true
util_macros = { workspace = true, optional = true }
gpui_util.workspace = true
path = { workspace = true, features = ["serde"] }
[target.'cfg(not(target_family = "wasm"))'.dependencies]
smol.workspace = true
@ -66,3 +67,4 @@ windows.workspace = true
rand.workspace = true
util_macros.workspace = true
pretty_assertions.workspace = true
path = { workspace = true, features = ["test-support"] }

View file

@ -2,7 +2,6 @@ use globset::{GlobBuilder, 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};
@ -15,8 +14,10 @@ use std::{
sync::LazyLock,
};
use crate::rel_path::RelPath;
use crate::rel_path::RelPathBuf;
use path::rel_path::RelPath;
use path::rel_path::RelPathBuf;
pub use path::PathStyle;
/// Returns the path to the user's home directory.
pub fn home_dir() -> &'static PathBuf {
@ -349,199 +350,6 @@ impl AsRef<Path> for SanitizedPath {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PathStyle {
Posix,
Windows,
}
impl PathStyle {
#[cfg(target_os = "windows")]
pub const fn local() -> Self {
PathStyle::Windows
}
#[cfg(not(target_os = "windows"))]
pub const fn local() -> Self {
PathStyle::Posix
}
#[inline]
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 separators_ch(&self) -> &'static [char] {
match self {
PathStyle::Posix => &['/'],
PathStyle::Windows => &['\\', '/'],
}
}
pub fn is_absolute(&self, path_like: &str) -> bool {
path_like.starts_with('/')
|| *self == PathStyle::Windows
&& (path_like.starts_with('\\')
|| path_like
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic())
&& path_like[1..]
.strip_prefix(':')
.is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
}
pub fn is_windows(&self) -> bool {
*self == PathStyle::Windows
}
pub fn is_posix(&self) -> bool {
*self == PathStyle::Posix
}
pub fn join(self, left: impl AsRef<Path>, right: impl AsRef<Path>) -> Option<String> {
let right = right.as_ref().to_str()?;
if is_absolute(right, self) {
return None;
}
let left = left.as_ref().to_str()?;
if left.is_empty() {
Some(right.into())
} else {
Some(format!(
"{left}{}{right}",
if left.ends_with(self.primary_separator()) {
""
} else {
self.primary_separator()
}
))
}
}
pub fn join_path(
self,
left: impl AsRef<Path>,
right: impl AsRef<Path>,
) -> anyhow::Result<PathBuf> {
let left = left
.as_ref()
.to_str()
.ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
let right = right.as_ref();
let right_string = right
.to_str()
.ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
let joined = self
.join(left, right_string)
.ok_or_else(|| anyhow::anyhow!("Path must be relative: {right:?}"))?;
Ok(PathBuf::from(self.normalize(&joined)))
}
pub fn normalize(self, path_like: &str) -> String {
match self {
PathStyle::Windows => crate::normalize_path(Path::new(path_like))
.to_string_lossy()
.into_owned(),
PathStyle::Posix => {
let is_absolute = path_like.starts_with('/');
let remainder = if is_absolute {
path_like.trim_start_matches('/')
} else {
path_like
};
let mut components = Vec::new();
for component in remainder.split(self.separators_ch()) {
match component {
"" | "." => {}
".." => {
if components
.last()
.is_some_and(|component| *component != "..")
{
components.pop();
} else if !is_absolute {
components.push(component);
}
}
component => components.push(component),
}
}
let normalized = components.join(self.primary_separator());
if is_absolute && normalized.is_empty() {
"/".to_string()
} else if is_absolute {
format!("/{normalized}")
} else {
normalized
}
}
}
}
pub fn split(self, path_like: &str) -> (Option<&str>, &str) {
let Some(pos) = path_like.rfind(self.primary_separator()) else {
return (None, path_like);
};
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()?;
// Match behavior of std::path::Path, which is case-insensitive for drive letters (e.g., "C:" == "c:")
let stripped = if self.is_windows()
&& child.as_bytes().get(1) == Some(&b':')
&& parent.as_bytes().get(1) == Some(&b':')
&& child.as_bytes()[0].eq_ignore_ascii_case(&parent.as_bytes()[0])
{
child[2..].strip_prefix(&parent[2..])?
} else {
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)]
pub struct RemotePathBuf {
style: PathStyle,
@ -1538,7 +1346,7 @@ impl UrlExt for url::Url {
str_len.saturating_sub(self.scheme().len() + 3)
};
return match source_path_style {
PathStyle::Posix => {
PathStyle::Unix => {
file_url_segments_to_pathbuf_posix(estimated_capacity, host, segments)
}
PathStyle::Windows => {
@ -1576,7 +1384,7 @@ impl UrlExt for url::Url {
let path = String::from_utf8(bytes).map_err(|_| ())?;
debug_assert!(
PathStyle::Posix.is_absolute(&path),
PathStyle::Unix.is_absolute(&path),
"to_file_path() failed to produce an absolute Path"
);
@ -1659,7 +1467,7 @@ impl UrlExt for url::Url {
#[cfg(test)]
mod tests {
use crate::rel_path::rel_path;
use path::rel_path::rel_path;
use super::*;
use util_macros::perf;
@ -1676,7 +1484,7 @@ mod tests {
#[test]
fn test_join_path_uses_path_style_separator() {
let posix_path = PathStyle::Posix
let posix_path = PathStyle::Unix
.join_path(Path::new("/home/user/dev"), "worktrees")
.unwrap();
let windows_path = PathStyle::Windows
@ -1693,7 +1501,7 @@ mod tests {
#[test]
fn test_normalize_uses_path_style_separator() {
assert_eq!(
PathStyle::Posix.normalize("/home/user/dev/../worktrees/./zed"),
PathStyle::Unix.normalize("/home/user/dev/../worktrees/./zed"),
"/home/user/worktrees/zed"
);
assert_eq!(
@ -1703,7 +1511,7 @@ mod tests {
}
fn rel_path_entry(path: &'static str, is_file: bool) -> (&'static RelPath, bool) {
(RelPath::unix(path).unwrap(), is_file)
(RelPath::from_unix_str(path).unwrap(), is_file)
}
fn sorted_rel_paths(
@ -1838,22 +1646,22 @@ mod tests {
fn compare_rel_paths_mixed_case_insensitive() {
// Test that mixed mode is case-insensitive
let mut paths = vec![
(RelPath::unix("zebra.txt").unwrap(), true),
(RelPath::unix("Apple").unwrap(), false),
(RelPath::unix("banana.rs").unwrap(), true),
(RelPath::unix("Carrot").unwrap(), false),
(RelPath::unix("aardvark.txt").unwrap(), true),
(RelPath::from_unix_str("zebra.txt").unwrap(), true),
(RelPath::from_unix_str("Apple").unwrap(), false),
(RelPath::from_unix_str("banana.rs").unwrap(), true),
(RelPath::from_unix_str("Carrot").unwrap(), false),
(RelPath::from_unix_str("aardvark.txt").unwrap(), true),
];
paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
// Case-insensitive: aardvark < Apple < banana < Carrot < zebra
assert_eq!(
paths,
vec![
(RelPath::unix("aardvark.txt").unwrap(), true),
(RelPath::unix("Apple").unwrap(), false),
(RelPath::unix("banana.rs").unwrap(), true),
(RelPath::unix("Carrot").unwrap(), false),
(RelPath::unix("zebra.txt").unwrap(), true),
(RelPath::from_unix_str("aardvark.txt").unwrap(), true),
(RelPath::from_unix_str("Apple").unwrap(), false),
(RelPath::from_unix_str("banana.rs").unwrap(), true),
(RelPath::from_unix_str("Carrot").unwrap(), false),
(RelPath::from_unix_str("zebra.txt").unwrap(), true),
]
);
}
@ -1862,11 +1670,11 @@ mod tests {
fn compare_rel_paths_files_first_basic() {
// Test that files come before directories
let mut paths = vec![
(RelPath::unix("zebra.txt").unwrap(), true),
(RelPath::unix("Apple").unwrap(), false),
(RelPath::unix("banana.rs").unwrap(), true),
(RelPath::unix("Carrot").unwrap(), false),
(RelPath::unix("aardvark.txt").unwrap(), true),
(RelPath::from_unix_str("zebra.txt").unwrap(), true),
(RelPath::from_unix_str("Apple").unwrap(), false),
(RelPath::from_unix_str("banana.rs").unwrap(), true),
(RelPath::from_unix_str("Carrot").unwrap(), false),
(RelPath::from_unix_str("aardvark.txt").unwrap(), true),
];
paths
.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
@ -1874,11 +1682,11 @@ mod tests {
assert_eq!(
paths,
vec![
(RelPath::unix("aardvark.txt").unwrap(), true),
(RelPath::unix("banana.rs").unwrap(), true),
(RelPath::unix("zebra.txt").unwrap(), true),
(RelPath::unix("Apple").unwrap(), false),
(RelPath::unix("Carrot").unwrap(), false),
(RelPath::from_unix_str("aardvark.txt").unwrap(), true),
(RelPath::from_unix_str("banana.rs").unwrap(), true),
(RelPath::from_unix_str("zebra.txt").unwrap(), true),
(RelPath::from_unix_str("Apple").unwrap(), false),
(RelPath::from_unix_str("Carrot").unwrap(), false),
]
);
}
@ -1887,22 +1695,22 @@ mod tests {
fn compare_rel_paths_files_first_case_insensitive() {
// Test case-insensitive sorting within files and directories
let mut paths = vec![
(RelPath::unix("Zebra.txt").unwrap(), true),
(RelPath::unix("apple").unwrap(), false),
(RelPath::unix("Banana.rs").unwrap(), true),
(RelPath::unix("carrot").unwrap(), false),
(RelPath::unix("Aardvark.txt").unwrap(), true),
(RelPath::from_unix_str("Zebra.txt").unwrap(), true),
(RelPath::from_unix_str("apple").unwrap(), false),
(RelPath::from_unix_str("Banana.rs").unwrap(), true),
(RelPath::from_unix_str("carrot").unwrap(), false),
(RelPath::from_unix_str("Aardvark.txt").unwrap(), true),
];
paths
.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
assert_eq!(
paths,
vec![
(RelPath::unix("Aardvark.txt").unwrap(), true),
(RelPath::unix("Banana.rs").unwrap(), true),
(RelPath::unix("Zebra.txt").unwrap(), true),
(RelPath::unix("apple").unwrap(), false),
(RelPath::unix("carrot").unwrap(), false),
(RelPath::from_unix_str("Aardvark.txt").unwrap(), true),
(RelPath::from_unix_str("Banana.rs").unwrap(), true),
(RelPath::from_unix_str("Zebra.txt").unwrap(), true),
(RelPath::from_unix_str("apple").unwrap(), false),
(RelPath::from_unix_str("carrot").unwrap(), false),
]
);
}
@ -1911,22 +1719,22 @@ mod tests {
fn compare_rel_paths_files_first_numeric() {
// Test natural number sorting with files first
let mut paths = vec![
(RelPath::unix("file10.txt").unwrap(), true),
(RelPath::unix("dir2").unwrap(), false),
(RelPath::unix("file2.txt").unwrap(), true),
(RelPath::unix("dir10").unwrap(), false),
(RelPath::unix("file1.txt").unwrap(), true),
(RelPath::from_unix_str("file10.txt").unwrap(), true),
(RelPath::from_unix_str("dir2").unwrap(), false),
(RelPath::from_unix_str("file2.txt").unwrap(), true),
(RelPath::from_unix_str("dir10").unwrap(), false),
(RelPath::from_unix_str("file1.txt").unwrap(), true),
];
paths
.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
assert_eq!(
paths,
vec![
(RelPath::unix("file1.txt").unwrap(), true),
(RelPath::unix("file2.txt").unwrap(), true),
(RelPath::unix("file10.txt").unwrap(), true),
(RelPath::unix("dir2").unwrap(), false),
(RelPath::unix("dir10").unwrap(), false),
(RelPath::from_unix_str("file1.txt").unwrap(), true),
(RelPath::from_unix_str("file2.txt").unwrap(), true),
(RelPath::from_unix_str("file10.txt").unwrap(), true),
(RelPath::from_unix_str("dir2").unwrap(), false),
(RelPath::from_unix_str("dir10").unwrap(), false),
]
);
}
@ -1935,18 +1743,18 @@ mod tests {
fn compare_rel_paths_mixed_case() {
// Test case-insensitive sorting with varied capitalization
let mut paths = vec![
(RelPath::unix("README.md").unwrap(), true),
(RelPath::unix("readme.txt").unwrap(), true),
(RelPath::unix("ReadMe.rs").unwrap(), true),
(RelPath::from_unix_str("README.md").unwrap(), true),
(RelPath::from_unix_str("readme.txt").unwrap(), true),
(RelPath::from_unix_str("ReadMe.rs").unwrap(), true),
];
paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
// All "readme" variants should group together, sorted by extension
assert_eq!(
paths,
vec![
(RelPath::unix("README.md").unwrap(), true),
(RelPath::unix("ReadMe.rs").unwrap(), true),
(RelPath::unix("readme.txt").unwrap(), true),
(RelPath::from_unix_str("README.md").unwrap(), true),
(RelPath::from_unix_str("ReadMe.rs").unwrap(), true),
(RelPath::from_unix_str("readme.txt").unwrap(), true),
]
);
}
@ -1955,20 +1763,20 @@ mod tests {
fn compare_rel_paths_mixed_files_and_dirs() {
// Verify directories and files are still mixed
let mut paths = vec![
(RelPath::unix("file2.txt").unwrap(), true),
(RelPath::unix("Dir1").unwrap(), false),
(RelPath::unix("file1.txt").unwrap(), true),
(RelPath::unix("dir2").unwrap(), false),
(RelPath::from_unix_str("file2.txt").unwrap(), true),
(RelPath::from_unix_str("Dir1").unwrap(), false),
(RelPath::from_unix_str("file1.txt").unwrap(), true),
(RelPath::from_unix_str("dir2").unwrap(), false),
];
paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
// Case-insensitive: dir1, dir2, file1, file2 (all mixed)
assert_eq!(
paths,
vec![
(RelPath::unix("Dir1").unwrap(), false),
(RelPath::unix("dir2").unwrap(), false),
(RelPath::unix("file1.txt").unwrap(), true),
(RelPath::unix("file2.txt").unwrap(), true),
(RelPath::from_unix_str("Dir1").unwrap(), false),
(RelPath::from_unix_str("dir2").unwrap(), false),
(RelPath::from_unix_str("file1.txt").unwrap(), true),
(RelPath::from_unix_str("file2.txt").unwrap(), true),
]
);
}
@ -1976,28 +1784,28 @@ mod tests {
#[perf]
fn compare_rel_paths_mixed_same_name_different_case_file_and_dir() {
let mut paths = vec![
(RelPath::unix("Hello.txt").unwrap(), true),
(RelPath::unix("hello").unwrap(), false),
(RelPath::from_unix_str("Hello.txt").unwrap(), true),
(RelPath::from_unix_str("hello").unwrap(), false),
];
paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
assert_eq!(
paths,
vec![
(RelPath::unix("hello").unwrap(), false),
(RelPath::unix("Hello.txt").unwrap(), true),
(RelPath::from_unix_str("hello").unwrap(), false),
(RelPath::from_unix_str("Hello.txt").unwrap(), true),
]
);
let mut paths = vec![
(RelPath::unix("hello").unwrap(), false),
(RelPath::unix("Hello.txt").unwrap(), true),
(RelPath::from_unix_str("hello").unwrap(), false),
(RelPath::from_unix_str("Hello.txt").unwrap(), true),
];
paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
assert_eq!(
paths,
vec![
(RelPath::unix("hello").unwrap(), false),
(RelPath::unix("Hello.txt").unwrap(), true),
(RelPath::from_unix_str("hello").unwrap(), false),
(RelPath::from_unix_str("Hello.txt").unwrap(), true),
]
);
}
@ -2006,19 +1814,19 @@ mod tests {
fn compare_rel_paths_mixed_with_nested_paths() {
// Test that nested paths still work correctly
let mut paths = vec![
(RelPath::unix("src/main.rs").unwrap(), true),
(RelPath::unix("Cargo.toml").unwrap(), true),
(RelPath::unix("src").unwrap(), false),
(RelPath::unix("target").unwrap(), false),
(RelPath::from_unix_str("src/main.rs").unwrap(), true),
(RelPath::from_unix_str("Cargo.toml").unwrap(), true),
(RelPath::from_unix_str("src").unwrap(), false),
(RelPath::from_unix_str("target").unwrap(), false),
];
paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
assert_eq!(
paths,
vec![
(RelPath::unix("Cargo.toml").unwrap(), true),
(RelPath::unix("src").unwrap(), false),
(RelPath::unix("src/main.rs").unwrap(), true),
(RelPath::unix("target").unwrap(), false),
(RelPath::from_unix_str("Cargo.toml").unwrap(), true),
(RelPath::from_unix_str("src").unwrap(), false),
(RelPath::from_unix_str("src/main.rs").unwrap(), true),
(RelPath::from_unix_str("target").unwrap(), false),
]
);
}
@ -2027,20 +1835,20 @@ mod tests {
fn compare_rel_paths_files_first_with_nested() {
// Files come before directories, even with nested paths
let mut paths = vec![
(RelPath::unix("src/lib.rs").unwrap(), true),
(RelPath::unix("README.md").unwrap(), true),
(RelPath::unix("src").unwrap(), false),
(RelPath::unix("tests").unwrap(), false),
(RelPath::from_unix_str("src/lib.rs").unwrap(), true),
(RelPath::from_unix_str("README.md").unwrap(), true),
(RelPath::from_unix_str("src").unwrap(), false),
(RelPath::from_unix_str("tests").unwrap(), false),
];
paths
.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
assert_eq!(
paths,
vec![
(RelPath::unix("README.md").unwrap(), true),
(RelPath::unix("src").unwrap(), false),
(RelPath::unix("src/lib.rs").unwrap(), true),
(RelPath::unix("tests").unwrap(), false),
(RelPath::from_unix_str("README.md").unwrap(), true),
(RelPath::from_unix_str("src").unwrap(), false),
(RelPath::from_unix_str("src/lib.rs").unwrap(), true),
(RelPath::from_unix_str("tests").unwrap(), false),
]
);
}
@ -2049,19 +1857,19 @@ mod tests {
fn compare_rel_paths_mixed_dotfiles() {
// Test that dotfiles are handled correctly in mixed mode
let mut paths = vec![
(RelPath::unix(".gitignore").unwrap(), true),
(RelPath::unix("README.md").unwrap(), true),
(RelPath::unix(".github").unwrap(), false),
(RelPath::unix("src").unwrap(), false),
(RelPath::from_unix_str(".gitignore").unwrap(), true),
(RelPath::from_unix_str("README.md").unwrap(), true),
(RelPath::from_unix_str(".github").unwrap(), false),
(RelPath::from_unix_str("src").unwrap(), false),
];
paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
assert_eq!(
paths,
vec![
(RelPath::unix(".github").unwrap(), false),
(RelPath::unix(".gitignore").unwrap(), true),
(RelPath::unix("README.md").unwrap(), true),
(RelPath::unix("src").unwrap(), false),
(RelPath::from_unix_str(".github").unwrap(), false),
(RelPath::from_unix_str(".gitignore").unwrap(), true),
(RelPath::from_unix_str("README.md").unwrap(), true),
(RelPath::from_unix_str("src").unwrap(), false),
]
);
}
@ -2070,20 +1878,20 @@ mod tests {
fn compare_rel_paths_files_first_dotfiles() {
// Test that dotfiles come first when they're files
let mut paths = vec![
(RelPath::unix(".gitignore").unwrap(), true),
(RelPath::unix("README.md").unwrap(), true),
(RelPath::unix(".github").unwrap(), false),
(RelPath::unix("src").unwrap(), false),
(RelPath::from_unix_str(".gitignore").unwrap(), true),
(RelPath::from_unix_str("README.md").unwrap(), true),
(RelPath::from_unix_str(".github").unwrap(), false),
(RelPath::from_unix_str("src").unwrap(), false),
];
paths
.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
assert_eq!(
paths,
vec![
(RelPath::unix(".gitignore").unwrap(), true),
(RelPath::unix("README.md").unwrap(), true),
(RelPath::unix(".github").unwrap(), false),
(RelPath::unix("src").unwrap(), false),
(RelPath::from_unix_str(".gitignore").unwrap(), true),
(RelPath::from_unix_str("README.md").unwrap(), true),
(RelPath::from_unix_str(".github").unwrap(), false),
(RelPath::from_unix_str("src").unwrap(), false),
]
);
}
@ -2092,17 +1900,17 @@ mod tests {
fn compare_rel_paths_mixed_same_stem_different_extension() {
// Files with same stem but different extensions should sort by extension
let mut paths = vec![
(RelPath::unix("file.rs").unwrap(), true),
(RelPath::unix("file.md").unwrap(), true),
(RelPath::unix("file.txt").unwrap(), true),
(RelPath::from_unix_str("file.rs").unwrap(), true),
(RelPath::from_unix_str("file.md").unwrap(), true),
(RelPath::from_unix_str("file.txt").unwrap(), true),
];
paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
assert_eq!(
paths,
vec![
(RelPath::unix("file.md").unwrap(), true),
(RelPath::unix("file.rs").unwrap(), true),
(RelPath::unix("file.txt").unwrap(), true),
(RelPath::from_unix_str("file.md").unwrap(), true),
(RelPath::from_unix_str("file.rs").unwrap(), true),
(RelPath::from_unix_str("file.txt").unwrap(), true),
]
);
}
@ -2111,18 +1919,18 @@ mod tests {
fn compare_rel_paths_files_first_same_stem() {
// Same stem files should still sort by extension with files_first
let mut paths = vec![
(RelPath::unix("main.rs").unwrap(), true),
(RelPath::unix("main.c").unwrap(), true),
(RelPath::unix("main").unwrap(), false),
(RelPath::from_unix_str("main.rs").unwrap(), true),
(RelPath::from_unix_str("main.c").unwrap(), true),
(RelPath::from_unix_str("main").unwrap(), false),
];
paths
.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
assert_eq!(
paths,
vec![
(RelPath::unix("main.c").unwrap(), true),
(RelPath::unix("main.rs").unwrap(), true),
(RelPath::unix("main").unwrap(), false),
(RelPath::from_unix_str("main.c").unwrap(), true),
(RelPath::from_unix_str("main.rs").unwrap(), true),
(RelPath::from_unix_str("main").unwrap(), false),
]
);
}
@ -2131,19 +1939,19 @@ mod tests {
fn compare_rel_paths_mixed_deep_nesting() {
// Test sorting with deeply nested paths
let mut paths = vec![
(RelPath::unix("a/b/c.txt").unwrap(), true),
(RelPath::unix("A/B.txt").unwrap(), true),
(RelPath::unix("a.txt").unwrap(), true),
(RelPath::unix("A.txt").unwrap(), true),
(RelPath::from_unix_str("a/b/c.txt").unwrap(), true),
(RelPath::from_unix_str("A/B.txt").unwrap(), true),
(RelPath::from_unix_str("a.txt").unwrap(), true),
(RelPath::from_unix_str("A.txt").unwrap(), true),
];
paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
assert_eq!(
paths,
vec![
(RelPath::unix("a/b/c.txt").unwrap(), true),
(RelPath::unix("A/B.txt").unwrap(), true),
(RelPath::unix("a.txt").unwrap(), true),
(RelPath::unix("A.txt").unwrap(), true),
(RelPath::from_unix_str("a/b/c.txt").unwrap(), true),
(RelPath::from_unix_str("A/B.txt").unwrap(), true),
(RelPath::from_unix_str("a.txt").unwrap(), true),
(RelPath::from_unix_str("A.txt").unwrap(), true),
]
);
}
@ -2813,7 +2621,7 @@ mod tests {
// fn edge_of_glob() {
// let path = Path::new("/work/node_modules");
// let path_matcher =
// PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap();
// PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Unix).unwrap();
// assert!(
// path_matcher.is_match(path),
// "Path matcher should match {path:?}"
@ -2823,7 +2631,7 @@ mod tests {
// #[perf]
// fn file_in_dirs() {
// let path = Path::new("/work/.env");
// let path_matcher = PathMatcher::new(&["**/.env".to_owned()], PathStyle::Posix).unwrap();
// let path_matcher = PathMatcher::new(&["**/.env".to_owned()], PathStyle::Unix).unwrap();
// assert!(
// path_matcher.is_match(path),
// "Path matcher should match {path:?}"
@ -2839,7 +2647,7 @@ mod tests {
// fn project_search() {
// let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules");
// let path_matcher =
// PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap();
// PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Unix).unwrap();
// assert!(
// path_matcher.is_match(path),
// "Path matcher should match {path:?}"
@ -3264,28 +3072,28 @@ mod tests {
fn test_strip_prefix() {
let expected = [
(
PathStyle::Posix,
PathStyle::Unix,
"/a/b/c",
"/a/b",
Some(rel_path("c").into_arc()),
),
(
PathStyle::Posix,
PathStyle::Unix,
"/a/b/c",
"/a/b/",
Some(rel_path("c").into_arc()),
),
(
PathStyle::Posix,
PathStyle::Unix,
"/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::Unix, "/a/b/c", "", None),
(PathStyle::Unix, "/a/b//c", "/a/b/", None),
(PathStyle::Unix, "/a/bc", "/a/b", None),
(
PathStyle::Posix,
PathStyle::Unix,
"/a/b/c",
"/a/b/c",
Some(rel_path("").into_arc()),
@ -3390,19 +3198,19 @@ mod tests {
let url = url::Url::parse("file:///home/user/file.txt").unwrap();
assert_eq!(
url.to_file_path_ext(PathStyle::Posix),
url.to_file_path_ext(PathStyle::Unix),
Ok(PathBuf::from("/home/user/file.txt"))
);
let url = url::Url::parse("file:///").unwrap();
assert_eq!(
url.to_file_path_ext(PathStyle::Posix),
url.to_file_path_ext(PathStyle::Unix),
Ok(PathBuf::from("/"))
);
let url = url::Url::parse("file:///a/b/c/d/e").unwrap();
assert_eq!(
url.to_file_path_ext(PathStyle::Posix),
url.to_file_path_ext(PathStyle::Unix),
Ok(PathBuf::from("/a/b/c/d/e"))
);
}
@ -3413,19 +3221,19 @@ mod tests {
let url = url::Url::parse("file:///home/user/file%20with%20spaces.txt").unwrap();
assert_eq!(
url.to_file_path_ext(PathStyle::Posix),
url.to_file_path_ext(PathStyle::Unix),
Ok(PathBuf::from("/home/user/file with spaces.txt"))
);
let url = url::Url::parse("file:///path%2Fwith%2Fencoded%2Fslashes").unwrap();
assert_eq!(
url.to_file_path_ext(PathStyle::Posix),
url.to_file_path_ext(PathStyle::Unix),
Ok(PathBuf::from("/path/with/encoded/slashes"))
);
let url = url::Url::parse("file:///special%23chars%3F.txt").unwrap();
assert_eq!(
url.to_file_path_ext(PathStyle::Posix),
url.to_file_path_ext(PathStyle::Unix),
Ok(PathBuf::from("/special#chars?.txt"))
);
}
@ -3436,7 +3244,7 @@ mod tests {
let url = url::Url::parse("file://localhost/home/user/file.txt").unwrap();
assert_eq!(
url.to_file_path_ext(PathStyle::Posix),
url.to_file_path_ext(PathStyle::Unix),
Ok(PathBuf::from("/home/user/file.txt"))
);
}
@ -3446,7 +3254,7 @@ mod tests {
use super::UrlExt;
let url = url::Url::parse("file://somehost/home/user/file.txt").unwrap();
assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(()));
assert_eq!(url.to_file_path_ext(PathStyle::Unix), Err(()));
}
#[test]
@ -3455,13 +3263,13 @@ mod tests {
let url = url::Url::parse("file:///C:").unwrap();
assert_eq!(
url.to_file_path_ext(PathStyle::Posix),
url.to_file_path_ext(PathStyle::Unix),
Ok(PathBuf::from("/C:/"))
);
let url = url::Url::parse("file:///D|").unwrap();
assert_eq!(
url.to_file_path_ext(PathStyle::Posix),
url.to_file_path_ext(PathStyle::Unix),
Ok(PathBuf::from("/D|/"))
);
}
@ -3574,11 +3382,11 @@ mod tests {
use super::UrlExt;
let url = url::Url::parse("http://example.com/path").unwrap();
assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(()));
assert_eq!(url.to_file_path_ext(PathStyle::Unix), Err(()));
assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
let url = url::Url::parse("https://example.com/path").unwrap();
assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(()));
assert_eq!(url.to_file_path_ext(PathStyle::Unix), Err(()));
assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
}

View file

@ -7,7 +7,6 @@ pub mod path_list;
pub mod paths;
pub mod process;
pub mod redact;
pub mod rel_path;
pub mod schemars;
pub mod serde;
pub mod shell;
@ -21,7 +20,7 @@ pub mod time;
use anyhow::Result;
use itertools::Either;
use regex::Regex;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::sync::LazyLock;
use std::{
borrow::Cow,
@ -31,6 +30,9 @@ use std::{
use unicase::UniCase;
pub use gpui_util::*;
pub use path::PathExt;
pub use path::normalize_path;
pub use path::rel_path;
pub use take_until::*;
#[cfg(any(test, feature = "test-support"))]
@ -781,36 +783,6 @@ impl<O> From<anyhow::Result<O>> for ConnectionResult<O> {
}
}
/// Normalizes a path by resolving `.` and `..` components without
/// requiring the path to exist on disk (unlike `canonicalize`).
pub fn normalize_path(path: &Path) -> PathBuf {
use std::path::Component;
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -9110,11 +9110,11 @@ mod tests {
path: util::rel_path::rel_path("dir/__init__.py").into(),
};
assert_eq!(
dirty_message_for(Some(project_path), PathStyle::Posix),
dirty_message_for(Some(project_path), PathStyle::Unix),
"`dir/__init__.py` contains unsaved edits. Do you want to save it?"
);
assert_eq!(
dirty_message_for(None, PathStyle::Posix),
dirty_message_for(None, PathStyle::Unix),
"This buffer contains unsaved edits. Do you want to save it?"
);
}

View file

@ -1435,7 +1435,8 @@ impl WorkspaceDb {
relative_worktree_path == String::default()
);
let Some(relative_path) = RelPath::unix(&relative_worktree_path).log_err() else {
let Some(relative_path) = RelPath::from_unix_str(&relative_worktree_path).log_err()
else {
continue;
};
if worktree_root_path != String::default()
@ -2496,7 +2497,7 @@ impl WorkspaceDb {
as_json: serde_json::Value::from_str(&json).ok()?,
},
Arc::from(worktree_root_path.as_ref()),
RelPath::from_proto(&relative_worktree_path).log_err()?,
RelPath::from_unix_str(&relative_worktree_path).log_err()?.into(),
))
},
)

View file

@ -534,7 +534,7 @@ mod tests {
#[test]
fn accepts_ancestor_or_equal() {
let project = Path::new("/Users/me/dev/delta/wt/t1");
let style = PathStyle::Posix;
let style = PathStyle::Unix;
assert_eq!(
validate_trust_scope("/Users/me/dev/delta/wt", project, None, style).unwrap(),
PathBuf::from("/Users/me/dev/delta/wt"),
@ -551,7 +551,7 @@ mod tests {
#[test]
fn rejects_non_ancestor_relative_or_empty() {
let project = Path::new("/Users/me/dev/delta/wt/t1");
let style = PathStyle::Posix;
let style = PathStyle::Unix;
assert!(validate_trust_scope("/Users/other", project, None, style).is_err());
assert!(validate_trust_scope("relative/path", project, None, style).is_err());
assert!(validate_trust_scope(" ", project, None, style).is_err());
@ -565,7 +565,7 @@ mod tests {
fn expands_leading_tilde() {
let home = Path::new("/Users/me");
let project = Path::new("/Users/me/dev/wt/t1");
let style = PathStyle::Posix;
let style = PathStyle::Unix;
assert_eq!(
validate_trust_scope("~/dev/wt", project, Some(home), style).unwrap(),
PathBuf::from("/Users/me/dev/wt"),

View file

@ -498,7 +498,9 @@ impl Worktree {
abs_path
.file_name()
.and_then(|f| f.to_str())
.map_or(RelPath::empty_arc(), |f| RelPath::unix(f).unwrap().into()),
.map_or(RelPath::empty_arc(), |f| {
RelPath::from_unix_str(f).unwrap().into()
}),
abs_path.clone(),
PathStyle::local(),
),
@ -541,7 +543,7 @@ impl Worktree {
} else {
if let Some(file_name) = abs_path.file_name()
&& let Some(file_name) = file_name.to_str()
&& let Ok(path) = RelPath::unix(file_name)
&& let Ok(path) = RelPath::from_unix_str(file_name)
{
entry.is_private = !share_private_files && settings.is_path_private(path);
entry.is_hidden = settings.is_path_hidden(path);
@ -586,7 +588,8 @@ impl Worktree {
cx.new(|cx: &mut Context<Self>| {
let mut snapshot = Snapshot::new(
WorktreeId::from_proto(worktree.id),
RelPath::from_proto(&worktree.root_name).unwrap_or_else(|_| RelPath::empty_arc()),
RelPath::from_unix_str(&worktree.root_name)
.map_or_else(|_| RelPath::empty_arc(), Into::into),
Path::new(&worktree.abs_path).into(),
path_style,
);
@ -681,9 +684,11 @@ impl Worktree {
for entry in &update.updated_entries {
// Remote updates don't distinguish creation from
// modification, so report `AddedOrUpdated`.
if let Some(path) = RelPath::from_proto(&entry.path).log_err() {
if let Some(path) =
RelPath::from_unix_str(&entry.path).log_err()
{
changed_entries.push((
path,
path.into(),
ProjectEntryId::from_proto(entry.id),
PathChange::AddedOrUpdated,
));
@ -792,7 +797,7 @@ impl Worktree {
pub fn metadata_proto(&self) -> proto::WorktreeMetadata {
proto::WorktreeMetadata {
id: self.id().to_proto(),
root_name: self.root_name().to_proto(),
root_name: self.root_name().as_unix_str().to_owned(),
visible: self.is_visible(),
abs_path: self.abs_path().to_string_lossy().into_owned(),
root_repo_common_dir: self
@ -931,7 +936,7 @@ impl Worktree {
let request = this.client.request(proto::CreateProjectEntry {
worktree_id: worktree_id.to_proto(),
project_id,
path: path.as_ref().to_proto(),
path: path.as_ref().as_unix_str().to_owned(),
content,
is_directory,
});
@ -1094,9 +1099,11 @@ impl Worktree {
anyhow::Ok((
this.scan_id(),
this.create_entry(
RelPath::from_proto(&request.path).with_context(|| {
format!("received invalid relative path {:?}", request.path)
})?,
RelPath::from_unix_str(&request.path)
.with_context(|| {
format!("received invalid relative path {:?}", request.path)
})?
.into(),
request.is_directory,
request.content,
cx,
@ -1696,7 +1703,7 @@ impl LocalWorktree {
let refresh_full_path = lowest_ancestor.join(refresh_path);
refreshes.push(this.as_local_mut().unwrap().refresh_entry(
refresh_full_path,
refresh_full_path.into(),
None,
cx,
));
@ -2159,7 +2166,9 @@ impl LocalWorktree {
.as_path()
.file_name()
.and_then(|f| f.to_str())
.map_or(RelPath::empty_arc(), |f| RelPath::unix(f).unwrap().into());
.map_or(RelPath::empty_arc(), |f| {
RelPath::from_unix_str(f).unwrap().into()
});
self.snapshot.update_abs_path(new_path, root_name);
self.restart_background_scanners(cx);
}
@ -2372,7 +2381,7 @@ impl RemoteWorktree {
let Some(filename) = root_path_to_copy
.file_name()
.and_then(|name| name.to_str())
.and_then(|filename| RelPath::unix(filename).ok())
.and_then(|filename| RelPath::from_unix_str(filename).ok())
else {
continue;
};
@ -2401,7 +2410,7 @@ impl RemoteWorktree {
requests.push(proto::CreateProjectEntry {
project_id,
worktree_id,
path: target_path.to_proto(),
path: target_path.as_unix_str().to_owned(),
is_directory,
content,
});
@ -2490,7 +2499,7 @@ impl Snapshot {
project_id,
worktree_id,
abs_path: self.abs_path().to_string_lossy().into_owned(),
root_name: self.root_name().to_proto(),
root_name: self.root_name().as_unix_str().to_owned(),
root_repo_common_dir: self
.root_repo_common_dir()
.map(|p| p.to_string_lossy().into_owned()),
@ -2596,10 +2605,10 @@ impl Snapshot {
update.updated_entries.len(),
update.removed_entries.len()
);
if let Some(root_name) = RelPath::from_proto(&update.root_name).log_err() {
if let Some(root_name) = RelPath::from_unix_str(&update.root_name).log_err() {
self.update_abs_path(
SanitizedPath::new_arc(&Path::new(&update.abs_path)),
root_name,
root_name.into(),
);
}
@ -2894,7 +2903,7 @@ impl LocalSnapshot {
project_id,
worktree_id,
abs_path: self.abs_path().to_string_lossy().into_owned(),
root_name: self.root_name().to_proto(),
root_name: self.root_name().as_unix_str().to_owned(),
root_repo_common_dir: self
.root_repo_common_dir()
.map(|p| p.to_string_lossy().into_owned()),
@ -3101,7 +3110,7 @@ impl LocalSnapshot {
assert!(self.entry_for_path(ignore_parent_path).is_some());
assert!(
self.entry_for_path(
&ignore_parent_path.join(RelPath::unix(GITIGNORE).unwrap())
&ignore_parent_path.join(RelPath::from_unix_str(GITIGNORE).unwrap())
)
.is_some()
);
@ -3693,7 +3702,7 @@ impl language::File for File {
rpc::proto::File {
worktree_id: self.worktree.read(cx).id().to_proto(),
entry_id: self.entry_id.map(|id| id.to_proto()),
path: self.path.as_ref().to_proto(),
path: self.path.as_ref().as_unix_str().to_owned(),
mtime: self.disk_state.mtime().map(|time| time.into()),
is_deleted: self.disk_state.is_deleted(),
is_historic: matches!(self.disk_state, DiskState::Historic { .. }),
@ -3778,7 +3787,9 @@ impl File {
Ok(Self {
worktree,
path: RelPath::from_proto(&proto.path).context("invalid path in file protobuf")?,
path: RelPath::from_unix_str(&proto.path)
.context("invalid path in file protobuf")?
.into(),
disk_state,
entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
is_local: false,
@ -5152,10 +5163,11 @@ impl BackgroundScanner {
let child_name = child_abs_path.file_name().unwrap();
let Some(child_path) = child_name
.to_str()
.and_then(|name| Some(job.path.join(RelPath::unix(name).ok()?)))
.and_then(|name| Some(job.path.join(RelPath::from_unix_str(name).ok()?)))
else {
continue;
};
let child_path: Arc<RelPath> = child_path.into();
if self.track_git_repositories {
if child_name == DOT_GIT {
@ -5295,7 +5307,7 @@ impl BackgroundScanner {
{
let relative_path = job
.path
.join(RelPath::unix(child_name.to_str().unwrap()).unwrap());
.join(RelPath::from_unix_str(child_name.to_str().unwrap()).unwrap());
if self.is_path_private(&relative_path) {
log::debug!("detected private file: {relative_path:?}");
child_entry.is_private = true;
@ -5677,7 +5689,8 @@ impl BackgroundScanner {
}
}
let ignore_path = parent_path.join(RelPath::unix(GITIGNORE).unwrap());
let ignore_path =
parent_path.join(RelPath::from_unix_str(GITIGNORE).unwrap());
if snapshot.snapshot.entry_for_path(&ignore_path).is_none() {
return false;
}
@ -5904,7 +5917,9 @@ impl BackgroundScanner {
.entry_for_id(work_directory_id)
.is_some_and(|entry| {
snapshot
.entry_for_path(&entry.path.join(RelPath::unix(DOT_GIT).unwrap()))
.entry_for_path(
&entry.path.join(RelPath::from_unix_str(DOT_GIT).unwrap()),
)
.is_some()
});
@ -6284,7 +6299,7 @@ impl WorktreeModelHandle for Entity<Worktree> {
// Check if condition is already met before waiting for events
let file_exists = || {
tree.read_with(cx, |tree, _| {
tree.entry_for_path(RelPath::unix(file_name).unwrap())
tree.entry_for_path(RelPath::from_unix_str(file_name).unwrap())
.is_some()
})
};
@ -6304,7 +6319,7 @@ impl WorktreeModelHandle for Entity<Worktree> {
// Check if condition is already met before waiting for events
let file_gone = || {
tree.read_with(cx, |tree, _| {
tree.entry_for_path(RelPath::unix(file_name).unwrap())
tree.entry_for_path(RelPath::from_unix_str(file_name).unwrap())
.is_none()
})
};
@ -6671,7 +6686,7 @@ impl<'a> From<&'a Entry> for proto::Entry {
Self {
id: entry.id.to_proto(),
is_dir: entry.is_dir(),
path: entry.path.as_ref().to_proto(),
path: entry.path.as_ref().as_unix_str().to_owned(),
inode: entry.inode,
mtime: entry.mtime.map(|time| time.into()),
is_ignored: entry.is_ignored,
@ -6699,14 +6714,14 @@ impl TryFrom<(&CharBag, &PathMatcher, proto::Entry)> for Entry {
EntryKind::File
};
let path =
RelPath::from_proto(&entry.path).context("invalid relative path in proto message")?;
let path = RelPath::from_unix_str(&entry.path)
.context("invalid relative path in proto message")?;
let char_bag = char_bag_for_path(*root_char_bag, &path);
let is_always_included = always_included.is_match(&path);
Ok(Entry {
id: ProjectEntryId::from_proto(entry.id),
kind,
path,
path: path.into(),
inode: entry.inode,
mtime: entry.mtime.map(|time| time.into()),
size: entry.size.unwrap_or(0),

View file

@ -3224,7 +3224,7 @@ fn randomly_mutate_worktree(
if is_dir { "dir" } else { "file" },
child_path,
);
let task = worktree.create_entry(child_path, is_dir, None, cx);
let task = worktree.create_entry(child_path.into(), is_dir, None, cx);
cx.background_spawn(async move {
task.await?;
Ok(())
@ -5190,7 +5190,7 @@ async fn test_remote_worktree_without_git_emits_root_repo_event_after_first_upda
root_repo_is_linked_worktree: false,
},
client,
PathStyle::Posix,
PathStyle::Unix,
cx,
)
});
@ -5287,7 +5287,7 @@ async fn test_remote_worktree_with_git_emits_root_repo_event_when_repo_info_arri
root_repo_is_linked_worktree: false,
},
client,
PathStyle::Posix,
PathStyle::Unix,
cx,
)
});
@ -5386,7 +5386,7 @@ async fn test_remote_worktree_root_repo_metadata_cleared_only_by_completed_scan(
root_repo_is_linked_worktree: true,
},
client,
PathStyle::Posix,
PathStyle::Unix,
cx,
)
});