From f181a2f47be57d1dc57412d42ac44df2fc7eca8f Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 15 Jul 2026 10:33:25 +0200 Subject: [PATCH] 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 ... --- Cargo.lock | 16 +- Cargo.toml | 2 + crates/acp_thread/src/mention.rs | 38 +- crates/agent/src/agent.rs | 16 +- crates/agent/src/templates.rs | 2 +- crates/agent/src/tools/edit_session.rs | 4 +- crates/agent_ui/src/agent_panel.rs | 2 +- crates/agent_ui/src/mention_set.rs | 3 +- crates/client/src/telemetry.rs | 2 +- crates/collab/src/db/queries/projects.rs | 2 +- .../random_project_collaboration_tests.rs | 4 +- crates/debugger_ui/src/debugger_panel.rs | 8 +- crates/debugger_ui/src/new_process_modal.rs | 9 +- .../src/session/running/stack_frame_list.rs | 2 +- crates/dev_container/src/devcontainer_api.rs | 10 +- crates/dev_container/src/lib.rs | 5 +- crates/edit_prediction/src/udiff.rs | 4 +- .../src/edit_prediction_context.rs | 2 +- .../src/editable_context.rs | 2 +- crates/editor/src/clipboard.rs | 4 +- crates/editor/src/editor_tests.rs | 14 +- crates/editor/src/items.rs | 4 +- crates/extension/Cargo.toml | 1 + crates/extension/src/extension.rs | 2 +- crates/extension/src/extension_builder.rs | 3 +- crates/extension/src/extension_manifest.rs | 13 +- crates/extension_host/src/extension_host.rs | 2 +- .../src/wasm_host/wit/since_v0_1_0.rs | 2 +- .../src/wasm_host/wit/since_v0_8_0.rs | 4 +- crates/file_finder/src/file_finder.rs | 8 +- crates/file_finder/src/file_finder_tests.rs | 4 +- crates/fs/Cargo.toml | 1 + crates/fs/src/fs.rs | 2 +- .../fuzzy_nucleo/benches/match_benchmark.rs | 12 +- crates/git/src/repository.rs | 8 +- crates/git/src/status.rs | 4 +- crates/git_ui/src/diff_multibuffer.rs | 4 +- crates/git_ui/src/git_panel.rs | 4 +- crates/git_ui/src/multi_diff_view.rs | 6 +- crates/image_viewer/src/image_viewer.rs | 2 +- crates/language/src/language.rs | 4 +- crates/language_core/Cargo.toml | 6 +- crates/language_core/src/language_config.rs | 7 +- crates/language_core/src/language_core.rs | 6 +- crates/language_core/src/toolchain.rs | 2 +- crates/languages/src/eslint.rs | 2 +- crates/languages/src/json.rs | 8 +- crates/languages/src/python.rs | 37 +- crates/languages/src/rust.rs | 2 +- crates/languages/src/typescript.rs | 4 +- crates/multi_buffer/src/path_key.rs | 2 +- .../open_path_prompt/src/open_path_prompt.rs | 24 +- crates/path/Cargo.toml | 23 + crates/path/LICENSE-APACHE | 1 + crates/path/src/abs_path.rs | 286 +++++++++++ crates/path/src/path.rs | 263 ++++++++++ crates/{util => path}/src/rel_path.rs | 382 ++++++++++---- crates/paths/src/paths.rs | 14 +- crates/project/Cargo.toml | 9 +- crates/project/src/buffer_store.rs | 2 +- crates/project/src/git_store.rs | 54 +- crates/project/src/image_store.rs | 2 +- crates/project/src/lsp_command.rs | 6 +- crates/project/src/lsp_store.rs | 29 +- crates/project/src/manifest_tree/path_trie.rs | 7 +- crates/project/src/prettier_store.rs | 2 +- crates/project/src/project.rs | 12 +- crates/project/src/project_settings.rs | 4 +- crates/project/src/toolchain_store.rs | 19 +- crates/project/src/worktree_store.rs | 8 +- crates/project/tests/integration/git_store.rs | 34 +- .../tests/integration/project_search.rs | 4 +- .../tests/integration/project_tests.rs | 2 +- crates/project_panel/benches/sorting.rs | 2 +- crates/project_panel/src/project_panel.rs | 35 +- crates/project_symbols/src/project_symbols.rs | 2 +- .../src/dev_container_suggest.rs | 4 +- crates/recent_projects/src/remote_servers.rs | 4 +- crates/remote/src/transport/docker.rs | 18 +- crates/remote/src/transport/ssh.rs | 26 +- crates/remote/src/transport/wsl.rs | 32 +- crates/remote_server/src/headless_project.rs | 6 +- crates/settings/src/editorconfig_store.rs | 4 +- crates/settings/src/settings_store.rs | 30 +- crates/settings_ui/src/settings_ui.rs | 6 +- crates/util/Cargo.toml | 4 +- crates/util/src/paths.rs | 484 ++++++------------ crates/util/src/util.rs | 36 +- crates/workspace/src/pane.rs | 4 +- crates/workspace/src/persistence.rs | 5 +- crates/workspace/src/security_modal.rs | 6 +- crates/worktree/src/worktree.rs | 77 +-- .../tests/integration/worktree_tests.rs | 8 +- 93 files changed, 1458 insertions(+), 839 deletions(-) create mode 100644 crates/path/Cargo.toml create mode 120000 crates/path/LICENSE-APACHE create mode 100644 crates/path/src/abs_path.rs create mode 100644 crates/path/src/path.rs rename crates/{util => path}/src/rel_path.rs (62%) diff --git a/Cargo.lock b/Cargo.lock index b48dfe2dd5..a3cc732382 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 4f827f0c42..62c2381988 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 93e7b4d846..3686f2955c 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -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, diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index 381af5e2fa..f96174baac 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -446,18 +446,22 @@ impl gpui::EventEmitter for NativeAgent {} static RULES_FILE_REL_PATHS: LazyLock>> = 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>> = LazyLock::new(|| { - RelPath::unix(AGENTS_DIR_NAME) + RelPath::from_unix_str(AGENTS_DIR_NAME) .ok() .map(|path| path.into_arc()) }); static SKILLS_PREFIX: LazyLock>> = 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, 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 Vec Option { - 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) diff --git a/crates/agent_ui/src/mention_set.rs b/crates/agent_ui/src/mention_set.rs index 78579c8046..d04f222e18 100644 --- a/crates/agent_ui/src/mention_set.rs +++ b/crates/agent_ui/src/mention_set.rs @@ -1248,8 +1248,7 @@ fn full_mention_for_directory( |(worktree_path, full_path): (Arc, 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| { diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 0f7ee3174c..771a0f5605 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -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, )) diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 3cf82e8518..add702ff23 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -961,7 +961,7 @@ impl Database { let path_style = if project.windows_paths { PathStyle::Windows } else { - PathStyle::Posix + PathStyle::Unix }; let features: Vec = serde_json::from_str(&project.features).unwrap_or_default(); diff --git a/crates/collab/tests/integration/random_project_collaboration_tests.rs b/crates/collab/tests/integration/random_project_collaboration_tests.rs index c997f16ad3..391cef208d 100644 --- a/crates/collab/tests/integration/random_project_collaboration_tests.rs +++ b/crates/collab/tests/integration/random_project_collaboration_tests.rs @@ -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() diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index aa1bd4455c..8b2af4afbe 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -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), diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index eabcdfbd42..bb72a8dd17 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -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, }) diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index 738a44df7d..aa8f3483a6 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -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::::from(Path::new(path))) }) diff --git a/crates/dev_container/src/devcontainer_api.rs b/crates/dev_container/src/devcontainer_api.rs index a5ba5edd6f..395d50b6bd 100644 --- a/crates/dev_container/src/devcontainer_api.rs +++ b/crates/dev_container/src/devcontainer_api.rs @@ -172,13 +172,13 @@ pub fn find_devcontainer_configs(workspace: &Workspace, cx: &gpui::App) -> Vec Vec { 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 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 // 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 diff --git a/crates/dev_container/src/lib.rs b/crates/dev_container/src/lib.rs index 963c5a57a2..f34a81bef7 100644 --- a/crates/dev_container/src/lib.rs +++ b/crates/dev_container/src/lib.rs @@ -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" ))); diff --git a/crates/edit_prediction/src/udiff.rs b/crates/edit_prediction/src/udiff.rs index 8f095d8bc7..8411adde72 100644 --- a/crates/edit_prediction/src/udiff.rs +++ b/crates/edit_prediction/src/udiff.rs @@ -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()); } } diff --git a/crates/edit_prediction_context/src/edit_prediction_context.rs b/crates/edit_prediction_context/src/edit_prediction_context.rs index b5ab87a173..d2be8f2ce6 100644 --- a/crates/edit_prediction_context/src/edit_prediction_context.rs +++ b/crates/edit_prediction_context/src/edit_prediction_context.rs @@ -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(), diff --git a/crates/edit_prediction_context/src/editable_context.rs b/crates/edit_prediction_context/src/editable_context.rs index f985b5dbb0..ce27519141 100644 --- a/crates/edit_prediction_context/src/editable_context.rs +++ b/crates/edit_prediction_context/src/editable_context.rs @@ -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 { diff --git a/crates/editor/src/clipboard.rs b/crates/editor/src/clipboard.rs index 02c4d6c818..b6875bee48 100644 --- a/crates/editor/src/clipboard.rs +++ b/crates/editor/src/clipboard.rs @@ -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; diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index df2d411181..8e933079c7 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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, }; diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 4acdb10ff7..2bc890ed97 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -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 { 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(), }) } diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml index 59fc9c3c7a..55af1f4007 100644 --- a/crates/extension/Cargo.toml +++ b/crates/extension/Cargo.toml @@ -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 diff --git a/crates/extension/src/extension.rs b/crates/extension/src/extension.rs index 2ec8c8ea5f..850da7a69e 100644 --- a/crates/extension/src/extension.rs +++ b/crates/extension/src/extension.rs @@ -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( diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index 6229da6b8d..03583f86a2 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -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; diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index 6e9207a2e9..5ec589d718 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -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 { 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), } } diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 676577cbd0..9ebdef8cf2 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -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}, diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs index 288b31b220..83ba2ad3df 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs @@ -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() diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs index 25cd0c2d36..29971f00da 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs @@ -595,7 +595,7 @@ impl HostWorktree for WasmState { ) -> wasmtime::Result> { 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() diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 71c1d03efd..daf7b938e4 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -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()) }) { diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index 9dfdf6c08c..e83d7db791 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -4584,7 +4584,7 @@ fn collect_search_matches(picker: &Picker) -> 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) -> 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(_) => {} diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 2d8ee40ab2..37588bb0ee 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -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" } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index ec6e0554b9..afbfaebaed 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -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; diff --git a/crates/fuzzy_nucleo/benches/match_benchmark.rs b/crates/fuzzy_nucleo/benches/match_benchmark.rs index 8f6eedce49..ec1607a2bd 100644 --- a/crates/fuzzy_nucleo/benches/match_benchmark.rs +++ b/crates/fuzzy_nucleo/benches/match_benchmark.rs @@ -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, diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 0ff60eade2..2f378f4ada 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -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 + ?Sized>(s: &S) -> Result { - 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 { - 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 + ?Sized>(s: &S) -> RepoPath { - RepoPath(RelPath::unix(s.as_ref()).unwrap().into()) + RepoPath(RelPath::from_unix_str(s.as_ref()).unwrap().into()) } impl AsRef> for RepoPath { diff --git a/crates/git/src/status.rs b/crates/git/src/status.rs index de1a0c5409..1a04d02e75 100644 --- a/crates/git/src/status.rs +++ b/crates/git/src/status.rs @@ -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::>(); @@ -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 diff --git a/crates/git_ui/src/diff_multibuffer.rs b/crates/git_ui/src/diff_multibuffer.rs index 7c548952d7..b20ed28403 100644 --- a/crates/git_ui/src/diff_multibuffer.rs +++ b/crates/git_ui/src/diff_multibuffer.rs @@ -1007,7 +1007,7 @@ fn name_sort_path(repo_path: &RelPath) -> Arc { 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 { } synthetic.push_str(component); } - RelPath::unix(&synthetic) + RelPath::from_unix_str(&synthetic) .map(|path| path.into_arc()) .unwrap_or_else(|_| repo_path.into_arc()) } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 8a5f120280..e1c7d0e830 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -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| { diff --git a/crates/git_ui/src/multi_diff_view.rs b/crates/git_ui/src/multi_diff_view.rs index 47bf5a2b72..7834a7319f 100644 --- a/crates/git_ui/src/multi_diff_view.rs +++ b/crates/git_ui/src/multi_diff_view.rs @@ -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() diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index d881bcc256..3dc434deb7 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -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) { diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 4c57001cd6..b9f87265c0 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -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, diff --git a/crates/language_core/Cargo.toml b/crates/language_core/Cargo.toml index 91dca562a8..47a30d8316 100644 --- a/crates/language_core/Cargo.toml +++ b/crates/language_core/Cargo.toml @@ -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 = [] diff --git a/crates/language_core/src/language_config.rs b/crates/language_core/src/language_config.rs index 51903aef76..5eea2d123b 100644 --- a/crates/language_core/src/language_config.rs +++ b/crates/language_core/src/language_config.rs @@ -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 } diff --git a/crates/language_core/src/language_core.rs b/crates/language_core/src/language_core.rs index f3292e1978..8a45bd57a2 100644 --- a/crates/language_core/src/language_core.rs +++ b/crates/language_core/src/language_core.rs @@ -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; diff --git a/crates/language_core/src/toolchain.rs b/crates/language_core/src/toolchain.rs index 78bd69917f..f5b0919097 100644 --- a/crates/language_core/src/toolchain.rs +++ b/crates/language_core/src/toolchain.rs @@ -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}; diff --git a/crates/languages/src/eslint.rs b/crates/languages/src/eslint.rs index 2d0c88bd01..a876f392b9 100644 --- a/crates/languages/src/eslint.rs +++ b/crates/languages/src/eslint.rs @@ -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( diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index b97429c685..ff955fe086 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -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); } diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 7159379aed..becefed29e 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -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 { 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() + ); } } } diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 52e9a902a0..b34e92bbd9 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -292,7 +292,7 @@ impl ManifestProvider for CargoManifestProvider { ) -> Option> { 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)); } diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index e34a0b7dbf..a23b66021c 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -625,7 +625,9 @@ impl TypeScriptLspAdapter { async fn tsdk_path(&self, adapter: &Arc) -> 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(); diff --git a/crates/multi_buffer/src/path_key.rs b/crates/multi_buffer/src/path_key.rs index 18423a6960..436cd5763d 100644 --- a/crates/multi_buffer/src/path_key.rs +++ b/crates/multi_buffer/src/path_key.rs @@ -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(), } diff --git a/crates/open_path_prompt/src/open_path_prompt.rs b/crates/open_path_prompt/src/open_path_prompt.rs index 6fc35e697c..5fa29d453a 100644 --- a/crates/open_path_prompt/src/open_path_prompt.rs +++ b/crates/open_path_prompt/src/open_path_prompt.rs @@ -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"); } diff --git a/crates/path/Cargo.toml b/crates/path/Cargo.toml new file mode 100644 index 0000000000..73136332b0 --- /dev/null +++ b/crates/path/Cargo.toml @@ -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 diff --git a/crates/path/LICENSE-APACHE b/crates/path/LICENSE-APACHE new file mode 120000 index 0000000000..1cd601d0a3 --- /dev/null +++ b/crates/path/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/path/src/abs_path.rs b/crates/path/src/abs_path.rs new file mode 100644 index 0000000000..65f5c2832c --- /dev/null +++ b/crates/path/src/abs_path.rs @@ -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) -> 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 { + self.0.ancestors().map(|p| AbsPath::new_unchecked(p)) + } + + pub fn strip_prefix<'a>(&'a self, prefix: &AbsPath) -> Option> { + 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 for AbsPath { + fn as_ref(&self) -> &Path { + &self.0 + } +} + +impl AsRef for AbsPath { + fn as_ref(&self) -> &AbsPath { + self + } +} + +impl From<&AbsPath> for Arc { + fn from(path: &AbsPath) -> Self { + let arc: Arc = 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 { + fn from(path: &AbsPath) -> Self { + let arc: Rc = 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) -> anyhow::Result { + let path = path.into(); + if let Err(e) = AbsPath::new(&path) { + return Err(e); + } + Ok(Self(path)) + } + + pub fn home_dir() -> anyhow::Result { + 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) -> io::Result { + 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 for AbsPathBuf { + fn borrow(&self) -> &AbsPath { + self + } +} + +impl AsRef for AbsPathBuf { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl AsRef for AbsPathBuf { + fn as_ref(&self) -> &AbsPath { + self + } +} + +impl From 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 for AbsPathBuf { + fn eq(&self, other: &AbsPath) -> bool { + **self == *other + } +} + +impl PartialEq 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" + ); + } + } +} diff --git a/crates/path/src/path.rs b/crates/path/src/path.rs new file mode 100644 index 0000000000..c6a063264a --- /dev/null +++ b/crates/path/src/path.rs @@ -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; +} + +impl + ?Sized> PathExt for T { + fn to_rel_path_buf(&self) -> anyhow::Result { + 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, right: impl AsRef) -> Option { + 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, + right: impl AsRef, + ) -> anyhow::Result { + 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> { + 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 +} diff --git a/crates/util/src/rel_path.rs b/crates/path/src/rel_path.rs similarity index 62% rename from crates/util/src/rel_path.rs rename to crates/path/src/rel_path.rs index 382a4bad37..357fcb8205 100644 --- a/crates/util/src/rel_path.rs +++ b/crates/path/src/rel_path.rs @@ -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 + ?Sized>(path: &S) -> anyhow::Result<&Self> { + pub fn from_unix_str + ?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 { - 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> { - 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) -> 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(deserializer: D) -> std::result::Result - 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 { + Some(self.cmp(other)) } } -impl Into> for RelPathBuf { - fn into(self) -> Arc { - 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 for RelPathBuf { - fn as_ref(&self) -> &Path { - self.as_std_path() - } -} - -impl AsRef for RelPath { - fn as_ref(&self) -> &Path { - self.as_std_path() +impl From for Arc { + fn from(value: RelPathBuf) -> Self { + Arc::from(value.as_rel_path()) } } @@ -383,12 +411,24 @@ impl AsRef for RelPathBuf { } } +impl AsRef for RelPathBuf { + fn as_ref(&self) -> &Path { + self.as_std_path() + } +} + impl AsRef for RelPath { fn as_ref(&self) -> &RelPath { self } } +impl AsRef 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 { fn from(rel_path: &RelPath) -> Self { let bytes: Arc = 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 { + RelPath::from_unix_str(s) + } +} + +impl TryFrom<&str> for RelPathBuf { + type Error = anyhow::Error; + + fn try_from(s: &str) -> Result { + 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 for RelPath { @@ -428,26 +485,45 @@ impl PartialEq for RelPath { } } -pub trait PathExt { - fn to_rel_path_buf(&self) -> Result; +impl PartialEq for RelPathBuf { + fn eq(&self, other: &RelPath) -> bool { + self.as_rel_path() == other + } } -impl + ?Sized> PathExt for T { - fn to_rel_path_buf(&self) -> Result { - Ok(RelPath::new(self.as_ref(), PathStyle::local())?.into_owned()) +impl PartialEq 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 { - 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 { + 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(deserializer: D) -> std::result::Result + 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"); + } } diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index d222bd1c0c..9c16f1db28 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -68,7 +68,7 @@ static CONFIG_DIR: OnceLock = 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 } diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 5fd388ddaf..b3ee5a88a2 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -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"] } diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 816cf97c86..9fa7c324df 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -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)?; diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 8594904a17..26145b4a5c 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -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> { 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> { 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::::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, diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index 0b6dcfa007..667440a437 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -698,7 +698,7 @@ impl ImageStoreImpl for Entity { .request(rpc::proto::OpenImageByPath { project_id, worktree_id, - path: path.to_proto(), + path: path.as_unix_str().to_owned(), }) .await?; diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index bdc4189158..038950a045 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -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)), diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 71d5b30cf8..e98b70c3d1 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -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, diff --git a/crates/project/src/manifest_tree/path_trie.rs b/crates/project/src/manifest_tree/path_trie.rs index 99b4d523e0..ba0a3619e7 100644 --- a/crates/project/src/manifest_tree/path_trie.rs +++ b/crates/project/src/manifest_tree/path_trie.rs @@ -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 RootPathTrie