Represent relative paths using a dedicated, separator-agnostic type (#38744)

Max Brunsfeld , Cole Miller , Piotr Osiewicz , Peter Tripp , Smit Barmase , and Lukas Wirth created

Closes https://github.com/zed-industries/zed/issues/38690
Closes #37353

### Background

On Windows, paths are normally separated by `\`, unlike mac and linux
where they are separated by `/`. When editing code in a project that
uses a different path style than your local system (e.g. remoting from
Windows to Linux, using WSL, and collaboration between windows and unix
users), the correct separator for a path may differ from the "native"
separator.

Previously, to work around this, Zed converted paths' separators in
numerous places. This was applied to both absolute and relative paths,
leading to incorrect conversions in some cases.

### Solution

Many code paths in Zed use paths that are *relative* to either a
worktree root or a git repository. This PR introduces a dedicated type
for these paths called `RelPath`, which stores the path in the same way
regardless of host platform, and offers `Path`-like manipulation APIs.
RelPath supports *displaying* the path using either separator, so that
we can display paths in a style that is determined at runtime based on
the current project.

The representation of absolute paths is left untouched, for now.
Absolute paths are different from relative paths because (except in
contexts where we know that the path refers to the local filesystem)
they should generally be treated as opaque strings. Currently we use a
mix of types for these paths (std::path::Path, String, SanitizedPath).

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Peter Tripp <petertripp@gmail.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Co-authored-by: Lukas Wirth <me@lukaswirth.dev>

Change summary

Cargo.lock                                                            |   3 
crates/acp_thread/src/acp_thread.rs                                   |   2 
crates/acp_thread/src/diff.rs                                         |  26 
crates/action_log/src/action_log.rs                                   |  24 
crates/agent/src/agent.rs                                             |   6 
crates/agent/src/context.rs                                           |   5 
crates/agent/src/context_store.rs                                     |   7 
crates/agent/src/thread.rs                                            | 105 
crates/agent/src/thread_store.rs                                      |  36 
crates/agent2/src/agent.rs                                            |  16 
crates/agent2/src/db.rs                                               |  12 
crates/agent2/src/thread.rs                                           |  18 
crates/agent2/src/tools/copy_path_tool.rs                             |   4 
crates/agent2/src/tools/diagnostics_tool.rs                           |   6 
crates/agent2/src/tools/edit_file_tool.rs                             |  43 
crates/agent2/src/tools/find_path_tool.rs                             |  17 
crates/agent2/src/tools/grep_tool.rs                                  |   5 
crates/agent2/src/tools/list_directory_tool.rs                        |  40 
crates/agent2/src/tools/move_path_tool.rs                             |   2 
crates/agent2/src/tools/read_file_tool.rs                             |   6 
crates/agent_ui/src/acp/completion_provider.rs                        |  18 
crates/agent_ui/src/acp/message_editor.rs                             |  82 
crates/agent_ui/src/acp/thread_view.rs                                |  23 
crates/agent_ui/src/agent_ui.rs                                       |   2 
crates/agent_ui/src/context_picker.rs                                 |  15 
crates/agent_ui/src/context_picker/completion_provider.rs             |  79 
crates/agent_ui/src/context_picker/file_context_picker.rs             |  63 
crates/agent_ui/src/context_picker/symbol_context_picker.rs           |  36 
crates/agent_ui/src/text_thread_editor.rs                             |  12 
crates/assistant_slash_command/Cargo.toml                             |   1 
crates/assistant_slash_command/src/extension_slash_command.rs         |   7 
crates/assistant_slash_commands/Cargo.toml                            |   5 
crates/assistant_slash_commands/src/cargo_workspace_command.rs        | 159 
crates/assistant_slash_commands/src/diagnostics_command.rs            |  52 
crates/assistant_slash_commands/src/file_command.rs                   | 110 
crates/assistant_tools/src/copy_path_tool.rs                          |   4 
crates/assistant_tools/src/diagnostics_tool.rs                        |   6 
crates/assistant_tools/src/edit_file_tool.rs                          |  46 
crates/assistant_tools/src/find_path_tool.rs                          |  20 
crates/assistant_tools/src/grep_tool.rs                               |   3 
crates/assistant_tools/src/list_directory_tool.rs                     |  28 
crates/assistant_tools/src/move_path_tool.rs                          |   2 
crates/call/src/call_impl/room.rs                                     |   3 
crates/client/src/telemetry.rs                                        |  11 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql        |   3 
crates/collab/migrations/20250916173002_add_path_style_to_project.sql |   1 
crates/collab/src/db.rs                                               |   2 
crates/collab/src/db/queries/projects.rs                              |   9 
crates/collab/src/db/tables/project.rs                                |   1 
crates/collab/src/db/tests/db_tests.rs                                |   6 
crates/collab/src/rpc.rs                                              |   3 
crates/collab/src/tests/channel_buffer_tests.rs                       |  17 
crates/collab/src/tests/channel_guest_tests.rs                        |   3 
crates/collab/src/tests/editor_tests.rs                               |  98 
crates/collab/src/tests/following_tests.rs                            |  63 
crates/collab/src/tests/git_tests.rs                                  |  19 
crates/collab/src/tests/integration_tests.rs                          | 412 
crates/collab/src/tests/random_project_collaboration_tests.rs         |  93 
crates/collab/src/tests/remote_editing_collaboration_tests.rs         |  46 
crates/copilot/src/copilot.rs                                         |  20 
crates/dap/src/adapters.rs                                            |   4 
crates/dap_adapters/src/python.rs                                     |  17 
crates/debug_adapter_extension/src/extension_dap_adapter.rs           |   3 
crates/debugger_ui/src/debugger_panel.rs                              |  11 
crates/debugger_ui/src/new_process_modal.rs                           |  33 
crates/debugger_ui/src/session/running/breakpoint_list.rs             |  13 
crates/debugger_ui/src/session/running/module_list.rs                 |   2 
crates/debugger_ui/src/session/running/stack_frame_list.rs            |   2 
crates/debugger_ui/src/stack_trace_view.rs                            |   2 
crates/debugger_ui/src/tests/debugger_panel.rs                        |  12 
crates/debugger_ui/src/tests/inline_values.rs                         |   8 
crates/debugger_ui/src/tests/stack_frame_list.rs                      |  16 
crates/diagnostics/src/buffer_diagnostics.rs                          |  16 
crates/diagnostics/src/diagnostics_tests.rs                           |   8 
crates/docs_preprocessor/src/main.rs                                  |   3 
crates/edit_prediction_context/src/syntax_index.rs                    |  10 
crates/editor/src/clangd_ext.rs                                       |  49 
crates/editor/src/editor.rs                                           |  38 
crates/editor/src/editor_tests.rs                                     |  73 
crates/editor/src/element.rs                                          |  11 
crates/editor/src/git/blame.rs                                        |   9 
crates/editor/src/items.rs                                            |  41 
crates/editor/src/test.rs                                             |  26 
crates/editor/src/test/editor_test_context.rs                         |   6 
crates/eval/src/example.rs                                            |   4 
crates/eval/src/examples/add_arg_to_trait_method.rs                   |   8 
crates/eval/src/examples/code_block_citations.rs                      |   2 
crates/eval/src/instance.rs                                           |   2 
crates/extension/src/extension.rs                                     |   3 
crates/extension_host/src/extension_host.rs                           |   9 
crates/extension_host/src/headless_host.rs                            |   7 
crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs               |  11 
crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs               |  14 
crates/extensions_ui/src/extension_suggest.rs                         |  25 
crates/file_finder/src/file_finder.rs                                 | 314 
crates/file_finder/src/file_finder_tests.rs                           | 328 
crates/file_finder/src/open_path_prompt.rs                            |   6 
crates/file_finder/src/open_path_prompt_tests.rs                      |   8 
crates/fs/src/fake_git_repo.rs                                        |  12 
crates/fs/src/fs.rs                                                   |  54 
crates/fuzzy/Cargo.toml                                               |   3 
crates/fuzzy/src/matcher.rs                                           | 102 
crates/fuzzy/src/paths.rs                                             |  58 
crates/fuzzy/src/strings.rs                                           |   8 
crates/git/src/blame.rs                                               |   7 
crates/git/src/commit.rs                                              |  35 
crates/git/src/git.rs                                                 |  19 
crates/git/src/repository.rs                                          | 174 
crates/git/src/status.rs                                              |   7 
crates/git_ui/src/commit_view.rs                                      |  35 
crates/git_ui/src/git_panel.rs                                        |  96 
crates/git_ui/src/project_diff.rs                                     |  35 
crates/go_to_line/src/go_to_line.rs                                   |  10 
crates/image_viewer/src/image_viewer.rs                               |  23 
crates/inspector_ui/src/div_inspector.rs                              |   3 
crates/journal/src/journal.rs                                         |   2 
crates/language/src/buffer.rs                                         |  29 
crates/language/src/buffer_tests.rs                                   |   3 
crates/language/src/language.rs                                       |   3 
crates/language/src/language_registry.rs                              |   2 
crates/language/src/language_settings.rs                              |  19 
crates/language/src/manifest.rs                                       |   9 
crates/language/src/toolchain.rs                                      |  17 
crates/language_extension/src/extension_lsp_adapter.rs                |   4 
crates/language_tools/src/lsp_button.rs                               |   9 
crates/language_tools/src/lsp_log_view.rs                             |   2 
crates/language_tools/src/lsp_log_view_tests.rs                       |   2 
crates/languages/src/json.rs                                          |   9 
crates/languages/src/python.rs                                        |  10 
crates/languages/src/rust.rs                                          |   5 
crates/languages/src/typescript.rs                                    |  10 
crates/languages/src/vtsls.rs                                         |   4 
crates/languages/src/yaml.rs                                          |   4 
crates/markdown/src/markdown.rs                                       |   7 
crates/markdown/src/parser.rs                                         |   4 
crates/markdown/src/path_range.rs                                     |  38 
crates/multi_buffer/src/multi_buffer.rs                               |  13 
crates/multi_buffer/src/multi_buffer_tests.rs                         |   8 
crates/outline/src/outline.rs                                         |   4 
crates/outline_panel/src/outline_panel.rs                             | 195 
crates/paths/src/paths.rs                                             |  37 
crates/prettier/src/prettier.rs                                       |  17 
crates/project/Cargo.toml                                             |   1 
crates/project/src/agent_server_store.rs                              |  15 
crates/project/src/buffer_store.rs                                    |  23 
crates/project/src/context_server_store.rs                            |   6 
crates/project/src/debugger/breakpoint_store.rs                       |   4 
crates/project/src/debugger/dap_store.rs                              |  14 
crates/project/src/git_store.rs                                       | 139 
crates/project/src/git_store/conflict_set.rs                          |  17 
crates/project/src/git_store/git_traversal.rs                         | 238 
crates/project/src/image_store.rs                                     |  22 
crates/project/src/lsp_store.rs                                       | 322 
crates/project/src/manifest_tree.rs                                   |   7 
crates/project/src/manifest_tree/path_trie.rs                         | 107 
crates/project/src/manifest_tree/server_tree.rs                       |   6 
crates/project/src/prettier_store.rs                                  |   8 
crates/project/src/project.rs                                         | 326 
crates/project/src/project_settings.rs                                | 135 
crates/project/src/project_tests.rs                                   | 452 
crates/project/src/search.rs                                          |  23 
crates/project/src/task_inventory.rs                                  | 105 
crates/project/src/task_store.rs                                      |   2 
crates/project/src/terminals.rs                                       |  12 
crates/project/src/toolchain_store.rs                                 | 110 
crates/project/src/worktree_store.rs                                  | 373 
crates/project/src/yarn.rs                                            |   9 
crates/project_panel/Cargo.toml                                       |   1 
crates/project_panel/src/project_panel.rs                             | 473 
crates/project_panel/src/project_panel_tests.rs                       | 124 
crates/project_symbols/src/project_symbols.rs                         |  41 
crates/prompt_store/src/prompts.rs                                    |   7 
crates/proto/proto/call.proto                                         |   2 
crates/proto/proto/worktree.proto                                     |   4 
crates/proto/src/proto.rs                                             |  20 
crates/proto/src/typed_envelope.rs                                    | 153 
crates/remote/src/remote_client.rs                                    |   2 
crates/remote/src/transport.rs                                        |   7 
crates/remote/src/transport/ssh.rs                                    |  74 
crates/remote/src/transport/wsl.rs                                    |  77 
crates/remote_server/src/headless_project.rs                          |  29 
crates/remote_server/src/remote_editing_tests.rs                      | 112 
crates/repl/src/kernels/mod.rs                                        |   5 
crates/search/src/buffer_search.rs                                    |  12 
crates/search/src/project_search.rs                                   |  35 
crates/settings/src/settings_store.rs                                 |  81 
crates/svg_preview/src/svg_preview_view.rs                            |   1 
crates/tab_switcher/src/tab_switcher_tests.rs                         |   5 
crates/tasks_ui/src/modal.rs                                          |   4 
crates/tasks_ui/src/tasks_ui.rs                                       |   9 
crates/terminal_view/src/terminal_path_like_target.rs                 |  49 
crates/terminal_view/src/terminal_view.rs                             |   3 
crates/title_bar/src/title_bar.rs                                     |   8 
crates/toolchain_selector/src/active_toolchain.rs                     |  13 
crates/toolchain_selector/src/toolchain_selector.rs                   |  34 
crates/ui/src/components/label/highlighted_label.rs                   |   8 
crates/util/Cargo.toml                                                |   2 
crates/util/src/paths.rs                                              | 160 
crates/util/src/rel_path.rs                                           | 515 
crates/util/src/util.rs                                               |   1 
crates/vim/src/command.rs                                             |  68 
crates/vim/src/normal.rs                                              |   2 
crates/vim/src/state.rs                                               |   7 
crates/workspace/src/item.rs                                          |   7 
crates/workspace/src/pane.rs                                          |  45 
crates/workspace/src/persistence.rs                                   |  80 
crates/workspace/src/workspace.rs                                     |  69 
crates/worktree/src/worktree.rs                                       | 592 
crates/worktree/src/worktree_settings.rs                              |  21 
crates/worktree/src/worktree_tests.rs                                 | 539 
crates/zed/src/zed.rs                                                 |  37 
crates/zeta/src/license_detection.rs                                  |  12 
crates/zeta/src/zeta.rs                                               |   9 
crates/zeta2/src/zeta2.rs                                             |  18 
crates/zeta2_tools/src/zeta2_tools.rs                                 |  39 
crates/zeta_cli/src/main.rs                                           |  35 
216 files changed, 5,455 insertions(+), 4,939 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -892,6 +892,7 @@ dependencies = [
  "serde",
  "serde_json",
  "ui",
+ "util",
  "workspace",
  "workspace-hack",
 ]
@@ -12094,7 +12095,6 @@ dependencies = [
  "markdown",
  "node_runtime",
  "parking_lot",
- "pathdiff",
  "paths",
  "postage",
  "prettier",
@@ -12147,7 +12147,6 @@ dependencies = [
  "git",
  "git_ui",
  "gpui",
- "indexmap 2.9.0",
  "language",
  "menu",
  "pretty_assertions",

crates/acp_thread/src/acp_thread.rs 🔗

@@ -573,7 +573,7 @@ impl ToolCallContent {
             ))),
             acp::ToolCallContent::Diff { diff } => Ok(Self::Diff(cx.new(|cx| {
                 Diff::finalized(
-                    diff.path,
+                    diff.path.to_string_lossy().to_string(),
                     diff.old_text,
                     diff.new_text,
                     language_registry,

crates/acp_thread/src/diff.rs 🔗

@@ -6,12 +6,7 @@ use itertools::Itertools;
 use language::{
     Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, Rope, TextBuffer,
 };
-use std::{
-    cmp::Reverse,
-    ops::Range,
-    path::{Path, PathBuf},
-    sync::Arc,
-};
+use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
 use util::ResultExt;
 
 pub enum Diff {
@@ -21,7 +16,7 @@ pub enum Diff {
 
 impl Diff {
     pub fn finalized(
-        path: PathBuf,
+        path: String,
         old_text: Option<String>,
         new_text: String,
         language_registry: Arc<LanguageRegistry>,
@@ -36,7 +31,7 @@ impl Diff {
             let buffer = new_buffer.clone();
             async move |_, cx| {
                 let language = language_registry
-                    .language_for_file_path(&path)
+                    .language_for_file_path(Path::new(&path))
                     .await
                     .log_err();
 
@@ -152,12 +147,15 @@ impl Diff {
         let path = match self {
             Diff::Pending(PendingDiff {
                 new_buffer: buffer, ..
-            }) => buffer.read(cx).file().map(|file| file.path().as_ref()),
-            Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()),
+            }) => buffer
+                .read(cx)
+                .file()
+                .map(|file| file.path().display(file.path_style(cx))),
+            Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_str().into()),
         };
         format!(
             "Diff: {}\n```\n{}\n```\n",
-            path.unwrap_or(Path::new("untitled")).display(),
+            path.unwrap_or("untitled".into()),
             buffer_text
         )
     }
@@ -244,8 +242,8 @@ impl PendingDiff {
             .new_buffer
             .read(cx)
             .file()
-            .map(|file| file.path().as_ref())
-            .unwrap_or(Path::new("untitled"))
+            .map(|file| file.path().display(file.path_style(cx)))
+            .unwrap_or("untitled".into())
             .into();
 
         // Replace the buffer in the multibuffer with the snapshot
@@ -348,7 +346,7 @@ impl PendingDiff {
 }
 
 pub struct FinalizedDiff {
-    path: PathBuf,
+    path: String,
     base_text: Arc<String>,
     new_buffer: Entity<Buffer>,
     multibuffer: Entity<MultiBuffer>,

crates/action_log/src/action_log.rs 🔗

@@ -8,10 +8,7 @@ use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
 use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
 use std::{cmp, ops::Range, sync::Arc};
 use text::{Edit, Patch, Rope};
-use util::{
-    RangeExt, ResultExt as _,
-    paths::{PathStyle, RemotePathBuf},
-};
+use util::{RangeExt, ResultExt as _};
 
 /// Tracks actions performed by tools in a thread
 pub struct ActionLog {
@@ -62,7 +59,13 @@ impl ActionLog {
                 let file_path = buffer
                     .read(cx)
                     .file()
-                    .map(|file| RemotePathBuf::new(file.full_path(cx), PathStyle::Posix).to_proto())
+                    .map(|file| {
+                        let mut path = file.full_path(cx).to_string_lossy().into_owned();
+                        if file.path_style(cx).is_windows() {
+                            path = path.replace('\\', "/");
+                        }
+                        path
+                    })
                     .unwrap_or_else(|| format!("buffer_{}", buffer.entity_id()));
 
                 let mut result = String::new();
@@ -2301,7 +2304,7 @@ mod tests {
         .await;
         fs.set_head_for_repo(
             path!("/project/.git").as_ref(),
-            &[("file.txt".into(), "a\nb\nc\nd\ne\nf\ng\nh\ni\nj".into())],
+            &[("file.txt", "a\nb\nc\nd\ne\nf\ng\nh\ni\nj".into())],
             "0000000",
         );
         cx.run_until_parked();
@@ -2384,7 +2387,7 @@ mod tests {
         // - Ignores the last line edit (j stays as j)
         fs.set_head_for_repo(
             path!("/project/.git").as_ref(),
-            &[("file.txt".into(), "A\nb\nc\nf\nG\nh\ni\nj".into())],
+            &[("file.txt", "A\nb\nc\nf\nG\nh\ni\nj".into())],
             "0000001",
         );
         cx.run_until_parked();
@@ -2415,10 +2418,7 @@ mod tests {
         // Make another commit that accepts the NEW line but with different content
         fs.set_head_for_repo(
             path!("/project/.git").as_ref(),
-            &[(
-                "file.txt".into(),
-                "A\nb\nc\nf\nGGG\nh\nDIFFERENT\ni\nj".into(),
-            )],
+            &[("file.txt", "A\nb\nc\nf\nGGG\nh\nDIFFERENT\ni\nj".into())],
             "0000002",
         );
         cx.run_until_parked();
@@ -2444,7 +2444,7 @@ mod tests {
         // Final commit that accepts all remaining edits
         fs.set_head_for_repo(
             path!("/project/.git").as_ref(),
-            &[("file.txt".into(), "A\nb\nc\nf\nGGG\nh\nNEW\ni\nJ".into())],
+            &[("file.txt", "A\nb\nc\nf\nGGG\nh\nNEW\ni\nJ".into())],
             "0000003",
         );
         cx.run_until_parked();

crates/agent/src/agent.rs 🔗

@@ -9,12 +9,14 @@ pub mod tool_use;
 
 pub use context::{AgentContext, ContextId, ContextLoadResult};
 pub use context_store::ContextStore;
+use fs::Fs;
+use std::sync::Arc;
 pub use thread::{
     LastRestoreCheckpoint, Message, MessageCrease, MessageId, MessageSegment, Thread, ThreadError,
     ThreadEvent, ThreadFeedback, ThreadId, ThreadSummary, TokenUsageRatio,
 };
 pub use thread_store::{SerializedThread, TextThreadStore, ThreadStore};
 
-pub fn init(cx: &mut gpui::App) {
-    thread_store::init(cx);
+pub fn init(fs: Arc<dyn Fs>, cx: &mut gpui::App) {
+    thread_store::init(fs, cx);
 }

crates/agent/src/context.rs 🔗

@@ -18,6 +18,7 @@ use std::path::PathBuf;
 use std::{ops::Range, path::Path, sync::Arc};
 use text::{Anchor, OffsetRangeExt as _};
 use util::markdown::MarkdownCodeBlock;
+use util::rel_path::RelPath;
 use util::{ResultExt as _, post_inc};
 
 pub const RULES_ICON: IconName = IconName::Reader;
@@ -242,7 +243,7 @@ pub struct DirectoryContext {
 #[derive(Debug, Clone)]
 pub struct DirectoryContextDescendant {
     /// Path within the directory.
-    pub rel_path: Arc<Path>,
+    pub rel_path: Arc<RelPath>,
     pub fenced_codeblock: SharedString,
 }
 
@@ -968,7 +969,7 @@ pub fn load_context(
     })
 }
 
-fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
+fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<Arc<RelPath>> {
     let mut files = Vec::new();
 
     for entry in worktree.child_entries(path) {

crates/agent/src/context_store.rs 🔗

@@ -14,7 +14,10 @@ use futures::{self, FutureExt};
 use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity};
 use language::{Buffer, File as _};
 use language_model::LanguageModelImage;
-use project::{Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file};
+use project::{
+    Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file,
+    lsp_store::SymbolLocation,
+};
 use prompt_store::UserPromptId;
 use ref_cast::RefCast as _;
 use std::{
@@ -500,7 +503,7 @@ impl ContextStore {
                 let Some(context_path) = buffer.project_path(cx) else {
                     return false;
                 };
-                if context_path != symbol.path {
+                if symbol.path != SymbolLocation::InProject(context_path) {
                     return false;
                 }
                 let context_range = context.range.to_point_utf16(&buffer.snapshot());

crates/agent/src/thread.rs 🔗

@@ -234,7 +234,6 @@ impl MessageSegment {
 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 pub struct ProjectSnapshot {
     pub worktree_snapshots: Vec<WorktreeSnapshot>,
-    pub unsaved_buffer_paths: Vec<String>,
     pub timestamp: DateTime<Utc>,
 }
 
@@ -2857,27 +2856,11 @@ impl Thread {
             .map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx))
             .collect();
 
-        cx.spawn(async move |_, cx| {
+        cx.spawn(async move |_, _| {
             let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
 
-            let mut unsaved_buffers = Vec::new();
-            cx.update(|app_cx| {
-                let buffer_store = project.read(app_cx).buffer_store();
-                for buffer_handle in buffer_store.read(app_cx).buffers() {
-                    let buffer = buffer_handle.read(app_cx);
-                    if buffer.is_dirty()
-                        && let Some(file) = buffer.file()
-                    {
-                        let path = file.path().to_string_lossy().to_string();
-                        unsaved_buffers.push(path);
-                    }
-                }
-            })
-            .ok();
-
             Arc::new(ProjectSnapshot {
                 worktree_snapshots,
-                unsaved_buffer_paths: unsaved_buffers,
                 timestamp: Utc::now(),
             })
         })
@@ -3275,6 +3258,7 @@ mod tests {
     use agent_settings::{AgentProfileId, AgentSettings};
     use assistant_tool::ToolRegistry;
     use assistant_tools;
+    use fs::Fs;
     use futures::StreamExt;
     use futures::future::BoxFuture;
     use futures::stream::BoxStream;
@@ -3298,9 +3282,10 @@ mod tests {
 
     #[gpui::test]
     async fn test_message_with_context(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
         let project = create_test_project(
+            &fs,
             cx,
             json!({"code.rs": "fn main() {\n    println!(\"Hello, world!\");\n}"}),
         )
@@ -3375,9 +3360,10 @@ fn main() {{
 
     #[gpui::test]
     async fn test_only_include_new_contexts(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
         let project = create_test_project(
+            &fs,
             cx,
             json!({
                 "file1.rs": "fn function1() {}\n",
@@ -3531,9 +3517,10 @@ fn main() {{
 
     #[gpui::test]
     async fn test_message_without_files(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
         let project = create_test_project(
+            &fs,
             cx,
             json!({"code.rs": "fn main() {\n    println!(\"Hello, world!\");\n}"}),
         )
@@ -3610,9 +3597,10 @@ fn main() {{
     #[gpui::test]
     #[ignore] // turn this test on when project_notifications tool is re-enabled
     async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
         let project = create_test_project(
+            &fs,
             cx,
             json!({"code.rs": "fn main() {\n    println!(\"Hello, world!\");\n}"}),
         )
@@ -3738,9 +3726,10 @@ fn main() {{
 
     #[gpui::test]
     async fn test_storing_profile_setting_per_thread(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
         let project = create_test_project(
+            &fs,
             cx,
             json!({"code.rs": "fn main() {\n    println!(\"Hello, world!\");\n}"}),
         )
@@ -3760,9 +3749,10 @@ fn main() {{
 
     #[gpui::test]
     async fn test_serializing_thread_profile(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
         let project = create_test_project(
+            &fs,
             cx,
             json!({"code.rs": "fn main() {\n    println!(\"Hello, world!\");\n}"}),
         )
@@ -3803,9 +3793,10 @@ fn main() {{
 
     #[gpui::test]
     async fn test_temperature_setting(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
         let project = create_test_project(
+            &fs,
             cx,
             json!({"code.rs": "fn main() {\n    println!(\"Hello, world!\");\n}"}),
         )
@@ -3897,9 +3888,9 @@ fn main() {{
 
     #[gpui::test]
     async fn test_thread_summary(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
-        let project = create_test_project(cx, json!({})).await;
+        let project = create_test_project(&fs, cx, json!({})).await;
 
         let (_, _thread_store, thread, _context_store, model) =
             setup_test_environment(cx, project.clone()).await;
@@ -3982,9 +3973,9 @@ fn main() {{
 
     #[gpui::test]
     async fn test_thread_summary_error_set_manually(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
-        let project = create_test_project(cx, json!({})).await;
+        let project = create_test_project(&fs, cx, json!({})).await;
 
         let (_, _thread_store, thread, _context_store, model) =
             setup_test_environment(cx, project.clone()).await;
@@ -4004,9 +3995,9 @@ fn main() {{
 
     #[gpui::test]
     async fn test_thread_summary_error_retry(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
-        let project = create_test_project(cx, json!({})).await;
+        let project = create_test_project(&fs, cx, json!({})).await;
 
         let (_, _thread_store, thread, _context_store, model) =
             setup_test_environment(cx, project.clone()).await;
@@ -4158,9 +4149,9 @@ fn main() {{
 
     #[gpui::test]
     async fn test_retry_on_overloaded_error(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
-        let project = create_test_project(cx, json!({})).await;
+        let project = create_test_project(&fs, cx, json!({})).await;
         let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
 
         // Enable Burn Mode to allow retries
@@ -4236,9 +4227,9 @@ fn main() {{
 
     #[gpui::test]
     async fn test_retry_on_internal_server_error(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
-        let project = create_test_project(cx, json!({})).await;
+        let project = create_test_project(&fs, cx, json!({})).await;
         let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
 
         // Enable Burn Mode to allow retries
@@ -4318,9 +4309,9 @@ fn main() {{
 
     #[gpui::test]
     async fn test_exponential_backoff_on_retries(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
-        let project = create_test_project(cx, json!({})).await;
+        let project = create_test_project(&fs, cx, json!({})).await;
         let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
 
         // Enable Burn Mode to allow retries
@@ -4438,9 +4429,9 @@ fn main() {{
 
     #[gpui::test]
     async fn test_max_retries_exceeded(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
-        let project = create_test_project(cx, json!({})).await;
+        let project = create_test_project(&fs, cx, json!({})).await;
         let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
 
         // Enable Burn Mode to allow retries
@@ -4529,9 +4520,9 @@ fn main() {{
 
     #[gpui::test]
     async fn test_retry_message_removed_on_retry(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
-        let project = create_test_project(cx, json!({})).await;
+        let project = create_test_project(&fs, cx, json!({})).await;
         let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
 
         // Enable Burn Mode to allow retries
@@ -4702,9 +4693,9 @@ fn main() {{
 
     #[gpui::test]
     async fn test_successful_completion_clears_retry_state(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
-        let project = create_test_project(cx, json!({})).await;
+        let project = create_test_project(&fs, cx, json!({})).await;
         let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
 
         // Enable Burn Mode to allow retries
@@ -4868,9 +4859,9 @@ fn main() {{
 
     #[gpui::test]
     async fn test_rate_limit_retry_single_attempt(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
-        let project = create_test_project(cx, json!({})).await;
+        let project = create_test_project(&fs, cx, json!({})).await;
         let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
 
         // Enable Burn Mode to allow retries
@@ -5053,9 +5044,9 @@ fn main() {{
 
     #[gpui::test]
     async fn test_ui_only_messages_not_sent_to_model(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
-        let project = create_test_project(cx, json!({})).await;
+        let project = create_test_project(&fs, cx, json!({})).await;
         let (_, _, thread, _, model) = setup_test_environment(cx, project.clone()).await;
 
         // Insert a regular user message
@@ -5153,9 +5144,9 @@ fn main() {{
 
     #[gpui::test]
     async fn test_no_retry_without_burn_mode(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
-        let project = create_test_project(cx, json!({})).await;
+        let project = create_test_project(&fs, cx, json!({})).await;
         let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
 
         // Ensure we're in Normal mode (not Burn mode)
@@ -5226,9 +5217,9 @@ fn main() {{
 
     #[gpui::test]
     async fn test_retry_canceled_on_stop(cx: &mut TestAppContext) {
-        init_test_settings(cx);
+        let fs = init_test_settings(cx);
 
-        let project = create_test_project(cx, json!({})).await;
+        let project = create_test_project(&fs, cx, json!({})).await;
         let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
 
         // Enable Burn Mode to allow retries
@@ -5334,7 +5325,8 @@ fn main() {{
         cx.run_until_parked();
     }
 
-    fn init_test_settings(cx: &mut TestAppContext) {
+    fn init_test_settings(cx: &mut TestAppContext) -> Arc<dyn Fs> {
+        let fs = FakeFs::new(cx.executor());
         cx.update(|cx| {
             let settings_store = SettingsStore::test(cx);
             cx.set_global(settings_store);
@@ -5342,7 +5334,7 @@ fn main() {{
             Project::init_settings(cx);
             AgentSettings::register(cx);
             prompt_store::init(cx);
-            thread_store::init(cx);
+            thread_store::init(fs.clone(), cx);
             workspace::init_settings(cx);
             language_model::init_settings(cx);
             ThemeSettings::register(cx);
@@ -5356,16 +5348,17 @@ fn main() {{
             ));
             assistant_tools::init(http_client, cx);
         });
+        fs
     }
 
     // Helper to create a test project with test files
     async fn create_test_project(
+        fs: &Arc<dyn Fs>,
         cx: &mut TestAppContext,
         files: serde_json::Value,
     ) -> Entity<Project> {
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(path!("/test"), files).await;
-        Project::test(fs, [path!("/test").as_ref()], cx).await
+        fs.as_fake().insert_tree(path!("/test"), files).await;
+        Project::test(fs.clone(), [path!("/test").as_ref()], cx).await
     }
 
     async fn setup_test_environment(

crates/agent/src/thread_store.rs 🔗

@@ -10,6 +10,7 @@ use assistant_tool::{Tool, ToolId, ToolWorkingSet};
 use chrono::{DateTime, Utc};
 use collections::HashMap;
 use context_server::ContextServerId;
+use fs::{Fs, RemoveOptions};
 use futures::{
     FutureExt as _, StreamExt as _,
     channel::{mpsc, oneshot},
@@ -39,7 +40,7 @@ use std::{
     rc::Rc,
     sync::{Arc, Mutex},
 };
-use util::ResultExt as _;
+use util::{ResultExt as _, rel_path::RelPath};
 
 use zed_env_vars::ZED_STATELESS;
 
@@ -85,8 +86,8 @@ const RULES_FILE_NAMES: [&str; 9] = [
     "GEMINI.md",
 ];
 
-pub fn init(cx: &mut App) {
-    ThreadsDatabase::init(cx);
+pub fn init(fs: Arc<dyn Fs>, cx: &mut App) {
+    ThreadsDatabase::init(fs, cx);
 }
 
 /// A system prompt shared by all threads created by this ThreadStore
@@ -234,7 +235,7 @@ impl ThreadStore {
                 if items.iter().any(|(path, _, _)| {
                     RULES_FILE_NAMES
                         .iter()
-                        .any(|name| path.as_ref() == Path::new(name))
+                        .any(|name| path.as_ref() == RelPath::new(name).unwrap())
                 }) {
                     self.enqueue_system_prompt_reload();
                 }
@@ -327,7 +328,7 @@ impl ThreadStore {
         cx: &mut App,
     ) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
         let tree = worktree.read(cx);
-        let root_name = tree.root_name().into();
+        let root_name = tree.root_name_str().into();
         let abs_path = tree.abs_path();
 
         let mut context = WorktreeContext {
@@ -367,7 +368,7 @@ impl ThreadStore {
             .into_iter()
             .filter_map(|name| {
                 worktree
-                    .entry_for_path(name)
+                    .entry_for_path(RelPath::new(name).unwrap())
                     .filter(|entry| entry.is_file())
                     .map(|entry| entry.path.clone())
             })
@@ -869,13 +870,13 @@ impl ThreadsDatabase {
         GlobalThreadsDatabase::global(cx).0.clone()
     }
 
-    fn init(cx: &mut App) {
+    fn init(fs: Arc<dyn Fs>, cx: &mut App) {
         let executor = cx.background_executor().clone();
         let database_future = executor
             .spawn({
                 let executor = executor.clone();
                 let threads_dir = paths::data_dir().join("threads");
-                async move { ThreadsDatabase::new(threads_dir, executor) }
+                async move { ThreadsDatabase::new(fs, threads_dir, executor).await }
             })
             .then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
             .boxed()
@@ -884,13 +885,17 @@ impl ThreadsDatabase {
         cx.set_global(GlobalThreadsDatabase(database_future));
     }
 
-    pub fn new(threads_dir: PathBuf, executor: BackgroundExecutor) -> Result<Self> {
-        std::fs::create_dir_all(&threads_dir)?;
+    pub async fn new(
+        fs: Arc<dyn Fs>,
+        threads_dir: PathBuf,
+        executor: BackgroundExecutor,
+    ) -> Result<Self> {
+        fs.create_dir(&threads_dir).await?;
 
         let sqlite_path = threads_dir.join("threads.db");
         let mdb_path = threads_dir.join("threads-db.1.mdb");
 
-        let needs_migration_from_heed = mdb_path.exists();
+        let needs_migration_from_heed = fs.is_file(&mdb_path).await;
 
         let connection = if *ZED_STATELESS {
             Connection::open_memory(Some("THREAD_FALLBACK_DB"))
@@ -932,7 +937,14 @@ impl ThreadsDatabase {
                 .spawn(async move {
                     log::info!("Starting threads.db migration");
                     Self::migrate_from_heed(&mdb_path, db_connection, executor_clone)?;
-                    std::fs::remove_dir_all(mdb_path)?;
+                    fs.remove_dir(
+                        &mdb_path,
+                        RemoveOptions {
+                            recursive: true,
+                            ignore_if_not_exists: true,
+                        },
+                    )
+                    .await?;
                     log::info!("threads.db migrated to sqlite");
                     Ok::<(), anyhow::Error>(())
                 })

crates/agent2/src/agent.rs 🔗

@@ -27,6 +27,7 @@ use std::path::{Path, PathBuf};
 use std::rc::Rc;
 use std::sync::Arc;
 use util::ResultExt;
+use util::rel_path::RelPath;
 
 const RULES_FILE_NAMES: [&str; 9] = [
     ".rules",
@@ -434,7 +435,7 @@ impl NativeAgent {
         cx: &mut App,
     ) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
         let tree = worktree.read(cx);
-        let root_name = tree.root_name().into();
+        let root_name = tree.root_name_str().into();
         let abs_path = tree.abs_path();
 
         let mut context = WorktreeContext {
@@ -474,7 +475,7 @@ impl NativeAgent {
             .into_iter()
             .filter_map(|name| {
                 worktree
-                    .entry_for_path(name)
+                    .entry_for_path(RelPath::new(name).unwrap())
                     .filter(|entry| entry.is_file())
                     .map(|entry| entry.path.clone())
             })
@@ -558,7 +559,7 @@ impl NativeAgent {
                 if items.iter().any(|(path, _, _)| {
                     RULES_FILE_NAMES
                         .iter()
-                        .any(|name| path.as_ref() == Path::new(name))
+                        .any(|name| path.as_ref() == RelPath::new(name).unwrap())
                 }) {
                     self.project_context_needs_refresh.send(()).ok();
                 }
@@ -1208,7 +1209,7 @@ mod tests {
     use language_model::fake_provider::FakeLanguageModel;
     use serde_json::json;
     use settings::SettingsStore;
-    use util::path;
+    use util::{path, rel_path::rel_path};
 
     #[gpui::test]
     async fn test_maintaining_project_context(cx: &mut TestAppContext) {
@@ -1258,14 +1259,17 @@ mod tests {
         fs.insert_file("/a/.rules", Vec::new()).await;
         cx.run_until_parked();
         agent.read_with(cx, |agent, cx| {
-            let rules_entry = worktree.read(cx).entry_for_path(".rules").unwrap();
+            let rules_entry = worktree
+                .read(cx)
+                .entry_for_path(rel_path(".rules"))
+                .unwrap();
             assert_eq!(
                 agent.project_context.read(cx).worktrees,
                 vec![WorktreeContext {
                     root_name: "a".into(),
                     abs_path: Path::new("/a").into(),
                     rules_file: Some(RulesFileContext {
-                        path_in_worktree: Path::new(".rules").into(),
+                        path_in_worktree: rel_path(".rules").into(),
                         text: "".into(),
                         project_entry_id: rules_entry.id.to_usize()
                     })

crates/agent2/src/db.rs 🔗

@@ -422,17 +422,15 @@ mod tests {
     use agent::MessageSegment;
     use agent::context::LoadedContext;
     use client::Client;
-    use fs::FakeFs;
+    use fs::{FakeFs, Fs};
     use gpui::AppContext;
     use gpui::TestAppContext;
     use http_client::FakeHttpClient;
     use language_model::Role;
     use project::Project;
-    use serde_json::json;
     use settings::SettingsStore;
-    use util::test::TempTree;
 
-    fn init_test(cx: &mut TestAppContext) {
+    fn init_test(fs: Arc<dyn Fs>, cx: &mut TestAppContext) {
         env_logger::try_init().ok();
         cx.update(|cx| {
             let settings_store = SettingsStore::test(cx);
@@ -443,7 +441,7 @@ mod tests {
             let http_client = FakeHttpClient::with_404_response();
             let clock = Arc::new(clock::FakeSystemClock::new());
             let client = Client::new(clock, http_client, cx);
-            agent::init(cx);
+            agent::init(fs, cx);
             agent_settings::init(cx);
             language_model::init(client, cx);
         });
@@ -451,10 +449,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_retrieving_old_thread(cx: &mut TestAppContext) {
-        let tree = TempTree::new(json!({}));
-        util::paths::set_home_dir(tree.path().into());
-        init_test(cx);
         let fs = FakeFs::new(cx.executor());
+        init_test(fs.clone(), cx);
         let project = Project::test(fs, [], cx).await;
 
         // Save a thread using the old agent.

crates/agent2/src/thread.rs 🔗

@@ -879,27 +879,11 @@ impl Thread {
             .map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx))
             .collect();
 
-        cx.spawn(async move |_, cx| {
+        cx.spawn(async move |_, _| {
             let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
 
-            let mut unsaved_buffers = Vec::new();
-            cx.update(|app_cx| {
-                let buffer_store = project.read(app_cx).buffer_store();
-                for buffer_handle in buffer_store.read(app_cx).buffers() {
-                    let buffer = buffer_handle.read(app_cx);
-                    if buffer.is_dirty()
-                        && let Some(file) = buffer.file()
-                    {
-                        let path = file.path().to_string_lossy().to_string();
-                        unsaved_buffers.push(path);
-                    }
-                }
-            })
-            .ok();
-
             Arc::new(ProjectSnapshot {
                 worktree_snapshots,
-                unsaved_buffer_paths: unsaved_buffers,
                 timestamp: Utc::now(),
             })
         })

crates/agent2/src/tools/copy_path_tool.rs 🔗

@@ -84,9 +84,7 @@ impl AgentTool for CopyPathTool {
                 .and_then(|project_path| project.entry_for_path(&project_path, cx))
             {
                 Some(entity) => match project.find_project_path(&input.destination_path, cx) {
-                    Some(project_path) => {
-                        project.copy_entry(entity.id, None, project_path.path, cx)
-                    }
+                    Some(project_path) => project.copy_entry(entity.id, project_path, cx),
                     None => Task::ready(Err(anyhow!(
                         "Destination path {} was outside the project.",
                         input.destination_path

crates/agent2/src/tools/diagnostics_tool.rs 🔗

@@ -6,7 +6,7 @@ use language::{DiagnosticSeverity, OffsetRangeExt};
 use project::Project;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use std::{fmt::Write, path::Path, sync::Arc};
+use std::{fmt::Write, sync::Arc};
 use ui::SharedString;
 use util::markdown::MarkdownInlineCode;
 
@@ -147,9 +147,7 @@ impl AgentTool for DiagnosticsTool {
                         has_diagnostics = true;
                         output.push_str(&format!(
                             "{}: {} error(s), {} warning(s)\n",
-                            Path::new(worktree.read(cx).root_name())
-                                .join(project_path.path)
-                                .display(),
+                            worktree.read(cx).absolutize(&project_path.path).display(),
                             summary.error_count,
                             summary.warning_count
                         ));

crates/agent2/src/tools/edit_file_tool.rs 🔗

@@ -17,10 +17,12 @@ use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::Settings;
 use smol::stream::StreamExt as _;
+use std::ffi::OsStr;
 use std::path::{Path, PathBuf};
 use std::sync::Arc;
 use ui::SharedString;
 use util::ResultExt;
+use util::rel_path::RelPath;
 
 const DEFAULT_UI_TEXT: &str = "Editing file";
 
@@ -148,12 +150,11 @@ impl EditFileTool {
 
         // If any path component matches the local settings folder, then this could affect
         // the editor in ways beyond the project source, so prompt.
-        let local_settings_folder = paths::local_settings_folder_relative_path();
+        let local_settings_folder = paths::local_settings_folder_name();
         let path = Path::new(&input.path);
-        if path
-            .components()
-            .any(|component| component.as_os_str() == local_settings_folder.as_os_str())
-        {
+        if path.components().any(|component| {
+            component.as_os_str() == <_ as AsRef<OsStr>>::as_ref(&local_settings_folder)
+        }) {
             return event_stream.authorize(
                 format!("{} (local settings)", input.display_description),
                 cx,
@@ -162,6 +163,7 @@ impl EditFileTool {
 
         // It's also possible that the global config dir is configured to be inside the project,
         // so check for that edge case too.
+        // TODO this is broken when remoting
         if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
             && canonical_path.starts_with(paths::config_dir())
         {
@@ -216,9 +218,7 @@ impl AgentTool for EditFileTool {
                         .read(cx)
                         .short_full_path_for_project_path(&project_path, cx)
                 })
-                .unwrap_or(Path::new(&input.path).into())
-                .to_string_lossy()
-                .to_string()
+                .unwrap_or(input.path.to_string_lossy().to_string())
                 .into(),
             Err(raw_input) => {
                 if let Some(input) =
@@ -235,9 +235,7 @@ impl AgentTool for EditFileTool {
                                     .read(cx)
                                     .short_full_path_for_project_path(&project_path, cx)
                             })
-                            .unwrap_or(Path::new(&input.path).into())
-                            .to_string_lossy()
-                            .to_string()
+                            .unwrap_or(input.path)
                             .into();
                     }
 
@@ -478,7 +476,7 @@ impl AgentTool for EditFileTool {
     ) -> Result<()> {
         event_stream.update_diff(cx.new(|cx| {
             Diff::finalized(
-                output.input_path,
+                output.input_path.to_string_lossy().to_string(),
                 Some(output.old_text.to_string()),
                 output.new_text,
                 self.language_registry.clone(),
@@ -542,10 +540,12 @@ fn resolve_path(
             let file_name = input
                 .path
                 .file_name()
+                .and_then(|file_name| file_name.to_str())
+                .and_then(|file_name| RelPath::new(file_name).ok())
                 .context("Can't create file: invalid filename")?;
 
             let new_file_path = parent_project_path.map(|parent| ProjectPath {
-                path: Arc::from(parent.path.join(file_name)),
+                path: parent.path.join(file_name),
                 ..parent
             });
 
@@ -690,13 +690,10 @@ mod tests {
         cx.update(|cx| resolve_path(&input, project, cx))
     }
 
+    #[track_caller]
     fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
-        let actual = path
-            .expect("Should return valid path")
-            .path
-            .to_str()
-            .unwrap()
-            .replace("\\", "/"); // Naive Windows paths normalization
+        let actual = path.expect("Should return valid path").path;
+        let actual = actual.as_str();
         assert_eq!(actual, expected);
     }
 
@@ -1408,8 +1405,8 @@ mod tests {
             // Parent directory references - find_project_path resolves these
             (
                 "project/../other",
-                false,
-                "Path with .. is resolved by find_project_path",
+                true,
+                "Path with .. that goes outside of root directory",
             ),
             (
                 "project/./src/file.rs",
@@ -1437,16 +1434,18 @@ mod tests {
                 )
             });
 
+            cx.run_until_parked();
+
             if should_confirm {
                 stream_rx.expect_authorization().await;
             } else {
-                auth.await.unwrap();
                 assert!(
                     stream_rx.try_next().is_err(),
                     "Failed for case: {} - path: {} - expected no confirmation but got one",
                     description,
                     path
                 );
+                auth.await.unwrap();
             }
         }
     }

crates/agent2/src/tools/find_path_tool.rs 🔗

@@ -156,10 +156,14 @@ impl AgentTool for FindPathTool {
 }
 
 fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
-    let path_matcher = match PathMatcher::new([
-        // Sometimes models try to search for "". In this case, return all paths in the project.
-        if glob.is_empty() { "*" } else { glob },
-    ]) {
+    let path_style = project.read(cx).path_style(cx);
+    let path_matcher = match PathMatcher::new(
+        [
+            // Sometimes models try to search for "". In this case, return all paths in the project.
+            if glob.is_empty() { "*" } else { glob },
+        ],
+        path_style,
+    ) {
         Ok(matcher) => matcher,
         Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
     };
@@ -173,9 +177,8 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
         let mut results = Vec::new();
         for snapshot in snapshots {
             for entry in snapshot.entries(false, 0) {
-                let root_name = PathBuf::from(snapshot.root_name());
-                if path_matcher.is_match(root_name.join(&entry.path)) {
-                    results.push(snapshot.abs_path().join(entry.path.as_ref()));
+                if path_matcher.is_match(snapshot.root_name().join(&entry.path).as_std_path()) {
+                    results.push(snapshot.absolutize(&entry.path));
                 }
             }
         }

crates/agent2/src/tools/grep_tool.rs 🔗

@@ -110,12 +110,15 @@ impl AgentTool for GrepTool {
         const CONTEXT_LINES: u32 = 2;
         const MAX_ANCESTOR_LINES: u32 = 10;
 
+        let path_style = self.project.read(cx).path_style(cx);
+
         let include_matcher = match PathMatcher::new(
             input
                 .include_pattern
                 .as_ref()
                 .into_iter()
                 .collect::<Vec<_>>(),
+            path_style,
         ) {
             Ok(matcher) => matcher,
             Err(error) => {
@@ -132,7 +135,7 @@ impl AgentTool for GrepTool {
                 .iter()
                 .chain(global_settings.private_files.sources().iter());
 
-            match PathMatcher::new(exclude_patterns) {
+            match PathMatcher::new(exclude_patterns, path_style) {
                 Ok(matcher) => matcher,
                 Err(error) => {
                     return Task::ready(Err(anyhow!("invalid exclude pattern: {error}")));

crates/agent2/src/tools/list_directory_tool.rs 🔗

@@ -2,12 +2,12 @@ use crate::{AgentTool, ToolCallEventStream};
 use agent_client_protocol::ToolKind;
 use anyhow::{Result, anyhow};
 use gpui::{App, Entity, SharedString, Task};
-use project::{Project, WorktreeSettings};
+use project::{Project, ProjectPath, WorktreeSettings};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::Settings;
 use std::fmt::Write;
-use std::{path::Path, sync::Arc};
+use std::sync::Arc;
 use util::markdown::MarkdownInlineCode;
 
 /// Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase.
@@ -86,13 +86,13 @@ impl AgentTool for ListDirectoryTool {
                 .read(cx)
                 .worktrees(cx)
                 .filter_map(|worktree| {
-                    worktree.read(cx).root_entry().and_then(|entry| {
-                        if entry.is_dir() {
-                            entry.path.to_str()
-                        } else {
-                            None
-                        }
-                    })
+                    let worktree = worktree.read(cx);
+                    let root_entry = worktree.root_entry()?;
+                    if root_entry.is_dir() {
+                        Some(root_entry.path.display(worktree.path_style()))
+                    } else {
+                        None
+                    }
                 })
                 .collect::<Vec<_>>()
                 .join("\n");
@@ -143,7 +143,7 @@ impl AgentTool for ListDirectoryTool {
         }
 
         let worktree_snapshot = worktree.read(cx).snapshot();
-        let worktree_root_name = worktree.read(cx).root_name().to_string();
+        let worktree_root_name = worktree.read(cx).root_name();
 
         let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else {
             return Task::ready(Err(anyhow!("Path not found: {}", input.path)));
@@ -165,25 +165,17 @@ impl AgentTool for ListDirectoryTool {
                 continue;
             }
 
-            if self
-                .project
-                .read(cx)
-                .find_project_path(&entry.path, cx)
-                .map(|project_path| {
-                    let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
-
-                    worktree_settings.is_path_excluded(&project_path.path)
-                        || worktree_settings.is_path_private(&project_path.path)
-                })
-                .unwrap_or(false)
+            let project_path: ProjectPath = (worktree_snapshot.id(), entry.path.clone()).into();
+            if worktree_settings.is_path_excluded(&project_path.path)
+                || worktree_settings.is_path_private(&project_path.path)
             {
                 continue;
             }
 
-            let full_path = Path::new(&worktree_root_name)
+            let full_path = worktree_root_name
                 .join(&entry.path)
-                .display()
-                .to_string();
+                .display(worktree_snapshot.path_style())
+                .into_owned();
             if entry.is_dir() {
                 folders.push(full_path);
             } else {

crates/agent2/src/tools/move_path_tool.rs 🔗

@@ -98,7 +98,7 @@ impl AgentTool for MovePathTool {
                 .and_then(|project_path| project.entry_for_path(&project_path, cx))
             {
                 Some(entity) => match project.find_project_path(&input.destination_path, cx) {
-                    Some(project_path) => project.rename_entry(entity.id, project_path.path, cx),
+                    Some(project_path) => project.rename_entry(entity.id, project_path, cx),
                     None => Task::ready(Err(anyhow!(
                         "Destination path {} was outside the project.",
                         input.destination_path

crates/agent2/src/tools/read_file_tool.rs 🔗

@@ -82,12 +82,12 @@ impl AgentTool for ReadFileTool {
         {
             match (input.start_line, input.end_line) {
                 (Some(start), Some(end)) => {
-                    format!("Read file `{}` (lines {}-{})", path.display(), start, end,)
+                    format!("Read file `{path}` (lines {}-{})", start, end,)
                 }
                 (Some(start), None) => {
-                    format!("Read file `{}` (from line {})", path.display(), start)
+                    format!("Read file `{path}` (from line {})", start)
                 }
-                _ => format!("Read file `{}`", path.display()),
+                _ => format!("Read file `{path}`"),
             }
             .into()
         } else {

crates/agent_ui/src/acp/completion_provider.rs 🔗

@@ -1,5 +1,6 @@
 use std::cell::RefCell;
 use std::ops::Range;
+use std::path::PathBuf;
 use std::rc::Rc;
 use std::sync::Arc;
 use std::sync::atomic::AtomicBool;
@@ -13,7 +14,7 @@ use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{App, Entity, Task, WeakEntity};
 use language::{Buffer, CodeLabel, HighlightId};
 use lsp::CompletionContext;
-use project::lsp_store::CompletionDocumentation;
+use project::lsp_store::{CompletionDocumentation, SymbolLocation};
 use project::{
     Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, Project,
     ProjectPath, Symbol, WorktreeId,
@@ -22,6 +23,7 @@ use prompt_store::PromptStore;
 use rope::Point;
 use text::{Anchor, ToPoint as _};
 use ui::prelude::*;
+use util::rel_path::RelPath;
 use workspace::Workspace;
 
 use crate::AgentPanel;
@@ -187,7 +189,7 @@ impl ContextPickerCompletionProvider {
 
     pub(crate) fn completion_for_path(
         project_path: ProjectPath,
-        path_prefix: &str,
+        path_prefix: &RelPath,
         is_recent: bool,
         is_directory: bool,
         source_range: Range<Anchor>,
@@ -195,10 +197,12 @@ impl ContextPickerCompletionProvider {
         project: Entity<Project>,
         cx: &mut App,
     ) -> Option<Completion> {
+        let path_style = project.read(cx).path_style(cx);
         let (file_name, directory) =
             crate::context_picker::file_context_picker::extract_file_name_and_directory(
                 &project_path.path,
                 path_prefix,
+                path_style,
             );
 
         let label =
@@ -250,7 +254,15 @@ impl ContextPickerCompletionProvider {
 
         let label = CodeLabel::plain(symbol.name.clone(), None);
 
-        let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
+        let abs_path = match &symbol.path {
+            SymbolLocation::InProject(project_path) => {
+                project.read(cx).absolute_path(&project_path, cx)?
+            }
+            SymbolLocation::OutsideProject {
+                abs_path,
+                signature: _,
+            } => PathBuf::from(abs_path.as_ref()),
+        };
         let uri = MentionUri::Symbol {
             abs_path,
             name: symbol.name.clone(),

crates/agent_ui/src/acp/message_editor.rs 🔗

@@ -48,7 +48,7 @@ use std::{
 use text::OffsetRangeExt;
 use theme::ThemeSettings;
 use ui::{ButtonLike, TintColor, Toggleable, prelude::*};
-use util::{ResultExt, debug_panic};
+use util::{ResultExt, debug_panic, paths::PathStyle, rel_path::RelPath};
 use workspace::{Workspace, notifications::NotifyResultExt as _};
 use zed_actions::agent::Chat;
 
@@ -108,6 +108,11 @@ impl MessageEditor {
             available_commands.clone(),
         ));
         let mention_set = MentionSet::default();
+        // TODO: fix mentions when remoting with mixed path styles.
+        let host_and_guest_paths_differ = project
+            .read(cx)
+            .remote_client()
+            .is_some_and(|client| client.read(cx).path_style() != PathStyle::local());
         let editor = cx.new(|cx| {
             let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
             let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
@@ -117,7 +122,9 @@ impl MessageEditor {
             editor.set_show_indent_guides(false, cx);
             editor.set_soft_wrap();
             editor.set_use_modal_editing(true);
-            editor.set_completion_provider(Some(completion_provider.clone()));
+            if !host_and_guest_paths_differ {
+                editor.set_completion_provider(Some(completion_provider.clone()));
+            }
             editor.set_context_menu_options(ContextMenuOptions {
                 min_entries_visible: 12,
                 max_entries_visible: 12,
@@ -947,6 +954,7 @@ impl MessageEditor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        let path_style = self.project.read(cx).path_style(cx);
         let buffer = self.editor.read(cx).buffer().clone();
         let Some(buffer) = buffer.read(cx).as_singleton() else {
             return;
@@ -956,18 +964,15 @@ impl MessageEditor {
             let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
                 continue;
             };
-            let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else {
+            let Some(worktree) = self.project.read(cx).worktree_for_id(path.worktree_id, cx) else {
                 continue;
             };
-            let path_prefix = abs_path
-                .file_name()
-                .unwrap_or(path.path.as_os_str())
-                .display()
-                .to_string();
+            let abs_path = worktree.read(cx).absolutize(&path.path);
             let (file_name, _) =
                 crate::context_picker::file_context_picker::extract_file_name_and_directory(
                     &path.path,
-                    &path_prefix,
+                    worktree.read(cx).root_name(),
+                    path_style,
                 );
 
             let uri = if entry.is_dir() {
@@ -1176,7 +1181,7 @@ fn full_mention_for_directory(
     abs_path: &Path,
     cx: &mut App,
 ) -> Task<Result<Mention>> {
-    fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc<Path>, PathBuf)> {
+    fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, PathBuf)> {
         let mut files = Vec::new();
 
         for entry in worktree.child_entries(path) {
@@ -1261,7 +1266,7 @@ fn full_mention_for_directory(
     })
 }
 
-fn render_directory_contents(entries: Vec<(Arc<Path>, PathBuf, String)>) -> String {
+fn render_directory_contents(entries: Vec<(Arc<RelPath>, PathBuf, String)>) -> String {
     let mut output = String::new();
     for (_relative_path, full_path, content) in entries {
         let fence = codeblock_fence_for_path(Some(&full_path), None);
@@ -1595,7 +1600,7 @@ mod tests {
     use serde_json::json;
     use text::Point;
     use ui::{App, Context, IntoElement, Render, SharedString, Window};
-    use util::{path, uri};
+    use util::{path, paths::PathStyle, rel_path::rel_path, uri};
     use workspace::{AppState, Item, Workspace};
 
     use crate::acp::{
@@ -2105,16 +2110,18 @@ mod tests {
         let mut cx = VisualTestContext::from_window(*window, cx);
 
         let paths = vec![
-            path!("a/one.txt"),
-            path!("a/two.txt"),
-            path!("a/three.txt"),
-            path!("a/four.txt"),
-            path!("b/five.txt"),
-            path!("b/six.txt"),
-            path!("b/seven.txt"),
-            path!("b/eight.txt"),
+            rel_path("a/one.txt"),
+            rel_path("a/two.txt"),
+            rel_path("a/three.txt"),
+            rel_path("a/four.txt"),
+            rel_path("b/five.txt"),
+            rel_path("b/six.txt"),
+            rel_path("b/seven.txt"),
+            rel_path("b/eight.txt"),
         ];
 
+        let slash = PathStyle::local().separator();
+
         let mut opened_editors = Vec::new();
         for path in paths {
             let buffer = workspace
@@ -2122,7 +2129,7 @@ mod tests {
                     workspace.open_path(
                         ProjectPath {
                             worktree_id,
-                            path: Path::new(path).into(),
+                            path: path.into(),
                         },
                         None,
                         false,
@@ -2183,10 +2190,10 @@ mod tests {
             assert_eq!(
                 current_completion_labels(editor),
                 &[
-                    "eight.txt dir/b/",
-                    "seven.txt dir/b/",
-                    "six.txt dir/b/",
-                    "five.txt dir/b/",
+                    format!("eight.txt dir{slash}b{slash}"),
+                    format!("seven.txt dir{slash}b{slash}"),
+                    format!("six.txt dir{slash}b{slash}"),
+                    format!("five.txt dir{slash}b{slash}"),
                 ]
             );
             editor.set_text("", window, cx);
@@ -2214,14 +2221,14 @@ mod tests {
             assert_eq!(
                 current_completion_labels(editor),
                 &[
-                    "eight.txt dir/b/",
-                    "seven.txt dir/b/",
-                    "six.txt dir/b/",
-                    "five.txt dir/b/",
-                    "Files & Directories",
-                    "Symbols",
-                    "Threads",
-                    "Fetch"
+                    format!("eight.txt dir{slash}b{slash}"),
+                    format!("seven.txt dir{slash}b{slash}"),
+                    format!("six.txt dir{slash}b{slash}"),
+                    format!("five.txt dir{slash}b{slash}"),
+                    "Files & Directories".into(),
+                    "Symbols".into(),
+                    "Threads".into(),
+                    "Fetch".into()
                 ]
             );
         });
@@ -2248,7 +2255,10 @@ mod tests {
         editor.update(&mut cx, |editor, cx| {
             assert_eq!(editor.text(cx), "Lorem @file one");
             assert!(editor.has_visible_completions_menu());
-            assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
+            assert_eq!(
+                current_completion_labels(editor),
+                vec![format!("one.txt dir{slash}a{slash}")]
+            );
         });
 
         editor.update_in(&mut cx, |editor, window, cx| {
@@ -2505,7 +2515,7 @@ mod tests {
                 format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png")
             );
             assert!(editor.has_visible_completions_menu());
-            assert_eq!(current_completion_labels(editor), &["x.png dir/"]);
+            assert_eq!(current_completion_labels(editor), &[format!("x.png dir{slash}")]);
         });
 
         editor.update_in(&mut cx, |editor, window, cx| {
@@ -2544,7 +2554,7 @@ mod tests {
                         format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png")
                     );
                     assert!(editor.has_visible_completions_menu());
-                    assert_eq!(current_completion_labels(editor), &["x.png dir/"]);
+                    assert_eq!(current_completion_labels(editor), &[format!("x.png dir{slash}")]);
                 });
 
         editor.update_in(&mut cx, |editor, window, cx| {

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -3704,29 +3704,32 @@ impl AcpThreadView {
             |(index, (buffer, _diff))| {
                 let file = buffer.read(cx).file()?;
                 let path = file.path();
+                let path_style = file.path_style(cx);
+                let separator = file.path_style(cx).separator();
 
                 let file_path = path.parent().and_then(|parent| {
-                    let parent_str = parent.to_string_lossy();
-
-                    if parent_str.is_empty() {
+                    if parent.is_empty() {
                         None
                     } else {
                         Some(
-                            Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
-                                .color(Color::Muted)
-                                .size(LabelSize::XSmall)
-                                .buffer_font(cx),
+                            Label::new(format!(
+                                "{separator}{}{separator}",
+                                parent.display(path_style)
+                            ))
+                            .color(Color::Muted)
+                            .size(LabelSize::XSmall)
+                            .buffer_font(cx),
                         )
                     }
                 });
 
                 let file_name = path.file_name().map(|name| {
-                    Label::new(name.to_string_lossy().to_string())
+                    Label::new(name.to_string())
                         .size(LabelSize::XSmall)
                         .buffer_font(cx)
                 });
 
-                let file_icon = FileIcons::get_icon(path, cx)
+                let file_icon = FileIcons::get_icon(path.as_std_path(), cx)
                     .map(Icon::from_path)
                     .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
                     .unwrap_or_else(|| {
@@ -4569,7 +4572,7 @@ impl AcpThreadView {
                 .read(cx)
                 .visible_worktrees(cx)
                 .next()
-                .map(|worktree| worktree.read(cx).root_name().to_string())
+                .map(|worktree| worktree.read(cx).root_name_str().to_string())
         });
 
         if let Some(screen_window) = cx

crates/agent_ui/src/agent_ui.rs 🔗

@@ -264,7 +264,7 @@ pub fn init(
         init_language_model_settings(cx);
     }
     assistant_slash_command::init(cx);
-    agent::init(cx);
+    agent::init(fs.clone(), cx);
     agent_panel::init(cx);
     context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
     TextThreadEditor::init(cx);

crates/agent_ui/src/context_picker.rs 🔗

@@ -33,6 +33,8 @@ use thread_context_picker::{
 use ui::{
     ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
 };
+use util::paths::PathStyle;
+use util::rel_path::RelPath;
 use workspace::{Workspace, notifications::NotifyResultExt};
 
 use agent::{
@@ -228,12 +230,19 @@ impl ContextPicker {
         let context_picker = cx.entity();
 
         let menu = ContextMenu::build(window, cx, move |menu, _window, cx| {
+            let Some(workspace) = self.workspace.upgrade() else {
+                return menu;
+            };
+            let path_style = workspace.read(cx).path_style(cx);
             let recent = self.recent_entries(cx);
             let has_recent = !recent.is_empty();
             let recent_entries = recent
                 .into_iter()
                 .enumerate()
-                .map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
+                .map(|(ix, entry)| {
+                    self.recent_menu_item(context_picker.clone(), ix, entry, path_style)
+                })
+                .collect::<Vec<_>>();
 
             let entries = self
                 .workspace
@@ -395,6 +404,7 @@ impl ContextPicker {
         context_picker: Entity<ContextPicker>,
         ix: usize,
         entry: RecentEntry,
+        path_style: PathStyle,
     ) -> ContextMenuItem {
         match entry {
             RecentEntry::File {
@@ -413,6 +423,7 @@ impl ContextPicker {
                             &path,
                             &path_prefix,
                             false,
+                            path_style,
                             context_store.clone(),
                             cx,
                         )
@@ -586,7 +597,7 @@ impl Render for ContextPicker {
 pub(crate) enum RecentEntry {
     File {
         project_path: ProjectPath,
-        path_prefix: Arc<str>,
+        path_prefix: Arc<RelPath>,
     },
     Thread(ThreadContextEntry),
 }

crates/agent_ui/src/context_picker/completion_provider.rs 🔗

@@ -13,6 +13,7 @@ use http_client::HttpClientWithUrl;
 use itertools::Itertools;
 use language::{Buffer, CodeLabel, HighlightId};
 use lsp::CompletionContext;
+use project::lsp_store::SymbolLocation;
 use project::{
     Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, ProjectPath,
     Symbol, WorktreeId,
@@ -22,6 +23,8 @@ use rope::Point;
 use text::{Anchor, OffsetRangeExt, ToPoint};
 use ui::prelude::*;
 use util::ResultExt as _;
+use util::paths::PathStyle;
+use util::rel_path::RelPath;
 use workspace::Workspace;
 
 use agent::{
@@ -574,11 +577,12 @@ impl ContextPickerCompletionProvider {
 
     fn completion_for_path(
         project_path: ProjectPath,
-        path_prefix: &str,
+        path_prefix: &RelPath,
         is_recent: bool,
         is_directory: bool,
         excerpt_id: ExcerptId,
         source_range: Range<Anchor>,
+        path_style: PathStyle,
         editor: Entity<Editor>,
         context_store: Entity<ContextStore>,
         cx: &App,
@@ -586,6 +590,7 @@ impl ContextPickerCompletionProvider {
         let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
             &project_path.path,
             path_prefix,
+            path_style,
         );
 
         let label =
@@ -657,17 +662,22 @@ impl ContextPickerCompletionProvider {
         workspace: Entity<Workspace>,
         cx: &mut App,
     ) -> Option<Completion> {
+        let path_style = workspace.read(cx).path_style(cx);
+        let SymbolLocation::InProject(symbol_path) = &symbol.path else {
+            return None;
+        };
         let path_prefix = workspace
             .read(cx)
             .project()
             .read(cx)
-            .worktree_for_id(symbol.path.worktree_id, cx)?
+            .worktree_for_id(symbol_path.worktree_id, cx)?
             .read(cx)
             .root_name();
 
         let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
-            &symbol.path.path,
+            &symbol_path.path,
             path_prefix,
+            path_style,
         );
         let full_path = if let Some(directory) = directory {
             format!("{}{}", directory, file_name)
@@ -768,6 +778,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
         let text_thread_store = self.text_thread_store.clone();
         let editor = self.editor.clone();
         let http_client = workspace.read(cx).client().http_client();
+        let path_style = workspace.read(cx).path_style(cx);
 
         let MentionCompletion { mode, argument, .. } = state;
         let query = argument.unwrap_or_else(|| "".to_string());
@@ -834,6 +845,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                                 mat.is_dir,
                                 excerpt_id,
                                 source_range.clone(),
+                                path_style,
                                 editor.clone(),
                                 context_store.clone(),
                                 cx,
@@ -1064,7 +1076,7 @@ mod tests {
     use serde_json::json;
     use settings::SettingsStore;
     use std::{ops::Deref, rc::Rc};
-    use util::path;
+    use util::{path, rel_path::rel_path};
     use workspace::{AppState, Item};
 
     #[test]
@@ -1215,16 +1227,18 @@ mod tests {
         let mut cx = VisualTestContext::from_window(*window.deref(), cx);
 
         let paths = vec![
-            path!("a/one.txt"),
-            path!("a/two.txt"),
-            path!("a/three.txt"),
-            path!("a/four.txt"),
-            path!("b/five.txt"),
-            path!("b/six.txt"),
-            path!("b/seven.txt"),
-            path!("b/eight.txt"),
+            rel_path("a/one.txt"),
+            rel_path("a/two.txt"),
+            rel_path("a/three.txt"),
+            rel_path("a/four.txt"),
+            rel_path("b/five.txt"),
+            rel_path("b/six.txt"),
+            rel_path("b/seven.txt"),
+            rel_path("b/eight.txt"),
         ];
 
+        let slash = PathStyle::local().separator();
+
         let mut opened_editors = Vec::new();
         for path in paths {
             let buffer = workspace
@@ -1232,7 +1246,7 @@ mod tests {
                     workspace.open_path(
                         ProjectPath {
                             worktree_id,
-                            path: Path::new(path).into(),
+                            path: path.into(),
                         },
                         None,
                         false,
@@ -1308,13 +1322,13 @@ mod tests {
             assert_eq!(
                 current_completion_labels(editor),
                 &[
-                    "seven.txt dir/b/",
-                    "six.txt dir/b/",
-                    "five.txt dir/b/",
-                    "four.txt dir/a/",
-                    "Files & Directories",
-                    "Symbols",
-                    "Fetch"
+                    format!("seven.txt dir{slash}b{slash}"),
+                    format!("six.txt dir{slash}b{slash}"),
+                    format!("five.txt dir{slash}b{slash}"),
+                    format!("four.txt dir{slash}a{slash}"),
+                    "Files & Directories".into(),
+                    "Symbols".into(),
+                    "Fetch".into()
                 ]
             );
         });
@@ -1341,7 +1355,10 @@ mod tests {
         editor.update(&mut cx, |editor, cx| {
             assert_eq!(editor.text(cx), "Lorem @file one");
             assert!(editor.has_visible_completions_menu());
-            assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
+            assert_eq!(
+                current_completion_labels(editor),
+                vec![format!("one.txt dir{slash}a{slash}")]
+            );
         });
 
         editor.update_in(&mut cx, |editor, window, cx| {
@@ -1350,7 +1367,10 @@ mod tests {
         });
 
         editor.update(&mut cx, |editor, cx| {
-            assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) ");
+            assert_eq!(
+                editor.text(cx),
+                format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) ")
+            );
             assert!(!editor.has_visible_completions_menu());
             assert_eq!(
                 fold_ranges(editor, cx),
@@ -1361,7 +1381,10 @@ mod tests {
         cx.simulate_input(" ");
 
         editor.update(&mut cx, |editor, cx| {
-            assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt)  ");
+            assert_eq!(
+                editor.text(cx),
+                format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt)  ")
+            );
             assert!(!editor.has_visible_completions_menu());
             assert_eq!(
                 fold_ranges(editor, cx),
@@ -1374,7 +1397,7 @@ mod tests {
         editor.update(&mut cx, |editor, cx| {
             assert_eq!(
                 editor.text(cx),
-                "Lorem [@one.txt](@file:dir/a/one.txt)  Ipsum ",
+                format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt)  Ipsum "),
             );
             assert!(!editor.has_visible_completions_menu());
             assert_eq!(
@@ -1388,7 +1411,7 @@ mod tests {
         editor.update(&mut cx, |editor, cx| {
             assert_eq!(
                 editor.text(cx),
-                "Lorem [@one.txt](@file:dir/a/one.txt)  Ipsum @file ",
+                format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt)  Ipsum @file "),
             );
             assert!(editor.has_visible_completions_menu());
             assert_eq!(
@@ -1406,7 +1429,7 @@ mod tests {
         editor.update(&mut cx, |editor, cx| {
             assert_eq!(
                 editor.text(cx),
-                "Lorem [@one.txt](@file:dir/a/one.txt)  Ipsum [@seven.txt](@file:dir/b/seven.txt) "
+                format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt)  Ipsum [@seven.txt](@file:dir{slash}b{slash}seven.txt) ")
             );
             assert!(!editor.has_visible_completions_menu());
             assert_eq!(
@@ -1423,7 +1446,7 @@ mod tests {
         editor.update(&mut cx, |editor, cx| {
             assert_eq!(
                 editor.text(cx),
-                "Lorem [@one.txt](@file:dir/a/one.txt)  Ipsum [@seven.txt](@file:dir/b/seven.txt) \n@"
+                format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt)  Ipsum [@seven.txt](@file:dir{slash}b{slash}seven.txt) \n@")
             );
             assert!(editor.has_visible_completions_menu());
             assert_eq!(
@@ -1444,7 +1467,7 @@ mod tests {
         editor.update(&mut cx, |editor, cx| {
             assert_eq!(
                 editor.text(cx),
-                "Lorem [@one.txt](@file:dir/a/one.txt)  Ipsum [@seven.txt](@file:dir/b/seven.txt) \n[@six.txt](@file:dir/b/six.txt) "
+                format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt)  Ipsum [@seven.txt](@file:dir{slash}b{slash}seven.txt) \n[@six.txt](@file:dir{slash}b{slash}six.txt) ")
             );
             assert!(!editor.has_visible_completions_menu());
             assert_eq!(

crates/agent_ui/src/context_picker/file_context_picker.rs 🔗

@@ -1,4 +1,3 @@
-use std::path::Path;
 use std::sync::Arc;
 use std::sync::atomic::AtomicBool;
 
@@ -10,7 +9,7 @@ use gpui::{
 use picker::{Picker, PickerDelegate};
 use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
 use ui::{ListItem, Tooltip, prelude::*};
-use util::ResultExt as _;
+use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
 use workspace::Workspace;
 
 use crate::context_picker::ContextPicker;
@@ -161,6 +160,8 @@ impl PickerDelegate for FileContextPickerDelegate {
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
         let FileMatch { mat, .. } = &self.matches.get(ix)?;
+        let workspace = self.workspace.upgrade()?;
+        let path_style = workspace.read(cx).path_style(cx);
 
         Some(
             ListItem::new(ix)
@@ -172,6 +173,7 @@ impl PickerDelegate for FileContextPickerDelegate {
                     &mat.path,
                     &mat.path_prefix,
                     mat.is_dir,
+                    path_style,
                     self.context_store.clone(),
                     cx,
                 )),
@@ -214,14 +216,13 @@ pub(crate) fn search_files(
 
         let file_matches = project.worktrees(cx).flat_map(|worktree| {
             let worktree = worktree.read(cx);
-            let path_prefix: Arc<str> = worktree.root_name().into();
             worktree.entries(false, 0).map(move |entry| FileMatch {
                 mat: PathMatch {
                     score: 0.,
                     positions: Vec::new(),
                     worktree_id: worktree.id().to_usize(),
                     path: entry.path.clone(),
-                    path_prefix: path_prefix.clone(),
+                    path_prefix: worktree.root_name().into(),
                     distance_to_relative_ancestor: 0,
                     is_dir: entry.is_dir(),
                 },
@@ -269,51 +270,31 @@ pub(crate) fn search_files(
 }
 
 pub fn extract_file_name_and_directory(
-    path: &Path,
-    path_prefix: &str,
+    path: &RelPath,
+    path_prefix: &RelPath,
+    path_style: PathStyle,
 ) -> (SharedString, Option<SharedString>) {
-    if path == Path::new("") {
-        (
-            SharedString::from(
-                path_prefix
-                    .trim_end_matches(std::path::MAIN_SEPARATOR)
-                    .to_string(),
-            ),
-            None,
-        )
-    } else {
-        let file_name = path
-            .file_name()
-            .unwrap_or_default()
-            .to_string_lossy()
-            .to_string()
-            .into();
-
-        let mut directory = path_prefix
-            .trim_end_matches(std::path::MAIN_SEPARATOR)
-            .to_string();
-        if !directory.ends_with('/') {
-            directory.push('/');
-        }
-        if let Some(parent) = path.parent().filter(|parent| parent != &Path::new("")) {
-            directory.push_str(&parent.to_string_lossy());
-            directory.push('/');
-        }
-
-        (file_name, Some(directory.into()))
-    }
+    let full_path = path_prefix.join(path);
+    let file_name = full_path.file_name().unwrap_or_default();
+    let display_path = full_path.display(path_style);
+    let (directory, file_name) = display_path.split_at(display_path.len() - file_name.len());
+    (
+        file_name.to_string().into(),
+        Some(SharedString::new(directory)).filter(|dir| !dir.is_empty()),
+    )
 }
 
 pub fn render_file_context_entry(
     id: ElementId,
     worktree_id: WorktreeId,
-    path: &Arc<Path>,
-    path_prefix: &Arc<str>,
+    path: &Arc<RelPath>,
+    path_prefix: &Arc<RelPath>,
     is_directory: bool,
+    path_style: PathStyle,
     context_store: WeakEntity<ContextStore>,
     cx: &App,
 ) -> Stateful<Div> {
-    let (file_name, directory) = extract_file_name_and_directory(path, path_prefix);
+    let (file_name, directory) = extract_file_name_and_directory(path, path_prefix, path_style);
 
     let added = context_store.upgrade().and_then(|context_store| {
         let project_path = ProjectPath {
@@ -330,9 +311,9 @@ pub fn render_file_context_entry(
     });
 
     let file_icon = if is_directory {
-        FileIcons::get_folder_icon(false, path, cx)
+        FileIcons::get_folder_icon(false, path.as_std_path(), cx)
     } else {
-        FileIcons::get_icon(path, cx)
+        FileIcons::get_icon(path.as_std_path(), cx)
     }
     .map(Icon::from_path)
     .unwrap_or_else(|| Icon::new(IconName::File));

crates/agent_ui/src/context_picker/symbol_context_picker.rs 🔗

@@ -2,13 +2,14 @@ use std::cmp::Reverse;
 use std::sync::Arc;
 use std::sync::atomic::AtomicBool;
 
-use anyhow::Result;
+use anyhow::{Result, anyhow};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
     App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
 };
 use ordered_float::OrderedFloat;
 use picker::{Picker, PickerDelegate};
+use project::lsp_store::SymbolLocation;
 use project::{DocumentSymbol, Symbol};
 use ui::{ListItem, prelude::*};
 use util::ResultExt as _;
@@ -191,7 +192,10 @@ pub(crate) fn add_symbol(
 ) -> Task<Result<(Option<AgentContextHandle>, bool)>> {
     let project = workspace.read(cx).project().clone();
     let open_buffer_task = project.update(cx, |project, cx| {
-        project.open_buffer(symbol.path.clone(), cx)
+        let SymbolLocation::InProject(symbol_path) = &symbol.path else {
+            return Task::ready(Err(anyhow!("can't add symbol from outside of project")));
+        };
+        project.open_buffer(symbol_path.clone(), cx)
     });
     cx.spawn(async move |cx| {
         let buffer = open_buffer_task.await?;
@@ -291,10 +295,11 @@ pub(crate) fn search_symbols(
                         .map(|(id, symbol)| {
                             StringMatchCandidate::new(id, symbol.label.filter_text())
                         })
-                        .partition(|candidate| {
-                            project
-                                .entry_for_path(&symbols[candidate.id].path, cx)
-                                .is_some_and(|e| !e.is_ignored)
+                        .partition(|candidate| match &symbols[candidate.id].path {
+                            SymbolLocation::InProject(project_path) => project
+                                .entry_for_path(project_path, cx)
+                                .is_some_and(|e| !e.is_ignored),
+                            SymbolLocation::OutsideProject { .. } => false,
                         })
                 })
                 .log_err()
@@ -360,13 +365,18 @@ fn compute_symbol_entries(
 }
 
 pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
-    let path = entry
-        .symbol
-        .path
-        .path
-        .file_name()
-        .map(|s| s.to_string_lossy())
-        .unwrap_or_default();
+    let path = match &entry.symbol.path {
+        SymbolLocation::InProject(project_path) => {
+            project_path.path.file_name().unwrap_or_default().into()
+        }
+        SymbolLocation::OutsideProject {
+            abs_path,
+            signature: _,
+        } => abs_path
+            .file_name()
+            .map(|f| f.to_string_lossy())
+            .unwrap_or_default(),
+    };
     let symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1);
 
     h_flex()

crates/agent_ui/src/text_thread_editor.rs 🔗

@@ -1431,10 +1431,14 @@ impl TextThreadEditor {
             else {
                 continue;
             };
-            let worktree_root_name = worktree.read(cx).root_name().to_string();
-            let mut full_path = PathBuf::from(worktree_root_name.clone());
-            full_path.push(&project_path.path);
-            file_slash_command_args.push(full_path.to_string_lossy().to_string());
+            let path_style = worktree.read(cx).path_style();
+            let full_path = worktree
+                .read(cx)
+                .root_name()
+                .join(&project_path.path)
+                .display(path_style)
+                .into_owned();
+            file_slash_command_args.push(full_path);
         }
 
         let cmd_name = FileSlashCommand.name();

crates/assistant_slash_command/Cargo.toml 🔗

@@ -25,6 +25,7 @@ parking_lot.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 ui.workspace = true
+util.workspace = true
 workspace.workspace = true
 workspace-hack.workspace = true
 

crates/assistant_slash_command/src/extension_slash_command.rs 🔗

@@ -1,12 +1,11 @@
-use std::path::PathBuf;
-use std::sync::{Arc, atomic::AtomicBool};
-
 use anyhow::Result;
 use async_trait::async_trait;
 use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate};
 use gpui::{App, Task, WeakEntity, Window};
 use language::{BufferSnapshot, LspAdapterDelegate};
+use std::sync::{Arc, atomic::AtomicBool};
 use ui::prelude::*;
+use util::rel_path::RelPath;
 use workspace::Workspace;
 
 use crate::{
@@ -54,7 +53,7 @@ impl WorktreeDelegate for WorktreeDelegateAdapter {
         self.0.worktree_root_path().to_string_lossy().to_string()
     }
 
-    async fn read_text_file(&self, path: PathBuf) -> Result<String> {
+    async fn read_text_file(&self, path: &RelPath) -> Result<String> {
         self.0.read_text_file(path).await
     }
 

crates/assistant_slash_commands/Cargo.toml 🔗

@@ -41,6 +41,9 @@ worktree.workspace = true
 workspace-hack.workspace = true
 
 [dev-dependencies]
+fs = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
 pretty_assertions.workspace = true
-settings.workspace = true
+project = { workspace = true, features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }
 zlog.workspace = true

crates/assistant_slash_commands/src/cargo_workspace_command.rs 🔗

@@ -0,0 +1,159 @@
+use anyhow::{Context as _, Result, anyhow};
+use assistant_slash_command::{
+    ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
+    SlashCommandResult,
+};
+use fs::Fs;
+use gpui::{App, Entity, Task, WeakEntity};
+use language::{BufferSnapshot, LspAdapterDelegate};
+use project::{Project, ProjectPath};
+use std::{
+    fmt::Write,
+    path::Path,
+    sync::{Arc, atomic::AtomicBool},
+};
+use ui::prelude::*;
+use util::rel_path::RelPath;
+use workspace::Workspace;
+
+pub struct CargoWorkspaceSlashCommand;
+
+impl CargoWorkspaceSlashCommand {
+    async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
+        let buffer = fs.load(path_to_cargo_toml).await?;
+        let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
+
+        let mut message = String::new();
+        writeln!(message, "You are in a Rust project.")?;
+
+        if let Some(workspace) = cargo_toml.workspace {
+            writeln!(
+                message,
+                "The project is a Cargo workspace with the following members:"
+            )?;
+            for member in workspace.members {
+                writeln!(message, "- {member}")?;
+            }
+
+            if !workspace.default_members.is_empty() {
+                writeln!(message, "The default members are:")?;
+                for member in workspace.default_members {
+                    writeln!(message, "- {member}")?;
+                }
+            }
+
+            if !workspace.dependencies.is_empty() {
+                writeln!(
+                    message,
+                    "The following workspace dependencies are installed:"
+                )?;
+                for dependency in workspace.dependencies.keys() {
+                    writeln!(message, "- {dependency}")?;
+                }
+            }
+        } else if let Some(package) = cargo_toml.package {
+            writeln!(
+                message,
+                "The project name is \"{name}\".",
+                name = package.name
+            )?;
+
+            let description = package
+                .description
+                .as_ref()
+                .and_then(|description| description.get().ok().cloned());
+            if let Some(description) = description.as_ref() {
+                writeln!(message, "It describes itself as \"{description}\".")?;
+            }
+
+            if !cargo_toml.dependencies.is_empty() {
+                writeln!(message, "The following dependencies are installed:")?;
+                for dependency in cargo_toml.dependencies.keys() {
+                    writeln!(message, "- {dependency}")?;
+                }
+            }
+        }
+
+        Ok(message)
+    }
+
+    fn path_to_cargo_toml(project: Entity<Project>, cx: &mut App) -> Option<Arc<Path>> {
+        let worktree = project.read(cx).worktrees(cx).next()?;
+        let worktree = worktree.read(cx);
+        let entry = worktree.entry_for_path(RelPath::new("Cargo.toml").unwrap())?;
+        let path = ProjectPath {
+            worktree_id: worktree.id(),
+            path: entry.path.clone(),
+        };
+        Some(Arc::from(
+            project.read(cx).absolute_path(&path, cx)?.as_path(),
+        ))
+    }
+}
+
+impl SlashCommand for CargoWorkspaceSlashCommand {
+    fn name(&self) -> String {
+        "cargo-workspace".into()
+    }
+
+    fn description(&self) -> String {
+        "insert project workspace metadata".into()
+    }
+
+    fn menu_text(&self) -> String {
+        "Insert Project Workspace Metadata".into()
+    }
+
+    fn complete_argument(
+        self: Arc<Self>,
+        _arguments: &[String],
+        _cancel: Arc<AtomicBool>,
+        _workspace: Option<WeakEntity<Workspace>>,
+        _window: &mut Window,
+        _cx: &mut App,
+    ) -> Task<Result<Vec<ArgumentCompletion>>> {
+        Task::ready(Err(anyhow!("this command does not require argument")))
+    }
+
+    fn requires_argument(&self) -> bool {
+        false
+    }
+
+    fn run(
+        self: Arc<Self>,
+        _arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: BufferSnapshot,
+        workspace: WeakEntity<Workspace>,
+        _delegate: Option<Arc<dyn LspAdapterDelegate>>,
+        _window: &mut Window,
+        cx: &mut App,
+    ) -> Task<SlashCommandResult> {
+        let output = workspace.update(cx, |workspace, cx| {
+            let project = workspace.project().clone();
+            let fs = workspace.project().read(cx).fs().clone();
+            let path = Self::path_to_cargo_toml(project, cx);
+            let output = cx.background_spawn(async move {
+                let path = path.with_context(|| "Cargo.toml not found")?;
+                Self::build_message(fs, &path).await
+            });
+
+            cx.foreground_executor().spawn(async move {
+                let text = output.await?;
+                let range = 0..text.len();
+                Ok(SlashCommandOutput {
+                    text,
+                    sections: vec![SlashCommandOutputSection {
+                        range,
+                        icon: IconName::FileTree,
+                        label: "Project".into(),
+                        metadata: None,
+                    }],
+                    run_commands_in_text: false,
+                }
+                .into_event_stream())
+            })
+        });
+        output.unwrap_or_else(|error| Task::ready(Err(error)))
+    }
+}

crates/assistant_slash_commands/src/diagnostics_command.rs 🔗

@@ -13,12 +13,12 @@ use project::{DiagnosticSummary, PathMatchCandidateSet, Project};
 use rope::Point;
 use std::{
     fmt::Write,
-    path::{Path, PathBuf},
+    path::Path,
     sync::{Arc, atomic::AtomicBool},
 };
 use ui::prelude::*;
-use util::ResultExt;
-use util::paths::PathMatcher;
+use util::paths::{PathMatcher, PathStyle};
+use util::{ResultExt, rel_path::RelPath};
 use workspace::Workspace;
 
 use crate::create_label_for_command;
@@ -36,7 +36,7 @@ impl DiagnosticsSlashCommand {
         if query.is_empty() {
             let workspace = workspace.read(cx);
             let entries = workspace.recent_navigation_history(Some(10), cx);
-            let path_prefix: Arc<str> = Arc::default();
+            let path_prefix: Arc<RelPath> = RelPath::empty().into();
             Task::ready(
                 entries
                     .into_iter()
@@ -125,6 +125,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
         let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
             return Task::ready(Err(anyhow!("workspace was dropped")));
         };
+        let path_style = workspace.read(cx).project().read(cx).path_style(cx);
         let query = arguments.last().cloned().unwrap_or_default();
 
         let paths = self.search_paths(query.clone(), cancellation_flag.clone(), &workspace, cx);
@@ -134,11 +135,11 @@ impl SlashCommand for DiagnosticsSlashCommand {
                 .await
                 .into_iter()
                 .map(|path_match| {
-                    format!(
-                        "{}{}",
-                        path_match.path_prefix,
-                        path_match.path.to_string_lossy()
-                    )
+                    path_match
+                        .path_prefix
+                        .join(&path_match.path)
+                        .display(path_style)
+                        .to_string()
                 })
                 .collect();
 
@@ -183,9 +184,11 @@ impl SlashCommand for DiagnosticsSlashCommand {
             return Task::ready(Err(anyhow!("workspace was dropped")));
         };
 
-        let options = Options::parse(arguments);
+        let project = workspace.read(cx).project();
+        let path_style = project.read(cx).path_style(cx);
+        let options = Options::parse(arguments, path_style);
 
-        let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
+        let task = collect_diagnostics(project.clone(), options, cx);
 
         window.spawn(cx, async move |_| {
             task.await?
@@ -204,14 +207,14 @@ struct Options {
 const INCLUDE_WARNINGS_ARGUMENT: &str = "--include-warnings";
 
 impl Options {
-    fn parse(arguments: &[String]) -> Self {
+    fn parse(arguments: &[String], path_style: PathStyle) -> Self {
         let mut include_warnings = false;
         let mut path_matcher = None;
         for arg in arguments {
             if arg == INCLUDE_WARNINGS_ARGUMENT {
                 include_warnings = true;
             } else {
-                path_matcher = PathMatcher::new(&[arg.to_owned()]).log_err();
+                path_matcher = PathMatcher::new(&[arg.to_owned()], path_style).log_err();
             }
         }
         Self {
@@ -237,21 +240,15 @@ fn collect_diagnostics(
         None
     };
 
+    let path_style = project.read(cx).path_style(cx);
     let glob_is_exact_file_match = if let Some(path) = options
         .path_matcher
         .as_ref()
         .and_then(|pm| pm.sources().first())
     {
-        PathBuf::try_from(path)
-            .ok()
-            .and_then(|path| {
-                project.read(cx).worktrees(cx).find_map(|worktree| {
-                    let worktree = worktree.read(cx);
-                    let worktree_root_path = Path::new(worktree.root_name());
-                    let relative_path = path.strip_prefix(worktree_root_path).ok()?;
-                    worktree.absolutize(relative_path).ok()
-                })
-            })
+        project
+            .read(cx)
+            .find_project_path(Path::new(path), cx)
             .is_some()
     } else {
         false
@@ -263,9 +260,8 @@ fn collect_diagnostics(
         .diagnostic_summaries(false, cx)
         .flat_map(|(path, _, summary)| {
             let worktree = project.read(cx).worktree_for_id(path.worktree_id, cx)?;
-            let mut path_buf = PathBuf::from(worktree.read(cx).root_name());
-            path_buf.push(&path.path);
-            Some((path, path_buf, summary))
+            let full_path = worktree.read(cx).root_name().join(&path.path);
+            Some((path, full_path, summary))
         })
         .collect();
 
@@ -281,7 +277,7 @@ fn collect_diagnostics(
         let mut project_summary = DiagnosticSummary::default();
         for (project_path, path, summary) in diagnostic_summaries {
             if let Some(path_matcher) = &options.path_matcher
-                && !path_matcher.is_match(&path)
+                && !path_matcher.is_match(&path.as_std_path())
             {
                 continue;
             }
@@ -294,7 +290,7 @@ fn collect_diagnostics(
             }
 
             let last_end = output.text.len();
-            let file_path = path.to_string_lossy().to_string();
+            let file_path = path.display(path_style).to_string();
             if !glob_is_exact_file_match {
                 writeln!(&mut output.text, "{file_path}").unwrap();
             }

crates/assistant_slash_commands/src/file_command.rs 🔗

@@ -14,11 +14,11 @@ use smol::stream::StreamExt;
 use std::{
     fmt::Write,
     ops::{Range, RangeInclusive},
-    path::{Path, PathBuf},
+    path::Path,
     sync::{Arc, atomic::AtomicBool},
 };
 use ui::prelude::*;
-use util::ResultExt;
+use util::{ResultExt, rel_path::RelPath};
 use workspace::Workspace;
 use worktree::ChildEntriesOptions;
 
@@ -48,7 +48,7 @@ impl FileSlashCommand {
                         include_dirs: true,
                         include_ignored: false,
                     };
-                    let entries = worktree.child_entries_with_options(Path::new(""), options);
+                    let entries = worktree.child_entries_with_options(RelPath::empty(), options);
                     entries.map(move |entry| {
                         (
                             project::ProjectPath {
@@ -61,19 +61,18 @@ impl FileSlashCommand {
                 }))
                 .collect::<Vec<_>>();
 
-            let path_prefix: Arc<str> = Arc::default();
+            let path_prefix: Arc<RelPath> = RelPath::empty().into();
             Task::ready(
                 entries
                     .into_iter()
                     .filter_map(|(entry, is_dir)| {
                         let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
-                        let mut full_path = PathBuf::from(worktree.read(cx).root_name());
-                        full_path.push(&entry.path);
+                        let full_path = worktree.read(cx).root_name().join(&entry.path);
                         Some(PathMatch {
                             score: 0.,
                             positions: Vec::new(),
                             worktree_id: entry.worktree_id.to_usize(),
-                            path: full_path.into(),
+                            path: full_path,
                             path_prefix: path_prefix.clone(),
                             distance_to_relative_ancestor: 0,
                             is_dir,
@@ -149,6 +148,8 @@ impl SlashCommand for FileSlashCommand {
             return Task::ready(Err(anyhow!("workspace was dropped")));
         };
 
+        let path_style = workspace.read(cx).path_style(cx);
+
         let paths = self.search_paths(
             arguments.last().cloned().unwrap_or_default(),
             cancellation_flag,
@@ -161,14 +162,14 @@ impl SlashCommand for FileSlashCommand {
                 .await
                 .into_iter()
                 .filter_map(|path_match| {
-                    let text = format!(
-                        "{}{}",
-                        path_match.path_prefix,
-                        path_match.path.to_string_lossy()
-                    );
+                    let text = path_match
+                        .path_prefix
+                        .join(&path_match.path)
+                        .display(path_style)
+                        .to_string();
 
                     let mut label = CodeLabel::default();
-                    let file_name = path_match.path.file_name()?.to_string_lossy();
+                    let file_name = path_match.path.file_name()?;
                     let label_text = if path_match.is_dir {
                         format!("{}/ ", file_name)
                     } else {
@@ -247,14 +248,13 @@ fn collect_files(
     cx.spawn(async move |cx| {
         for snapshot in snapshots {
             let worktree_id = snapshot.id();
-            let mut directory_stack: Vec<Arc<Path>> = Vec::new();
-            let mut folded_directory_names_stack = Vec::new();
+            let path_style = snapshot.path_style();
+            let mut directory_stack: Vec<Arc<RelPath>> = Vec::new();
+            let mut folded_directory_names: Arc<RelPath> = RelPath::empty().into();
             let mut is_top_level_directory = true;
 
             for entry in snapshot.entries(false, 0) {
-                let mut path_including_worktree_name = PathBuf::new();
-                path_including_worktree_name.push(snapshot.root_name());
-                path_including_worktree_name.push(&entry.path);
+                let path_including_worktree_name = snapshot.root_name().join(&entry.path);
 
                 if !matchers
                     .iter()
@@ -277,13 +277,7 @@ fn collect_files(
                     )))?;
                 }
 
-                let filename = entry
-                    .path
-                    .file_name()
-                    .unwrap_or_default()
-                    .to_str()
-                    .unwrap_or_default()
-                    .to_string();
+                let filename = entry.path.file_name().unwrap_or_default().to_string();
 
                 if entry.is_dir() {
                     // Auto-fold directories that contain no files
@@ -292,24 +286,23 @@ fn collect_files(
                         if child_entries.next().is_none() && child.kind.is_dir() {
                             if is_top_level_directory {
                                 is_top_level_directory = false;
-                                folded_directory_names_stack.push(
-                                    path_including_worktree_name.to_string_lossy().to_string(),
-                                );
+                                folded_directory_names =
+                                    folded_directory_names.join(&path_including_worktree_name);
                             } else {
-                                folded_directory_names_stack.push(filename.to_string());
+                                folded_directory_names =
+                                    folded_directory_names.join(RelPath::new(&filename).unwrap());
                             }
                             continue;
                         }
                     } else {
                         // Skip empty directories
-                        folded_directory_names_stack.clear();
+                        folded_directory_names = RelPath::empty().into();
                         continue;
                     }
-                    let prefix_paths = folded_directory_names_stack.drain(..).as_slice().join("/");
-                    if prefix_paths.is_empty() {
+                    if folded_directory_names.is_empty() {
                         let label = if is_top_level_directory {
                             is_top_level_directory = false;
-                            path_including_worktree_name.to_string_lossy().to_string()
+                            path_including_worktree_name.display(path_style).to_string()
                         } else {
                             filename
                         };
@@ -320,28 +313,23 @@ fn collect_files(
                         }))?;
                         events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
                             SlashCommandContent::Text {
-                                text: label,
+                                text: label.to_string(),
                                 run_commands_in_text: false,
                             },
                         )))?;
                         directory_stack.push(entry.path.clone());
                     } else {
-                        // todo(windows)
-                        // Potential bug: this assumes that the path separator is always `\` on Windows
-                        let entry_name = format!(
-                            "{}{}{}",
-                            prefix_paths,
-                            std::path::MAIN_SEPARATOR_STR,
-                            &filename
-                        );
+                        let entry_name =
+                            folded_directory_names.join(RelPath::new(&filename).unwrap());
+                        let entry_name = entry_name.display(path_style);
                         events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
                             icon: IconName::Folder,
-                            label: entry_name.clone().into(),
+                            label: entry_name.to_string().into(),
                             metadata: None,
                         }))?;
                         events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
                             SlashCommandContent::Text {
-                                text: entry_name,
+                                text: entry_name.to_string(),
                                 run_commands_in_text: false,
                             },
                         )))?;
@@ -356,7 +344,7 @@ fn collect_files(
                 } else if entry.is_file() {
                     let Some(open_buffer_task) = project_handle
                         .update(cx, |project, cx| {
-                            project.open_buffer((worktree_id, &entry.path), cx)
+                            project.open_buffer((worktree_id, entry.path.clone()), cx)
                         })
                         .ok()
                     else {
@@ -367,7 +355,9 @@ fn collect_files(
                         let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
                         append_buffer_to_output(
                             &snapshot,
-                            Some(&path_including_worktree_name),
+                            Some(Path::new(
+                                path_including_worktree_name.display(path_style).as_ref(),
+                            )),
                             &mut output,
                         )
                         .log_err();
@@ -462,10 +452,9 @@ pub fn build_entry_output_section(
 /// This contains a small fork of the util::paths::PathMatcher, that is stricter about the prefix
 /// check. Only subpaths pass the prefix check, rather than any prefix.
 mod custom_path_matcher {
-    use std::{fmt::Debug as _, path::Path};
-
     use globset::{Glob, GlobSet, GlobSetBuilder};
-    use util::paths::SanitizedPath;
+    use std::fmt::Debug as _;
+    use util::{paths::SanitizedPath, rel_path::RelPath};
 
     #[derive(Clone, Debug, Default)]
     pub struct PathMatcher {
@@ -492,12 +481,12 @@ mod custom_path_matcher {
         pub fn new(globs: &[String]) -> Result<Self, globset::Error> {
             let globs = globs
                 .iter()
-                .map(|glob| Glob::new(&SanitizedPath::new(glob).to_glob_string()))
+                .map(|glob| Glob::new(&SanitizedPath::new(glob).to_string()))
                 .collect::<Result<Vec<_>, _>>()?;
             let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
             let sources_with_trailing_slash = globs
                 .iter()
-                .map(|glob| glob.glob().to_string() + std::path::MAIN_SEPARATOR_STR)
+                .map(|glob| glob.glob().to_string() + "/")
                 .collect();
             let mut glob_builder = GlobSetBuilder::new();
             for single_glob in globs {
@@ -511,16 +500,13 @@ mod custom_path_matcher {
             })
         }
 
-        pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
-            let other_path = other.as_ref();
+        pub fn is_match(&self, other: &RelPath) -> bool {
             self.sources
                 .iter()
                 .zip(self.sources_with_trailing_slash.iter())
                 .any(|(source, with_slash)| {
-                    let as_bytes = other_path.as_os_str().as_encoded_bytes();
-                    // todo(windows)
-                    // Potential bug: this assumes that the path separator is always `\` on Windows
-                    let with_slash = if source.ends_with(std::path::MAIN_SEPARATOR_STR) {
+                    let as_bytes = other.as_str().as_bytes();
+                    let with_slash = if source.ends_with('/') {
                         source.as_bytes()
                     } else {
                         with_slash.as_bytes()
@@ -528,13 +514,13 @@ mod custom_path_matcher {
 
                     as_bytes.starts_with(with_slash) || as_bytes.ends_with(source.as_bytes())
                 })
-                || self.glob.is_match(other_path)
-                || self.check_with_end_separator(other_path)
+                || self.glob.is_match(other)
+                || self.check_with_end_separator(other)
         }
 
-        fn check_with_end_separator(&self, path: &Path) -> bool {
-            let path_str = path.to_string_lossy();
-            let separator = std::path::MAIN_SEPARATOR_STR;
+        fn check_with_end_separator(&self, path: &RelPath) -> bool {
+            let path_str = path.as_str();
+            let separator = "/";
             if path_str.ends_with(separator) {
                 false
             } else {

crates/assistant_tools/src/copy_path_tool.rs 🔗

@@ -96,9 +96,7 @@ impl Tool for CopyPathTool {
                 .and_then(|project_path| project.entry_for_path(&project_path, cx))
             {
                 Some(entity) => match project.find_project_path(&input.destination_path, cx) {
-                    Some(project_path) => {
-                        project.copy_entry(entity.id, None, project_path.path, cx)
-                    }
+                    Some(project_path) => project.copy_entry(entity.id, project_path, cx),
                     None => Task::ready(Err(anyhow!(
                         "Destination path {} was outside the project.",
                         input.destination_path

crates/assistant_tools/src/diagnostics_tool.rs 🔗

@@ -8,7 +8,7 @@ use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchem
 use project::Project;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use std::{fmt::Write, path::Path, sync::Arc};
+use std::{fmt::Write, sync::Arc};
 use ui::IconName;
 use util::markdown::MarkdownInlineCode;
 
@@ -150,9 +150,7 @@ impl Tool for DiagnosticsTool {
                         has_diagnostics = true;
                         output.push_str(&format!(
                             "{}: {} error(s), {} warning(s)\n",
-                            Path::new(worktree.read(cx).root_name())
-                                .join(project_path.path)
-                                .display(),
+                            worktree.read(cx).absolutize(&project_path.path).display(),
                             summary.error_count,
                             summary.warning_count
                         ));

crates/assistant_tools/src/edit_file_tool.rs 🔗

@@ -38,6 +38,7 @@ use settings::Settings;
 use std::{
     cmp::Reverse,
     collections::HashSet,
+    ffi::OsStr,
     ops::Range,
     path::{Path, PathBuf},
     sync::Arc,
@@ -45,7 +46,7 @@ use std::{
 };
 use theme::ThemeSettings;
 use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
-use util::ResultExt;
+use util::{ResultExt, rel_path::RelPath};
 use workspace::Workspace;
 
 pub struct EditFileTool;
@@ -146,11 +147,11 @@ impl Tool for EditFileTool {
 
         // If any path component matches the local settings folder, then this could affect
         // the editor in ways beyond the project source, so prompt.
-        let local_settings_folder = paths::local_settings_folder_relative_path();
+        let local_settings_folder = paths::local_settings_folder_name();
         let path = Path::new(&input.path);
         if path
             .components()
-            .any(|component| component.as_os_str() == local_settings_folder.as_os_str())
+            .any(|c| c.as_os_str() == <str as AsRef<OsStr>>::as_ref(local_settings_folder))
         {
             return true;
         }
@@ -195,10 +196,10 @@ impl Tool for EditFileTool {
                 let mut description = input.display_description.clone();
 
                 // Add context about why confirmation may be needed
-                let local_settings_folder = paths::local_settings_folder_relative_path();
+                let local_settings_folder = paths::local_settings_folder_name();
                 if path
                     .components()
-                    .any(|c| c.as_os_str() == local_settings_folder.as_os_str())
+                    .any(|c| c.as_os_str() == <str as AsRef<OsStr>>::as_ref(local_settings_folder))
                 {
                     description.push_str(" (local settings)");
                 } else if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
@@ -377,7 +378,7 @@ impl Tool for EditFileTool {
                 .await;
 
             let output = EditFileToolOutput {
-                original_path: project_path.path.to_path_buf(),
+                original_path: project_path.path.as_std_path().to_owned(),
                 new_text,
                 old_text,
                 raw_output: Some(agent_output),
@@ -549,10 +550,11 @@ fn resolve_path(
             let file_name = input
                 .path
                 .file_name()
+                .and_then(|file_name| file_name.to_str())
                 .context("Can't create file: invalid filename")?;
 
             let new_file_path = parent_project_path.map(|parent| ProjectPath {
-                path: Arc::from(parent.path.join(file_name)),
+                path: parent.path.join(RelPath::new(file_name).unwrap()),
                 ..parent
             });
 
@@ -1236,7 +1238,7 @@ mod tests {
     use serde_json::json;
     use settings::SettingsStore;
     use std::fs;
-    use util::path;
+    use util::{path, rel_path::rel_path};
 
     #[gpui::test]
     async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
@@ -1355,14 +1357,10 @@ mod tests {
         cx.update(|cx| resolve_path(&input, project, cx))
     }
 
+    #[track_caller]
     fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
-        let actual = path
-            .expect("Should return valid path")
-            .path
-            .to_str()
-            .unwrap()
-            .replace("\\", "/"); // Naive Windows paths normalization
-        assert_eq!(actual, expected);
+        let actual = path.expect("Should return valid path").path;
+        assert_eq!(actual.as_ref(), rel_path(expected));
     }
 
     #[test]
@@ -1976,25 +1974,22 @@ mod tests {
         let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await;
 
         // Get the actual local settings folder name
-        let local_settings_folder = paths::local_settings_folder_relative_path();
+        let local_settings_folder = paths::local_settings_folder_name();
 
         // Test various config path patterns
         let test_cases = vec![
             (
-                format!("{}/settings.json", local_settings_folder.display()),
+                format!("{local_settings_folder}/settings.json"),
                 true,
                 "Top-level local settings file".to_string(),
             ),
             (
-                format!(
-                    "myproject/{}/settings.json",
-                    local_settings_folder.display()
-                ),
+                format!("myproject/{local_settings_folder}/settings.json"),
                 true,
                 "Local settings in project path".to_string(),
             ),
             (
-                format!("src/{}/config.toml", local_settings_folder.display()),
+                format!("src/{local_settings_folder}/config.toml"),
                 true,
                 "Local settings in subdirectory".to_string(),
             ),
@@ -2205,12 +2200,7 @@ mod tests {
             ("", false, "Empty path is treated as project root"),
             // Root directory
             ("/", true, "Root directory should be outside project"),
-            // Parent directory references - find_project_path resolves these
-            (
-                "project/../other",
-                false,
-                "Path with .. is resolved by find_project_path",
-            ),
+            ("project/../other", true, "Path with .. is outside project"),
             (
                 "project/./src/file.rs",
                 false,

crates/assistant_tools/src/find_path_tool.rs 🔗

@@ -161,10 +161,13 @@ impl Tool for FindPathTool {
 }
 
 fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
-    let path_matcher = match PathMatcher::new([
-        // Sometimes models try to search for "". In this case, return all paths in the project.
-        if glob.is_empty() { "*" } else { glob },
-    ]) {
+    let path_matcher = match PathMatcher::new(
+        [
+            // Sometimes models try to search for "". In this case, return all paths in the project.
+            if glob.is_empty() { "*" } else { glob },
+        ],
+        project.read(cx).path_style(cx),
+    ) {
         Ok(matcher) => matcher,
         Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
     };
@@ -178,10 +181,15 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
         Ok(snapshots
             .iter()
             .flat_map(|snapshot| {
-                let root_name = PathBuf::from(snapshot.root_name());
                 snapshot
                     .entries(false, 0)
-                    .map(move |entry| root_name.join(&entry.path))
+                    .map(move |entry| {
+                        snapshot
+                            .root_name()
+                            .join(&entry.path)
+                            .as_std_path()
+                            .to_path_buf()
+                    })
                     .filter(|path| path_matcher.is_match(&path))
             })
             .collect())

crates/assistant_tools/src/grep_tool.rs 🔗

@@ -125,6 +125,7 @@ impl Tool for GrepTool {
                 .as_ref()
                 .into_iter()
                 .collect::<Vec<_>>(),
+            project.read(cx).path_style(cx),
         ) {
             Ok(matcher) => matcher,
             Err(error) => {
@@ -141,7 +142,7 @@ impl Tool for GrepTool {
                 .iter()
                 .chain(global_settings.private_files.sources().iter());
 
-            match PathMatcher::new(exclude_patterns) {
+            match PathMatcher::new(exclude_patterns, project.read(cx).path_style(cx)) {
                 Ok(matcher) => matcher,
                 Err(error) => {
                     return Task::ready(Err(anyhow!("invalid exclude pattern: {error}"))).into();

crates/assistant_tools/src/list_directory_tool.rs 🔗

@@ -4,11 +4,11 @@ use anyhow::{Result, anyhow};
 use assistant_tool::{Tool, ToolResult};
 use gpui::{AnyWindowHandle, App, Entity, Task};
 use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use project::{Project, WorktreeSettings};
+use project::{Project, ProjectPath, WorktreeSettings};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::Settings;
-use std::{fmt::Write, path::Path, sync::Arc};
+use std::{fmt::Write, sync::Arc};
 use ui::IconName;
 use util::markdown::MarkdownInlineCode;
 
@@ -100,7 +100,7 @@ impl Tool for ListDirectoryTool {
                 .filter_map(|worktree| {
                     worktree.read(cx).root_entry().and_then(|entry| {
                         if entry.is_dir() {
-                            entry.path.to_str()
+                            Some(entry.path.as_str())
                         } else {
                             None
                         }
@@ -158,7 +158,6 @@ impl Tool for ListDirectoryTool {
         }
 
         let worktree_snapshot = worktree.read(cx).snapshot();
-        let worktree_root_name = worktree.read(cx).root_name().to_string();
 
         let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else {
             return Task::ready(Err(anyhow!("Path not found: {}", input.path))).into();
@@ -180,23 +179,22 @@ impl Tool for ListDirectoryTool {
                 continue;
             }
 
-            if project
-                .read(cx)
-                .find_project_path(&entry.path, cx)
-                .map(|project_path| {
-                    let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
+            let project_path = ProjectPath {
+                worktree_id: worktree_snapshot.id(),
+                path: entry.path.clone(),
+            };
+            let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
 
-                    worktree_settings.is_path_excluded(&project_path.path)
-                        || worktree_settings.is_path_private(&project_path.path)
-                })
-                .unwrap_or(false)
+            if worktree_settings.is_path_excluded(&project_path.path)
+                || worktree_settings.is_path_private(&project_path.path)
             {
                 continue;
             }
 
-            let full_path = Path::new(&worktree_root_name)
+            let full_path = worktree_snapshot
+                .root_name()
                 .join(&entry.path)
-                .display()
+                .display(worktree_snapshot.path_style())
                 .to_string();
             if entry.is_dir() {
                 folders.push(full_path);

crates/assistant_tools/src/move_path_tool.rs 🔗

@@ -108,7 +108,7 @@ impl Tool for MovePathTool {
                 .and_then(|project_path| project.entry_for_path(&project_path, cx))
             {
                 Some(entity) => match project.find_project_path(&input.destination_path, cx) {
-                    Some(project_path) => project.rename_entry(entity.id, project_path.path, cx),
+                    Some(project_path) => project.rename_entry(entity.id, project_path, cx),
                     None => Task::ready(Err(anyhow!(
                         "Destination path {} was outside the project.",
                         input.destination_path

crates/call/src/call_impl/room.rs 🔗

@@ -24,7 +24,7 @@ use postage::{sink::Sink, stream::Stream, watch};
 use project::Project;
 use settings::Settings as _;
 use std::{future::Future, mem, rc::Rc, sync::Arc, time::Duration};
-use util::{ResultExt, TryFutureExt, post_inc};
+use util::{ResultExt, TryFutureExt, paths::PathStyle, post_inc};
 
 pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
 
@@ -1163,6 +1163,7 @@ impl Room {
             room_id: self.id(),
             worktrees: project.read(cx).worktree_metadata_protos(cx),
             is_ssh_project: project.read(cx).is_via_remote_server(),
+            windows_paths: Some(project.read(cx).path_style(cx) == PathStyle::Windows),
         });
 
         cx.spawn(async move |this, cx| {

crates/client/src/telemetry.rs 🔗

@@ -405,7 +405,7 @@ impl Telemetry {
         let mut project_types: HashSet<&str> = HashSet::new();
 
         for (path, _, _) in updated_entries_set.iter() {
-            let Some(file_name) = path.file_name().and_then(|f| f.to_str()) else {
+            let Some(file_name) = path.file_name() else {
                 continue;
             };
 
@@ -601,6 +601,7 @@ mod tests {
     use http_client::FakeHttpClient;
     use std::collections::HashMap;
     use telemetry_events::FlexibleEvent;
+    use util::rel_path::RelPath;
     use worktree::{PathChange, ProjectEntryId, WorktreeId};
 
     #[gpui::test]
@@ -855,12 +856,12 @@ mod tests {
         let entries: Vec<_> = file_paths
             .into_iter()
             .enumerate()
-            .map(|(i, path)| {
-                (
-                    Arc::from(std::path::Path::new(path)),
+            .filter_map(|(i, path)| {
+                Some((
+                    Arc::from(RelPath::new(path).ok()?),
                     ProjectEntryId::from_proto(i as u64 + 1),
                     PathChange::Added,
-                )
+                ))
             })
             .collect();
         let updated_entries: UpdatedEntriesSet = Arc::from(entries.as_slice());

crates/collab/migrations.sqlite/20221109000000_test_schema.sql 🔗

@@ -61,7 +61,8 @@ CREATE TABLE "projects" (
     "host_user_id" INTEGER REFERENCES users (id),
     "host_connection_id" INTEGER,
     "host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
-    "unregistered" BOOLEAN NOT NULL DEFAULT FALSE
+    "unregistered" BOOLEAN NOT NULL DEFAULT FALSE,
+    "windows_paths" BOOLEAN NOT NULL DEFAULT FALSE
 );
 
 CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");

crates/collab/src/db.rs 🔗

@@ -34,6 +34,7 @@ use std::{
 };
 use time::PrimitiveDateTime;
 use tokio::sync::{Mutex, OwnedMutexGuard};
+use util::paths::PathStyle;
 use worktree_settings_file::LocalSettingsKind;
 
 #[cfg(test)]
@@ -598,6 +599,7 @@ pub struct Project {
     pub worktrees: BTreeMap<u64, Worktree>,
     pub repositories: Vec<proto::UpdateRepository>,
     pub language_servers: Vec<LanguageServer>,
+    pub path_style: PathStyle,
 }
 
 pub struct ProjectCollaborator {

crates/collab/src/db/queries/projects.rs 🔗

@@ -33,6 +33,7 @@ impl Database {
         connection: ConnectionId,
         worktrees: &[proto::WorktreeMetadata],
         is_ssh_project: bool,
+        windows_paths: bool,
     ) -> Result<TransactionGuard<(ProjectId, proto::Room)>> {
         self.room_transaction(room_id, |tx| async move {
             let participant = room_participant::Entity::find()
@@ -69,6 +70,7 @@ impl Database {
                     connection.owner_id as i32,
                 ))),
                 id: ActiveValue::NotSet,
+                windows_paths: ActiveValue::set(windows_paths),
             }
             .insert(&*tx)
             .await?;
@@ -1046,6 +1048,12 @@ impl Database {
             .all(tx)
             .await?;
 
+        let path_style = if project.windows_paths {
+            PathStyle::Windows
+        } else {
+            PathStyle::Posix
+        };
+
         let project = Project {
             id: project.id,
             role,
@@ -1073,6 +1081,7 @@ impl Database {
                     capabilities: language_server.capabilities,
                 })
                 .collect(),
+            path_style,
         };
         Ok((project, replica_id as ReplicaId))
     }

crates/collab/src/db/tables/project.rs 🔗

@@ -12,6 +12,7 @@ pub struct Model {
     pub host_user_id: Option<UserId>,
     pub host_connection_id: Option<i32>,
     pub host_connection_server_id: Option<ServerId>,
+    pub windows_paths: bool,
 }
 
 impl Model {

crates/collab/src/db/tests/db_tests.rs 🔗

@@ -558,18 +558,18 @@ async fn test_project_count(db: &Arc<Database>) {
         .unwrap();
     assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
 
-    db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[], false)
+    db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[], false, false)
         .await
         .unwrap();
     assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1);
 
-    db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[], false)
+    db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[], false, false)
         .await
         .unwrap();
     assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
 
     // Projects shared by admins aren't counted.
-    db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[], false)
+    db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[], false, false)
         .await
         .unwrap();
     assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);

crates/collab/src/rpc.rs 🔗

@@ -36,6 +36,7 @@ use reqwest_client::ReqwestClient;
 use rpc::proto::split_repository_update;
 use supermaven_api::{CreateExternalUserRequest, SupermavenAdminApi};
 use tracing::Span;
+use util::paths::PathStyle;
 
 use futures::{
     FutureExt, SinkExt, StreamExt, TryStreamExt, channel::oneshot, future::BoxFuture,
@@ -1879,6 +1880,7 @@ async fn share_project(
             session.connection_id,
             &request.worktrees,
             request.is_ssh_project,
+            request.windows_paths.unwrap_or(false),
         )
         .await?;
     response.send(proto::ShareProjectResponse {
@@ -2012,6 +2014,7 @@ async fn join_project(
         language_servers,
         language_server_capabilities,
         role: project.role.into(),
+        windows_paths: project.path_style == PathStyle::Windows,
     })?;
 
     for (worktree_id, worktree) in mem::take(&mut project.worktrees) {

crates/collab/src/tests/channel_buffer_tests.rs 🔗

@@ -13,6 +13,7 @@ use gpui::{BackgroundExecutor, Context, Entity, TestAppContext, Window};
 use rpc::{RECEIVE_TIMEOUT, proto::PeerId};
 use serde_json::json;
 use std::ops::Range;
+use util::rel_path::rel_path;
 use workspace::CollaboratorId;
 
 #[gpui::test]
@@ -256,7 +257,13 @@ async fn test_channel_notes_participant_indices(
     executor.start_waiting();
     let editor_a = workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id_a, "file.txt"), None, true, window, cx)
+            workspace.open_path(
+                (worktree_id_a, rel_path("file.txt")),
+                None,
+                true,
+                window,
+                cx,
+            )
         })
         .await
         .unwrap()
@@ -265,7 +272,13 @@ async fn test_channel_notes_participant_indices(
     executor.start_waiting();
     let editor_b = workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id_a, "file.txt"), None, true, window, cx)
+            workspace.open_path(
+                (worktree_id_a, rel_path("file.txt")),
+                None,
+                true,
+                window,
+                cx,
+            )
         })
         .await
         .unwrap()

crates/collab/src/tests/channel_guest_tests.rs 🔗

@@ -4,6 +4,7 @@ use chrono::Utc;
 use editor::Editor;
 use gpui::{BackgroundExecutor, TestAppContext};
 use rpc::proto;
+use util::rel_path::rel_path;
 
 #[gpui::test]
 async fn test_channel_guests(
@@ -55,7 +56,7 @@ async fn test_channel_guests(
         project_b
             .update(cx_b, |project, cx| {
                 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
-                project.create_entry((worktree_id, "b.txt"), false, cx)
+                project.create_entry((worktree_id, rel_path("b.txt")), false, cx)
             })
             .await
             .is_err()

crates/collab/src/tests/editor_tests.rs 🔗

@@ -16,6 +16,7 @@ use editor::{
 };
 use fs::Fs;
 use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex};
+use git::repository::repo_path;
 use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
 use indoc::indoc;
 use language::FakeLspAdapter;
@@ -38,7 +39,7 @@ use std::{
     },
 };
 use text::Point;
-use util::{path, uri};
+use util::{path, rel_path::rel_path, uri};
 use workspace::{CloseIntent, Workspace};
 
 #[gpui::test(iterations = 10)]
@@ -97,7 +98,7 @@ async fn test_host_disconnect(
 
     let editor_b = workspace_b
         .update(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "b.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("b.txt")), None, true, window, cx)
         })
         .unwrap()
         .await
@@ -205,7 +206,9 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
 
     // Open a buffer as client A
     let buffer_a = project_a
-        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .update(cx_a, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+        })
         .await
         .unwrap();
     let cx_a = cx_a.add_empty_window();
@@ -222,7 +225,9 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
     let cx_b = cx_b.add_empty_window();
     // Open a buffer as client B
     let buffer_b = project_b
-        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .update(cx_b, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+        })
         .await
         .unwrap();
     let editor_b = cx_b
@@ -334,7 +339,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
 
     // Open a file in an editor as the guest.
     let buffer_b = project_b
-        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+        .update(cx_b, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("main.rs")), cx)
+        })
         .await
         .unwrap();
     let cx_b = cx_b.add_empty_window();
@@ -408,7 +415,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
 
     // Open the buffer on the host.
     let buffer_a = project_a
-        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+        .update(cx_a, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("main.rs")), cx)
+        })
         .await
         .unwrap();
     cx_a.executor().run_until_parked();
@@ -599,7 +608,7 @@ async fn test_collaborating_with_code_actions(
     let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
     let editor_b = workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -825,7 +834,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
     let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
     let editor_b = workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "one.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("one.rs")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -1072,7 +1081,7 @@ async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
     let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
     let editor_b = workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "one.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("one.rs")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -1412,7 +1421,10 @@ async fn test_share_project(
     project_b
         .update(cx_b, |project, cx| {
             let worktree = project.worktrees(cx).next().unwrap();
-            let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
+            let entry = worktree
+                .read(cx)
+                .entry_for_path(rel_path("ignored-dir"))
+                .unwrap();
             project.expand_entry(worktree_id, entry.id, cx).unwrap()
         })
         .await
@@ -1435,17 +1447,21 @@ async fn test_share_project(
 
     // Open the same file as client B and client A.
     let buffer_b = project_b
-        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
+        .update(cx_b, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("b.txt")), cx)
+        })
         .await
         .unwrap();
 
     buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
 
     project_a.read_with(cx_a, |project, cx| {
-        assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
+        assert!(project.has_open_buffer((worktree_id, rel_path("b.txt")), cx))
     });
     let buffer_a = project_a
-        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
+        .update(cx_a, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("b.txt")), cx)
+        })
         .await
         .unwrap();
 
@@ -1553,7 +1569,9 @@ async fn test_on_input_format_from_host_to_guest(
 
     // Open a file in an editor as the host.
     let buffer_a = project_a
-        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+        .update(cx_a, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("main.rs")), cx)
+        })
         .await
         .unwrap();
     let cx_a = cx_a.add_empty_window();
@@ -1586,7 +1604,9 @@ async fn test_on_input_format_from_host_to_guest(
 
     // Open the buffer on the guest and see that the formatting worked
     let buffer_b = project_b
-        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+        .update(cx_b, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("main.rs")), cx)
+        })
         .await
         .unwrap();
 
@@ -1686,7 +1706,9 @@ async fn test_on_input_format_from_guest_to_host(
 
     // Open a file in an editor as the guest.
     let buffer_b = project_b
-        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+        .update(cx_b, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("main.rs")), cx)
+        })
         .await
         .unwrap();
     let cx_b = cx_b.add_empty_window();
@@ -1732,7 +1754,9 @@ async fn test_on_input_format_from_guest_to_host(
 
     // Open the buffer on the host and see that the formatting worked
     let buffer_a = project_a
-        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+        .update(cx_a, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("main.rs")), cx)
+        })
         .await
         .unwrap();
     executor.run_until_parked();
@@ -1881,7 +1905,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         .unwrap();
     let editor_a = workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -1931,7 +1955,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
     let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
     let editor_b = workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -2126,7 +2150,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
 
     let editor_a = workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -2135,7 +2159,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
 
     let editor_b = workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -2313,7 +2337,7 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
         .unwrap();
     let editor_a = workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -2373,7 +2397,7 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
     let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
     let editor_b = workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -2594,7 +2618,7 @@ async fn test_lsp_pull_diagnostics(
         .unwrap();
     let editor_a_main = workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -2953,7 +2977,7 @@ async fn test_lsp_pull_diagnostics(
     let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
     let editor_b_main = workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -3001,7 +3025,7 @@ async fn test_lsp_pull_diagnostics(
 
     let editor_b_lib = workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "lib.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("lib.rs")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -3355,7 +3379,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
     };
     client_a.fs().set_blame_for_repo(
         Path::new(path!("/my-repo/.git")),
-        vec![("file.txt".into(), blame)],
+        vec![(repo_path("file.txt"), blame)],
     );
 
     let (project_a, worktree_id) = client_a.build_local_project(path!("/my-repo"), cx_a).await;
@@ -3368,7 +3392,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
     let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
     let editor_a = workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("file.txt")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -3380,7 +3404,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
     let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
     let editor_b = workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("file.txt")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -3558,13 +3582,13 @@ async fn test_collaborating_with_editorconfig(
         .unwrap();
     let main_buffer_a = project_a
         .update(cx_a, |p, cx| {
-            p.open_buffer((worktree_id, "src/main.rs"), cx)
+            p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
         })
         .await
         .unwrap();
     let other_buffer_a = project_a
         .update(cx_a, |p, cx| {
-            p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
+            p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
         })
         .await
         .unwrap();
@@ -3592,13 +3616,13 @@ async fn test_collaborating_with_editorconfig(
     let project_b = client_b.join_remote_project(project_id, cx_b).await;
     let main_buffer_b = project_b
         .update(cx_b, |p, cx| {
-            p.open_buffer((worktree_id, "src/main.rs"), cx)
+            p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
         })
         .await
         .unwrap();
     let other_buffer_b = project_b
         .update(cx_b, |p, cx| {
-            p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
+            p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
         })
         .await
         .unwrap();
@@ -3717,7 +3741,7 @@ fn main() { let foo = other::foo(); }"};
 
     let editorconfig_buffer_b = project_b
         .update(cx_b, |p, cx| {
-            p.open_buffer((worktree_id, "src/other_mod/.editorconfig"), cx)
+            p.open_buffer((worktree_id, rel_path("src/other_mod/.editorconfig")), cx)
         })
         .await
         .unwrap();
@@ -3794,7 +3818,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
     let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
     let project_path = ProjectPath {
         worktree_id,
-        path: Arc::from(Path::new(&"test.txt")),
+        path: rel_path(&"test.txt").into(),
     };
     let abs_path = project_a.read_with(cx_a, |project, cx| {
         project
@@ -4017,7 +4041,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
 
     let editor_a = workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -4026,7 +4050,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
 
     let editor_b = workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
         })
         .await
         .unwrap()

crates/collab/src/tests/following_tests.rs 🔗

@@ -16,7 +16,7 @@ use rpc::proto::PeerId;
 use serde_json::json;
 use settings::SettingsStore;
 use text::{Point, ToPoint};
-use util::{path, test::sample_text};
+use util::{path, rel_path::rel_path, test::sample_text};
 use workspace::{CollaboratorId, SplitDirection, Workspace, item::ItemHandle as _};
 
 use super::TestClient;
@@ -86,7 +86,7 @@ async fn test_basic_following(
     let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
     let editor_a1 = workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -94,7 +94,7 @@ async fn test_basic_following(
         .unwrap();
     let editor_a2 = workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("2.txt")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -104,7 +104,7 @@ async fn test_basic_following(
     // Client B opens an editor.
     let editor_b1 = workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -146,7 +146,7 @@ async fn test_basic_following(
     });
     assert_eq!(
         cx_b.read(|cx| editor_b2.project_path(cx)),
-        Some((worktree_id, "2.txt").into())
+        Some((worktree_id, rel_path("2.txt")).into())
     );
     assert_eq!(
         editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
@@ -286,12 +286,12 @@ async fn test_basic_following(
     let multibuffer_a = cx_a.new(|cx| {
         let buffer_a1 = project_a.update(cx, |project, cx| {
             project
-                .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
+                .get_open_buffer(&(worktree_id, rel_path("1.txt")).into(), cx)
                 .unwrap()
         });
         let buffer_a2 = project_a.update(cx, |project, cx| {
             project
-                .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
+                .get_open_buffer(&(worktree_id, rel_path("2.txt")).into(), cx)
                 .unwrap()
         });
         let mut result = MultiBuffer::new(Capability::ReadWrite);
@@ -618,13 +618,13 @@ async fn test_following_tab_order(
     //Open 1, 3 in that order on client A
     workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
         })
         .await
         .unwrap();
     workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "3.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("3.txt")), None, true, window, cx)
         })
         .await
         .unwrap();
@@ -632,14 +632,7 @@ async fn test_following_tab_order(
     let pane_paths = |pane: &Entity<workspace::Pane>, cx: &mut VisualTestContext| {
         pane.update(cx, |pane, cx| {
             pane.items()
-                .map(|item| {
-                    item.project_path(cx)
-                        .unwrap()
-                        .path
-                        .to_str()
-                        .unwrap()
-                        .to_owned()
-                })
+                .map(|item| item.project_path(cx).unwrap().path.as_str().to_owned())
                 .collect::<Vec<_>>()
         })
     };
@@ -656,7 +649,7 @@ async fn test_following_tab_order(
     //Open just 2 on client B
     workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("2.txt")), None, true, window, cx)
         })
         .await
         .unwrap();
@@ -668,7 +661,7 @@ async fn test_following_tab_order(
     //Open just 1 on client B
     workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
         })
         .await
         .unwrap();
@@ -728,7 +721,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
     let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
     workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -739,7 +732,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
     let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
     workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("2.txt")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -816,14 +809,14 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
     // Clients A and B each open a new file.
     workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "3.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("3.txt")), None, true, window, cx)
         })
         .await
         .unwrap();
 
     workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "4.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("4.txt")), None, true, window, cx)
         })
         .await
         .unwrap();
@@ -1259,7 +1252,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
 
     let _editor_a1 = workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -1359,7 +1352,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
     // When client B activates a different item in the original pane, it automatically stops following client A.
     workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("2.txt")), None, true, window, cx)
         })
         .await
         .unwrap();
@@ -1492,7 +1485,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
 
     workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id_a, "w.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id_a, rel_path("w.rs")), None, true, window, cx)
         })
         .await
         .unwrap();
@@ -1545,7 +1538,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
     // b moves to x.rs in a's project, and a follows
     workspace_b_project_a
         .update_in(&mut cx_b2, |workspace, window, cx| {
-            workspace.open_path((worktree_id_a, "x.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id_a, rel_path("x.rs")), None, true, window, cx)
         })
         .await
         .unwrap();
@@ -1574,7 +1567,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
     // b moves to y.rs in b's project, a is still following but can't yet see
     workspace_b
         .update_in(cx_b, |workspace, window, cx| {
-            workspace.open_path((worktree_id_b, "y.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id_b, rel_path("y.rs")), None, true, window, cx)
         })
         .await
         .unwrap();
@@ -1759,7 +1752,7 @@ async fn test_following_into_excluded_file(
     // Client A opens editors for a regular file and an excluded file.
     let editor_for_regular = workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
         })
         .await
         .unwrap()
@@ -1767,7 +1760,13 @@ async fn test_following_into_excluded_file(
         .unwrap();
     let editor_for_excluded_a = workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, window, cx)
+            workspace.open_path(
+                (worktree_id, rel_path(".git/COMMIT_EDITMSG")),
+                None,
+                true,
+                window,
+                cx,
+            )
         })
         .await
         .unwrap()
@@ -1805,7 +1804,7 @@ async fn test_following_into_excluded_file(
     });
     assert_eq!(
         cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
-        Some((worktree_id, ".git/COMMIT_EDITMSG").into())
+        Some((worktree_id, rel_path(".git/COMMIT_EDITMSG")).into())
     );
     assert_eq!(
         editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
@@ -2051,7 +2050,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
     // Client A opens a local buffer in their unshared project.
     let _unshared_editor_a1 = workspace_a
         .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
         })
         .await
         .unwrap()

crates/collab/src/tests/git_tests.rs 🔗

@@ -1,7 +1,4 @@
-use std::{
-    path::{Path, PathBuf},
-    sync::Arc,
-};
+use std::path::Path;
 
 use call::ActiveCall;
 use git::status::{FileStatus, StatusCode, TrackedStatus};
@@ -9,7 +6,7 @@ use git_ui::project_diff::ProjectDiff;
 use gpui::{TestAppContext, VisualTestContext};
 use project::ProjectPath;
 use serde_json::json;
-use util::path;
+use util::{path, rel_path::rel_path};
 use workspace::Workspace;
 
 //
@@ -41,13 +38,13 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
         )
         .await;
 
-    client_a.fs().set_git_content_for_repo(
+    client_a.fs().set_head_and_index_for_repo(
         Path::new(path!("/a/.git")),
         &[
-            ("changed.txt".into(), "before\n".to_string(), None),
-            ("unchanged.txt".into(), "unchanged\n".to_string(), None),
-            ("deleted.txt".into(), "deleted\n".to_string(), None),
-            ("secret.pem".into(), "shh\n".to_string(), None),
+            ("changed.txt", "before\n".to_string()),
+            ("unchanged.txt", "unchanged\n".to_string()),
+            ("deleted.txt", "deleted\n".to_string()),
+            ("secret.pem", "shh\n".to_string()),
         ],
     );
     let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
@@ -109,7 +106,7 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
     project_b.update(cx_b, |project, cx| {
         let project_path = ProjectPath {
             worktree_id,
-            path: Arc::from(PathBuf::from("unchanged.txt")),
+            path: rel_path("unchanged.txt").into(),
         };
         let status = project.project_path_git_status(&project_path, cx);
         assert_eq!(

crates/collab/src/tests/integration_tests.rs 🔗

@@ -14,7 +14,10 @@ use client::{RECEIVE_TIMEOUT, User};
 use collections::{HashMap, HashSet};
 use fs::{FakeFs, Fs as _, RemoveOptions};
 use futures::{StreamExt as _, channel::mpsc};
-use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode};
+use git::{
+    repository::repo_path,
+    status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode},
+};
 use gpui::{
     App, BackgroundExecutor, Entity, Modifiers, MouseButton, MouseDownEvent, TestAppContext,
     UpdateGlobal, px, size,
@@ -30,7 +33,7 @@ use parking_lot::Mutex;
 use pretty_assertions::assert_eq;
 use project::{
     DiagnosticSummary, HoverBlockKind, Project, ProjectPath,
-    lsp_store::{FormatTrigger, LspFormatTarget},
+    lsp_store::{FormatTrigger, LspFormatTarget, SymbolLocation},
     search::{SearchQuery, SearchResult},
 };
 use prompt_store::PromptBuilder;
@@ -49,7 +52,7 @@ use std::{
     time::Duration,
 };
 use unindent::Unindent as _;
-use util::{path, uri};
+use util::{path, rel_path::rel_path, uri};
 use workspace::Pane;
 
 #[ctor::ctor]
@@ -1418,7 +1421,9 @@ async fn test_unshare_project(
     assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
 
     project_b
-        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .update(cx_b, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+        })
         .await
         .unwrap();
 
@@ -1454,7 +1459,9 @@ async fn test_unshare_project(
 
     assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
     project_c2
-        .update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .update(cx_c, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+        })
         .await
         .unwrap();
 
@@ -1584,11 +1591,15 @@ async fn test_project_reconnect(
     });
 
     let buffer_a1 = project_a1
-        .update(cx_a, |p, cx| p.open_buffer((worktree1_id, "a.txt"), cx))
+        .update(cx_a, |p, cx| {
+            p.open_buffer((worktree1_id, rel_path("a.txt")), cx)
+        })
         .await
         .unwrap();
     let buffer_b1 = project_b1
-        .update(cx_b, |p, cx| p.open_buffer((worktree1_id, "a.txt"), cx))
+        .update(cx_b, |p, cx| {
+            p.open_buffer((worktree1_id, rel_path("a.txt")), cx)
+        })
         .await
         .unwrap();
 
@@ -1675,20 +1686,15 @@ async fn test_project_reconnect(
         assert!(project.is_shared());
         assert!(worktree_a1.read(cx).has_update_observer());
         assert_eq!(
-            worktree_a1
-                .read(cx)
-                .snapshot()
-                .paths()
-                .map(|p| p.to_str().unwrap())
-                .collect::<Vec<_>>(),
+            worktree_a1.read(cx).snapshot().paths().collect::<Vec<_>>(),
             vec![
-                path!("a.txt"),
-                path!("b.txt"),
-                path!("subdir2"),
-                path!("subdir2/f.txt"),
-                path!("subdir2/g.txt"),
-                path!("subdir2/h.txt"),
-                path!("subdir2/i.txt")
+                rel_path("a.txt"),
+                rel_path("b.txt"),
+                rel_path("subdir2"),
+                rel_path("subdir2/f.txt"),
+                rel_path("subdir2/g.txt"),
+                rel_path("subdir2/h.txt"),
+                rel_path("subdir2/i.txt")
             ]
         );
         assert!(worktree_a3.read(cx).has_update_observer());
@@ -1697,7 +1703,7 @@ async fn test_project_reconnect(
                 .read(cx)
                 .snapshot()
                 .paths()
-                .map(|p| p.to_str().unwrap())
+                .map(|p| p.as_str())
                 .collect::<Vec<_>>(),
             vec!["w.txt", "x.txt", "y.txt"]
         );
@@ -1712,16 +1718,15 @@ async fn test_project_reconnect(
                 .read(cx)
                 .snapshot()
                 .paths()
-                .map(|p| p.to_str().unwrap())
                 .collect::<Vec<_>>(),
             vec![
-                path!("a.txt"),
-                path!("b.txt"),
-                path!("subdir2"),
-                path!("subdir2/f.txt"),
-                path!("subdir2/g.txt"),
-                path!("subdir2/h.txt"),
-                path!("subdir2/i.txt")
+                rel_path("a.txt"),
+                rel_path("b.txt"),
+                rel_path("subdir2"),
+                rel_path("subdir2/f.txt"),
+                rel_path("subdir2/g.txt"),
+                rel_path("subdir2/h.txt"),
+                rel_path("subdir2/i.txt")
             ]
         );
         assert!(project.worktree_for_id(worktree2_id, cx).is_none());
@@ -1732,7 +1737,7 @@ async fn test_project_reconnect(
                 .read(cx)
                 .snapshot()
                 .paths()
-                .map(|p| p.to_str().unwrap())
+                .map(|p| p.as_str())
                 .collect::<Vec<_>>(),
             vec!["w.txt", "x.txt", "y.txt"]
         );
@@ -1809,16 +1814,15 @@ async fn test_project_reconnect(
                 .read(cx)
                 .snapshot()
                 .paths()
-                .map(|p| p.to_str().unwrap())
                 .collect::<Vec<_>>(),
             vec![
-                path!("a.txt"),
-                path!("b.txt"),
-                path!("subdir2"),
-                path!("subdir2/f.txt"),
-                path!("subdir2/g.txt"),
-                path!("subdir2/h.txt"),
-                path!("subdir2/j.txt")
+                rel_path("a.txt"),
+                rel_path("b.txt"),
+                rel_path("subdir2"),
+                rel_path("subdir2/f.txt"),
+                rel_path("subdir2/g.txt"),
+                rel_path("subdir2/h.txt"),
+                rel_path("subdir2/j.txt")
             ]
         );
         assert!(project.worktree_for_id(worktree2_id, cx).is_none());
@@ -1829,7 +1833,7 @@ async fn test_project_reconnect(
                 .read(cx)
                 .snapshot()
                 .paths()
-                .map(|p| p.to_str().unwrap())
+                .map(|p| p.as_str())
                 .collect::<Vec<_>>(),
             vec!["z.txt"]
         );
@@ -2370,11 +2374,15 @@ async fn test_propagate_saves_and_fs_changes(
 
     // Open and edit a buffer as both guests B and C.
     let buffer_b = project_b
-        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
+        .update(cx_b, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("file1.rs")), cx)
+        })
         .await
         .unwrap();
     let buffer_c = project_c
-        .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
+        .update(cx_c, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("file1.rs")), cx)
+        })
         .await
         .unwrap();
 
@@ -2390,7 +2398,9 @@ async fn test_propagate_saves_and_fs_changes(
 
     // Open and edit that buffer as the host.
     let buffer_a = project_a
-        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
+        .update(cx_a, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("file1.rs")), cx)
+        })
         .await
         .unwrap();
 
@@ -2461,27 +2471,21 @@ async fn test_propagate_saves_and_fs_changes(
 
     worktree_a.read_with(cx_a, |tree, _| {
         assert_eq!(
-            tree.paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
+            tree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
             ["file1.js", "file3", "file4"]
         )
     });
 
     worktree_b.read_with(cx_b, |tree, _| {
         assert_eq!(
-            tree.paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
+            tree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
             ["file1.js", "file3", "file4"]
         )
     });
 
     worktree_c.read_with(cx_c, |tree, _| {
         assert_eq!(
-            tree.paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
+            tree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
             ["file1.js", "file3", "file4"]
         )
     });
@@ -2489,17 +2493,17 @@ async fn test_propagate_saves_and_fs_changes(
     // Ensure buffer files are updated as well.
 
     buffer_a.read_with(cx_a, |buffer, _| {
-        assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
+        assert_eq!(buffer.file().unwrap().path().as_str(), "file1.js");
         assert_eq!(buffer.language().unwrap().name(), "JavaScript".into());
     });
 
     buffer_b.read_with(cx_b, |buffer, _| {
-        assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
+        assert_eq!(buffer.file().unwrap().path().as_str(), "file1.js");
         assert_eq!(buffer.language().unwrap().name(), "JavaScript".into());
     });
 
     buffer_c.read_with(cx_c, |buffer, _| {
-        assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
+        assert_eq!(buffer.file().unwrap().path().as_str(), "file1.js");
         assert_eq!(buffer.language().unwrap().name(), "JavaScript".into());
     });
 
@@ -2524,7 +2528,7 @@ async fn test_propagate_saves_and_fs_changes(
     project_a
         .update(cx_a, |project, cx| {
             let path = ProjectPath {
-                path: Arc::from(Path::new("file3.rs")),
+                path: rel_path("file3.rs").into(),
                 worktree_id: worktree_a.read(cx).id(),
             };
 
@@ -2538,7 +2542,7 @@ async fn test_propagate_saves_and_fs_changes(
     new_buffer_b.read_with(cx_b, |buffer_b, _| {
         assert_eq!(
             buffer_b.file().unwrap().path().as_ref(),
-            Path::new("file3.rs")
+            rel_path("file3.rs")
         );
 
         new_buffer_a.read_with(cx_a, |buffer_a, _| {
@@ -2621,19 +2625,20 @@ async fn test_git_diff_base_change(
     "
     .unindent();
 
-    client_a.fs().set_index_for_repo(
-        Path::new("/dir/.git"),
-        &[("a.txt".into(), staged_text.clone())],
-    );
+    client_a
+        .fs()
+        .set_index_for_repo(Path::new("/dir/.git"), &[("a.txt", staged_text.clone())]);
     client_a.fs().set_head_for_repo(
         Path::new("/dir/.git"),
-        &[("a.txt".into(), committed_text.clone())],
+        &[("a.txt", committed_text.clone())],
         "deadbeef",
     );
 
     // Create the buffer
     let buffer_local_a = project_local
-        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .update(cx_a, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+        })
         .await
         .unwrap();
     let local_unstaged_diff_a = project_local
@@ -2661,7 +2666,9 @@ async fn test_git_diff_base_change(
 
     // Create remote buffer
     let remote_buffer_a = project_remote
-        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .update(cx_b, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+        })
         .await
         .unwrap();
     let remote_unstaged_diff_a = project_remote
@@ -2717,11 +2724,11 @@ async fn test_git_diff_base_change(
     // Update the index text of the open buffer
     client_a.fs().set_index_for_repo(
         Path::new("/dir/.git"),
-        &[("a.txt".into(), new_staged_text.clone())],
+        &[("a.txt", new_staged_text.clone())],
     );
     client_a.fs().set_head_for_repo(
         Path::new("/dir/.git"),
-        &[("a.txt".into(), new_committed_text.clone())],
+        &[("a.txt", new_committed_text.clone())],
         "deadbeef",
     );
 
@@ -2790,12 +2797,14 @@ async fn test_git_diff_base_change(
 
     client_a.fs().set_index_for_repo(
         Path::new("/dir/sub/.git"),
-        &[("b.txt".into(), staged_text.clone())],
+        &[("b.txt", staged_text.clone())],
     );
 
     // Create the buffer
     let buffer_local_b = project_local
-        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
+        .update(cx_a, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("sub/b.txt")), cx)
+        })
         .await
         .unwrap();
     let local_unstaged_diff_b = project_local
@@ -2823,7 +2832,9 @@ async fn test_git_diff_base_change(
 
     // Create remote buffer
     let remote_buffer_b = project_remote
-        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
+        .update(cx_b, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("sub/b.txt")), cx)
+        })
         .await
         .unwrap();
     let remote_unstaged_diff_b = project_remote
@@ -2851,7 +2862,7 @@ async fn test_git_diff_base_change(
     // Updatet the staged text
     client_a.fs().set_index_for_repo(
         Path::new("/dir/sub/.git"),
-        &[("b.txt".into(), new_staged_text.clone())],
+        &[("b.txt", new_staged_text.clone())],
     );
 
     // Wait for buffer_local_b to receive it
@@ -3011,21 +3022,21 @@ async fn test_git_status_sync(
     // and b.txt is unmerged.
     client_a.fs().set_head_for_repo(
         path!("/dir/.git").as_ref(),
-        &[("b.txt".into(), "B".into()), ("c.txt".into(), "c".into())],
+        &[("b.txt", "B".into()), ("c.txt", "c".into())],
         "deadbeef",
     );
     client_a.fs().set_index_for_repo(
         path!("/dir/.git").as_ref(),
         &[
-            ("a.txt".into(), "".into()),
-            ("b.txt".into(), "B".into()),
-            ("c.txt".into(), "c".into()),
+            ("a.txt", "".into()),
+            ("b.txt", "B".into()),
+            ("c.txt", "c".into()),
         ],
     );
     client_a.fs().set_unmerged_paths_for_repo(
         path!("/dir/.git").as_ref(),
         &[(
-            "b.txt".into(),
+            repo_path("b.txt"),
             UnmergedStatus {
                 first_head: UnmergedStatusCode::Updated,
                 second_head: UnmergedStatusCode::Deleted,
@@ -3056,13 +3067,8 @@ async fn test_git_status_sync(
     executor.run_until_parked();
 
     #[track_caller]
-    fn assert_status(
-        file: impl AsRef<Path>,
-        status: Option<FileStatus>,
-        project: &Project,
-        cx: &App,
-    ) {
-        let file = file.as_ref();
+    fn assert_status(file: &str, status: Option<FileStatus>, project: &Project, cx: &App) {
+        let file = repo_path(file);
         let repos = project
             .repositories(cx)
             .values()
@@ -3072,7 +3078,7 @@ async fn test_git_status_sync(
         let repo = repos.into_iter().next().unwrap();
         assert_eq!(
             repo.read(cx)
-                .status_for_path(&file.into())
+                .status_for_path(&file)
                 .map(|entry| entry.status),
             status
         );
@@ -3107,7 +3113,7 @@ async fn test_git_status_sync(
     // and modify c.txt in the working copy.
     client_a.fs().set_index_for_repo(
         path!("/dir/.git").as_ref(),
-        &[("a.txt".into(), "a".into()), ("c.txt".into(), "c".into())],
+        &[("a.txt", "a".into()), ("c.txt", "c".into())],
     );
     client_a
         .fs()
@@ -3202,7 +3208,7 @@ async fn test_fs_operations(
 
     let entry = project_b
         .update(cx_b, |project, cx| {
-            project.create_entry((worktree_id, "c.txt"), false, cx)
+            project.create_entry((worktree_id, rel_path("c.txt")), false, cx)
         })
         .await
         .unwrap()
@@ -3211,27 +3217,21 @@ async fn test_fs_operations(
 
     worktree_a.read_with(cx_a, |worktree, _| {
         assert_eq!(
-            worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
+            worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
             ["a.txt", "b.txt", "c.txt"]
         );
     });
 
     worktree_b.read_with(cx_b, |worktree, _| {
         assert_eq!(
-            worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
+            worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
             ["a.txt", "b.txt", "c.txt"]
         );
     });
 
     project_b
         .update(cx_b, |project, cx| {
-            project.rename_entry(entry.id, Path::new("d.txt"), cx)
+            project.rename_entry(entry.id, (worktree_id, rel_path("d.txt")).into(), cx)
         })
         .await
         .unwrap()
@@ -3240,27 +3240,21 @@ async fn test_fs_operations(
 
     worktree_a.read_with(cx_a, |worktree, _| {
         assert_eq!(
-            worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
+            worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
             ["a.txt", "b.txt", "d.txt"]
         );
     });
 
     worktree_b.read_with(cx_b, |worktree, _| {
         assert_eq!(
-            worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
+            worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
             ["a.txt", "b.txt", "d.txt"]
         );
     });
 
     let dir_entry = project_b
         .update(cx_b, |project, cx| {
-            project.create_entry((worktree_id, "DIR"), true, cx)
+            project.create_entry((worktree_id, rel_path("DIR")), true, cx)
         })
         .await
         .unwrap()
@@ -3269,27 +3263,21 @@ async fn test_fs_operations(
 
     worktree_a.read_with(cx_a, |worktree, _| {
         assert_eq!(
-            worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
+            worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
             ["DIR", "a.txt", "b.txt", "d.txt"]
         );
     });
 
     worktree_b.read_with(cx_b, |worktree, _| {
         assert_eq!(
-            worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
+            worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
             ["DIR", "a.txt", "b.txt", "d.txt"]
         );
     });
 
     project_b
         .update(cx_b, |project, cx| {
-            project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
+            project.create_entry((worktree_id, rel_path("DIR/e.txt")), false, cx)
         })
         .await
         .unwrap()
@@ -3298,7 +3286,7 @@ async fn test_fs_operations(
 
     project_b
         .update(cx_b, |project, cx| {
-            project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
+            project.create_entry((worktree_id, rel_path("DIR/SUBDIR")), true, cx)
         })
         .await
         .unwrap()
@@ -3307,7 +3295,7 @@ async fn test_fs_operations(
 
     project_b
         .update(cx_b, |project, cx| {
-            project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
+            project.create_entry((worktree_id, rel_path("DIR/SUBDIR/f.txt")), false, cx)
         })
         .await
         .unwrap()
@@ -3316,43 +3304,41 @@ async fn test_fs_operations(
 
     worktree_a.read_with(cx_a, |worktree, _| {
         assert_eq!(
-            worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
+            worktree.paths().collect::<Vec<_>>(),
             [
-                path!("DIR"),
-                path!("DIR/SUBDIR"),
-                path!("DIR/SUBDIR/f.txt"),
-                path!("DIR/e.txt"),
-                path!("a.txt"),
-                path!("b.txt"),
-                path!("d.txt")
+                rel_path("DIR"),
+                rel_path("DIR/SUBDIR"),
+                rel_path("DIR/SUBDIR/f.txt"),
+                rel_path("DIR/e.txt"),
+                rel_path("a.txt"),
+                rel_path("b.txt"),
+                rel_path("d.txt")
             ]
         );
     });
 
     worktree_b.read_with(cx_b, |worktree, _| {
         assert_eq!(
-            worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
+            worktree.paths().collect::<Vec<_>>(),
             [
-                path!("DIR"),
-                path!("DIR/SUBDIR"),
-                path!("DIR/SUBDIR/f.txt"),
-                path!("DIR/e.txt"),
-                path!("a.txt"),
-                path!("b.txt"),
-                path!("d.txt")
+                rel_path("DIR"),
+                rel_path("DIR/SUBDIR"),
+                rel_path("DIR/SUBDIR/f.txt"),
+                rel_path("DIR/e.txt"),
+                rel_path("a.txt"),
+                rel_path("b.txt"),
+                rel_path("d.txt")
             ]
         );
     });
 
     project_b
         .update(cx_b, |project, cx| {
-            project.copy_entry(entry.id, None, Path::new("f.txt"), cx)
+            project.copy_entry(
+                entry.id,
+                (worktree_b.read(cx).id(), rel_path("f.txt")).into(),
+                cx,
+            )
         })
         .await
         .unwrap()
@@ -3360,38 +3346,32 @@ async fn test_fs_operations(
 
     worktree_a.read_with(cx_a, |worktree, _| {
         assert_eq!(
-            worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
+            worktree.paths().collect::<Vec<_>>(),
             [
-                path!("DIR"),
-                path!("DIR/SUBDIR"),
-                path!("DIR/SUBDIR/f.txt"),
-                path!("DIR/e.txt"),
-                path!("a.txt"),
-                path!("b.txt"),
-                path!("d.txt"),
-                path!("f.txt")
+                rel_path("DIR"),
+                rel_path("DIR/SUBDIR"),
+                rel_path("DIR/SUBDIR/f.txt"),
+                rel_path("DIR/e.txt"),
+                rel_path("a.txt"),
+                rel_path("b.txt"),
+                rel_path("d.txt"),
+                rel_path("f.txt")
             ]
         );
     });
 
     worktree_b.read_with(cx_b, |worktree, _| {
         assert_eq!(
-            worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
+            worktree.paths().collect::<Vec<_>>(),
             [
-                path!("DIR"),
-                path!("DIR/SUBDIR"),
-                path!("DIR/SUBDIR/f.txt"),
-                path!("DIR/e.txt"),
-                path!("a.txt"),
-                path!("b.txt"),
-                path!("d.txt"),
-                path!("f.txt")
+                rel_path("DIR"),
+                rel_path("DIR/SUBDIR"),
+                rel_path("DIR/SUBDIR/f.txt"),
+                rel_path("DIR/e.txt"),
+                rel_path("a.txt"),
+                rel_path("b.txt"),
+                rel_path("d.txt"),
+                rel_path("f.txt")
             ]
         );
     });
@@ -3406,20 +3386,14 @@ async fn test_fs_operations(
 
     worktree_a.read_with(cx_a, |worktree, _| {
         assert_eq!(
-            worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
+            worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
             ["a.txt", "b.txt", "d.txt", "f.txt"]
         );
     });
 
     worktree_b.read_with(cx_b, |worktree, _| {
         assert_eq!(
-            worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
+            worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
             ["a.txt", "b.txt", "d.txt", "f.txt"]
         );
     });
@@ -3433,20 +3407,14 @@ async fn test_fs_operations(
 
     worktree_a.read_with(cx_a, |worktree, _| {
         assert_eq!(
-            worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
+            worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
             ["a.txt", "b.txt", "f.txt"]
         );
     });
 
     worktree_b.read_with(cx_b, |worktree, _| {
         assert_eq!(
-            worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
+            worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
             ["a.txt", "b.txt", "f.txt"]
         );
     });
@@ -3511,8 +3479,8 @@ async fn test_local_settings(
                 ))
                 .collect::<Vec<_>>(),
             &[
-                (Path::new("").into(), Some(2)),
-                (Path::new("a").into(), Some(8)),
+                (rel_path("").into(), Some(2)),
+                (rel_path("a").into(), Some(8)),
             ]
         )
     });
@@ -3533,10 +3501,7 @@ async fn test_local_settings(
                     content.all_languages.defaults.tab_size.map(Into::into)
                 ))
                 .collect::<Vec<_>>(),
-            &[
-                (Path::new("").into(), None),
-                (Path::new("a").into(), Some(8)),
-            ]
+            &[(rel_path("").into(), None), (rel_path("a").into(), Some(8)),]
         )
     });
 
@@ -3567,8 +3532,8 @@ async fn test_local_settings(
                 ))
                 .collect::<Vec<_>>(),
             &[
-                (Path::new("a").into(), Some(8)),
-                (Path::new("b").into(), Some(4)),
+                (rel_path("a").into(), Some(8)),
+                (rel_path("b").into(), Some(4)),
             ]
         )
     });
@@ -3599,7 +3564,7 @@ async fn test_local_settings(
                 .local_settings(worktree_b.read(cx).id())
                 .map(|(path, content)| (path, content.all_languages.defaults.hard_tabs))
                 .collect::<Vec<_>>(),
-            &[(Path::new("a").into(), Some(true))],
+            &[(rel_path("a").into(), Some(true))],
         )
     });
 }
@@ -3636,7 +3601,9 @@ async fn test_buffer_conflict_after_save(
 
     // Open a buffer as client B
     let buffer_b = project_b
-        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .update(cx_b, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+        })
         .await
         .unwrap();
 
@@ -3700,7 +3667,9 @@ async fn test_buffer_reloading(
 
     // Open a buffer as client B
     let buffer_b = project_b
-        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .update(cx_b, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+        })
         .await
         .unwrap();
 
@@ -3758,12 +3727,16 @@ async fn test_editing_while_guest_opens_buffer(
 
     // Open a buffer as client A
     let buffer_a = project_a
-        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .update(cx_a, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+        })
         .await
         .unwrap();
 
     // Start opening the same buffer as client B
-    let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
+    let open_buffer = project_b.update(cx_b, |p, cx| {
+        p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+    });
     let buffer_b = cx_b.executor().spawn(open_buffer);
 
     // Edit the buffer as client A while client B is still opening it.
@@ -3810,7 +3783,9 @@ async fn test_leaving_worktree_while_opening_buffer(
     project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
 
     // Begin opening a buffer as client B, but leave the project before the open completes.
-    let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
+    let open_buffer = project_b.update(cx_b, |p, cx| {
+        p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+    });
     let buffer_b = cx_b.executor().spawn(open_buffer);
     cx_b.update(|_| drop(project_b));
     drop(buffer_b);
@@ -3852,7 +3827,9 @@ async fn test_canceling_buffer_opening(
     let project_b = client_b.join_remote_project(project_id, cx_b).await;
 
     let buffer_a = project_a
-        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .update(cx_a, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+        })
         .await
         .unwrap();
 
@@ -3928,7 +3905,7 @@ async fn test_leaving_project(
     let buffer_b1 = project_b1
         .update(cx_b, |project, cx| {
             let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
-            project.open_buffer((worktree_id, "a.txt"), cx)
+            project.open_buffer((worktree_id, rel_path("a.txt")), cx)
         })
         .await
         .unwrap();
@@ -3966,7 +3943,7 @@ async fn test_leaving_project(
     let buffer_b2 = project_b2
         .update(cx_b, |project, cx| {
             let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
-            project.open_buffer((worktree_id, "a.txt"), cx)
+            project.open_buffer((worktree_id, rel_path("a.txt")), cx)
         })
         .await
         .unwrap();
@@ -4131,7 +4108,7 @@ async fn test_collaborating_with_diagnostics(
             &[(
                 ProjectPath {
                     worktree_id,
-                    path: Arc::from(Path::new("a.rs")),
+                    path: rel_path("a.rs").into(),
                 },
                 LanguageServerId(0),
                 DiagnosticSummary {
@@ -4167,7 +4144,7 @@ async fn test_collaborating_with_diagnostics(
         &[(
             ProjectPath {
                 worktree_id,
-                path: Arc::from(Path::new("a.rs")),
+                path: rel_path("a.rs").into(),
             },
             LanguageServerId(0),
             DiagnosticSummary {
@@ -4208,7 +4185,7 @@ async fn test_collaborating_with_diagnostics(
             [(
                 ProjectPath {
                     worktree_id,
-                    path: Arc::from(Path::new("a.rs")),
+                    path: rel_path("a.rs").into(),
                 },
                 LanguageServerId(0),
                 DiagnosticSummary {
@@ -4225,7 +4202,7 @@ async fn test_collaborating_with_diagnostics(
             [(
                 ProjectPath {
                     worktree_id,
-                    path: Arc::from(Path::new("a.rs")),
+                    path: rel_path("a.rs").into(),
                 },
                 LanguageServerId(0),
                 DiagnosticSummary {
@@ -4237,7 +4214,9 @@ async fn test_collaborating_with_diagnostics(
     });
 
     // Open the file with the errors on client B. They should be present.
-    let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
+    let open_buffer = project_b.update(cx_b, |p, cx| {
+        p.open_buffer((worktree_id, rel_path("a.rs")), cx)
+    });
     let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
 
     buffer_b.read_with(cx_b, |buffer, _| {
@@ -4356,7 +4335,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
     let project_b = client_b.join_remote_project(project_id, cx_b).await;
     let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
         project_b.update(cx_b, |p, cx| {
-            p.open_buffer_with_lsp((worktree_id, file_name), cx)
+            p.open_buffer_with_lsp((worktree_id, rel_path(file_name)), cx)
         })
     }))
     .await
@@ -4454,7 +4433,9 @@ async fn test_reloading_buffer_manually(
         .await;
     let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
     let buffer_a = project_a
-        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
+        .update(cx_a, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("a.rs")), cx)
+        })
         .await
         .unwrap();
     let project_id = active_call_a
@@ -4464,7 +4445,9 @@ async fn test_reloading_buffer_manually(
 
     let project_b = client_b.join_remote_project(project_id, cx_b).await;
 
-    let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
+    let open_buffer = project_b.update(cx_b, |p, cx| {
+        p.open_buffer((worktree_id, rel_path("a.rs")), cx)
+    });
     let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
     buffer_b.update(cx_b, |buffer, cx| {
         buffer.edit([(4..7, "six")], None, cx);
@@ -4562,7 +4545,9 @@ async fn test_formatting_buffer(
     let project_b = client_b.join_remote_project(project_id, cx_b).await;
 
     let buffer_b = project_b
-        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
+        .update(cx_b, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("a.rs")), cx)
+        })
         .await
         .unwrap();
 
@@ -4688,7 +4673,9 @@ async fn test_prettier_formatting_buffer(
         .await;
     let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
     let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
-    let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
+    let open_buffer = project_a.update(cx_a, |p, cx| {
+        p.open_buffer((worktree_id, rel_path("a.ts")), cx)
+    });
     let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
 
     let project_id = active_call_a
@@ -4698,7 +4685,7 @@ async fn test_prettier_formatting_buffer(
     let project_b = client_b.join_remote_project(project_id, cx_b).await;
     let (buffer_b, _) = project_b
         .update(cx_b, |p, cx| {
-            p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
+            p.open_buffer_with_lsp((worktree_id, rel_path("a.ts")), cx)
         })
         .await
         .unwrap();
@@ -4838,7 +4825,7 @@ async fn test_definition(
     // Open the file on client B.
     let (buffer_b, _handle) = project_b
         .update(cx_b, |p, cx| {
-            p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
+            p.open_buffer_with_lsp((worktree_id, rel_path("a.rs")), cx)
         })
         .await
         .unwrap();
@@ -5016,7 +5003,7 @@ async fn test_references(
     // Open the file on client B.
     let (buffer_b, _handle) = project_b
         .update(cx_b, |p, cx| {
-            p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
+            p.open_buffer_with_lsp((worktree_id, rel_path("one.rs")), cx)
         })
         .await
         .unwrap();
@@ -5088,7 +5075,7 @@ async fn test_references(
         let three_buffer = references[2].buffer.read(cx);
         assert_eq!(
             two_buffer.file().unwrap().path().as_ref(),
-            Path::new("two.rs")
+            rel_path("two.rs")
         );
         assert_eq!(references[1].buffer, references[0].buffer);
         assert_eq!(
@@ -5288,7 +5275,7 @@ async fn test_document_highlights(
     // Open the file on client B.
     let (buffer_b, _handle) = project_b
         .update(cx_b, |p, cx| {
-            p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
+            p.open_buffer_with_lsp((worktree_id, rel_path("main.rs")), cx)
         })
         .await
         .unwrap();
@@ -5431,7 +5418,7 @@ async fn test_lsp_hover(
     // Open the file as the guest
     let (buffer_b, _handle) = project_b
         .update(cx_b, |p, cx| {
-            p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
+            p.open_buffer_with_lsp((worktree_id, rel_path("main.rs")), cx)
         })
         .await
         .unwrap();
@@ -5623,7 +5610,7 @@ async fn test_project_symbols(
     // Cause the language server to start.
     let _buffer = project_b
         .update(cx_b, |p, cx| {
-            p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
+            p.open_buffer_with_lsp((worktree_id, rel_path("one.rs")), cx)
         })
         .await
         .unwrap();
@@ -5673,7 +5660,10 @@ async fn test_project_symbols(
 
     // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
     let mut fake_symbol = symbols[0].clone();
-    fake_symbol.path.path = Path::new(path!("/code/secrets")).into();
+    fake_symbol.path = SymbolLocation::OutsideProject {
+        abs_path: Path::new(path!("/code/secrets")).into(),
+        signature: [0x17; 32],
+    };
     let error = project_b
         .update(cx_b, |project, cx| {
             project.open_buffer_for_symbol(&fake_symbol, cx)

crates/collab/src/tests/random_project_collaboration_tests.rs 🔗

@@ -27,7 +27,11 @@ use std::{
     rc::Rc,
     sync::Arc,
 };
-use util::{ResultExt, path};
+use util::{
+    ResultExt, path,
+    paths::PathStyle,
+    rel_path::{RelPath, RelPathBuf, rel_path},
+};
 
 #[gpui::test(
     iterations = 100,
@@ -66,7 +70,7 @@ enum ClientOperation {
     OpenBuffer {
         project_root_name: String,
         is_local: bool,
-        full_path: PathBuf,
+        full_path: RelPathBuf,
     },
     SearchProject {
         project_root_name: String,
@@ -77,24 +81,24 @@ enum ClientOperation {
     EditBuffer {
         project_root_name: String,
         is_local: bool,
-        full_path: PathBuf,
+        full_path: RelPathBuf,
         edits: Vec<(Range<usize>, Arc<str>)>,
     },
     CloseBuffer {
         project_root_name: String,
         is_local: bool,
-        full_path: PathBuf,
+        full_path: RelPathBuf,
     },
     SaveBuffer {
         project_root_name: String,
         is_local: bool,
-        full_path: PathBuf,
+        full_path: RelPathBuf,
         detach: bool,
     },
     RequestLspDataInBuffer {
         project_root_name: String,
         is_local: bool,
-        full_path: PathBuf,
+        full_path: RelPathBuf,
         offset: usize,
         kind: LspRequestKind,
         detach: bool,
@@ -102,7 +106,7 @@ enum ClientOperation {
     CreateWorktreeEntry {
         project_root_name: String,
         is_local: bool,
-        full_path: PathBuf,
+        full_path: RelPathBuf,
         is_dir: bool,
     },
     WriteFsEntry {
@@ -119,7 +123,7 @@ enum ClientOperation {
 enum GitOperation {
     WriteGitIndex {
         repo_path: PathBuf,
-        contents: Vec<(PathBuf, String)>,
+        contents: Vec<(RelPathBuf, String)>,
     },
     WriteGitBranch {
         repo_path: PathBuf,
@@ -127,7 +131,7 @@ enum GitOperation {
     },
     WriteGitStatuses {
         repo_path: PathBuf,
-        statuses: Vec<(PathBuf, FileStatus)>,
+        statuses: Vec<(RelPathBuf, FileStatus)>,
     },
 }
 
@@ -311,8 +315,8 @@ impl RandomizedTest for ProjectCollaborationTest {
                             let Some(worktree) = worktree else { continue };
                             let is_dir = rng.random::<bool>();
                             let mut full_path =
-                                worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
-                            full_path.push(gen_file_name(rng));
+                                worktree.read_with(cx, |w, _| w.root_name().to_rel_path_buf());
+                            full_path.push(rel_path(&gen_file_name(rng)));
                             if !is_dir {
                                 full_path.set_extension("rs");
                             }
@@ -346,8 +350,18 @@ impl RandomizedTest for ProjectCollaborationTest {
                                 continue;
                             };
 
-                            let full_path = buffer
-                                .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
+                            let full_path = buffer.read_with(cx, |buffer, cx| {
+                                let file = buffer.file().unwrap();
+                                let worktree = project
+                                    .read(cx)
+                                    .worktree_for_id(file.worktree_id(cx), cx)
+                                    .unwrap();
+                                worktree
+                                    .read(cx)
+                                    .root_name()
+                                    .join(file.path())
+                                    .to_rel_path_buf()
+                            });
 
                             match rng.random_range(0..100_u32) {
                                 // Close the buffer
@@ -436,16 +450,16 @@ impl RandomizedTest for ProjectCollaborationTest {
                                     .filter(|e| e.is_file())
                                     .choose(rng)
                                     .unwrap();
-                                if entry.path.as_ref() == Path::new("") {
-                                    Path::new(worktree.root_name()).into()
+                                if entry.path.as_ref().is_empty() {
+                                    worktree.root_name().into()
                                 } else {
-                                    Path::new(worktree.root_name()).join(&entry.path)
+                                    worktree.root_name().join(&entry.path)
                                 }
                             });
                             break ClientOperation::OpenBuffer {
                                 project_root_name,
                                 is_local,
-                                full_path,
+                                full_path: full_path.to_rel_path_buf(),
                             };
                         }
                     }
@@ -940,7 +954,11 @@ impl RandomizedTest for ProjectCollaborationTest {
                     }
 
                     for (path, _) in contents.iter() {
-                        if !client.fs().files().contains(&repo_path.join(path)) {
+                        if !client
+                            .fs()
+                            .files()
+                            .contains(&repo_path.join(path.as_std_path()))
+                        {
                             return Err(TestError::Inapplicable);
                         }
                     }
@@ -954,8 +972,8 @@ impl RandomizedTest for ProjectCollaborationTest {
 
                     let dot_git_dir = repo_path.join(".git");
                     let contents = contents
-                        .into_iter()
-                        .map(|(path, contents)| (path.into(), contents))
+                        .iter()
+                        .map(|(path, contents)| (path.as_str(), contents.clone()))
                         .collect::<Vec<_>>();
                     if client.fs().metadata(&dot_git_dir).await?.is_none() {
                         client.fs().create_dir(&dot_git_dir).await?;
@@ -993,7 +1011,11 @@ impl RandomizedTest for ProjectCollaborationTest {
                         return Err(TestError::Inapplicable);
                     }
                     for (path, _) in statuses.iter() {
-                        if !client.fs().files().contains(&repo_path.join(path)) {
+                        if !client
+                            .fs()
+                            .files()
+                            .contains(&repo_path.join(path.as_std_path()))
+                        {
                             return Err(TestError::Inapplicable);
                         }
                     }
@@ -1009,7 +1031,7 @@ impl RandomizedTest for ProjectCollaborationTest {
 
                     let statuses = statuses
                         .iter()
-                        .map(|(path, val)| (path.as_path(), *val))
+                        .map(|(path, val)| (path.as_str(), *val))
                         .collect::<Vec<_>>();
 
                     if client.fs().metadata(&dot_git_dir).await?.is_none() {
@@ -1426,7 +1448,7 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
         repo_path: &Path,
         rng: &mut StdRng,
         client: &TestClient,
-    ) -> Vec<PathBuf> {
+    ) -> Vec<RelPathBuf> {
         let mut paths = client
             .fs()
             .files()
@@ -1440,7 +1462,11 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
 
         paths
             .iter()
-            .map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf())
+            .map(|path| {
+                RelPath::from_std_path(path.strip_prefix(repo_path).unwrap(), PathStyle::local())
+                    .unwrap()
+                    .to_rel_path_buf()
+            })
             .collect::<Vec<_>>()
     }
 
@@ -1487,7 +1513,7 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
 fn buffer_for_full_path(
     client: &TestClient,
     project: &Entity<Project>,
-    full_path: &PathBuf,
+    full_path: &RelPath,
     cx: &TestAppContext,
 ) -> Option<Entity<language::Buffer>> {
     client
@@ -1495,7 +1521,12 @@ fn buffer_for_full_path(
         .iter()
         .find(|buffer| {
             buffer.read_with(cx, |buffer, cx| {
-                buffer.file().unwrap().full_path(cx) == *full_path
+                let file = buffer.file().unwrap();
+                let Some(worktree) = project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
+                else {
+                    return false;
+                };
+                worktree.read(cx).root_name().join(&file.path()).as_ref() == full_path
             })
         })
         .cloned()
@@ -1536,23 +1567,23 @@ fn root_name_for_project(project: &Entity<Project>, cx: &TestAppContext) -> Stri
             .next()
             .unwrap()
             .read(cx)
-            .root_name()
+            .root_name_str()
             .to_string()
     })
 }
 
 fn project_path_for_full_path(
     project: &Entity<Project>,
-    full_path: &Path,
+    full_path: &RelPath,
     cx: &TestAppContext,
 ) -> Option<ProjectPath> {
     let mut components = full_path.components();
-    let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
-    let path = components.as_path().into();
+    let root_name = components.next().unwrap();
+    let path = components.rest().into();
     let worktree_id = project.read_with(cx, |project, cx| {
         project.worktrees(cx).find_map(|worktree| {
             let worktree = worktree.read(cx);
-            if worktree.root_name() == root_name {
+            if worktree.root_name_str() == root_name {
                 Some(worktree.id())
             } else {
                 None

crates/collab/src/tests/remote_editing_collaboration_tests.rs 🔗

@@ -33,7 +33,7 @@ use std::{
     sync::{Arc, atomic::AtomicUsize},
 };
 use task::TcpArgumentsTemplate;
-use util::path;
+use util::{path, rel_path::rel_path};
 
 #[gpui::test(iterations = 10)]
 async fn test_sharing_an_ssh_remote_project(
@@ -124,26 +124,26 @@ async fn test_sharing_an_ssh_remote_project(
 
     worktree_a.update(cx_a, |worktree, _cx| {
         assert_eq!(
-            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
+            worktree.paths().collect::<Vec<_>>(),
             vec![
-                Path::new(".zed"),
-                Path::new(".zed/settings.json"),
-                Path::new("README.md"),
-                Path::new("src"),
-                Path::new("src/lib.rs"),
+                rel_path(".zed"),
+                rel_path(".zed/settings.json"),
+                rel_path("README.md"),
+                rel_path("src"),
+                rel_path("src/lib.rs"),
             ]
         );
     });
 
     worktree_b.update(cx_b, |worktree, _cx| {
         assert_eq!(
-            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
+            worktree.paths().collect::<Vec<_>>(),
             vec![
-                Path::new(".zed"),
-                Path::new(".zed/settings.json"),
-                Path::new("README.md"),
-                Path::new("src"),
-                Path::new("src/lib.rs"),
+                rel_path(".zed"),
+                rel_path(".zed/settings.json"),
+                rel_path("README.md"),
+                rel_path("src"),
+                rel_path("src/lib.rs"),
             ]
         );
     });
@@ -151,7 +151,7 @@ async fn test_sharing_an_ssh_remote_project(
     // User B can open buffers in the remote project.
     let buffer_b = project_b
         .update(cx_b, |project, cx| {
-            project.open_buffer((worktree_id, "src/lib.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
         })
         .await
         .unwrap();
@@ -177,7 +177,7 @@ async fn test_sharing_an_ssh_remote_project(
                 buffer_b.clone(),
                 ProjectPath {
                     worktree_id: worktree_id.to_owned(),
-                    path: Arc::from(Path::new("src/renamed.rs")),
+                    path: rel_path("src/renamed.rs").into(),
                 },
                 cx,
             )
@@ -194,14 +194,8 @@ async fn test_sharing_an_ssh_remote_project(
     cx_b.run_until_parked();
     cx_b.update(|cx| {
         assert_eq!(
-            buffer_b
-                .read(cx)
-                .file()
-                .unwrap()
-                .path()
-                .to_string_lossy()
-                .to_string(),
-            path!("src/renamed.rs").to_string()
+            buffer_b.read(cx).file().unwrap().path().as_ref(),
+            rel_path("src/renamed.rs")
         );
     });
 }
@@ -489,7 +483,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
     // Opens the buffer and formats it
     let (buffer_b, _handle) = project_b
         .update(cx_b, |p, cx| {
-            p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
+            p.open_buffer_with_lsp((worktree_id, rel_path("a.ts")), cx)
         })
         .await
         .expect("user B opens buffer for formatting");
@@ -547,7 +541,9 @@ async fn test_ssh_collaboration_formatting_with_prettier(
 
     // User A opens and formats the same buffer too
     let buffer_a = project_a
-        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
+        .update(cx_a, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("a.ts")), cx)
+        })
         .await
         .expect("user A opens buffer for formatting");
 

crates/copilot/src/copilot.rs 🔗

@@ -40,6 +40,7 @@ use std::{
     sync::Arc,
 };
 use sum_tree::Dimensions;
+use util::rel_path::RelPath;
 use util::{ResultExt, fs::remove_matching};
 use workspace::Workspace;
 
@@ -963,8 +964,7 @@ impl Copilot {
         let hard_tabs = settings.hard_tabs;
         let relative_path = buffer
             .file()
-            .map(|file| file.path().to_path_buf())
-            .unwrap_or_default();
+            .map_or(RelPath::empty().into(), |file| file.path().clone());
 
         cx.background_spawn(async move {
             let (version, snapshot) = snapshot.await?;
@@ -975,7 +975,7 @@ impl Copilot {
                         tab_size: tab_size.into(),
                         indent_size: 1,
                         insert_spaces: !hard_tabs,
-                        relative_path: relative_path.to_string_lossy().into(),
+                        relative_path: relative_path.to_proto(),
                         position: point_to_lsp(position),
                         version: version.try_into().unwrap(),
                     },
@@ -1194,7 +1194,7 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
 mod tests {
     use super::*;
     use gpui::TestAppContext;
-    use util::path;
+    use util::{path, paths::PathStyle, rel_path::rel_path};
 
     #[gpui::test(iterations = 10)]
     async fn test_buffer_management(cx: &mut TestAppContext) {
@@ -1258,7 +1258,7 @@ mod tests {
             buffer.file_updated(
                 Arc::new(File {
                     abs_path: path!("/root/child/buffer-1").into(),
-                    path: Path::new("child/buffer-1").into(),
+                    path: rel_path("child/buffer-1").into(),
                 }),
                 cx,
             )
@@ -1355,7 +1355,7 @@ mod tests {
 
     struct File {
         abs_path: PathBuf,
-        path: Arc<Path>,
+        path: Arc<RelPath>,
     }
 
     impl language::File for File {
@@ -1369,15 +1369,19 @@ mod tests {
             }
         }
 
-        fn path(&self) -> &Arc<Path> {
+        fn path(&self) -> &Arc<RelPath> {
             &self.path
         }
 
+        fn path_style(&self, _: &App) -> PathStyle {
+            PathStyle::local()
+        }
+
         fn full_path(&self, _: &App) -> PathBuf {
             unimplemented!()
         }
 
-        fn file_name<'a>(&'a self, _: &'a App) -> &'a std::ffi::OsStr {
+        fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
             unimplemented!()
         }
 

crates/dap/src/adapters.rs 🔗

@@ -24,7 +24,7 @@ use std::{
     sync::Arc,
 };
 use task::{DebugScenario, TcpArgumentsTemplate, ZedDebugConfig};
-use util::archive::extract_zip;
+use util::{archive::extract_zip, rel_path::RelPath};
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum DapStatus {
@@ -44,7 +44,7 @@ pub trait DapDelegate: Send + Sync + 'static {
     fn fs(&self) -> Arc<dyn Fs>;
     fn output_to_console(&self, msg: String);
     async fn which(&self, command: &OsStr) -> Option<PathBuf>;
-    async fn read_text_file(&self, path: PathBuf) -> Result<String>;
+    async fn read_text_file(&self, path: &RelPath) -> Result<String>;
     async fn shell_env(&self) -> collections::HashMap<String, String>;
 }
 

crates/dap_adapters/src/python.rs 🔗

@@ -20,7 +20,7 @@ use std::{
     ffi::OsStr,
     path::{Path, PathBuf},
 };
-use util::{ResultExt, maybe};
+use util::{ResultExt, maybe, paths::PathStyle, rel_path::RelPath};
 
 #[derive(Default)]
 pub(crate) struct PythonDebugAdapter {
@@ -726,13 +726,16 @@ impl DebugAdapter for PythonDebugAdapter {
             .config
             .get("cwd")
             .and_then(|cwd| {
-                cwd.as_str()
-                    .map(Path::new)?
-                    .strip_prefix(delegate.worktree_root_path())
-                    .ok()
+                RelPath::from_std_path(
+                    cwd.as_str()
+                        .map(Path::new)?
+                        .strip_prefix(delegate.worktree_root_path())
+                        .ok()?,
+                    PathStyle::local(),
+                )
+                .ok()
             })
-            .unwrap_or_else(|| "".as_ref())
-            .into();
+            .unwrap_or_else(|| RelPath::empty().into());
         let toolchain = delegate
             .toolchain_store()
             .active_toolchain(

crates/debug_adapter_extension/src/extension_dap_adapter.rs 🔗

@@ -15,6 +15,7 @@ use dap::{
 use extension::{Extension, WorktreeDelegate};
 use gpui::AsyncApp;
 use task::{DebugScenario, ZedDebugConfig};
+use util::rel_path::RelPath;
 
 pub(crate) struct ExtensionDapAdapter {
     extension: Arc<dyn Extension>,
@@ -57,7 +58,7 @@ impl WorktreeDelegate for WorktreeDelegateAdapter {
         self.0.worktree_root_path().to_string_lossy().to_string()
     }
 
-    async fn read_text_file(&self, path: PathBuf) -> Result<String> {
+    async fn read_text_file(&self, path: &RelPath) -> Result<String> {
         self.0.read_text_file(path).await
     }
 

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -33,6 +33,7 @@ use std::sync::{Arc, LazyLock};
 use task::{DebugScenario, TaskContext};
 use tree_sitter::{Query, StreamingIterator as _};
 use ui::{ContextMenu, Divider, PopoverMenuHandle, Tab, Tooltip, prelude::*};
+use util::rel_path::RelPath;
 use util::{ResultExt, debug_panic, maybe};
 use workspace::SplitDirection;
 use workspace::item::SaveOptions;
@@ -1061,14 +1062,14 @@ impl DebugPanel {
                 directory_in_worktree: dir,
                 ..
             } => {
-                let relative_path = if dir.ends_with(".vscode") {
-                    dir.join("launch.json")
+                let relative_path = if dir.ends_with(RelPath::new(".vscode").unwrap()) {
+                    dir.join(RelPath::new("launch.json").unwrap())
                 } else {
-                    dir.join("debug.json")
+                    dir.join(RelPath::new("debug.json").unwrap())
                 };
                 ProjectPath {
                     worktree_id: id,
-                    path: Arc::from(relative_path),
+                    path: relative_path,
                 }
             }
             _ => return self.save_scenario(scenario, worktree_id, window, cx),
@@ -1129,7 +1130,7 @@ impl DebugPanel {
                     let fs =
                         workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
 
-                    path.push(paths::local_settings_folder_relative_path());
+                    path.push(paths::local_settings_folder_name());
                     if !fs.is_dir(path.as_path()).await {
                         fs.create_dir(path.as_path()).await?;
                     }

crates/debugger_ui/src/new_process_modal.rs 🔗

@@ -32,7 +32,7 @@ use ui::{
     SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div,
     h_flex, relative, rems, v_flex,
 };
-use util::ResultExt;
+use util::{ResultExt, rel_path::RelPath};
 use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane};
 
 use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
@@ -1026,29 +1026,27 @@ impl DebugDelegate {
                     let mut path = if worktrees.len() > 1
                         && let Some(worktree) = project.worktree_for_id(*worktree_id, cx)
                     {
-                        let worktree_path = worktree.read(cx).abs_path();
-                        let full_path = worktree_path.join(directory_in_worktree);
-                        full_path
+                        worktree
+                            .read(cx)
+                            .root_name()
+                            .join(directory_in_worktree)
+                            .to_rel_path_buf()
                     } else {
-                        directory_in_worktree.clone()
+                        directory_in_worktree.to_rel_path_buf()
                     };
 
-                    match path
-                        .components()
-                        .next_back()
-                        .and_then(|component| component.as_os_str().to_str())
-                    {
+                    match path.components().next_back() {
                         Some(".zed") => {
-                            path.push("debug.json");
+                            path.push(RelPath::new("debug.json").unwrap());
                         }
                         Some(".vscode") => {
-                            path.push("launch.json");
+                            path.push(RelPath::new("launch.json").unwrap());
                         }
                         _ => {}
                     }
-                    Some(path.display().to_string())
+                    path.display(project.path_style(cx)).to_string()
                 })
-                .unwrap_or_else(|_| Some(directory_in_worktree.display().to_string())),
+                .ok(),
             Some(TaskSourceKind::AbsPath { abs_path, .. }) => {
                 Some(abs_path.to_string_lossy().into_owned())
             }
@@ -1135,7 +1133,7 @@ impl DebugDelegate {
                         id: _,
                         directory_in_worktree: dir,
                         id_base: _,
-                    } => dir.ends_with(".zed"),
+                    } => dir.ends_with(RelPath::new(".zed").unwrap()),
                     _ => false,
                 });
 
@@ -1154,7 +1152,10 @@ impl DebugDelegate {
                                     id: _,
                                     directory_in_worktree: dir,
                                     id_base: _,
-                                } => !(hide_vscode && dir.ends_with(".vscode")),
+                                } => {
+                                    !(hide_vscode
+                                        && dir.ends_with(RelPath::new(".vscode").unwrap()))
+                                }
                                 _ => true,
                             })
                             .filter(|(_, scenario)| valid_adapters.contains(&scenario.adapter))

crates/debugger_ui/src/session/running/breakpoint_list.rs 🔗

@@ -26,6 +26,7 @@ use ui::{
     Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render,
     StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*,
 };
+use util::rel_path::RelPath;
 use workspace::Workspace;
 use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
 
@@ -663,6 +664,7 @@ impl Render for BreakpointList {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
         let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
         self.breakpoints.clear();
+        let path_style = self.worktree_store.read(cx).path_style();
         let weak = cx.weak_entity();
         let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
             let relative_worktree_path = self
@@ -673,7 +675,7 @@ impl Render for BreakpointList {
                     worktree
                         .read(cx)
                         .is_visible()
-                        .then(|| Path::new(worktree.read(cx).root_name()).join(relative_path))
+                        .then(|| worktree.read(cx).root_name().join(&relative_path))
                 });
             breakpoints.sort_by_key(|breakpoint| breakpoint.row);
             let weak = weak.clone();
@@ -683,14 +685,9 @@ impl Render for BreakpointList {
 
                 let dir = relative_worktree_path
                     .clone()
-                    .unwrap_or_else(|| PathBuf::from(&*breakpoint.path))
+                    .or_else(|| RelPath::from_std_path(&breakpoint.path, path_style).ok())?
                     .parent()
-                    .and_then(|parent| {
-                        parent
-                            .to_str()
-                            .map(ToOwned::to_owned)
-                            .map(SharedString::from)
-                    });
+                    .map(|parent| SharedString::from(parent.display(path_style).to_string()));
                 let name = file_name
                     .to_str()
                     .map(ToOwned::to_owned)

crates/debugger_ui/src/stack_trace_view.rs 🔗

@@ -181,7 +181,7 @@ impl StackTraceView {
 
                 let project_path = ProjectPath {
                     worktree_id: worktree.read_with(cx, |tree, _| tree.id())?,
-                    path: relative_path.into(),
+                    path: relative_path,
                 };
 
                 if let Some(buffer) = this

crates/debugger_ui/src/tests/debugger_panel.rs 🔗

@@ -32,7 +32,7 @@ use std::{
 };
 use terminal_view::terminal_panel::TerminalPanel;
 use tests::{active_debug_session_panel, init_test, init_test_workspace};
-use util::path;
+use util::{path, rel_path::rel_path};
 use workspace::item::SaveOptions;
 use workspace::{Item, dock::Panel};
 
@@ -1114,7 +1114,7 @@ async fn test_send_breakpoints_when_editor_has_been_saved(
 
     let buffer = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "main.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
         })
         .await
         .unwrap();
@@ -1276,14 +1276,14 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
 
     let first = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "main.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
         })
         .await
         .unwrap();
 
     let second = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "second.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("second.rs")), cx)
         })
         .await
         .unwrap();
@@ -1499,14 +1499,14 @@ async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut T
 
     let main_buffer = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "main.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
         })
         .await
         .unwrap();
 
     let second_buffer = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "second.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("second.rs")), cx)
         })
         .await
         .unwrap();

crates/debugger_ui/src/tests/inline_values.rs 🔗

@@ -7,7 +7,7 @@ use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_python, tr
 use project::{FakeFs, Project};
 use serde_json::json;
 use unindent::Unindent as _;
-use util::path;
+use util::{path, rel_path::rel_path};
 
 use crate::{
     debugger_panel::DebugPanel,
@@ -215,7 +215,7 @@ fn main() {
 
     let buffer = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "main.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
         })
         .await
         .unwrap();
@@ -1584,7 +1584,7 @@ def process_data(untyped_param, typed_param: int, another_typed: str):
 
     let buffer = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "main.py"), cx)
+            project.open_buffer((worktree_id, rel_path("main.py")), cx)
         })
         .await
         .unwrap();
@@ -2082,7 +2082,7 @@ async fn test_inline_values_util(
 
     let buffer = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "main.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
         })
         .await
         .unwrap();

crates/debugger_ui/src/tests/stack_frame_list.rs 🔗

@@ -13,7 +13,7 @@ use project::{FakeFs, Project};
 use serde_json::json;
 use std::sync::Arc;
 use unindent::Unindent as _;
-use util::path;
+use util::{path, rel_path::rel_path};
 
 #[gpui::test]
 async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
@@ -331,12 +331,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
             let project_path = editors[0]
                 .update(cx, |editor, cx| editor.project_path(cx))
                 .unwrap();
-            let expected = if cfg!(target_os = "windows") {
-                "src\\test.js"
-            } else {
-                "src/test.js"
-            };
-            assert_eq!(expected, project_path.path.to_string_lossy());
+            assert_eq!(rel_path("src/test.js"), project_path.path.as_ref());
             assert_eq!(test_file_content, editors[0].read(cx).text(cx));
             assert_eq!(
                 vec![2..3],
@@ -399,12 +394,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
         let project_path = editors[0]
             .update(cx, |editor, cx| editor.project_path(cx))
             .unwrap();
-        let expected = if cfg!(target_os = "windows") {
-            "src\\module.js"
-        } else {
-            "src/module.js"
-        };
-        assert_eq!(expected, project_path.path.to_string_lossy());
+        assert_eq!(rel_path("src/module.js"), project_path.path.as_ref());
         assert_eq!(module_file_content, editors[0].read(cx).text(cx));
         assert_eq!(
             vec![0..1],

crates/diagnostics/src/buffer_diagnostics.rs 🔗

@@ -28,7 +28,6 @@ use std::{
 };
 use text::{Anchor, BufferSnapshot, OffsetRangeExt};
 use ui::{Button, ButtonStyle, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
-use util::paths::PathExt;
 use workspace::{
     ItemHandle, ItemNavHistory, ToolbarItemLocation, Workspace,
     item::{BreadcrumbText, Item, ItemEvent, TabContentParams},
@@ -783,15 +782,16 @@ impl Item for BufferDiagnosticsEditor {
     }
 
     // Builds the content to be displayed in the tab.
-    fn tab_content(&self, params: TabContentParams, _window: &Window, _cx: &App) -> AnyElement {
+    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
+        let path_style = self.project.read(cx).path_style(cx);
         let error_count = self.summary.error_count;
         let warning_count = self.summary.warning_count;
         let label = Label::new(
             self.project_path
                 .path
                 .file_name()
-                .map(|f| f.to_sanitized_string())
-                .unwrap_or_else(|| self.project_path.path.to_sanitized_string()),
+                .map(|s| s.to_string())
+                .unwrap_or_else(|| self.project_path.path.display(path_style).to_string()),
         );
 
         h_flex()
@@ -827,11 +827,12 @@ impl Item for BufferDiagnosticsEditor {
         "Buffer Diagnostics".into()
     }
 
-    fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
+    fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
+        let path_style = self.project.read(cx).path_style(cx);
         Some(
             format!(
                 "Buffer Diagnostics - {}",
-                self.project_path.path.to_sanitized_string()
+                self.project_path.path.display(path_style)
             )
             .into(),
         )
@@ -848,7 +849,8 @@ impl Item for BufferDiagnosticsEditor {
 
 impl Render for BufferDiagnosticsEditor {
     fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let filename = self.project_path.path.to_sanitized_string();
+        let path_style = self.project.read(cx).path_style(cx);
+        let filename = self.project_path.path.display(path_style).to_string();
         let error_count = self.summary.error_count;
         let warning_count = match self.include_warnings {
             true => self.summary.warning_count,

crates/diagnostics/src/diagnostics_tests.rs 🔗

@@ -27,7 +27,7 @@ use std::{
     str::FromStr,
 };
 use unindent::Unindent as _;
-use util::{RandomCharIter, path, post_inc};
+use util::{RandomCharIter, path, post_inc, rel_path::rel_path};
 
 #[ctor::ctor]
 fn init_logger() {
@@ -1609,7 +1609,7 @@ async fn test_buffer_diagnostics(cx: &mut TestAppContext) {
         worktree_id: project.read_with(cx, |project, cx| {
             project.worktrees(cx).next().unwrap().read(cx).id()
         }),
-        path: Arc::from(Path::new("main.rs")),
+        path: rel_path("main.rs").into(),
     };
     let buffer = project
         .update(cx, |project, cx| {
@@ -1763,7 +1763,7 @@ async fn test_buffer_diagnostics_without_warnings(cx: &mut TestAppContext) {
         worktree_id: project.read_with(cx, |project, cx| {
             project.worktrees(cx).next().unwrap().read(cx).id()
         }),
-        path: Arc::from(Path::new("main.rs")),
+        path: rel_path("main.rs").into(),
     };
     let buffer = project
         .update(cx, |project, cx| {
@@ -1892,7 +1892,7 @@ async fn test_buffer_diagnostics_multiple_servers(cx: &mut TestAppContext) {
         worktree_id: project.read_with(cx, |project, cx| {
             project.worktrees(cx).next().unwrap().read(cx).id()
         }),
-        path: Arc::from(Path::new("main.rs")),
+        path: rel_path("main.rs").into(),
     };
     let buffer = project
         .update(cx, |project, cx| {

crates/docs_preprocessor/src/main.rs 🔗

@@ -9,7 +9,6 @@ use std::collections::{HashMap, HashSet};
 use std::io::{self, Read};
 use std::process;
 use std::sync::{LazyLock, OnceLock};
-use util::paths::PathExt;
 
 static KEYMAP_MACOS: LazyLock<KeymapFile> = LazyLock::new(|| {
     load_keymap("keymaps/default-macos.json").expect("Failed to load MacOS keymap")
@@ -345,7 +344,7 @@ fn handle_postprocessing() -> Result<()> {
     let mut queue = Vec::with_capacity(64);
     queue.push(root_dir.clone());
     while let Some(dir) = queue.pop() {
-        for entry in std::fs::read_dir(&dir).context(dir.to_sanitized_string())? {
+        for entry in std::fs::read_dir(&dir).context("failed to read docs dir")? {
             let Ok(entry) = entry else {
                 continue;
             };

crates/edit_prediction_context/src/syntax_index.rs 🔗

@@ -324,7 +324,7 @@ impl SyntaxIndex {
             cx.spawn(async move |_this, cx| {
                 let loaded_file = load_task.await?;
                 let language = language_registry
-                    .language_for_file_path(&project_path.path)
+                    .language_for_file_path(&project_path.path.as_std_path())
                     .await
                     .ok();
 
@@ -549,7 +549,7 @@ impl SyntaxIndexState {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use std::{path::Path, sync::Arc};
+    use std::sync::Arc;
 
     use gpui::TestAppContext;
     use indoc::indoc;
@@ -558,7 +558,7 @@ mod tests {
     use serde_json::json;
     use settings::SettingsStore;
     use text::OffsetRangeExt as _;
-    use util::path;
+    use util::{path, rel_path::rel_path};
 
     use crate::syntax_index::SyntaxIndex;
 
@@ -739,7 +739,7 @@ mod tests {
                 .read(cx)
                 .path_for_entry(*project_entry_id, cx)
                 .unwrap();
-            assert_eq!(project_path.path.as_ref(), Path::new(path),);
+            assert_eq!(project_path.path.as_ref(), rel_path(path),);
             declaration
         } else {
             panic!("Expected a buffer declaration, found {:?}", declaration);
@@ -764,7 +764,7 @@ mod tests {
                     .unwrap()
                     .path
                     .as_ref(),
-                Path::new(path),
+                rel_path(path),
             );
             declaration
         } else {

crates/editor/src/clangd_ext.rs 🔗

@@ -4,6 +4,7 @@ use language::Language;
 use project::lsp_store::lsp_ext_command::SwitchSourceHeaderResult;
 use rpc::proto;
 use url::Url;
+use util::paths::PathStyle;
 use workspace::{OpenOptions, OpenVisible};
 
 use crate::lsp_ext::find_specific_language_server_in_selection;
@@ -38,7 +39,11 @@ pub fn switch_source_header(
     let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
     cx.spawn_in(window, async move |_editor, cx| {
         let source_file = buffer.read_with(cx, |buffer, _| {
-            buffer.file().map(|file| file.path()).map(|path| path.to_string_lossy().to_string()).unwrap_or_else(|| "Unknown".to_string())
+            buffer
+                .file()
+                .map(|file| file.path())
+                .map(|path| path.display(PathStyle::local()).to_string())
+                .unwrap_or_else(|| "Unknown".to_string())
         })?;
 
         let switch_source_header = if let Some((client, project_id)) = upstream_client {
@@ -53,18 +58,22 @@ pub fn switch_source_header(
                 .context("lsp ext switch source header proto request")?;
             SwitchSourceHeaderResult(response.target_file)
         } else {
-            project.update(cx, |project, cx| {
-                project.request_lsp(
-                    buffer,
-                    project::LanguageServerToQuery::Other(server_to_query),
-                    project::lsp_store::lsp_ext_command::SwitchSourceHeader,
-                    cx,
-                )
-            })?.await.with_context(|| format!("Switch source/header LSP request for path \"{source_file}\" failed"))?
+            project
+                .update(cx, |project, cx| {
+                    project.request_lsp(
+                        buffer,
+                        project::LanguageServerToQuery::Other(server_to_query),
+                        project::lsp_store::lsp_ext_command::SwitchSourceHeader,
+                        cx,
+                    )
+                })?
+                .await
+                .with_context(|| {
+                    format!("Switch source/header LSP request for path \"{source_file}\" failed")
+                })?
         };
 
         if switch_source_header.0.is_empty() {
-            log::info!("Clangd returned an empty string when requesting to switch source/header from \"{source_file}\"" );
             return Ok(());
         }
 
@@ -75,18 +84,24 @@ pub fn switch_source_header(
             )
         })?;
 
-        let path = goto.to_file_path().map_err(|()| {
-            anyhow::anyhow!("URL conversion to file path failed for \"{goto}\"")
-        })?;
+        let path = goto
+            .to_file_path()
+            .map_err(|()| anyhow::anyhow!("URL conversion to file path failed for \"{goto}\""))?;
 
         workspace
             .update_in(cx, |workspace, window, cx| {
-                workspace.open_abs_path(path, OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
+                workspace.open_abs_path(
+                    path,
+                    OpenOptions {
+                        visible: Some(OpenVisible::None),
+                        ..Default::default()
+                    },
+                    window,
+                    cx,
+                )
             })
             .with_context(|| {
-                format!(
-                    "Switch source/header could not open \"{goto}\" in workspace"
-                )
+                format!("Switch source/header could not open \"{goto}\" in workspace")
             })?
             .await
             .map(|_| ())

crates/editor/src/editor.rs 🔗

@@ -2494,7 +2494,7 @@ impl Editor {
             if let Some(extension) = singleton_buffer
                 .read(cx)
                 .file()
-                .and_then(|file| file.path().extension()?.to_str())
+                .and_then(|file| file.path().extension())
             {
                 key_context.set("extension", extension.to_string());
             }
@@ -7603,7 +7603,7 @@ impl Editor {
         let extension = buffer
             .read(cx)
             .file()
-            .and_then(|file| Some(file.path().extension()?.to_string_lossy().to_string()));
+            .and_then(|file| Some(file.path().extension()?.to_string()));
 
         let event_type = match accepted {
             true => "Edit Prediction Accepted",
@@ -19263,10 +19263,6 @@ impl Editor {
             {
                 return Some(dir.to_owned());
             }
-
-            if let Some(project_path) = buffer.read(cx).project_path(cx) {
-                return Some(project_path.path.to_path_buf());
-            }
         }
 
         None
@@ -19294,16 +19290,6 @@ impl Editor {
         })
     }
 
-    fn target_file_path(&self, cx: &mut Context<Self>) -> Option<PathBuf> {
-        self.active_excerpt(cx).and_then(|(_, buffer, _)| {
-            let project_path = buffer.read(cx).project_path(cx)?;
-            let project = self.project()?.read(cx);
-            let entry = project.entry_for_path(&project_path, cx)?;
-            let path = entry.path.to_path_buf();
-            Some(path)
-        })
-    }
-
     pub fn reveal_in_finder(
         &mut self,
         _: &RevealInFileManager,
@@ -19336,9 +19322,12 @@ impl Editor {
         _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        if let Some(path) = self.target_file_path(cx)
-            && let Some(path) = path.to_str()
-        {
+        if let Some(path) = self.active_excerpt(cx).and_then(|(_, buffer, _)| {
+            let project = self.project()?.read(cx);
+            let path = buffer.read(cx).file()?.path();
+            let path = path.display(project.path_style(cx));
+            Some(path)
+        }) {
             cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
         } else {
             cx.propagate();
@@ -19414,16 +19403,14 @@ impl Editor {
     ) {
         if let Some(file) = self.target_file(cx)
             && let Some(file_stem) = file.path().file_stem()
-            && let Some(name) = file_stem.to_str()
         {
-            cx.write_to_clipboard(ClipboardItem::new_string(name.to_string()));
+            cx.write_to_clipboard(ClipboardItem::new_string(file_stem.to_string()));
         }
     }
 
     pub fn copy_file_name(&mut self, _: &CopyFileName, _: &mut Window, cx: &mut Context<Self>) {
         if let Some(file) = self.target_file(cx)
-            && let Some(file_name) = file.path().file_name()
-            && let Some(name) = file_name.to_str()
+            && let Some(name) = file.path().file_name()
         {
             cx.write_to_clipboard(ClipboardItem::new_string(name.to_string()));
         }
@@ -19691,9 +19678,8 @@ impl Editor {
         cx: &mut Context<Self>,
     ) {
         let selection = self.selections.newest::<Point>(cx).start.row + 1;
-        if let Some(file) = self.target_file(cx)
-            && let Some(path) = file.path().to_str()
-        {
+        if let Some(file) = self.target_file(cx) {
+            let path = file.path().display(file.path_style(cx));
             cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}")));
         }
     }

crates/editor/src/editor_tests.rs 🔗

@@ -56,6 +56,7 @@ use text::ToPoint as _;
 use unindent::Unindent;
 use util::{
     assert_set_eq, path,
+    rel_path::rel_path,
     test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
     uri,
 };
@@ -11142,19 +11143,19 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
 
     let buffer_1 = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "main.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
         })
         .await
         .unwrap();
     let buffer_2 = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "other.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("other.rs")), cx)
         })
         .await
         .unwrap();
     let buffer_3 = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "lib.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
         })
         .await
         .unwrap();
@@ -11329,19 +11330,19 @@ async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
     // Open three buffers
     let buffer_1 = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "file1.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
         })
         .await
         .unwrap();
     let buffer_2 = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "file2.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
         })
         .await
         .unwrap();
     let buffer_3 = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "file3.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
         })
         .await
         .unwrap();
@@ -14677,7 +14678,7 @@ async fn test_multiline_completion(cx: &mut TestAppContext) {
         .unwrap();
     let editor = workspace
         .update(cx, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
         })
         .unwrap()
         .await
@@ -16394,7 +16395,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
     leader.update(cx, |leader, cx| {
         leader.buffer.update(cx, |multibuffer, cx| {
             multibuffer.set_excerpts_for_path(
-                PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
+                PathKey::namespaced(1, "b.txt".into()),
                 buffer_1.clone(),
                 vec![
                     Point::row_range(0..3),
@@ -16405,7 +16406,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
                 cx,
             );
             multibuffer.set_excerpts_for_path(
-                PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
+                PathKey::namespaced(1, "a.txt".into()),
                 buffer_2.clone(),
                 vec![Point::row_range(0..6), Point::row_range(8..12)],
                 0,
@@ -16897,7 +16898,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
         .unwrap();
     let editor_handle = workspace
         .update(cx, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
         })
         .unwrap()
         .await
@@ -20878,9 +20879,9 @@ async fn test_display_diff_hunks(cx: &mut TestAppContext) {
     fs.set_head_for_repo(
         path!("/test/.git").as_ref(),
         &[
-            ("file-1".into(), "one\n".into()),
-            ("file-2".into(), "two\n".into()),
-            ("file-3".into(), "three\n".into()),
+            ("file-1", "one\n".into()),
+            ("file-2", "two\n".into()),
+            ("file-3", "three\n".into()),
         ],
         "deadbeef",
     );
@@ -20904,7 +20905,7 @@ async fn test_display_diff_hunks(cx: &mut TestAppContext) {
         for buffer in &buffers {
             let snapshot = buffer.read(cx).snapshot();
             multibuffer.set_excerpts_for_path(
-                PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
+                PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().as_str().into()),
                 buffer.clone(),
                 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
                 2,
@@ -21657,19 +21658,19 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
 
     let buffer_1 = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "first.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("first.rs")), cx)
         })
         .await
         .unwrap();
     let buffer_2 = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "second.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("second.rs")), cx)
         })
         .await
         .unwrap();
     let buffer_3 = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "third.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("third.rs")), cx)
         })
         .await
         .unwrap();
@@ -21825,19 +21826,19 @@ async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
 
     let buffer_1 = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "first.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("first.rs")), cx)
         })
         .await
         .unwrap();
     let buffer_2 = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "second.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("second.rs")), cx)
         })
         .await
         .unwrap();
     let buffer_3 = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "third.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("third.rs")), cx)
         })
         .await
         .unwrap();
@@ -21960,7 +21961,7 @@ async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut Test
 
     let buffer_1 = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "main.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
         })
         .await
         .unwrap();
@@ -22499,7 +22500,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
 
     let buffer = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "main.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
         })
         .await
         .unwrap();
@@ -22613,7 +22614,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
 
     let buffer = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "main.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
         })
         .await
         .unwrap();
@@ -22783,7 +22784,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
 
     let buffer = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, "main.rs"), cx)
+            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
         })
         .await
         .unwrap();
@@ -23371,7 +23372,7 @@ println!("5");
     let editor_1 = workspace
         .update_in(cx, |workspace, window, cx| {
             workspace.open_path(
-                (worktree_id, "main.rs"),
+                (worktree_id, rel_path("main.rs")),
                 Some(pane_1.downgrade()),
                 true,
                 window,
@@ -23414,7 +23415,7 @@ println!("5");
     let editor_2 = workspace
         .update_in(cx, |workspace, window, cx| {
             workspace.open_path(
-                (worktree_id, "main.rs"),
+                (worktree_id, rel_path("main.rs")),
                 Some(pane_2.downgrade()),
                 true,
                 window,
@@ -23453,7 +23454,7 @@ println!("5");
     let _other_editor_1 = workspace
         .update_in(cx, |workspace, window, cx| {
             workspace.open_path(
-                (worktree_id, "lib.rs"),
+                (worktree_id, rel_path("lib.rs")),
                 Some(pane_1.downgrade()),
                 true,
                 window,
@@ -23489,7 +23490,7 @@ println!("5");
     let _other_editor_2 = workspace
         .update_in(cx, |workspace, window, cx| {
             workspace.open_path(
-                (worktree_id, "lib.rs"),
+                (worktree_id, rel_path("lib.rs")),
                 Some(pane_2.downgrade()),
                 true,
                 window,
@@ -23526,7 +23527,7 @@ println!("5");
     let _editor_1_reopened = workspace
         .update_in(cx, |workspace, window, cx| {
             workspace.open_path(
-                (worktree_id, "main.rs"),
+                (worktree_id, rel_path("main.rs")),
                 Some(pane_1.downgrade()),
                 true,
                 window,
@@ -23540,7 +23541,7 @@ println!("5");
     let _editor_2_reopened = workspace
         .update_in(cx, |workspace, window, cx| {
             workspace.open_path(
-                (worktree_id, "main.rs"),
+                (worktree_id, rel_path("main.rs")),
                 Some(pane_2.downgrade()),
                 true,
                 window,
@@ -23634,7 +23635,7 @@ println!("5");
     let editor = workspace
         .update_in(cx, |workspace, window, cx| {
             workspace.open_path(
-                (worktree_id, "main.rs"),
+                (worktree_id, rel_path("main.rs")),
                 Some(pane.downgrade()),
                 true,
                 window,
@@ -23693,7 +23694,7 @@ println!("5");
     let _editor_reopened = workspace
         .update_in(cx, |workspace, window, cx| {
             workspace.open_path(
-                (worktree_id, "main.rs"),
+                (worktree_id, rel_path("main.rs")),
                 Some(pane.downgrade()),
                 true,
                 window,
@@ -23860,7 +23861,7 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
         .unwrap();
     let editor = workspace
         .update(cx, |workspace, window, cx| {
-            workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
+            workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
         })
         .unwrap()
         .await
@@ -24054,7 +24055,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
     let main_editor = workspace
         .update_in(cx, |workspace, window, cx| {
             workspace.open_path(
-                (worktree_id, "main.rs"),
+                (worktree_id, rel_path("main.rs")),
                 Some(pane.downgrade()),
                 true,
                 window,
@@ -25636,7 +25637,7 @@ async fn test_document_colors(cx: &mut TestAppContext) {
                     .path();
                 assert_eq!(
                     editor_file.as_ref(),
-                    Path::new("first.rs"),
+                    rel_path("first.rs"),
                     "Both editors should be opened for the same file"
                 )
             }
@@ -25816,7 +25817,7 @@ async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
 
     let handle = workspace
         .update_in(cx, |workspace, window, cx| {
-            let project_path = (worktree_id, "one.pdf");
+            let project_path = (worktree_id, rel_path("one.pdf"));
             workspace.open_path(project_path, None, true, window, cx)
         })
         .await

crates/editor/src/element.rs 🔗

@@ -3779,13 +3779,15 @@ impl EditorElement {
             .as_ref()
             .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
             .unwrap_or_default();
-        let can_open_excerpts = Editor::can_open_excerpts_in_file(for_excerpt.buffer.file());
+        let file = for_excerpt.buffer.file();
+        let can_open_excerpts = Editor::can_open_excerpts_in_file(file);
+        let path_style = file.map(|file| file.path_style(cx));
         let relative_path = for_excerpt.buffer.resolve_file_path(cx, include_root);
         let filename = relative_path
             .as_ref()
             .and_then(|path| Some(path.file_name()?.to_string_lossy().to_string()));
         let parent_path = relative_path.as_ref().and_then(|path| {
-            Some(path.parent()?.to_string_lossy().to_string() + std::path::MAIN_SEPARATOR_STR)
+            Some(path.parent()?.to_string_lossy().to_string() + path_style?.separator())
         });
         let focus_handle = editor.focus_handle(cx);
         let colors = cx.theme().colors();
@@ -3990,12 +3992,13 @@ impl EditorElement {
                         && let Some(worktree) =
                             project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
                     {
+                        let path_style = file.path_style(cx);
                         let worktree = worktree.read(cx);
                         let relative_path = file.path();
                         let entry_for_path = worktree.entry_for_path(relative_path);
                         let abs_path = entry_for_path.map(|e| {
                             e.canonical_path.as_deref().map_or_else(
-                                || worktree.abs_path().join(relative_path),
+                                || worktree.absolutize(relative_path),
                                 Path::to_path_buf,
                             )
                         });
@@ -4031,7 +4034,7 @@ impl EditorElement {
                                     Some(Box::new(zed_actions::workspace::CopyRelativePath)),
                                     window.handler_for(&editor, move |_, _, cx| {
                                         cx.write_to_clipboard(ClipboardItem::new_string(
-                                            relative_path.to_string_lossy().to_string(),
+                                            relative_path.display(path_style).to_string(),
                                         ));
                                     }),
                                 )

crates/editor/src/git/blame.rs 🔗

@@ -698,6 +698,7 @@ async fn parse_commit_messages(
 #[cfg(test)]
 mod tests {
     use super::*;
+    use git::repository::repo_path;
     use gpui::Context;
     use language::{Point, Rope};
     use project::FakeFs;
@@ -850,7 +851,7 @@ mod tests {
         fs.set_blame_for_repo(
             Path::new("/my-repo/.git"),
             vec![(
-                "file.txt".into(),
+                repo_path("file.txt"),
                 Blame {
                     entries: vec![
                         blame_entry("1b1b1b", 0..1),
@@ -967,7 +968,7 @@ mod tests {
         fs.set_blame_for_repo(
             Path::new(path!("/my-repo/.git")),
             vec![(
-                "file.txt".into(),
+                repo_path("file.txt"),
                 Blame {
                     entries: vec![blame_entry("1b1b1b", 0..4)],
                     ..Default::default()
@@ -1135,7 +1136,7 @@ mod tests {
         fs.set_blame_for_repo(
             Path::new(path!("/my-repo/.git")),
             vec![(
-                "file.txt".into(),
+                repo_path("file.txt"),
                 Blame {
                     entries: blame_entries,
                     ..Default::default()
@@ -1178,7 +1179,7 @@ mod tests {
                     fs.set_blame_for_repo(
                         Path::new(path!("/my-repo/.git")),
                         vec![(
-                            "file.txt".into(),
+                            repo_path("file.txt"),
                             Blame {
                                 entries: blame_entries,
                                 ..Default::default()

crates/editor/src/items.rs 🔗

@@ -651,7 +651,7 @@ impl Item for Editor {
 
     fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString {
         if let Some(path) = path_for_buffer(&self.buffer, detail, true, cx) {
-            path.to_string_lossy().to_string().into()
+            path.to_string().into()
         } else {
             // Use the same logic as the displayed title for consistency
             self.buffer.read(cx).title(cx).to_string().into()
@@ -667,7 +667,7 @@ impl Item for Editor {
             .file_icons
             .then(|| {
                 path_for_buffer(&self.buffer, 0, true, cx)
-                    .and_then(|path| FileIcons::get_icon(path.as_ref(), cx))
+                    .and_then(|path| FileIcons::get_icon(Path::new(&*path), cx))
             })
             .flatten()
             .map(Icon::from_path)
@@ -703,8 +703,7 @@ impl Item for Editor {
 
         let description = params.detail.and_then(|detail| {
             let path = path_for_buffer(&self.buffer, detail, false, cx)?;
-            let description = path.to_string_lossy();
-            let description = description.trim();
+            let description = path.trim();
 
             if description.is_empty() {
                 return None;
@@ -898,10 +897,7 @@ impl Item for Editor {
             .as_singleton()
             .expect("cannot call save_as on an excerpt list");
 
-        let file_extension = path
-            .path
-            .extension()
-            .map(|a| a.to_string_lossy().to_string());
+        let file_extension = path.path.extension().map(|a| a.to_string());
         self.report_editor_event(
             ReportEditorEvent::Saved { auto_saved: false },
             file_extension,
@@ -1167,7 +1163,7 @@ impl SerializableItem for Editor {
                     let (worktree, path) = project.find_worktree(&abs_path, cx)?;
                     let project_path = ProjectPath {
                         worktree_id: worktree.read(cx).id(),
-                        path: path.into(),
+                        path: path,
                     };
                     Some(project.open_path(project_path, cx))
                 });
@@ -1288,7 +1284,7 @@ impl SerializableItem for Editor {
             project
                 .read(cx)
                 .worktree_for_id(worktree_id, cx)
-                .and_then(|worktree| worktree.read(cx).absolutize(file.path()).ok())
+                .map(|worktree| worktree.read(cx).absolutize(file.path()))
                 .or_else(|| {
                     let full_path = file.full_path(cx);
                     let project_path = project.read(cx).find_project_path(&full_path, cx)?;
@@ -1882,7 +1878,7 @@ fn path_for_buffer<'a>(
     height: usize,
     include_filename: bool,
     cx: &'a App,
-) -> Option<Cow<'a, Path>> {
+) -> Option<Cow<'a, str>> {
     let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
     path_for_file(file.as_ref(), height, include_filename, cx)
 }
@@ -1892,7 +1888,7 @@ fn path_for_file<'a>(
     mut height: usize,
     include_filename: bool,
     cx: &'a App,
-) -> Option<Cow<'a, Path>> {
+) -> Option<Cow<'a, str>> {
     // Ensure we always render at least the filename.
     height += 1;
 
@@ -1906,22 +1902,21 @@ fn path_for_file<'a>(
         }
     }
 
-    // Here we could have just always used `full_path`, but that is very
-    // allocation-heavy and so we try to use a `Cow<Path>` if we haven't
-    // traversed all the way up to the worktree's root.
+    // The full_path method allocates, so avoid calling it if height is zero.
     if height > 0 {
-        let full_path = file.full_path(cx);
-        if include_filename {
-            Some(full_path.into())
-        } else {
-            Some(full_path.parent()?.to_path_buf().into())
+        let mut full_path = file.full_path(cx);
+        if !include_filename {
+            if !full_path.pop() {
+                return None;
+            }
         }
+        Some(full_path.to_string_lossy().to_string().into())
     } else {
         let mut path = file.path().strip_prefix(prefix).ok()?;
         if !include_filename {
             path = path.parent()?;
         }
-        Some(path.into())
+        Some(path.display(file.path_style(cx)))
     }
 }
 
@@ -1936,12 +1931,12 @@ mod tests {
     use language::{LanguageMatcher, TestFile};
     use project::FakeFs;
     use std::path::{Path, PathBuf};
-    use util::path;
+    use util::{path, rel_path::RelPath};
 
     #[gpui::test]
     fn test_path_for_file(cx: &mut App) {
         let file = TestFile {
-            path: Path::new("").into(),
+            path: RelPath::empty().into(),
             root_name: String::new(),
             local_root: None,
         };

crates/editor/src/test.rs 🔗

@@ -217,15 +217,7 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo
                 height,
             } => {
                 lines[row.0 as usize].push_str(&cx.update(|_, cx| {
-                    format!(
-                        "§ {}",
-                        first_excerpt
-                            .buffer
-                            .file()
-                            .unwrap()
-                            .file_name(cx)
-                            .to_string_lossy()
-                    )
+                    format!("§ {}", first_excerpt.buffer.file().unwrap().file_name(cx))
                 }));
                 for row in row.0 + 1..row.0 + height {
                     lines[row as usize].push_str("§ -----");
@@ -237,17 +229,11 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo
                 }
             }
             Block::BufferHeader { excerpt, height } => {
-                lines[row.0 as usize].push_str(&cx.update(|_, cx| {
-                    format!(
-                        "§ {}",
-                        excerpt
-                            .buffer
-                            .file()
-                            .unwrap()
-                            .file_name(cx)
-                            .to_string_lossy()
-                    )
-                }));
+                lines[row.0 as usize].push_str(
+                    &cx.update(|_, cx| {
+                        format!("§ {}", excerpt.buffer.file().unwrap().file_name(cx))
+                    }),
+                );
                 for row in row.0 + 1..row.0 + height {
                     lines[row as usize].push_str("§ -----");
                 }

crates/editor/src/test/editor_test_context.rs 🔗

@@ -296,7 +296,7 @@ impl EditorTestContext {
         let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
         fs.set_head_for_repo(
             &Self::root_path().join(".git"),
-            &[(path.into(), diff_base.to_string())],
+            &[(path.as_str(), diff_base.to_string())],
             "deadbeef",
         );
         self.cx.run_until_parked();
@@ -317,7 +317,7 @@ impl EditorTestContext {
         let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
         fs.set_index_for_repo(
             &Self::root_path().join(".git"),
-            &[(path.into(), diff_base.to_string())],
+            &[(path.as_str(), diff_base.to_string())],
         );
         self.cx.run_until_parked();
     }
@@ -329,7 +329,7 @@ impl EditorTestContext {
         let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
         let mut found = None;
         fs.with_git_state(&Self::root_path().join(".git"), false, |git_state| {
-            found = git_state.index_contents.get(path.as_ref()).cloned();
+            found = git_state.index_contents.get(&path.into()).cloned();
         })
         .unwrap();
         assert_eq!(expected, found.as_deref());

crates/eval/src/example.rs 🔗

@@ -1,7 +1,6 @@
 use std::{
     error::Error,
     fmt::{self, Debug},
-    path::Path,
     sync::{Arc, Mutex},
     time::Duration,
 };
@@ -20,6 +19,7 @@ use collections::HashMap;
 use futures::{FutureExt as _, StreamExt, channel::mpsc, select_biased};
 use gpui::{App, AppContext, AsyncApp, Entity};
 use language_model::{LanguageModel, Role, StopReason};
+use util::rel_path::RelPath;
 
 pub const THREAD_EVENT_TIMEOUT: Duration = Duration::from_secs(60 * 2);
 
@@ -354,7 +354,7 @@ impl ExampleContext {
         Ok(response)
     }
 
-    pub fn edits(&self) -> HashMap<Arc<Path>, FileEdits> {
+    pub fn edits(&self) -> HashMap<Arc<RelPath>, FileEdits> {
         self.agent_thread
             .read_with(&self.app, |thread, cx| {
                 let action_log = thread.action_log().read(cx);

crates/eval/src/examples/add_arg_to_trait_method.rs 🔗

@@ -1,8 +1,7 @@
-use std::path::Path;
-
 use agent_settings::AgentProfileId;
 use anyhow::Result;
 use async_trait::async_trait;
+use util::rel_path::RelPath;
 
 use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion, LanguageServer};
 
@@ -68,7 +67,7 @@ impl Example for AddArgToTraitMethod {
 
         for tool_name in add_ignored_window_paths {
             let path_str = format!("crates/assistant_tools/src/{}.rs", tool_name);
-            let edits = edits.get(Path::new(&path_str));
+            let edits = edits.get(RelPath::new(&path_str).unwrap());
 
             let ignored = edits.is_some_and(|edits| {
                 edits.has_added_line("        _window: Option<gpui::AnyWindowHandle>,\n")
@@ -86,7 +85,8 @@ impl Example for AddArgToTraitMethod {
 
         // Adds unignored argument to `batch_tool`
 
-        let batch_tool_edits = edits.get(Path::new("crates/assistant_tools/src/batch_tool.rs"));
+        let batch_tool_edits =
+            edits.get(RelPath::new("crates/assistant_tools/src/batch_tool.rs").unwrap());
 
         cx.assert(
             batch_tool_edits.is_some_and(|edits| {

crates/eval/src/instance.rs 🔗

@@ -250,7 +250,7 @@ impl ExampleInstance {
                     worktree
                         .files(false, 0)
                         .find_map(|e| {
-                            if e.path.clone().extension().and_then(|ext| ext.to_str())
+                            if e.path.clone().extension()
                                 == Some(&language_server.file_extension)
                             {
                                 Some(ProjectPath {

crates/extension/src/extension.rs 🔗

@@ -16,6 +16,7 @@ use gpui::{App, Task};
 use language::LanguageName;
 use semantic_version::SemanticVersion;
 use task::{SpawnInTerminal, ZedDebugConfig};
+use util::rel_path::RelPath;
 
 pub use crate::capabilities::*;
 pub use crate::extension_events::*;
@@ -33,7 +34,7 @@ pub fn init(cx: &mut App) {
 pub trait WorktreeDelegate: Send + Sync + 'static {
     fn id(&self) -> u64;
     fn root_path(&self) -> String;
-    async fn read_text_file(&self, path: PathBuf) -> Result<String>;
+    async fn read_text_file(&self, path: &RelPath) -> Result<String>;
     async fn which(&self, binary_name: String) -> Option<String>;
     async fn shell_env(&self) -> Vec<(String, String)>;
 }

crates/extension_host/src/extension_host.rs 🔗

@@ -1752,7 +1752,14 @@ impl ExtensionStore {
             })?
             .await?;
             let dest_dir = RemotePathBuf::new(
-                PathBuf::from(&response.tmp_dir).join(missing_extension.clone().id),
+                path_style
+                    .join(&response.tmp_dir, &missing_extension.id)
+                    .with_context(|| {
+                        format!(
+                            "failed to construct destination path: {:?}, {:?}",
+                            response.tmp_dir, missing_extension.id,
+                        )
+                    })?,
                 path_style,
             );
             log::info!("Uploading extension {}", missing_extension.clone().id);

crates/extension_host/src/headless_host.rs 🔗

@@ -1,10 +1,7 @@
 use std::{path::PathBuf, sync::Arc};
 
 use anyhow::{Context as _, Result};
-use client::{
-    TypedEnvelope,
-    proto::{self, FromProto},
-};
+use client::{TypedEnvelope, proto};
 use collections::{HashMap, HashSet};
 use extension::{
     Extension, ExtensionDebugAdapterProviderProxy, ExtensionHostProxy, ExtensionLanguageProxy,
@@ -342,7 +339,7 @@ impl HeadlessExtensionStore {
                         version: extension.version,
                         dev: extension.dev,
                     },
-                    PathBuf::from_proto(envelope.payload.tmp_dir),
+                    PathBuf::from(envelope.payload.tmp_dir),
                     cx,
                 )
             })?

crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs 🔗

@@ -16,6 +16,7 @@ use std::{
     path::{Path, PathBuf},
     sync::{Arc, OnceLock},
 };
+use util::rel_path::RelPath;
 use util::{archive::extract_zip, fs::make_file_executable, maybe};
 use wasmtime::component::{Linker, Resource};
 
@@ -421,12 +422,12 @@ impl ExtensionImports for WasmState {
     ) -> wasmtime::Result<Result<String, String>> {
         self.on_main_thread(|cx| {
             async move {
-                let location = location
-                    .as_ref()
-                    .map(|location| ::settings::SettingsLocation {
+                let location = location.as_ref().and_then(|location| {
+                    Some(::settings::SettingsLocation {
                         worktree_id: WorktreeId::from_proto(location.worktree_id),
-                        path: Path::new(&location.path),
-                    });
+                        path: RelPath::new(&location.path).ok()?,
+                    })
+                });
 
                 cx.update(|cx| match category.as_str() {
                     "language" => {

crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs 🔗

@@ -31,7 +31,7 @@ use std::{
 };
 use task::{SpawnInTerminal, ZedDebugConfig};
 use url::Url;
-use util::{archive::extract_zip, fs::make_file_executable, maybe};
+use util::{archive::extract_zip, fs::make_file_executable, maybe, rel_path::RelPath};
 use wasmtime::component::{Linker, Resource};
 
 pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 6, 0);
@@ -564,7 +564,7 @@ impl HostWorktree for WasmState {
     ) -> wasmtime::Result<Result<String, String>> {
         let delegate = self.table.get(&delegate)?;
         Ok(delegate
-            .read_text_file(path.into())
+            .read_text_file(RelPath::new(&path)?)
             .await
             .map_err(|error| error.to_string()))
     }
@@ -914,12 +914,12 @@ impl ExtensionImports for WasmState {
     ) -> wasmtime::Result<Result<String, String>> {
         self.on_main_thread(|cx| {
             async move {
-                let location = location
-                    .as_ref()
-                    .map(|location| ::settings::SettingsLocation {
+                let location = location.as_ref().and_then(|location| {
+                    Some(::settings::SettingsLocation {
                         worktree_id: WorktreeId::from_proto(location.worktree_id),
-                        path: Path::new(&location.path),
-                    });
+                        path: RelPath::new(&location.path).ok()?,
+                    })
+                });
 
                 cx.update(|cx| match category.as_str() {
                     "language" => {

crates/extensions_ui/src/extension_suggest.rs 🔗

@@ -1,5 +1,4 @@
 use std::collections::HashMap;
-use std::path::Path;
 use std::sync::{Arc, OnceLock};
 
 use db::kvp::KEY_VALUE_STORE;
@@ -8,6 +7,7 @@ use extension_host::ExtensionStore;
 use gpui::{AppContext as _, Context, Entity, SharedString, Window};
 use language::Buffer;
 use ui::prelude::*;
+use util::rel_path::RelPath;
 use workspace::notifications::simple_message_notification::MessageNotification;
 use workspace::{Workspace, notifications::NotificationId};
 
@@ -100,15 +100,9 @@ struct SuggestedExtension {
 }
 
 /// Returns the suggested extension for the given [`Path`].
-fn suggested_extension(path: impl AsRef<Path>) -> Option<SuggestedExtension> {
-    let path = path.as_ref();
-
-    let file_extension: Option<Arc<str>> = path
-        .extension()
-        .and_then(|extension| Some(extension.to_str()?.into()));
-    let file_name: Option<Arc<str>> = path
-        .file_name()
-        .and_then(|file_name| Some(file_name.to_str()?.into()));
+fn suggested_extension(path: &RelPath) -> Option<SuggestedExtension> {
+    let file_extension: Option<Arc<str>> = path.extension().map(|extension| extension.into());
+    let file_name: Option<Arc<str>> = path.file_name().map(|name| name.into());
 
     let (file_name_or_extension, extension_id) = None
         // We suggest against file names first, as these suggestions will be more
@@ -210,39 +204,40 @@ pub(crate) fn suggest(buffer: Entity<Buffer>, window: &mut Window, cx: &mut Cont
 #[cfg(test)]
 mod tests {
     use super::*;
+    use util::rel_path::rel_path;
 
     #[test]
     pub fn test_suggested_extension() {
         assert_eq!(
-            suggested_extension("Cargo.toml"),
+            suggested_extension(rel_path("Cargo.toml")),
             Some(SuggestedExtension {
                 extension_id: "toml".into(),
                 file_name_or_extension: "toml".into()
             })
         );
         assert_eq!(
-            suggested_extension("Cargo.lock"),
+            suggested_extension(rel_path("Cargo.lock")),
             Some(SuggestedExtension {
                 extension_id: "toml".into(),
                 file_name_or_extension: "Cargo.lock".into()
             })
         );
         assert_eq!(
-            suggested_extension("Dockerfile"),
+            suggested_extension(rel_path("Dockerfile")),
             Some(SuggestedExtension {
                 extension_id: "dockerfile".into(),
                 file_name_or_extension: "Dockerfile".into()
             })
         );
         assert_eq!(
-            suggested_extension("a/b/c/d/.gitignore"),
+            suggested_extension(rel_path("a/b/c/d/.gitignore")),
             Some(SuggestedExtension {
                 extension_id: "git-firefly".into(),
                 file_name_or_extension: ".gitignore".into()
             })
         );
         assert_eq!(
-            suggested_extension("a/b/c/d/test.gleam"),
+            suggested_extension(rel_path("a/b/c/d/test.gleam")),
             Some(SuggestedExtension {
                 extension_id: "gleam".into(),
                 file_name_or_extension: "gleam".into()

crates/file_finder/src/file_finder.rs 🔗

@@ -39,7 +39,12 @@ use ui::{
     ButtonLike, ContextMenu, HighlightedLabel, Indicator, KeyBinding, ListItem, ListItemSpacing,
     PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
 };
-use util::{ResultExt, maybe, paths::PathWithPosition, post_inc};
+use util::{
+    ResultExt, maybe,
+    paths::{PathStyle, PathWithPosition},
+    post_inc,
+    rel_path::RelPath,
+};
 use workspace::{
     ModalView, OpenOptions, OpenVisible, SplitDirection, Workspace, item::PreviewTabsSettings,
     notifications::NotifyResultExt, pane,
@@ -126,38 +131,34 @@ impl FileFinder {
         let project = workspace.project().read(cx);
         let fs = project.fs();
 
-        let currently_opened_path = workspace
-            .active_item(cx)
-            .and_then(|item| item.project_path(cx))
-            .map(|project_path| {
-                let abs_path = project
-                    .worktree_for_id(project_path.worktree_id, cx)
-                    .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
-                FoundPath::new(project_path, abs_path)
-            });
+        let currently_opened_path = workspace.active_item(cx).and_then(|item| {
+            let project_path = item.project_path(cx)?;
+            let abs_path = project
+                .worktree_for_id(project_path.worktree_id, cx)?
+                .read(cx)
+                .absolutize(&project_path.path);
+            Some(FoundPath::new(project_path, abs_path))
+        });
 
         let history_items = workspace
             .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
             .into_iter()
             .filter_map(|(project_path, abs_path)| {
                 if project.entry_for_path(&project_path, cx).is_some() {
-                    return Some(Task::ready(Some(FoundPath::new(project_path, abs_path))));
+                    return Some(Task::ready(Some(FoundPath::new(project_path, abs_path?))));
                 }
                 let abs_path = abs_path?;
                 if project.is_local() {
                     let fs = fs.clone();
                     Some(cx.background_spawn(async move {
                         if fs.is_file(&abs_path).await {
-                            Some(FoundPath::new(project_path, Some(abs_path)))
+                            Some(FoundPath::new(project_path, abs_path))
                         } else {
                             None
                         }
                     }))
                 } else {
-                    Some(Task::ready(Some(FoundPath::new(
-                        project_path,
-                        Some(abs_path),
-                    ))))
+                    Some(Task::ready(Some(FoundPath::new(project_path, abs_path))))
                 }
             })
             .collect::<Vec<_>>();
@@ -465,7 +466,7 @@ enum Match {
 }
 
 impl Match {
-    fn relative_path(&self) -> Option<&Arc<Path>> {
+    fn relative_path(&self) -> Option<&Arc<RelPath>> {
         match self {
             Match::History { path, .. } => Some(&path.project.path),
             Match::Search(panel_match) => Some(&panel_match.0.path),
@@ -475,20 +476,14 @@ impl Match {
 
     fn abs_path(&self, project: &Entity<Project>, cx: &App) -> Option<PathBuf> {
         match self {
-            Match::History { path, .. } => path.absolute.clone().or_else(|| {
+            Match::History { path, .. } => Some(path.absolute.clone()),
+            Match::Search(ProjectPanelOrdMatch(path_match)) => Some(
                 project
                     .read(cx)
-                    .worktree_for_id(path.project.worktree_id, cx)?
+                    .worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx)?
                     .read(cx)
-                    .absolutize(&path.project.path)
-                    .ok()
-            }),
-            Match::Search(ProjectPanelOrdMatch(path_match)) => project
-                .read(cx)
-                .worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx)?
-                .read(cx)
-                .absolutize(&path_match.path)
-                .ok(),
+                    .absolutize(&path_match.path),
+            ),
             Match::CreateNew(_) => None,
         }
     }
@@ -671,10 +666,9 @@ impl Matches {
         }
 
         if let Some(filename) = panel_match.0.path.file_name() {
-            let path_str = panel_match.0.path.to_string_lossy();
-            let filename_str = filename.to_string_lossy();
+            let path_str = panel_match.0.path.as_str();
 
-            if let Some(filename_pos) = path_str.rfind(&*filename_str)
+            if let Some(filename_pos) = path_str.rfind(filename)
                 && panel_match.0.positions[0] >= filename_pos
             {
                 let mut prev_position = panel_match.0.positions[0];
@@ -696,7 +690,7 @@ fn matching_history_items<'a>(
     history_items: impl IntoIterator<Item = &'a FoundPath>,
     currently_opened: Option<&'a FoundPath>,
     query: &FileSearchQuery,
-) -> HashMap<Arc<Path>, Match> {
+) -> HashMap<Arc<RelPath>, Match> {
     let mut candidates_paths = HashMap::default();
 
     let history_items_by_worktrees = history_items
@@ -714,7 +708,7 @@ fn matching_history_items<'a>(
                         .project
                         .path
                         .file_name()?
-                        .to_string_lossy()
+                        .to_string()
                         .to_lowercase()
                         .chars(),
                 ),
@@ -768,11 +762,11 @@ fn matching_history_items<'a>(
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 struct FoundPath {
     project: ProjectPath,
-    absolute: Option<PathBuf>,
+    absolute: PathBuf,
 }
 
 impl FoundPath {
-    fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
+    fn new(project: ProjectPath, absolute: PathBuf) -> Self {
         Self { project, absolute }
     }
 }
@@ -944,47 +938,44 @@ impl FileFinderDelegate {
                 extend_old_matches,
             );
 
-            let filename = &query.raw_query;
-            let mut query_path = Path::new(filename);
-            // add option of creating new file only if path is relative
-            let available_worktree = self
-                .project
-                .read(cx)
-                .visible_worktrees(cx)
-                .filter(|worktree| !worktree.read(cx).is_single_file())
-                .collect::<Vec<_>>();
-            let worktree_count = available_worktree.len();
-            let mut expect_worktree = available_worktree.first().cloned();
-            for worktree in available_worktree {
-                let worktree_root = worktree
+            let path_style = self.project.read(cx).path_style(cx);
+            let query_path = query.raw_query.as_str();
+            if let Ok(mut query_path) = RelPath::from_std_path(Path::new(query_path), path_style) {
+                let available_worktree = self
+                    .project
                     .read(cx)
-                    .abs_path()
-                    .file_name()
-                    .map_or(String::new(), |f| f.to_string_lossy().to_string());
-                if worktree_count > 1 && query_path.starts_with(&worktree_root) {
-                    query_path = query_path
-                        .strip_prefix(&worktree_root)
-                        .unwrap_or(query_path);
-                    expect_worktree = Some(worktree);
-                    break;
+                    .visible_worktrees(cx)
+                    .filter(|worktree| !worktree.read(cx).is_single_file())
+                    .collect::<Vec<_>>();
+                let worktree_count = available_worktree.len();
+                let mut expect_worktree = available_worktree.first().cloned();
+                for worktree in available_worktree {
+                    let worktree_root = worktree.read(cx).root_name();
+                    if worktree_count > 1 {
+                        if let Ok(suffix) = query_path.strip_prefix(worktree_root) {
+                            query_path = suffix.into();
+                            expect_worktree = Some(worktree);
+                            break;
+                        }
+                    }
                 }
-            }
 
-            if let Some(FoundPath { ref project, .. }) = self.currently_opened_path {
-                let worktree_id = project.worktree_id;
-                expect_worktree = self.project.read(cx).worktree_for_id(worktree_id, cx);
-            }
+                if let Some(FoundPath { ref project, .. }) = self.currently_opened_path {
+                    let worktree_id = project.worktree_id;
+                    expect_worktree = self.project.read(cx).worktree_for_id(worktree_id, cx);
+                }
 
-            if let Some(worktree) = expect_worktree {
-                let worktree = worktree.read(cx);
-                if query_path.is_relative()
-                    && worktree.entry_for_path(&query_path).is_none()
-                    && !filename.ends_with("/")
-                {
-                    self.matches.matches.push(Match::CreateNew(ProjectPath {
-                        worktree_id: worktree.id(),
-                        path: Arc::from(query_path),
-                    }));
+                if let Some(worktree) = expect_worktree {
+                    let worktree = worktree.read(cx);
+                    if worktree.entry_for_path(&query_path).is_none()
+                        && !query.raw_query.ends_with("/")
+                        && !(path_style.is_windows() && query.raw_query.ends_with("\\"))
+                    {
+                        self.matches.matches.push(Match::CreateNew(ProjectPath {
+                            worktree_id: worktree.id(),
+                            path: query_path,
+                        }));
+                    }
                 }
             }
 
@@ -1009,8 +1000,8 @@ impl FileFinderDelegate {
         path_match: &Match,
         window: &mut Window,
         cx: &App,
-        ix: usize,
     ) -> (HighlightedLabel, HighlightedLabel) {
+        let path_style = self.project.read(cx).path_style(cx);
         let (file_name, file_name_positions, mut full_path, mut full_path_positions) =
             match &path_match {
                 Match::History {
@@ -1018,68 +1009,52 @@ impl FileFinderDelegate {
                     panel_match,
                 } => {
                     let worktree_id = entry_path.project.worktree_id;
-                    let project_relative_path = &entry_path.project.path;
-                    let has_worktree = self
+                    let worktree = self
                         .project
                         .read(cx)
                         .worktree_for_id(worktree_id, cx)
-                        .is_some();
-
-                    if let Some(absolute_path) =
-                        entry_path.absolute.as_ref().filter(|_| !has_worktree)
-                    {
+                        .filter(|worktree| worktree.read(cx).is_visible());
+
+                    if let Some(panel_match) = panel_match {
+                        self.labels_for_path_match(&panel_match.0, path_style)
+                    } else if let Some(worktree) = worktree {
+                        let full_path =
+                            worktree.read(cx).root_name().join(&entry_path.project.path);
+                        let mut components = full_path.components();
+                        let filename = components.next_back().unwrap_or("");
+                        let prefix = components.rest();
                         (
-                            absolute_path
-                                .file_name()
-                                .map_or_else(
-                                    || project_relative_path.to_string_lossy(),
-                                    |file_name| file_name.to_string_lossy(),
-                                )
-                                .to_string(),
+                            filename.to_string(),
                             Vec::new(),
-                            absolute_path.to_string_lossy().to_string(),
+                            prefix.display(path_style).to_string() + path_style.separator(),
                             Vec::new(),
                         )
                     } else {
-                        let mut path = Arc::clone(project_relative_path);
-                        if project_relative_path.as_ref() == Path::new("")
-                            && let Some(absolute_path) = &entry_path.absolute
-                        {
-                            path = Arc::from(absolute_path.as_path());
-                        }
-
-                        let mut path_match = PathMatch {
-                            score: ix as f64,
-                            positions: Vec::new(),
-                            worktree_id: worktree_id.to_usize(),
-                            path,
-                            is_dir: false, // File finder doesn't support directories
-                            path_prefix: "".into(),
-                            distance_to_relative_ancestor: usize::MAX,
-                        };
-                        if let Some(found_path_match) = &panel_match {
-                            path_match
-                                .positions
-                                .extend(found_path_match.0.positions.iter())
-                        }
-
-                        self.labels_for_path_match(&path_match)
+                        (
+                            entry_path
+                                .absolute
+                                .file_name()
+                                .map_or(String::new(), |f| f.to_string_lossy().into_owned()),
+                            Vec::new(),
+                            entry_path.absolute.parent().map_or(String::new(), |path| {
+                                path.to_string_lossy().into_owned() + path_style.separator()
+                            }),
+                            Vec::new(),
+                        )
                     }
                 }
-                Match::Search(path_match) => self.labels_for_path_match(&path_match.0),
+                Match::Search(path_match) => self.labels_for_path_match(&path_match.0, path_style),
                 Match::CreateNew(project_path) => (
-                    format!("Create file: {}", project_path.path.display()),
+                    format!("Create file: {}", project_path.path.display(path_style)),
                     vec![],
                     String::from(""),
                     vec![],
                 ),
             };
 
-        if file_name_positions.is_empty()
-            && let Some(user_home_path) = std::env::var("HOME").ok()
-        {
-            let user_home_path = user_home_path.trim();
-            if !user_home_path.is_empty() && full_path.starts_with(user_home_path) {
+        if file_name_positions.is_empty() {
+            let user_home_path = util::paths::home_dir().to_string_lossy();
+            if !user_home_path.is_empty() && full_path.starts_with(&*user_home_path) {
                 full_path.replace_range(0..user_home_path.len(), "~");
                 full_path_positions.retain_mut(|pos| {
                     if *pos >= user_home_path.len() {
@@ -1147,17 +1122,13 @@ impl FileFinderDelegate {
     fn labels_for_path_match(
         &self,
         path_match: &PathMatch,
+        path_style: PathStyle,
     ) -> (String, Vec<usize>, String, Vec<usize>) {
-        let path = &path_match.path;
-        let path_string = path.to_string_lossy();
-        let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
+        let full_path = path_match.path_prefix.join(&path_match.path);
         let mut path_positions = path_match.positions.clone();
 
-        let file_name = path.file_name().map_or_else(
-            || path_match.path_prefix.to_string(),
-            |file_name| file_name.to_string_lossy().to_string(),
-        );
-        let file_name_start = path_match.path_prefix.len() + path_string.len() - file_name.len();
+        let file_name = full_path.file_name().unwrap_or("");
+        let file_name_start = full_path.as_str().len() - file_name.len();
         let file_name_positions = path_positions
             .iter()
             .filter_map(|pos| {
@@ -1167,12 +1138,33 @@ impl FileFinderDelegate {
                     None
                 }
             })
-            .collect();
+            .collect::<Vec<_>>();
 
-        let full_path = full_path.trim_end_matches(&file_name).to_string();
+        let full_path = full_path
+            .display(path_style)
+            .trim_end_matches(&file_name)
+            .to_string();
         path_positions.retain(|idx| *idx < full_path.len());
 
-        (file_name, file_name_positions, full_path, path_positions)
+        debug_assert!(
+            file_name_positions
+                .iter()
+                .all(|ix| file_name[*ix..].chars().next().is_some()),
+            "invalid file name positions {file_name:?} {file_name_positions:?}"
+        );
+        debug_assert!(
+            path_positions
+                .iter()
+                .all(|ix| full_path[*ix..].chars().next().is_some()),
+            "invalid path positions {full_path:?} {path_positions:?}"
+        );
+
+        (
+            file_name.to_string(),
+            file_name_positions,
+            full_path,
+            path_positions,
+        )
     }
 
     fn lookup_absolute_path(
@@ -1210,8 +1202,8 @@ impl FileFinderDelegate {
                                 score: 1.0,
                                 positions: Vec::new(),
                                 worktree_id: worktree.read(cx).id().to_usize(),
-                                path: Arc::from(relative_path),
-                                path_prefix: "".into(),
+                                path: relative_path,
+                                path_prefix: RelPath::empty().into(),
                                 is_dir: false, // File finder doesn't support directories
                                 distance_to_relative_ancestor: usize::MAX,
                             }));
@@ -1333,7 +1325,7 @@ impl PickerDelegate for FileFinderDelegate {
                     .all(|worktree| {
                         worktree
                             .read(cx)
-                            .entry_for_path(Path::new("a"))
+                            .entry_for_path(RelPath::new("a").unwrap())
                             .is_none_or(|entry| !entry.is_dir())
                     })
                 {
@@ -1351,7 +1343,7 @@ impl PickerDelegate for FileFinderDelegate {
                     .all(|worktree| {
                         worktree
                             .read(cx)
-                            .entry_for_path(Path::new("b"))
+                            .entry_for_path(RelPath::new("b").unwrap())
                             .is_none_or(|entry| !entry.is_dir())
                     })
                 {
@@ -1381,8 +1373,8 @@ impl PickerDelegate for FileFinderDelegate {
                         project
                             .worktree_for_id(history_item.project.worktree_id, cx)
                             .is_some()
-                            || ((project.is_local() || project.is_via_remote_server())
-                                && history_item.absolute.is_some())
+                            || project.is_local()
+                            || project.is_via_remote_server()
                     }),
                     self.currently_opened_path.as_ref(),
                     None,
@@ -1397,13 +1389,7 @@ impl PickerDelegate for FileFinderDelegate {
             Task::ready(())
         } else {
             let path_position = PathWithPosition::parse_str(raw_query);
-
-            #[cfg(windows)]
-            let raw_query = raw_query.trim().to_owned().replace("/", "\\");
-            #[cfg(not(windows))]
-            let raw_query = raw_query.trim();
-
-            let raw_query = raw_query.trim_end_matches(':').to_owned();
+            let raw_query = raw_query.trim().trim_end_matches(':').to_owned();
             let path = path_position.path.to_str();
             let path_trimmed = path.unwrap_or(&raw_query).trim_end_matches(':');
             let file_query_end = if path_trimmed == raw_query {
@@ -1505,38 +1491,18 @@ impl PickerDelegate for FileFinderDelegate {
                                 window,
                                 cx,
                             )
+                        } else if secondary {
+                            workspace.split_abs_path(path.absolute.clone(), false, window, cx)
                         } else {
-                            match path.absolute.as_ref() {
-                                Some(abs_path) => {
-                                    if secondary {
-                                        workspace.split_abs_path(
-                                            abs_path.to_path_buf(),
-                                            false,
-                                            window,
-                                            cx,
-                                        )
-                                    } else {
-                                        workspace.open_abs_path(
-                                            abs_path.to_path_buf(),
-                                            OpenOptions {
-                                                visible: Some(OpenVisible::None),
-                                                ..Default::default()
-                                            },
-                                            window,
-                                            cx,
-                                        )
-                                    }
-                                }
-                                None => split_or_open(
-                                    workspace,
-                                    ProjectPath {
-                                        worktree_id,
-                                        path: Arc::clone(&path.project.path),
-                                    },
-                                    window,
-                                    cx,
-                                ),
-                            }
+                            workspace.open_abs_path(
+                                path.absolute.clone(),
+                                OpenOptions {
+                                    visible: Some(OpenVisible::None),
+                                    ..Default::default()
+                                },
+                                window,
+                                cx,
+                            )
                         }
                     }
                     Match::Search(m) => split_or_open(
@@ -1615,7 +1581,7 @@ impl PickerDelegate for FileFinderDelegate {
                 .size(IconSize::Small)
                 .into_any_element(),
         };
-        let (file_name_label, full_path_label) = self.labels_for_match(path_match, window, cx, ix);
+        let (file_name_label, full_path_label) = self.labels_for_match(path_match, window, cx);
 
         let file_icon = maybe!({
             if !settings.file_icons {

crates/file_finder/src/file_finder_tests.rs 🔗

@@ -4,10 +4,10 @@ use super::*;
 use editor::Editor;
 use gpui::{Entity, TestAppContext, VisualTestContext};
 use menu::{Confirm, SelectNext, SelectPrevious};
-use pretty_assertions::assert_eq;
+use pretty_assertions::{assert_eq, assert_matches};
 use project::{FS_WATCH_LATENCY, RemoveOptions};
 use serde_json::json;
-use util::path;
+use util::{path, rel_path::rel_path};
 use workspace::{AppState, CloseActiveItem, OpenOptions, ToggleFileFinder, Workspace};
 
 #[ctor::ctor]
@@ -77,8 +77,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
             score: 0.5,
             positions: Vec::new(),
             worktree_id: 0,
-            path: Arc::from(Path::new("b0.5")),
-            path_prefix: Arc::default(),
+            path: rel_path("b0.5").into(),
+            path_prefix: rel_path("").into(),
             distance_to_relative_ancestor: 0,
             is_dir: false,
         }),
@@ -86,8 +86,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
             score: 1.0,
             positions: Vec::new(),
             worktree_id: 0,
-            path: Arc::from(Path::new("c1.0")),
-            path_prefix: Arc::default(),
+            path: rel_path("c1.0").into(),
+            path_prefix: rel_path("").into(),
             distance_to_relative_ancestor: 0,
             is_dir: false,
         }),
@@ -95,8 +95,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
             score: 1.0,
             positions: Vec::new(),
             worktree_id: 0,
-            path: Arc::from(Path::new("a1.0")),
-            path_prefix: Arc::default(),
+            path: rel_path("a1.0").into(),
+            path_prefix: rel_path("").into(),
             distance_to_relative_ancestor: 0,
             is_dir: false,
         }),
@@ -104,8 +104,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
             score: 0.5,
             positions: Vec::new(),
             worktree_id: 0,
-            path: Arc::from(Path::new("a0.5")),
-            path_prefix: Arc::default(),
+            path: rel_path("a0.5").into(),
+            path_prefix: rel_path("").into(),
             distance_to_relative_ancestor: 0,
             is_dir: false,
         }),
@@ -113,8 +113,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
             score: 1.0,
             positions: Vec::new(),
             worktree_id: 0,
-            path: Arc::from(Path::new("b1.0")),
-            path_prefix: Arc::default(),
+            path: rel_path("b1.0").into(),
+            path_prefix: rel_path("").into(),
             distance_to_relative_ancestor: 0,
             is_dir: false,
         }),
@@ -128,8 +128,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
                 score: 1.0,
                 positions: Vec::new(),
                 worktree_id: 0,
-                path: Arc::from(Path::new("a1.0")),
-                path_prefix: Arc::default(),
+                path: rel_path("a1.0").into(),
+                path_prefix: rel_path("").into(),
                 distance_to_relative_ancestor: 0,
                 is_dir: false,
             }),
@@ -137,8 +137,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
                 score: 1.0,
                 positions: Vec::new(),
                 worktree_id: 0,
-                path: Arc::from(Path::new("b1.0")),
-                path_prefix: Arc::default(),
+                path: rel_path("b1.0").into(),
+                path_prefix: rel_path("").into(),
                 distance_to_relative_ancestor: 0,
                 is_dir: false,
             }),
@@ -146,8 +146,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
                 score: 1.0,
                 positions: Vec::new(),
                 worktree_id: 0,
-                path: Arc::from(Path::new("c1.0")),
-                path_prefix: Arc::default(),
+                path: rel_path("c1.0").into(),
+                path_prefix: rel_path("").into(),
                 distance_to_relative_ancestor: 0,
                 is_dir: false,
             }),
@@ -155,8 +155,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
                 score: 0.5,
                 positions: Vec::new(),
                 worktree_id: 0,
-                path: Arc::from(Path::new("a0.5")),
-                path_prefix: Arc::default(),
+                path: rel_path("a0.5").into(),
+                path_prefix: rel_path("").into(),
                 distance_to_relative_ancestor: 0,
                 is_dir: false,
             }),
@@ -164,8 +164,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
                 score: 0.5,
                 positions: Vec::new(),
                 worktree_id: 0,
-                path: Arc::from(Path::new("b0.5")),
-                path_prefix: Arc::default(),
+                path: rel_path("b0.5").into(),
+                path_prefix: rel_path("").into(),
                 distance_to_relative_ancestor: 0,
                 is_dir: false,
             }),
@@ -366,7 +366,7 @@ async fn test_absolute_paths(cx: &mut TestAppContext) {
     picker.update(cx, |picker, _| {
         assert_eq!(
             collect_search_matches(picker).search_paths_only(),
-            vec![PathBuf::from("a/b/file2.txt")],
+            vec![rel_path("a/b/file2.txt").into()],
             "Matching abs path should be the only match"
         )
     });
@@ -388,7 +388,7 @@ async fn test_absolute_paths(cx: &mut TestAppContext) {
     picker.update(cx, |picker, _| {
         assert_eq!(
             collect_search_matches(picker).search_paths_only(),
-            Vec::<PathBuf>::new(),
+            Vec::new(),
             "Mismatching abs path should produce no matches"
         )
     });
@@ -421,7 +421,7 @@ async fn test_complex_path(cx: &mut TestAppContext) {
         assert_eq!(picker.delegate.matches.len(), 2);
         assert_eq!(
             collect_search_matches(picker).search_paths_only(),
-            vec![PathBuf::from("其他/S数据表格/task.xlsx")],
+            vec![rel_path("其他/S数据表格/task.xlsx").into()],
         )
     });
     cx.dispatch_action(Confirm);
@@ -713,13 +713,13 @@ async fn test_ignored_root(cx: &mut TestAppContext) {
         assert_eq!(
             matches.search,
             vec![
-                PathBuf::from("ignored-root/hi"),
-                PathBuf::from("tracked-root/hi"),
-                PathBuf::from("ignored-root/hiccup"),
-                PathBuf::from("tracked-root/hiccup"),
-                PathBuf::from("ignored-root/height"),
-                PathBuf::from("ignored-root/happiness"),
-                PathBuf::from("tracked-root/happiness"),
+                rel_path("ignored-root/hi").into(),
+                rel_path("tracked-root/hi").into(),
+                rel_path("ignored-root/hiccup").into(),
+                rel_path("tracked-root/hiccup").into(),
+                rel_path("ignored-root/height").into(),
+                rel_path("ignored-root/happiness").into(),
+                rel_path("tracked-root/happiness").into(),
             ],
             "All ignored files that were indexed are found for default ignored mode"
         );
@@ -738,14 +738,14 @@ async fn test_ignored_root(cx: &mut TestAppContext) {
         assert_eq!(
             matches.search,
             vec![
-                PathBuf::from("ignored-root/hi"),
-                PathBuf::from("tracked-root/hi"),
-                PathBuf::from("ignored-root/hiccup"),
-                PathBuf::from("tracked-root/hiccup"),
-                PathBuf::from("ignored-root/height"),
-                PathBuf::from("tracked-root/height"),
-                PathBuf::from("ignored-root/happiness"),
-                PathBuf::from("tracked-root/happiness"),
+                rel_path("ignored-root/hi").into(),
+                rel_path("tracked-root/hi").into(),
+                rel_path("ignored-root/hiccup").into(),
+                rel_path("tracked-root/hiccup").into(),
+                rel_path("ignored-root/height").into(),
+                rel_path("tracked-root/height").into(),
+                rel_path("ignored-root/happiness").into(),
+                rel_path("tracked-root/happiness").into(),
             ],
             "All ignored files should be found, for the toggled on ignored mode"
         );
@@ -765,9 +765,9 @@ async fn test_ignored_root(cx: &mut TestAppContext) {
         assert_eq!(
             matches.search,
             vec![
-                PathBuf::from("tracked-root/hi"),
-                PathBuf::from("tracked-root/hiccup"),
-                PathBuf::from("tracked-root/happiness"),
+                rel_path("tracked-root/hi").into(),
+                rel_path("tracked-root/hiccup").into(),
+                rel_path("tracked-root/happiness").into(),
             ],
             "Only non-ignored files should be found for the turned off ignored mode"
         );
@@ -812,13 +812,13 @@ async fn test_ignored_root(cx: &mut TestAppContext) {
         assert_eq!(
             matches.search,
             vec![
-                PathBuf::from("ignored-root/hi"),
-                PathBuf::from("tracked-root/hi"),
-                PathBuf::from("ignored-root/hiccup"),
-                PathBuf::from("tracked-root/hiccup"),
-                PathBuf::from("ignored-root/height"),
-                PathBuf::from("ignored-root/happiness"),
-                PathBuf::from("tracked-root/happiness"),
+                rel_path("ignored-root/hi").into(),
+                rel_path("tracked-root/hi").into(),
+                rel_path("ignored-root/hiccup").into(),
+                rel_path("tracked-root/hiccup").into(),
+                rel_path("ignored-root/height").into(),
+                rel_path("ignored-root/happiness").into(),
+                rel_path("tracked-root/happiness").into(),
             ],
             "Only for the worktree with the ignored root, all indexed ignored files are found in the auto ignored mode"
         );
@@ -838,16 +838,16 @@ async fn test_ignored_root(cx: &mut TestAppContext) {
         assert_eq!(
             matches.search,
             vec![
-                PathBuf::from("ignored-root/hi"),
-                PathBuf::from("tracked-root/hi"),
-                PathBuf::from("ignored-root/hiccup"),
-                PathBuf::from("tracked-root/hiccup"),
-                PathBuf::from("ignored-root/height"),
-                PathBuf::from("tracked-root/height"),
-                PathBuf::from("tracked-root/heights/height_1"),
-                PathBuf::from("tracked-root/heights/height_2"),
-                PathBuf::from("ignored-root/happiness"),
-                PathBuf::from("tracked-root/happiness"),
+                rel_path("ignored-root/hi").into(),
+                rel_path("tracked-root/hi").into(),
+                rel_path("ignored-root/hiccup").into(),
+                rel_path("tracked-root/hiccup").into(),
+                rel_path("ignored-root/height").into(),
+                rel_path("tracked-root/height").into(),
+                rel_path("tracked-root/heights/height_1").into(),
+                rel_path("tracked-root/heights/height_2").into(),
+                rel_path("ignored-root/happiness").into(),
+                rel_path("tracked-root/happiness").into(),
             ],
             "All ignored files that were indexed are found in the turned on ignored mode"
         );
@@ -867,9 +867,9 @@ async fn test_ignored_root(cx: &mut TestAppContext) {
         assert_eq!(
             matches.search,
             vec![
-                PathBuf::from("tracked-root/hi"),
-                PathBuf::from("tracked-root/hiccup"),
-                PathBuf::from("tracked-root/happiness"),
+                rel_path("tracked-root/hi").into(),
+                rel_path("tracked-root/hiccup").into(),
+                rel_path("tracked-root/happiness").into(),
             ],
             "Only non-ignored files should be found for the turned off ignored mode"
         );
@@ -910,7 +910,7 @@ async fn test_single_file_worktrees(cx: &mut TestAppContext) {
         assert_eq!(matches.len(), 1);
 
         let (file_name, file_name_positions, full_path, full_path_positions) =
-            delegate.labels_for_path_match(&matches[0]);
+            delegate.labels_for_path_match(&matches[0], PathStyle::local());
         assert_eq!(file_name, "the-file");
         assert_eq!(file_name_positions, &[0, 1, 4]);
         assert_eq!(full_path, "");
@@ -968,7 +968,7 @@ async fn test_create_file_for_multiple_worktrees(cx: &mut TestAppContext) {
 
     let b_path = ProjectPath {
         worktree_id: worktree_id2,
-        path: Arc::from(Path::new(path!("the-parent-dirb/fileb"))),
+        path: rel_path("the-parent-dirb/fileb").into(),
     };
     workspace
         .update_in(cx, |workspace, window, cx| {
@@ -1001,7 +1001,7 @@ async fn test_create_file_for_multiple_worktrees(cx: &mut TestAppContext) {
             project_path,
             Some(ProjectPath {
                 worktree_id: worktree_id2,
-                path: Arc::from(Path::new(path!("the-parent-dirb/filec")))
+                path: rel_path("the-parent-dirb/filec").into()
             })
         );
     });
@@ -1038,10 +1038,7 @@ async fn test_create_file_no_focused_with_multiple_worktrees(cx: &mut TestAppCon
     let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
     let (_worktree_id1, worktree_id2) = cx.read(|cx| {
         let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
-        (
-            WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize),
-            WorktreeId::from_usize(worktrees[1].entity_id().as_u64() as usize),
-        )
+        (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
     });
 
     let finder = open_file_picker(&workspace, cx);
@@ -1065,7 +1062,7 @@ async fn test_create_file_no_focused_with_multiple_worktrees(cx: &mut TestAppCon
             project_path,
             Some(ProjectPath {
                 worktree_id: worktree_id2,
-                path: Arc::from(Path::new("filec"))
+                path: rel_path("filec").into()
             })
         );
     });
@@ -1103,7 +1100,7 @@ async fn test_path_distance_ordering(cx: &mut TestAppContext) {
     // so that one should be sorted earlier
     let b_path = ProjectPath {
         worktree_id,
-        path: Arc::from(Path::new("dir2/b.txt")),
+        path: rel_path("dir2/b.txt").into(),
     };
     workspace
         .update_in(cx, |workspace, window, cx| {
@@ -1121,8 +1118,8 @@ async fn test_path_distance_ordering(cx: &mut TestAppContext) {
 
     finder.update(cx, |picker, _| {
         let matches = collect_search_matches(picker).search_paths_only();
-        assert_eq!(matches[0].as_path(), Path::new("dir2/a.txt"));
-        assert_eq!(matches[1].as_path(), Path::new("dir1/a.txt"));
+        assert_eq!(matches[0].as_ref(), rel_path("dir2/a.txt"));
+        assert_eq!(matches[1].as_ref(), rel_path("dir1/a.txt"));
     });
 }
 
@@ -1207,9 +1204,9 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) {
         vec![FoundPath::new(
             ProjectPath {
                 worktree_id,
-                path: Arc::from(Path::new("test/first.rs")),
+                path: rel_path("test/first.rs").into(),
             },
-            Some(PathBuf::from(path!("/src/test/first.rs")))
+            PathBuf::from(path!("/src/test/first.rs"))
         )],
         "Should show 1st opened item in the history when opening the 2nd item"
     );
@@ -1222,16 +1219,16 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) {
             FoundPath::new(
                 ProjectPath {
                     worktree_id,
-                    path: Arc::from(Path::new("test/second.rs")),
+                    path: rel_path("test/second.rs").into(),
                 },
-                Some(PathBuf::from(path!("/src/test/second.rs")))
+                PathBuf::from(path!("/src/test/second.rs"))
             ),
             FoundPath::new(
                 ProjectPath {
                     worktree_id,
-                    path: Arc::from(Path::new("test/first.rs")),
+                    path: rel_path("test/first.rs").into(),
                 },
-                Some(PathBuf::from(path!("/src/test/first.rs")))
+                PathBuf::from(path!("/src/test/first.rs"))
             ),
         ],
         "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
@@ -1246,23 +1243,23 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) {
             FoundPath::new(
                 ProjectPath {
                     worktree_id,
-                    path: Arc::from(Path::new("test/third.rs")),
+                    path: rel_path("test/third.rs").into(),
                 },
-                Some(PathBuf::from(path!("/src/test/third.rs")))
+                PathBuf::from(path!("/src/test/third.rs"))
             ),
             FoundPath::new(
                 ProjectPath {
                     worktree_id,
-                    path: Arc::from(Path::new("test/second.rs")),
+                    path: rel_path("test/second.rs").into(),
                 },
-                Some(PathBuf::from(path!("/src/test/second.rs")))
+                PathBuf::from(path!("/src/test/second.rs"))
             ),
             FoundPath::new(
                 ProjectPath {
                     worktree_id,
-                    path: Arc::from(Path::new("test/first.rs")),
+                    path: rel_path("test/first.rs").into(),
                 },
-                Some(PathBuf::from(path!("/src/test/first.rs")))
+                PathBuf::from(path!("/src/test/first.rs"))
             ),
         ],
         "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
@@ -1277,23 +1274,23 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) {
             FoundPath::new(
                 ProjectPath {
                     worktree_id,
-                    path: Arc::from(Path::new("test/second.rs")),
+                    path: rel_path("test/second.rs").into(),
                 },
-                Some(PathBuf::from(path!("/src/test/second.rs")))
+                PathBuf::from(path!("/src/test/second.rs"))
             ),
             FoundPath::new(
                 ProjectPath {
                     worktree_id,
-                    path: Arc::from(Path::new("test/third.rs")),
+                    path: rel_path("test/third.rs").into(),
                 },
-                Some(PathBuf::from(path!("/src/test/third.rs")))
+                PathBuf::from(path!("/src/test/third.rs"))
             ),
             FoundPath::new(
                 ProjectPath {
                     worktree_id,
-                    path: Arc::from(Path::new("test/first.rs")),
+                    path: rel_path("test/first.rs").into(),
                 },
-                Some(PathBuf::from(path!("/src/test/first.rs")))
+                PathBuf::from(path!("/src/test/first.rs"))
             ),
         ],
         "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
@@ -1301,6 +1298,62 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) {
     );
 }
 
+#[gpui::test]
+async fn test_history_match_positions(cx: &mut gpui::TestAppContext) {
+    let app_state = init_test(cx);
+
+    app_state
+        .fs
+        .as_fake()
+        .insert_tree(
+            path!("/src"),
+            json!({
+                "test": {
+                    "first.rs": "// First Rust file",
+                    "second.rs": "// Second Rust file",
+                    "third.rs": "// Third Rust file",
+                }
+            }),
+        )
+        .await;
+
+    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
+    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
+
+    workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
+
+    open_close_queried_buffer("efir", 1, "first.rs", &workspace, cx).await;
+    let history = open_close_queried_buffer("second", 1, "second.rs", &workspace, cx).await;
+    assert_eq!(history.len(), 1);
+
+    let picker = open_file_picker(&workspace, cx);
+    cx.simulate_input("fir");
+    picker.update_in(cx, |finder, window, cx| {
+        let matches = &finder.delegate.matches.matches;
+        assert_matches!(
+            matches.as_slice(),
+            [Match::History { .. }, Match::CreateNew { .. }]
+        );
+        assert_eq!(
+            matches[0].panel_match().unwrap().0.path.as_ref(),
+            rel_path("test/first.rs")
+        );
+        assert_eq!(matches[0].panel_match().unwrap().0.positions, &[5, 6, 7]);
+
+        let (file_label, path_label) =
+            finder
+                .delegate
+                .labels_for_match(&finder.delegate.matches.matches[0], window, cx);
+        assert_eq!(file_label.text(), "first.rs");
+        assert_eq!(file_label.highlight_indices(), &[0, 1, 2]);
+        assert_eq!(
+            path_label.text(),
+            format!("test{}", PathStyle::local().separator())
+        );
+        assert_eq!(path_label.highlight_indices(), &[] as &[usize]);
+    });
+}
+
 #[gpui::test]
 async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
     let app_state = init_test(cx);
@@ -1392,9 +1445,9 @@ async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
         vec![FoundPath::new(
             ProjectPath {
                 worktree_id: external_worktree_id,
-                path: Arc::from(Path::new("")),
+                path: rel_path("").into(),
             },
-            Some(PathBuf::from(path!("/external-src/test/third.rs")))
+            PathBuf::from(path!("/external-src/test/third.rs"))
         )],
         "Should show external file with its full path in the history after it was open"
     );
@@ -1407,16 +1460,16 @@ async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
             FoundPath::new(
                 ProjectPath {
                     worktree_id,
-                    path: Arc::from(Path::new("test/second.rs")),
+                    path: rel_path("test/second.rs").into(),
                 },
-                Some(PathBuf::from(path!("/src/test/second.rs")))
+                PathBuf::from(path!("/src/test/second.rs"))
             ),
             FoundPath::new(
                 ProjectPath {
                     worktree_id: external_worktree_id,
-                    path: Arc::from(Path::new("")),
+                    path: rel_path("").into(),
                 },
-                Some(PathBuf::from(path!("/external-src/test/third.rs")))
+                PathBuf::from(path!("/external-src/test/third.rs"))
             ),
         ],
         "Should keep external file with history updates",
@@ -1529,12 +1582,12 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
             assert_eq!(history_match, &FoundPath::new(
                 ProjectPath {
                     worktree_id,
-                    path: Arc::from(Path::new("test/first.rs")),
+                    path: rel_path("test/first.rs").into(),
                 },
-                Some(PathBuf::from(path!("/src/test/first.rs")))
+                PathBuf::from(path!("/src/test/first.rs")),
             ));
             assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
-            assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs"));
+            assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
         });
 
     let second_query = "fsdasdsa";
@@ -1572,12 +1625,12 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
             assert_eq!(history_match, &FoundPath::new(
                 ProjectPath {
                     worktree_id,
-                    path: Arc::from(Path::new("test/first.rs")),
+                    path: rel_path("test/first.rs").into(),
                 },
-                Some(PathBuf::from(path!("/src/test/first.rs")))
+                PathBuf::from(path!("/src/test/first.rs"))
             ));
             assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
-            assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs"));
+            assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
         });
 }
 
@@ -1626,13 +1679,16 @@ async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
         let search_matches = collect_search_matches(finder);
         assert_eq!(
             search_matches.history,
-            vec![PathBuf::from("test/1_qw"), PathBuf::from("test/6_qwqwqw"),],
+            vec![
+                rel_path("test/1_qw").into(),
+                rel_path("test/6_qwqwqw").into()
+            ],
         );
         assert_eq!(
             search_matches.search,
             vec![
-                PathBuf::from("test/5_qwqwqw"),
-                PathBuf::from("test/7_qwqwqw"),
+                rel_path("test/5_qwqwqw").into(),
+                rel_path("test/7_qwqwqw").into()
             ],
         );
     });
@@ -2083,10 +2139,10 @@ async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppCo
             assert_eq!(
                 search_entries,
                 vec![
-                    PathBuf::from("collab_ui/collab_ui.rs"),
-                    PathBuf::from("collab_ui/first.rs"),
-                    PathBuf::from("collab_ui/third.rs"),
-                    PathBuf::from("collab_ui/second.rs"),
+                    rel_path("collab_ui/collab_ui.rs").into(),
+                    rel_path("collab_ui/first.rs").into(),
+                    rel_path("collab_ui/third.rs").into(),
+                    rel_path("collab_ui/second.rs").into(),
                 ],
                 "Despite all search results having the same directory name, the most matching one should be on top"
             );
@@ -2135,8 +2191,8 @@ async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext)
         assert_eq!(
             collect_search_matches(picker).history,
             vec![
-                PathBuf::from("test/first.rs"),
-                PathBuf::from("test/third.rs"),
+                rel_path("test/first.rs").into(),
+                rel_path("test/third.rs").into(),
             ],
             "Should have all opened files in the history, except the ones that do not exist on disk"
         );
@@ -2766,15 +2822,15 @@ fn active_file_picker(
 
 #[derive(Debug, Default)]
 struct SearchEntries {
-    history: Vec<PathBuf>,
+    history: Vec<Arc<RelPath>>,
     history_found_paths: Vec<FoundPath>,
-    search: Vec<PathBuf>,
+    search: Vec<Arc<RelPath>>,
     search_matches: Vec<PathMatch>,
 }
 
 impl SearchEntries {
     #[track_caller]
-    fn search_paths_only(self) -> Vec<PathBuf> {
+    fn search_paths_only(self) -> Vec<Arc<RelPath>> {
         assert!(
             self.history.is_empty(),
             "Should have no history matches, but got: {:?}",
@@ -2802,20 +2858,15 @@ fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries
                 path: history_path,
                 panel_match: path_match,
             } => {
-                search_entries.history.push(
-                    path_match
-                        .as_ref()
-                        .map(|path_match| {
-                            Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)
-                        })
-                        .unwrap_or_else(|| {
-                            history_path
-                                .absolute
-                                .as_deref()
-                                .unwrap_or_else(|| &history_path.project.path)
-                                .to_path_buf()
-                        }),
-                );
+                if let Some(path_match) = path_match.as_ref() {
+                    search_entries
+                        .history
+                        .push(path_match.0.path_prefix.join(&path_match.0.path));
+                } else {
+                    // This occurs when the query is empty and we show history matches
+                    // that are outside the project.
+                    panic!("currently not exercised in tests");
+                }
                 search_entries
                     .history_found_paths
                     .push(history_path.clone());
@@ -2823,7 +2874,7 @@ fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries
             Match::Search(path_match) => {
                 search_entries
                     .search
-                    .push(Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path));
+                    .push(path_match.0.path_prefix.join(&path_match.0.path));
                 search_entries.search_matches.push(path_match.0.clone());
             }
             Match::CreateNew(_) => {}
@@ -2858,12 +2909,11 @@ fn assert_match_at_position(
         .get(match_index)
         .unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
     let match_file_name = match &match_item {
-        Match::History { path, .. } => path.absolute.as_deref().unwrap().file_name(),
+        Match::History { path, .. } => path.absolute.file_name().and_then(|s| s.to_str()),
         Match::Search(path_match) => path_match.0.path.file_name(),
         Match::CreateNew(project_path) => project_path.path.file_name(),
     }
-    .unwrap()
-    .to_string_lossy();
+    .unwrap();
     assert_eq!(match_file_name, expected_file_name);
 }
 
@@ -2901,11 +2951,11 @@ async fn test_filename_precedence(cx: &mut TestAppContext) {
         assert_eq!(
             search_matches,
             vec![
-                PathBuf::from("routes/+layout.svelte"),
-                PathBuf::from("layout/app.css"),
-                PathBuf::from("layout/app.d.ts"),
-                PathBuf::from("layout/app.html"),
-                PathBuf::from("layout/+page.svelte"),
+                rel_path("routes/+layout.svelte").into(),
+                rel_path("layout/app.css").into(),
+                rel_path("layout/app.d.ts").into(),
+                rel_path("layout/app.html").into(),
+                rel_path("layout/+page.svelte").into(),
             ],
             "File with 'layout' in filename should be prioritized over files in 'layout' directory"
         );

crates/file_finder/src/open_path_prompt.rs 🔗

@@ -7,7 +7,7 @@ use picker::{Picker, PickerDelegate};
 use project::{DirectoryItem, DirectoryLister};
 use settings::Settings;
 use std::{
-    path::{self, MAIN_SEPARATOR_STR, Path, PathBuf},
+    path::{self, Path, PathBuf},
     sync::{
         Arc,
         atomic::{self, AtomicBool},
@@ -217,7 +217,7 @@ impl OpenPathPrompt {
     ) {
         workspace.toggle_modal(window, cx, |window, cx| {
             let delegate =
-                OpenPathDelegate::new(tx, lister.clone(), creating_path, PathStyle::current());
+                OpenPathDelegate::new(tx, lister.clone(), creating_path, PathStyle::local());
             let picker = Picker::uniform_list(delegate, window, cx).width(rems(34.));
             let query = lister.default_query(cx);
             picker.set_query(query, window, cx);
@@ -822,7 +822,7 @@ impl PickerDelegate for OpenPathDelegate {
     }
 
     fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
-        Arc::from(format!("[directory{MAIN_SEPARATOR_STR}]filename.ext"))
+        Arc::from(format!("[directory{}]filename.ext", self.path_style.separator()).as_str())
     }
 
     fn separators_after_indices(&self) -> Vec<usize> {

crates/file_finder/src/open_path_prompt_tests.rs 🔗

@@ -37,7 +37,7 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) {
 
     let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 
-    let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
+    let (picker, cx) = build_open_path_prompt(project, false, PathStyle::local(), cx);
 
     insert_query(path!("sadjaoislkdjasldj"), &picker, cx).await;
     assert_eq!(collect_match_candidates(&picker, cx), Vec::<String>::new());
@@ -119,7 +119,7 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
 
     let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 
-    let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
+    let (picker, cx) = build_open_path_prompt(project, false, PathStyle::local(), cx);
 
     // Confirm completion for the query "/root", since it's a directory, it should add a trailing slash.
     let query = path!("/root");
@@ -227,7 +227,7 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
 
     let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 
-    let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
+    let (picker, cx) = build_open_path_prompt(project, false, PathStyle::local(), cx);
 
     // Support both forward and backward slashes.
     let query = "C:/root/";
@@ -372,7 +372,7 @@ async fn test_new_path_prompt(cx: &mut TestAppContext) {
 
     let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 
-    let (picker, cx) = build_open_path_prompt(project, true, PathStyle::current(), cx);
+    let (picker, cx) = build_open_path_prompt(project, true, PathStyle::local(), cx);
 
     insert_query(path!("/root"), &picker, cx).await;
     assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);

crates/fs/src/fake_git_repo.rs 🔗

@@ -17,6 +17,7 @@ use parking_lot::Mutex;
 use rope::Rope;
 use smol::future::FutureExt as _;
 use std::{path::PathBuf, sync::Arc};
+use util::{paths::PathStyle, rel_path::RelPath};
 
 #[derive(Clone)]
 pub struct FakeGitRepository {
@@ -82,7 +83,7 @@ impl GitRepository for FakeGitRepository {
             self.with_state_async(false, move |state| {
                 state
                     .index_contents
-                    .get(path.as_ref())
+                    .get(&path)
                     .context("not present in index")
                     .cloned()
             })
@@ -97,7 +98,7 @@ impl GitRepository for FakeGitRepository {
             self.with_state_async(false, move |state| {
                 state
                     .head_contents
-                    .get(path.as_ref())
+                    .get(&path)
                     .context("not present in HEAD")
                     .cloned()
             })
@@ -225,6 +226,7 @@ impl GitRepository for FakeGitRepository {
                     .read_file_sync(path)
                     .ok()
                     .map(|content| String::from_utf8(content).unwrap())?;
+                let repo_path = RelPath::from_std_path(repo_path, PathStyle::local()).ok()?;
                 Some((repo_path.into(), (content, is_ignored)))
             })
             .collect();
@@ -386,7 +388,11 @@ impl GitRepository for FakeGitRepository {
             let contents = paths
                 .into_iter()
                 .map(|path| {
-                    let abs_path = self.dot_git_path.parent().unwrap().join(&path);
+                    let abs_path = self
+                        .dot_git_path
+                        .parent()
+                        .unwrap()
+                        .join(&path.as_std_path());
                     Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) })
                 })
                 .collect::<Vec<_>>();

crates/fs/src/fs.rs 🔗

@@ -47,7 +47,7 @@ use collections::{BTreeMap, btree_map};
 use fake_git_repo::FakeGitRepositoryState;
 #[cfg(any(test, feature = "test-support"))]
 use git::{
-    repository::RepoPath,
+    repository::{RepoPath, repo_path},
     status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus},
 };
 #[cfg(any(test, feature = "test-support"))]
@@ -1608,13 +1608,13 @@ impl FakeFs {
         .unwrap();
     }
 
-    pub fn set_index_for_repo(&self, dot_git: &Path, index_state: &[(RepoPath, String)]) {
+    pub fn set_index_for_repo(&self, dot_git: &Path, index_state: &[(&str, String)]) {
         self.with_git_state(dot_git, true, |state| {
             state.index_contents.clear();
             state.index_contents.extend(
                 index_state
                     .iter()
-                    .map(|(path, content)| (path.clone(), content.clone())),
+                    .map(|(path, content)| (repo_path(path), content.clone())),
             );
         })
         .unwrap();
@@ -1623,7 +1623,7 @@ impl FakeFs {
     pub fn set_head_for_repo(
         &self,
         dot_git: &Path,
-        head_state: &[(RepoPath, String)],
+        head_state: &[(&str, String)],
         sha: impl Into<String>,
     ) {
         self.with_git_state(dot_git, true, |state| {
@@ -1631,50 +1631,22 @@ impl FakeFs {
             state.head_contents.extend(
                 head_state
                     .iter()
-                    .map(|(path, content)| (path.clone(), content.clone())),
+                    .map(|(path, content)| (repo_path(path), content.clone())),
             );
             state.refs.insert("HEAD".into(), sha.into());
         })
         .unwrap();
     }
 
-    pub fn set_git_content_for_repo(
-        &self,
-        dot_git: &Path,
-        head_state: &[(RepoPath, String, Option<String>)],
-    ) {
+    pub fn set_head_and_index_for_repo(&self, dot_git: &Path, contents_by_path: &[(&str, String)]) {
         self.with_git_state(dot_git, true, |state| {
             state.head_contents.clear();
             state.head_contents.extend(
-                head_state
+                contents_by_path
                     .iter()
-                    .map(|(path, head_content, _)| (path.clone(), head_content.clone())),
+                    .map(|(path, contents)| (repo_path(path), contents.clone())),
             );
-            state.index_contents.clear();
-            state.index_contents.extend(head_state.iter().map(
-                |(path, head_content, index_content)| {
-                    (
-                        path.clone(),
-                        index_content.as_ref().unwrap_or(head_content).clone(),
-                    )
-                },
-            ));
-        })
-        .unwrap();
-    }
-
-    pub fn set_head_and_index_for_repo(
-        &self,
-        dot_git: &Path,
-        contents_by_path: &[(RepoPath, String)],
-    ) {
-        self.with_git_state(dot_git, true, |state| {
-            state.head_contents.clear();
-            state.index_contents.clear();
-            state.head_contents.extend(contents_by_path.iter().cloned());
-            state
-                .index_contents
-                .extend(contents_by_path.iter().cloned());
+            state.index_contents = state.head_contents.clone();
         })
         .unwrap();
     }
@@ -1689,7 +1661,7 @@ impl FakeFs {
 
     /// Put the given git repository into a state with the given status,
     /// by mutating the head, index, and unmerged state.
-    pub fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, FileStatus)]) {
+    pub fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&str, FileStatus)]) {
         let workdir_path = dot_git.parent().unwrap();
         let workdir_contents = self.files_with_contents(workdir_path);
         self.with_git_state(dot_git, true, |state| {
@@ -1697,10 +1669,12 @@ impl FakeFs {
             state.head_contents.clear();
             state.unmerged_paths.clear();
             for (path, content) in workdir_contents {
-                let repo_path: RepoPath = path.strip_prefix(&workdir_path).unwrap().into();
+                use util::{paths::PathStyle, rel_path::RelPath};
+
+                let repo_path: RepoPath = RelPath::from_std_path(path.strip_prefix(&workdir_path).unwrap(), PathStyle::local()).unwrap().into();
                 let status = statuses
                     .iter()
-                    .find_map(|(p, status)| (**p == *repo_path.0).then_some(status));
+                    .find_map(|(p, status)| (*p == repo_path.as_str()).then_some(status));
                 let mut content = String::from_utf8_lossy(&content).to_string();
 
                 let mut index_content = None;

crates/fuzzy/Cargo.toml 🔗

@@ -17,3 +17,6 @@ gpui.workspace = true
 util.workspace = true
 log.workspace = true
 workspace-hack.workspace = true
+
+[dev-dependencies]
+util = {workspace = true, features = ["test-support"]}

crates/fuzzy/src/matcher.rs 🔗

@@ -1,5 +1,5 @@
 use std::{
-    borrow::{Borrow, Cow},
+    borrow::Borrow,
     collections::BTreeMap,
     sync::atomic::{self, AtomicBool},
 };
@@ -27,7 +27,7 @@ pub struct Matcher<'a> {
 
 pub trait MatchCandidate {
     fn has_chars(&self, bag: CharBag) -> bool;
-    fn to_string(&self) -> Cow<'_, str>;
+    fn candidate_chars(&self) -> impl Iterator<Item = char>;
 }
 
 impl<'a> Matcher<'a> {
@@ -83,7 +83,7 @@ impl<'a> Matcher<'a> {
             candidate_chars.clear();
             lowercase_candidate_chars.clear();
             extra_lowercase_chars.clear();
-            for (i, c) in candidate.borrow().to_string().chars().enumerate() {
+            for (i, c) in candidate.borrow().candidate_chars().enumerate() {
                 candidate_chars.push(c);
                 let mut char_lowercased = c.to_lowercase().collect::<Vec<_>>();
                 if char_lowercased.len() > 1 {
@@ -202,8 +202,6 @@ impl<'a> Matcher<'a> {
         cur_score: f64,
         extra_lowercase_chars: &BTreeMap<usize, usize>,
     ) -> f64 {
-        use std::path::MAIN_SEPARATOR;
-
         if query_idx == self.query.len() {
             return 1.0;
         }
@@ -245,17 +243,11 @@ impl<'a> Matcher<'a> {
                     None => continue,
                 }
             };
-            let is_path_sep = path_char == MAIN_SEPARATOR;
+            let is_path_sep = path_char == '/';
 
             if query_idx == 0 && is_path_sep {
                 last_slash = j_regular;
             }
-
-            #[cfg(not(target_os = "windows"))]
-            let need_to_score =
-                query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\');
-            // `query_char == '\\'` breaks `test_match_path_entries` on Windows, `\` is only used as a path separator on Windows.
-            #[cfg(target_os = "windows")]
             let need_to_score = query_char == path_char || (is_path_sep && query_char == '_');
             if need_to_score {
                 let curr = match prefix.get(j_regular) {
@@ -270,7 +262,7 @@ impl<'a> Matcher<'a> {
                         None => path[j_regular - 1 - prefix.len()],
                     };
 
-                    if last == MAIN_SEPARATOR {
+                    if last == '/' {
                         char_score = 0.9;
                     } else if (last == '-' || last == '_' || last == ' ' || last.is_numeric())
                         || (last.is_lowercase() && curr.is_uppercase())
@@ -291,7 +283,7 @@ impl<'a> Matcher<'a> {
                 // Apply a severe penalty if the case doesn't match.
                 // This will make the exact matches have higher score than the case-insensitive and the
                 // path insensitive matches.
-                if (self.smart_case || curr == MAIN_SEPARATOR) && self.query[query_idx] != curr {
+                if (self.smart_case || curr == '/') && self.query[query_idx] != curr {
                     char_score *= 0.001;
                 }
 
@@ -348,13 +340,12 @@ impl<'a> Matcher<'a> {
 
 #[cfg(test)]
 mod tests {
+    use util::rel_path::{RelPath, rel_path};
+
     use crate::{PathMatch, PathMatchCandidate};
 
     use super::*;
-    use std::{
-        path::{Path, PathBuf},
-        sync::Arc,
-    };
+    use std::sync::Arc;
 
     #[test]
     fn test_get_last_positions() {
@@ -376,7 +367,6 @@ mod tests {
         assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]);
     }
 
-    #[cfg(not(target_os = "windows"))]
     #[test]
     fn test_match_path_entries() {
         let paths = vec![
@@ -388,9 +378,9 @@ mod tests {
             "alphabravocharlie",
             "AlphaBravoCharlie",
             "thisisatestdir",
-            "/////ThisIsATestDir",
-            "/this/is/a/test/dir",
-            "/test/tiatd",
+            "ThisIsATestDir",
+            "this/is/a/test/dir",
+            "test/tiatd",
         ];
 
         assert_eq!(
@@ -404,63 +394,15 @@ mod tests {
         );
         assert_eq!(
             match_single_path_query("t/i/a/t/d", false, &paths),
-            vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),]
-        );
-
-        assert_eq!(
-            match_single_path_query("tiatd", false, &paths),
-            vec![
-                ("/test/tiatd", vec![6, 7, 8, 9, 10]),
-                ("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]),
-                ("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]),
-                ("thisisatestdir", vec![0, 2, 6, 7, 11]),
-            ]
-        );
-    }
-
-    /// todo(windows)
-    /// Now, on Windows, users can only use the backslash as a path separator.
-    /// I do want to support both the backslash and the forward slash as path separators on Windows.
-    #[cfg(target_os = "windows")]
-    #[test]
-    fn test_match_path_entries() {
-        let paths = vec![
-            "",
-            "a",
-            "ab",
-            "abC",
-            "abcd",
-            "alphabravocharlie",
-            "AlphaBravoCharlie",
-            "thisisatestdir",
-            "\\\\\\\\\\ThisIsATestDir",
-            "\\this\\is\\a\\test\\dir",
-            "\\test\\tiatd",
-        ];
-
-        assert_eq!(
-            match_single_path_query("abc", false, &paths),
-            vec![
-                ("abC", vec![0, 1, 2]),
-                ("abcd", vec![0, 1, 2]),
-                ("AlphaBravoCharlie", vec![0, 5, 10]),
-                ("alphabravocharlie", vec![4, 5, 10]),
-            ]
-        );
-        assert_eq!(
-            match_single_path_query("t\\i\\a\\t\\d", false, &paths),
-            vec![(
-                "\\this\\is\\a\\test\\dir",
-                vec![1, 5, 6, 8, 9, 10, 11, 15, 16]
-            ),]
+            vec![("this/is/a/test/dir", vec![0, 4, 5, 7, 8, 9, 10, 14, 15]),]
         );
 
         assert_eq!(
             match_single_path_query("tiatd", false, &paths),
             vec![
-                ("\\test\\tiatd", vec![6, 7, 8, 9, 10]),
-                ("\\this\\is\\a\\test\\dir", vec![1, 6, 9, 11, 16]),
-                ("\\\\\\\\\\ThisIsATestDir", vec![5, 9, 11, 12, 16]),
+                ("test/tiatd", vec![5, 6, 7, 8, 9]),
+                ("ThisIsATestDir", vec![0, 4, 6, 7, 11]),
+                ("this/is/a/test/dir", vec![0, 5, 8, 10, 15]),
                 ("thisisatestdir", vec![0, 2, 6, 7, 11]),
             ]
         );
@@ -491,7 +433,7 @@ mod tests {
             "aαbβ/cγdδ",
             "αβγδ/bcde",
             "c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f",
-            "/d/🆒/h",
+            "d/🆒/h",
         ];
         assert_eq!("1️⃣".len(), 7);
         assert_eq!(
@@ -602,9 +544,9 @@ mod tests {
         let query = query.chars().collect::<Vec<_>>();
         let query_chars = CharBag::from(&lowercase_query[..]);
 
-        let path_arcs: Vec<Arc<Path>> = paths
+        let path_arcs: Vec<Arc<RelPath>> = paths
             .iter()
-            .map(|path| Arc::from(PathBuf::from(path)))
+            .map(|path| Arc::from(rel_path(path)))
             .collect::<Vec<_>>();
         let mut path_entries = Vec::new();
         for (i, path) in paths.iter().enumerate() {
@@ -632,8 +574,8 @@ mod tests {
                 score,
                 worktree_id: 0,
                 positions: positions.clone(),
-                path: Arc::from(candidate.path),
-                path_prefix: "".into(),
+                path: candidate.path.into(),
+                path_prefix: RelPath::empty().into(),
                 distance_to_relative_ancestor: usize::MAX,
                 is_dir: false,
             },
@@ -647,7 +589,7 @@ mod tests {
                     paths
                         .iter()
                         .copied()
-                        .find(|p| result.path.as_ref() == Path::new(p))
+                        .find(|p| result.path.as_ref() == rel_path(p))
                         .unwrap(),
                     result.positions,
                 )

crates/fuzzy/src/paths.rs 🔗

@@ -1,13 +1,12 @@
 use gpui::BackgroundExecutor;
 use std::{
-    borrow::Cow,
     cmp::{self, Ordering},
-    path::Path,
     sync::{
         Arc,
         atomic::{self, AtomicBool},
     },
 };
+use util::{paths::PathStyle, rel_path::RelPath};
 
 use crate::{
     CharBag,
@@ -17,7 +16,7 @@ use crate::{
 #[derive(Clone, Debug)]
 pub struct PathMatchCandidate<'a> {
     pub is_dir: bool,
-    pub path: &'a Path,
+    pub path: &'a RelPath,
     pub char_bag: CharBag,
 }
 
@@ -26,8 +25,8 @@ pub struct PathMatch {
     pub score: f64,
     pub positions: Vec<usize>,
     pub worktree_id: usize,
-    pub path: Arc<Path>,
-    pub path_prefix: Arc<str>,
+    pub path: Arc<RelPath>,
+    pub path_prefix: Arc<RelPath>,
     pub is_dir: bool,
     /// Number of steps removed from a shared parent with the relative path
     /// Used to order closer paths first in the search list
@@ -41,8 +40,10 @@ pub trait PathMatchCandidateSet<'a>: Send + Sync {
     fn is_empty(&self) -> bool {
         self.len() == 0
     }
-    fn prefix(&self) -> Arc<str>;
+    fn root_is_file(&self) -> bool;
+    fn prefix(&self) -> Arc<RelPath>;
     fn candidates(&'a self, start: usize) -> Self::Candidates;
+    fn path_style(&self) -> PathStyle;
 }
 
 impl<'a> MatchCandidate for PathMatchCandidate<'a> {
@@ -50,8 +51,8 @@ impl<'a> MatchCandidate for PathMatchCandidate<'a> {
         self.char_bag.is_superset(bag)
     }
 
-    fn to_string(&self) -> Cow<'a, str> {
-        self.path.to_string_lossy()
+    fn candidate_chars(&self) -> impl Iterator<Item = char> {
+        self.path.as_str().chars()
     }
 }
 
@@ -109,8 +110,8 @@ pub fn match_fixed_path_set(
             worktree_id,
             positions: positions.clone(),
             is_dir: candidate.is_dir,
-            path: Arc::from(candidate.path),
-            path_prefix: Arc::default(),
+            path: candidate.path.into(),
+            path_prefix: RelPath::empty().into(),
             distance_to_relative_ancestor: usize::MAX,
         },
     );
@@ -121,7 +122,7 @@ pub fn match_fixed_path_set(
 pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
     candidate_sets: &'a [Set],
     query: &str,
-    relative_to: &Option<Arc<Path>>,
+    relative_to: &Option<Arc<RelPath>>,
     smart_case: bool,
     max_results: usize,
     cancel_flag: &AtomicBool,
@@ -132,12 +133,27 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
         return Vec::new();
     }
 
-    let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
-    let query = query.chars().collect::<Vec<_>>();
+    let path_style = candidate_sets[0].path_style();
+
+    let query = query
+        .chars()
+        .map(|char| {
+            if path_style.is_windows() && char == '\\' {
+                '/'
+            } else {
+                char
+            }
+        })
+        .collect::<Vec<_>>();
+
+    let lowercase_query = query
+        .iter()
+        .map(|query| query.to_ascii_lowercase())
+        .collect::<Vec<_>>();
 
-    let lowercase_query = &lowercase_query;
     let query = &query;
-    let query_char_bag = CharBag::from(&lowercase_query[..]);
+    let lowercase_query = &lowercase_query;
+    let query_char_bag = CharBag::from_iter(lowercase_query.iter().copied());
 
     let num_cpus = executor.num_cpus().min(path_count);
     let segment_size = path_count.div_ceil(num_cpus);
@@ -168,7 +184,11 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
                             let candidates = candidate_set.candidates(start).take(end - start);
 
                             let worktree_id = candidate_set.id();
-                            let prefix = candidate_set.prefix().chars().collect::<Vec<_>>();
+                            let mut prefix =
+                                candidate_set.prefix().as_str().chars().collect::<Vec<_>>();
+                            if !candidate_set.root_is_file() && !prefix.is_empty() {
+                                prefix.push('/');
+                            }
                             let lowercase_prefix = prefix
                                 .iter()
                                 .map(|c| c.to_ascii_lowercase())
@@ -219,7 +239,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
 
 /// Compute the distance from a given path to some other path
 /// If there is no shared path, returns usize::MAX
-fn distance_between_paths(path: &Path, relative_to: &Path) -> usize {
+fn distance_between_paths(path: &RelPath, relative_to: &RelPath) -> usize {
     let mut path_components = path.components();
     let mut relative_components = relative_to.components();
 
@@ -234,12 +254,12 @@ fn distance_between_paths(path: &Path, relative_to: &Path) -> usize {
 
 #[cfg(test)]
 mod tests {
-    use std::path::Path;
+    use util::rel_path::RelPath;
 
     use super::distance_between_paths;
 
     #[test]
     fn test_distance_between_paths_empty() {
-        distance_between_paths(Path::new(""), Path::new(""));
+        distance_between_paths(RelPath::empty(), RelPath::empty());
     }
 }

crates/fuzzy/src/strings.rs 🔗

@@ -4,7 +4,7 @@ use crate::{
 };
 use gpui::BackgroundExecutor;
 use std::{
-    borrow::{Borrow, Cow},
+    borrow::Borrow,
     cmp::{self, Ordering},
     iter,
     ops::Range,
@@ -28,13 +28,13 @@ impl StringMatchCandidate {
     }
 }
 
-impl<'a> MatchCandidate for &'a StringMatchCandidate {
+impl MatchCandidate for &StringMatchCandidate {
     fn has_chars(&self, bag: CharBag) -> bool {
         self.char_bag.is_superset(bag)
     }
 
-    fn to_string(&self) -> Cow<'a, str> {
-        self.string.as_str().into()
+    fn candidate_chars(&self) -> impl Iterator<Item = char> {
+        self.string.chars()
     }
 }
 

crates/git/src/blame.rs 🔗

@@ -1,4 +1,5 @@
 use crate::commit::get_messages;
+use crate::repository::RepoPath;
 use crate::{GitRemote, Oid};
 use anyhow::{Context as _, Result};
 use collections::{HashMap, HashSet};
@@ -33,7 +34,7 @@ impl Blame {
     pub async fn for_path(
         git_binary: &Path,
         working_directory: &Path,
-        path: &Path,
+        path: &RepoPath,
         content: &Rope,
         remote_url: Option<String>,
     ) -> Result<Self> {
@@ -66,7 +67,7 @@ const GIT_BLAME_NO_PATH: &str = "fatal: no such path";
 async fn run_git_blame(
     git_binary: &Path,
     working_directory: &Path,
-    path: &Path,
+    path: &RepoPath,
     contents: &Rope,
 ) -> Result<String> {
     let mut child = util::command::new_smol_command(git_binary)
@@ -76,7 +77,7 @@ async fn run_git_blame(
         .arg("-w")
         .arg("--contents")
         .arg("-")
-        .arg(path.as_os_str())
+        .arg(path.as_str())
         .stdin(Stdio::piped())
         .stdout(Stdio::piped())
         .stderr(Stdio::piped())

crates/git/src/commit.rs 🔗

@@ -39,7 +39,7 @@ pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<Hash
 }
 
 /// Parse the output of `git diff --name-status -z`
-pub fn parse_git_diff_name_status(content: &str) -> impl Iterator<Item = (&Path, StatusCode)> {
+pub fn parse_git_diff_name_status(content: &str) -> impl Iterator<Item = (&str, StatusCode)> {
     let mut parts = content.split('\0');
     std::iter::from_fn(move || {
         loop {
@@ -51,13 +51,14 @@ pub fn parse_git_diff_name_status(content: &str) -> impl Iterator<Item = (&Path,
                 "D" => StatusCode::Deleted,
                 _ => continue,
             };
-            return Some((Path::new(path), status));
+            return Some((path, status));
         }
     })
 }
 
 #[cfg(test)]
 mod tests {
+
     use super::*;
 
     #[test]
@@ -78,31 +79,19 @@ mod tests {
         assert_eq!(
             output,
             &[
-                (Path::new("Cargo.lock"), StatusCode::Modified),
-                (Path::new("crates/project/Cargo.toml"), StatusCode::Modified),
-                (
-                    Path::new("crates/project/src/buffer_store.rs"),
-                    StatusCode::Modified
-                ),
-                (Path::new("crates/project/src/git.rs"), StatusCode::Deleted),
-                (
-                    Path::new("crates/project/src/git_store.rs"),
-                    StatusCode::Added
-                ),
+                ("Cargo.lock", StatusCode::Modified),
+                ("crates/project/Cargo.toml", StatusCode::Modified),
+                ("crates/project/src/buffer_store.rs", StatusCode::Modified),
+                ("crates/project/src/git.rs", StatusCode::Deleted),
+                ("crates/project/src/git_store.rs", StatusCode::Added),
                 (
-                    Path::new("crates/project/src/git_store/git_traversal.rs"),
+                    "crates/project/src/git_store/git_traversal.rs",
                     StatusCode::Added,
                 ),
+                ("crates/project/src/project.rs", StatusCode::Modified),
+                ("crates/project/src/worktree_store.rs", StatusCode::Modified),
                 (
-                    Path::new("crates/project/src/project.rs"),
-                    StatusCode::Modified
-                ),
-                (
-                    Path::new("crates/project/src/worktree_store.rs"),
-                    StatusCode::Modified
-                ),
-                (
-                    Path::new("crates/project_panel/src/project_panel.rs"),
+                    "crates/project_panel/src/project_panel.rs",
                     StatusCode::Modified
                 ),
             ]

crates/git/src/git.rs 🔗

@@ -12,22 +12,17 @@ use anyhow::{Context as _, Result};
 pub use git2 as libgit;
 use gpui::{Action, actions};
 pub use repository::RemoteCommandOutput;
-pub use repository::WORK_DIRECTORY_REPO_PATH;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use std::ffi::OsStr;
 use std::fmt;
 use std::str::FromStr;
-use std::sync::LazyLock;
-
-pub static DOT_GIT: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".git"));
-pub static GITIGNORE: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".gitignore"));
-pub static FSMONITOR_DAEMON: LazyLock<&'static OsStr> =
-    LazyLock::new(|| OsStr::new("fsmonitor--daemon"));
-pub static LFS_DIR: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("lfs"));
-pub static COMMIT_MESSAGE: LazyLock<&'static OsStr> =
-    LazyLock::new(|| OsStr::new("COMMIT_EDITMSG"));
-pub static INDEX_LOCK: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("index.lock"));
+
+pub const DOT_GIT: &str = ".git";
+pub const GITIGNORE: &str = ".gitignore";
+pub const FSMONITOR_DAEMON: &str = "fsmonitor--daemon";
+pub const LFS_DIR: &str = "lfs";
+pub const COMMIT_MESSAGE: &str = "COMMIT_EDITMSG";
+pub const INDEX_LOCK: &str = "index.lock";
 
 actions!(
     git,

crates/git/src/repository.rs 🔗

@@ -12,12 +12,9 @@ use parking_lot::Mutex;
 use rope::Rope;
 use schemars::JsonSchema;
 use serde::Deserialize;
-use std::borrow::{Borrow, Cow};
 use std::ffi::{OsStr, OsString};
 use std::io::prelude::*;
-use std::path::Component;
 use std::process::{ExitStatus, Stdio};
-use std::sync::LazyLock;
 use std::{
     cmp::Ordering,
     future,
@@ -28,6 +25,8 @@ use std::{
 use sum_tree::MapSeekTarget;
 use thiserror::Error;
 use util::command::{new_smol_command, new_std_command};
+use util::paths::PathStyle;
+use util::rel_path::RelPath;
 use util::{ResultExt, paths};
 use uuid::Uuid;
 
@@ -719,16 +718,21 @@ impl GitRepository for RealGitRepository {
             let mut info_line = String::new();
             let mut newline = [b'\0'];
             for (path, status_code) in changes {
+                // git-show outputs `/`-delimited paths even on Windows.
+                let Ok(rel_path) = RelPath::new(path) else {
+                    continue;
+                };
+
                 match status_code {
                     StatusCode::Modified => {
-                        writeln!(&mut stdin, "{commit}:{}", path.display())?;
-                        writeln!(&mut stdin, "{parent_sha}:{}", path.display())?;
+                        writeln!(&mut stdin, "{commit}:{path}")?;
+                        writeln!(&mut stdin, "{parent_sha}:{path}")?;
                     }
                     StatusCode::Added => {
-                        writeln!(&mut stdin, "{commit}:{}", path.display())?;
+                        writeln!(&mut stdin, "{commit}:{path}")?;
                     }
                     StatusCode::Deleted => {
-                        writeln!(&mut stdin, "{parent_sha}:{}", path.display())?;
+                        writeln!(&mut stdin, "{parent_sha}:{path}")?;
                     }
                     _ => continue,
                 }
@@ -766,7 +770,7 @@ impl GitRepository for RealGitRepository {
                 }
 
                 files.push(CommitFile {
-                    path: path.into(),
+                    path: rel_path.into(),
                     old_text,
                     new_text,
                 })
@@ -824,7 +828,7 @@ impl GitRepository for RealGitRepository {
                 .current_dir(&working_directory?)
                 .envs(env.iter())
                 .args(["checkout", &commit, "--"])
-                .args(paths.iter().map(|path| path.as_ref()))
+                .args(paths.iter().map(|path| path.as_str()))
                 .output()
                 .await?;
             anyhow::ensure!(
@@ -846,13 +850,11 @@ impl GitRepository for RealGitRepository {
             .spawn(async move {
                 fn logic(repo: &git2::Repository, path: &RepoPath) -> Result<Option<String>> {
                     // This check is required because index.get_path() unwraps internally :(
-                    check_path_to_repo_path_errors(path)?;
-
                     let mut index = repo.index()?;
                     index.read(false)?;
 
                     const STAGE_NORMAL: i32 = 0;
-                    let oid = match index.get_path(path, STAGE_NORMAL) {
+                    let oid = match index.get_path(path.as_std_path(), STAGE_NORMAL) {
                         Some(entry) if entry.mode != GIT_MODE_SYMLINK => entry.id,
                         _ => return Ok(None),
                     };
@@ -876,7 +878,7 @@ impl GitRepository for RealGitRepository {
             .spawn(async move {
                 let repo = repo.lock();
                 let head = repo.head().ok()?.peel_to_tree().log_err()?;
-                let entry = head.get_path(&path).ok()?;
+                let entry = head.get_path(path.as_std_path()).ok()?;
                 if entry.filemode() == i32::from(git2::FileMode::Link) {
                     return None;
                 }
@@ -918,7 +920,7 @@ impl GitRepository for RealGitRepository {
                         .current_dir(&working_directory)
                         .envs(env.iter())
                         .args(["update-index", "--add", "--cacheinfo", "100644", sha])
-                        .arg(path.to_unix_style())
+                        .arg(path.as_str())
                         .output()
                         .await?;
 
@@ -933,7 +935,7 @@ impl GitRepository for RealGitRepository {
                         .current_dir(&working_directory)
                         .envs(env.iter())
                         .args(["update-index", "--force-remove"])
-                        .arg(path.to_unix_style())
+                        .arg(path.as_str())
                         .output()
                         .await?;
                     anyhow::ensure!(
@@ -1251,7 +1253,7 @@ impl GitRepository for RealGitRepository {
                         .current_dir(&working_directory?)
                         .envs(env.iter())
                         .args(["update-index", "--add", "--remove", "--"])
-                        .args(paths.iter().map(|p| p.to_unix_style()))
+                        .args(paths.iter().map(|p| p.as_str()))
                         .output()
                         .await?;
                     anyhow::ensure!(
@@ -1812,7 +1814,7 @@ fn git_status_args(path_prefixes: &[RepoPath]) -> Vec<OsString> {
         OsString::from("-z"),
     ];
     args.extend(path_prefixes.iter().map(|path_prefix| {
-        if path_prefix.0.as_ref() == Path::new("") {
+        if path_prefix.is_empty() {
             Path::new(".").into()
         } else {
             path_prefix.as_os_str().into()
@@ -2066,99 +2068,65 @@ async fn run_askpass_command(
     }
 }
 
-pub static WORK_DIRECTORY_REPO_PATH: LazyLock<RepoPath> =
-    LazyLock::new(|| RepoPath(Path::new("").into()));
-
 #[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
-pub struct RepoPath(pub Arc<Path>);
+pub struct RepoPath(pub Arc<RelPath>);
 
 impl RepoPath {
-    pub fn new(path: PathBuf) -> Self {
-        debug_assert!(path.is_relative(), "Repo paths must be relative");
-
-        RepoPath(path.into())
+    pub fn new<S: AsRef<str> + ?Sized>(s: &S) -> Result<Self> {
+        let rel_path = RelPath::new(s)?;
+        Ok(rel_path.into())
     }
 
-    pub fn from_str(path: &str) -> Self {
-        let path = Path::new(path);
-        debug_assert!(path.is_relative(), "Repo paths must be relative");
-
-        RepoPath(path.into())
+    pub fn from_proto(proto: &str) -> Result<Self> {
+        let rel_path = RelPath::from_proto(proto)?;
+        Ok(rel_path.into())
     }
 
-    pub fn to_unix_style(&self) -> Cow<'_, OsStr> {
-        #[cfg(target_os = "windows")]
-        {
-            use std::ffi::OsString;
-
-            let path = self.0.as_os_str().to_string_lossy().replace("\\", "/");
-            Cow::Owned(OsString::from(path))
-        }
-        #[cfg(not(target_os = "windows"))]
-        {
-            Cow::Borrowed(self.0.as_os_str())
-        }
+    pub fn from_std_path(path: &Path, path_style: PathStyle) -> Result<Self> {
+        let rel_path = RelPath::from_std_path(path, path_style)?;
+        Ok(rel_path.into())
     }
 }
 
-impl std::fmt::Display for RepoPath {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.0.to_string_lossy().fmt(f)
-    }
+#[cfg(any(test, feature = "test-support"))]
+pub fn repo_path<S: AsRef<str> + ?Sized>(s: &S) -> RepoPath {
+    RepoPath(RelPath::new(s).unwrap().into())
 }
 
-impl From<&Path> for RepoPath {
-    fn from(value: &Path) -> Self {
-        RepoPath::new(value.into())
+impl From<&RelPath> for RepoPath {
+    fn from(value: &RelPath) -> Self {
+        RepoPath(value.into())
     }
 }
 
-impl From<Arc<Path>> for RepoPath {
-    fn from(value: Arc<Path>) -> Self {
+impl From<Arc<RelPath>> for RepoPath {
+    fn from(value: Arc<RelPath>) -> Self {
         RepoPath(value)
     }
 }
 
-impl From<PathBuf> for RepoPath {
-    fn from(value: PathBuf) -> Self {
-        RepoPath::new(value)
-    }
-}
-
-impl From<&str> for RepoPath {
-    fn from(value: &str) -> Self {
-        Self::from_str(value)
-    }
-}
-
 impl Default for RepoPath {
     fn default() -> Self {
-        RepoPath(Path::new("").into())
-    }
-}
-
-impl AsRef<Path> for RepoPath {
-    fn as_ref(&self) -> &Path {
-        self.0.as_ref()
+        RepoPath(RelPath::empty().into())
     }
 }
 
 impl std::ops::Deref for RepoPath {
-    type Target = Path;
+    type Target = RelPath;
 
     fn deref(&self) -> &Self::Target {
         &self.0
     }
 }
 
-impl Borrow<Path> for RepoPath {
-    fn borrow(&self) -> &Path {
-        self.0.as_ref()
+impl AsRef<Path> for RepoPath {
+    fn as_ref(&self) -> &Path {
+        RelPath::as_ref(&self.0)
     }
 }
 
 #[derive(Debug)]
-pub struct RepoPathDescendants<'a>(pub &'a Path);
+pub struct RepoPathDescendants<'a>(pub &'a RepoPath);
 
 impl MapSeekTarget<RepoPath> for RepoPathDescendants<'_> {
     fn cmp_cursor(&self, key: &RepoPath) -> Ordering {
@@ -2244,35 +2212,6 @@ fn parse_upstream_track(upstream_track: &str) -> Result<UpstreamTracking> {
     }))
 }
 
-fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
-    match relative_file_path.components().next() {
-        None => anyhow::bail!("repo path should not be empty"),
-        Some(Component::Prefix(_)) => anyhow::bail!(
-            "repo path `{}` should be relative, not a windows prefix",
-            relative_file_path.to_string_lossy()
-        ),
-        Some(Component::RootDir) => {
-            anyhow::bail!(
-                "repo path `{}` should be relative",
-                relative_file_path.to_string_lossy()
-            )
-        }
-        Some(Component::CurDir) => {
-            anyhow::bail!(
-                "repo path `{}` should not start with `.`",
-                relative_file_path.to_string_lossy()
-            )
-        }
-        Some(Component::ParentDir) => {
-            anyhow::bail!(
-                "repo path `{}` should not start with `..`",
-                relative_file_path.to_string_lossy()
-            )
-        }
-        _ => Ok(()),
-    }
-}
-
 fn checkpoint_author_envs() -> HashMap<String, String> {
     HashMap::from_iter([
         ("GIT_AUTHOR_NAME".to_string(), "Zed".to_string()),
@@ -2299,12 +2238,9 @@ mod tests {
 
         let repo =
             RealGitRepository::new(&repo_dir.path().join(".git"), None, cx.executor()).unwrap();
-        repo.stage_paths(
-            vec![RepoPath::from_str("file")],
-            Arc::new(HashMap::default()),
-        )
-        .await
-        .unwrap();
+        repo.stage_paths(vec![repo_path("file")], Arc::new(HashMap::default()))
+            .await
+            .unwrap();
         repo.commit(
             "Initial commit".into(),
             None,
@@ -2328,12 +2264,9 @@ mod tests {
         smol::fs::write(&file_path, "modified after checkpoint")
             .await
             .unwrap();
-        repo.stage_paths(
-            vec![RepoPath::from_str("file")],
-            Arc::new(HashMap::default()),
-        )
-        .await
-        .unwrap();
+        repo.stage_paths(vec![repo_path("file")], Arc::new(HashMap::default()))
+            .await
+            .unwrap();
         repo.commit(
             "Commit after checkpoint".into(),
             None,
@@ -2466,12 +2399,9 @@ mod tests {
             RealGitRepository::new(&repo_dir.path().join(".git"), None, cx.executor()).unwrap();
 
         // initial commit
-        repo.stage_paths(
-            vec![RepoPath::from_str("main.rs")],
-            Arc::new(HashMap::default()),
-        )
-        .await
-        .unwrap();
+        repo.stage_paths(vec![repo_path("main.rs")], Arc::new(HashMap::default()))
+            .await
+            .unwrap();
         repo.commit(
             "Initial commit".into(),
             None,

crates/git/src/status.rs 🔗

@@ -1,8 +1,8 @@
 use crate::repository::RepoPath;
 use anyhow::Result;
 use serde::{Deserialize, Serialize};
-use std::{path::Path, str::FromStr, sync::Arc};
-use util::ResultExt;
+use std::{str::FromStr, sync::Arc};
+use util::{ResultExt, rel_path::RelPath};
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub enum FileStatus {
@@ -447,7 +447,8 @@ impl FromStr for GitStatus {
                 }
                 let status = entry.as_bytes()[0..2].try_into().unwrap();
                 let status = FileStatus::from_bytes(status).log_err()?;
-                let path = RepoPath(Path::new(path).into());
+                // git-status outputs `/`-delimited repo paths, even on Windows.
+                let path = RepoPath(RelPath::new(path).log_err()?.into());
                 Some((path, status))
             })
             .collect::<Vec<_>>();

crates/git_ui/src/commit_view.rs 🔗

@@ -14,13 +14,12 @@ use multi_buffer::PathKey;
 use project::{Project, WorktreeId, git_store::Repository};
 use std::{
     any::{Any, TypeId},
-    ffi::OsStr,
     fmt::Write as _,
-    path::{Path, PathBuf},
+    path::PathBuf,
     sync::Arc,
 };
 use ui::{Color, Icon, IconName, Label, LabelCommon as _, SharedString};
-use util::{ResultExt, truncate_and_trailoff};
+use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};
 use workspace::{
     Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,
     item::{BreadcrumbText, ItemEvent, TabContentParams},
@@ -40,7 +39,7 @@ struct GitBlob {
 }
 
 struct CommitMetadataFile {
-    title: Arc<Path>,
+    title: Arc<RelPath>,
     worktree_id: WorktreeId,
 }
 
@@ -129,7 +128,9 @@ impl CommitView {
         let mut metadata_buffer_id = None;
         if let Some(worktree_id) = first_worktree_id {
             let file = Arc::new(CommitMetadataFile {
-                title: PathBuf::from(format!("commit {}", commit.sha)).into(),
+                title: RelPath::new(&format!("commit {}", commit.sha))
+                    .unwrap()
+                    .into(),
                 worktree_id,
             });
             let buffer = cx.new(|cx| {
@@ -144,7 +145,7 @@ impl CommitView {
             });
             multibuffer.update(cx, |multibuffer, cx| {
                 multibuffer.set_excerpts_for_path(
-                    PathKey::namespaced(COMMIT_METADATA_NAMESPACE, file.title.clone()),
+                    PathKey::namespaced(COMMIT_METADATA_NAMESPACE, file.title.as_str().into()),
                     buffer.clone(),
                     vec![Point::zero()..buffer.read(cx).max_point()],
                     0,
@@ -192,7 +193,7 @@ impl CommitView {
                             .collect::<Vec<_>>();
                         let path = snapshot.file().unwrap().path().clone();
                         let _is_newly_added = multibuffer.set_excerpts_for_path(
-                            PathKey::namespaced(FILE_NAMESPACE, path),
+                            PathKey::namespaced(FILE_NAMESPACE, path.as_str().into()),
                             buffer,
                             diff_hunk_ranges,
                             multibuffer_context_lines(cx),
@@ -227,15 +228,19 @@ impl language::File for GitBlob {
         }
     }
 
-    fn path(&self) -> &Arc<Path> {
+    fn path_style(&self, _: &App) -> PathStyle {
+        PathStyle::Posix
+    }
+
+    fn path(&self) -> &Arc<RelPath> {
         &self.path.0
     }
 
     fn full_path(&self, _: &App) -> PathBuf {
-        self.path.to_path_buf()
+        self.path.as_std_path().to_path_buf()
     }
 
-    fn file_name<'a>(&'a self, _: &'a App) -> &'a OsStr {
+    fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
         self.path.file_name().unwrap()
     }
 
@@ -261,15 +266,19 @@ impl language::File for CommitMetadataFile {
         DiskState::New
     }
 
-    fn path(&self) -> &Arc<Path> {
+    fn path_style(&self, _: &App) -> PathStyle {
+        PathStyle::Posix
+    }
+
+    fn path(&self) -> &Arc<RelPath> {
         &self.title
     }
 
     fn full_path(&self, _: &App) -> PathBuf {
-        self.title.as_ref().into()
+        PathBuf::from(self.title.as_str().to_owned())
     }
 
-    fn file_name<'a>(&'a self, _: &'a App) -> &'a OsStr {
+    fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
         self.title.file_name().unwrap()
     }
 

crates/git_ui/src/git_panel.rs 🔗

@@ -53,7 +53,7 @@ use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore, StatusStyle};
 use std::future::Future;
 use std::ops::Range;
-use std::path::{Path, PathBuf};
+use std::path::Path;
 use std::{collections::HashSet, sync::Arc, time::Duration, usize};
 use strum::{IntoEnumIterator, VariantNames};
 use time::OffsetDateTime;
@@ -61,6 +61,7 @@ use ui::{
     Checkbox, CommonAnimationExt, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize,
     PopoverMenu, ScrollAxes, Scrollbars, SplitButton, Tooltip, WithScrollbar, prelude::*,
 };
+use util::paths::PathStyle;
 use util::{ResultExt, TryFutureExt, maybe};
 use workspace::SERIALIZATION_THROTTLE_TIME;
 
@@ -251,23 +252,22 @@ impl GitListEntry {
 #[derive(Debug, PartialEq, Eq, Clone)]
 pub struct GitStatusEntry {
     pub(crate) repo_path: RepoPath,
-    pub(crate) abs_path: PathBuf,
     pub(crate) status: FileStatus,
     pub(crate) staging: StageStatus,
 }
 
 impl GitStatusEntry {
-    fn display_name(&self) -> String {
+    fn display_name(&self, path_style: PathStyle) -> String {
         self.repo_path
             .file_name()
-            .map(|name| name.to_string_lossy().into_owned())
-            .unwrap_or_else(|| self.repo_path.to_string_lossy().into_owned())
+            .map(|name| name.to_owned())
+            .unwrap_or_else(|| self.repo_path.display(path_style).to_string())
     }
 
-    fn parent_dir(&self) -> Option<String> {
+    fn parent_dir(&self, path_style: PathStyle) -> Option<String> {
         self.repo_path
             .parent()
-            .map(|parent| parent.to_string_lossy().into_owned())
+            .map(|parent| parent.display(path_style).to_string())
     }
 }
 
@@ -826,6 +826,7 @@ impl GitPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        let path_style = self.project.read(cx).path_style(cx);
         maybe!({
             let list_entry = self.entries.get(self.selected_entry?)?.clone();
             let entry = list_entry.status_entry()?.to_owned();
@@ -841,8 +842,7 @@ impl GitPanel {
                         entry
                             .repo_path
                             .file_name()
-                            .unwrap_or(entry.repo_path.as_os_str())
-                            .to_string_lossy()
+                            .unwrap_or(entry.repo_path.display(path_style).as_ref()),
                     ),
                     None,
                     &["Restore", "Cancel"],
@@ -885,7 +885,7 @@ impl GitPanel {
             if entry.status.staging().has_staged() {
                 self.change_file_stage(false, vec![entry.clone()], cx);
             }
-            let filename = path.path.file_name()?.to_string_lossy();
+            let filename = path.path.file_name()?.to_string();
 
             if !entry.status.is_created() {
                 self.perform_checkout(vec![entry.clone()], window, cx);
@@ -1028,7 +1028,7 @@ impl GitPanel {
         let mut details = entries
             .iter()
             .filter_map(|entry| entry.repo_path.0.file_name())
-            .map(|filename| filename.to_string_lossy())
+            .map(|filename| filename.to_string())
             .take(5)
             .join("\n");
         if entries.len() > 5 {
@@ -1084,7 +1084,7 @@ impl GitPanel {
                     .repo_path
                     .0
                     .file_name()
-                    .map(|f| f.to_string_lossy())
+                    .map(|f| f.to_string())
                     .unwrap_or_default()
             })
             .take(5)
@@ -1721,7 +1721,7 @@ impl GitPanel {
             .repo_path
             .file_name()
             .unwrap_or_default()
-            .to_string_lossy();
+            .to_string();
 
         Some(format!("{} {}", action_text, file_name))
     }
@@ -1973,11 +1973,7 @@ impl GitPanel {
         cx.spawn_in(window, async move |this, cx| {
             let mut paths = path.await.ok()?.ok()??;
             let mut path = paths.pop()?;
-            let repo_name = repo
-                .split(std::path::MAIN_SEPARATOR_STR)
-                .last()?
-                .strip_suffix(".git")?
-                .to_owned();
+            let repo_name = repo.split("/").last()?.strip_suffix(".git")?.to_owned();
 
             let fs = this.read_with(cx, |this, _| this.fs.clone()).ok()?;
 
@@ -2558,6 +2554,7 @@ impl GitPanel {
     }
 
     fn update_visible_entries(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        let path_style = self.project.read(cx).path_style(cx);
         let bulk_staging = self.bulk_staging.take();
         let last_staged_path_prev_index = bulk_staging
             .as_ref()
@@ -2609,10 +2606,8 @@ impl GitPanel {
                 continue;
             }
 
-            let abs_path = repo.work_directory_abs_path.join(&entry.repo_path.0);
             let entry = GitStatusEntry {
                 repo_path: entry.repo_path.clone(),
-                abs_path,
                 status: entry.status,
                 staging,
             };
@@ -2623,8 +2618,8 @@ impl GitPanel {
             }
 
             let width_estimate = Self::item_width_estimate(
-                entry.parent_dir().map(|s| s.len()).unwrap_or(0),
-                entry.display_name().len(),
+                entry.parent_dir(path_style).map(|s| s.len()).unwrap_or(0),
+                entry.display_name(path_style).len(),
             );
 
             match max_width_item.as_mut() {
@@ -3634,7 +3629,7 @@ impl GitPanel {
         cx: &App,
     ) -> Option<AnyElement> {
         let repo = self.active_repository.as_ref()?.read(cx);
-        let project_path = (file.worktree_id(cx), file.path()).into();
+        let project_path = (file.worktree_id(cx), file.path().clone()).into();
         let repo_path = repo.project_path_to_repo_path(&project_path, cx)?;
         let ix = self.entry_by_path(&repo_path, cx)?;
         let entry = self.entries.get(ix)?;
@@ -3887,7 +3882,8 @@ impl GitPanel {
         window: &Window,
         cx: &Context<Self>,
     ) -> AnyElement {
-        let display_name = entry.display_name();
+        let path_style = self.project.read(cx).path_style(cx);
+        let display_name = entry.display_name(path_style);
 
         let selected = self.selected_entry == Some(ix);
         let marked = self.marked_entries.contains(&ix);
@@ -4060,11 +4056,14 @@ impl GitPanel {
                     .items_center()
                     .flex_1()
                     // .overflow_hidden()
-                    .when_some(entry.parent_dir(), |this, parent| {
+                    .when_some(entry.parent_dir(path_style), |this, parent| {
                         if !parent.is_empty() {
                             this.child(
-                                self.entry_label(format!("{}/", parent), path_color)
-                                    .when(status.is_deleted(), |this| this.strikethrough()),
+                                self.entry_label(
+                                    format!("{parent}{}", path_style.separator()),
+                                    path_color,
+                                )
+                                .when(status.is_deleted(), |this| this.strikethrough()),
                             )
                         } else {
                             this
@@ -4889,7 +4888,10 @@ impl Component for PanelRepoFooter {
 
 #[cfg(test)]
 mod tests {
-    use git::status::{StatusCode, UnmergedStatus, UnmergedStatusCode};
+    use git::{
+        repository::repo_path,
+        status::{StatusCode, UnmergedStatus, UnmergedStatusCode},
+    };
     use gpui::{TestAppContext, VisualTestContext};
     use project::{FakeFs, WorktreeSettings};
     use serde_json::json;
@@ -4941,14 +4943,8 @@ mod tests {
         fs.set_status_for_repo(
             Path::new(path!("/root/zed/.git")),
             &[
-                (
-                    Path::new("crates/gpui/gpui.rs"),
-                    StatusCode::Modified.worktree(),
-                ),
-                (
-                    Path::new("crates/util/util.rs"),
-                    StatusCode::Modified.worktree(),
-                ),
+                ("crates/gpui/gpui.rs", StatusCode::Modified.worktree()),
+                ("crates/util/util.rs", StatusCode::Modified.worktree()),
             ],
         );
 
@@ -4989,14 +4985,12 @@ mod tests {
                     header: Section::Tracked
                 }),
                 GitListEntry::Status(GitStatusEntry {
-                    abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
-                    repo_path: "crates/gpui/gpui.rs".into(),
+                    repo_path: repo_path("crates/gpui/gpui.rs"),
                     status: StatusCode::Modified.worktree(),
                     staging: StageStatus::Unstaged,
                 }),
                 GitListEntry::Status(GitStatusEntry {
-                    abs_path: path!("/root/zed/crates/util/util.rs").into(),
-                    repo_path: "crates/util/util.rs".into(),
+                    repo_path: repo_path("crates/util/util.rs"),
                     status: StatusCode::Modified.worktree(),
                     staging: StageStatus::Unstaged,
                 },),
@@ -5016,14 +5010,12 @@ mod tests {
                     header: Section::Tracked
                 }),
                 GitListEntry::Status(GitStatusEntry {
-                    abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
-                    repo_path: "crates/gpui/gpui.rs".into(),
+                    repo_path: repo_path("crates/gpui/gpui.rs"),
                     status: StatusCode::Modified.worktree(),
                     staging: StageStatus::Unstaged,
                 }),
                 GitListEntry::Status(GitStatusEntry {
-                    abs_path: path!("/root/zed/crates/util/util.rs").into(),
-                    repo_path: "crates/util/util.rs".into(),
+                    repo_path: repo_path("crates/util/util.rs"),
                     status: StatusCode::Modified.worktree(),
                     staging: StageStatus::Unstaged,
                 },),
@@ -5061,14 +5053,14 @@ mod tests {
         fs.set_status_for_repo(
             Path::new(path!("/root/project/.git")),
             &[
-                (Path::new("src/main.rs"), StatusCode::Modified.worktree()),
-                (Path::new("src/lib.rs"), StatusCode::Modified.worktree()),
-                (Path::new("tests/test.rs"), StatusCode::Modified.worktree()),
-                (Path::new("new_file.txt"), FileStatus::Untracked),
-                (Path::new("another_new.rs"), FileStatus::Untracked),
-                (Path::new("src/utils.rs"), FileStatus::Untracked),
+                ("src/main.rs", StatusCode::Modified.worktree()),
+                ("src/lib.rs", StatusCode::Modified.worktree()),
+                ("tests/test.rs", StatusCode::Modified.worktree()),
+                ("new_file.txt", FileStatus::Untracked),
+                ("another_new.rs", FileStatus::Untracked),
+                ("src/utils.rs", FileStatus::Untracked),
                 (
-                    Path::new("conflict.txt"),
+                    "conflict.txt",
                     UnmergedStatus {
                         first_head: UnmergedStatusCode::Updated,
                         second_head: UnmergedStatusCode::Updated,
@@ -5242,7 +5234,7 @@ mod tests {
 
         fs.set_status_for_repo(
             Path::new(path!("/root/project/.git")),
-            &[(Path::new("src/main.rs"), StatusCode::Modified.worktree())],
+            &[("src/main.rs", StatusCode::Modified.worktree())],
         );
 
         let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await;

crates/git_ui/src/project_diff.rs 🔗

@@ -243,7 +243,7 @@ impl ProjectDiff {
             TRACKED_NAMESPACE
         };
 
-        let path_key = PathKey::namespaced(namespace, entry.repo_path.0);
+        let path_key = PathKey::namespaced(namespace, entry.repo_path.as_str().into());
 
         self.move_to_path(path_key, window, cx)
     }
@@ -397,7 +397,7 @@ impl ProjectDiff {
                 } else {
                     TRACKED_NAMESPACE
                 };
-                let path_key = PathKey::namespaced(namespace, entry.repo_path.0.clone());
+                let path_key = PathKey::namespaced(namespace, entry.repo_path.as_str().into());
 
                 previous_paths.remove(&path_key);
                 let load_buffer = self
@@ -535,7 +535,7 @@ impl ProjectDiff {
         self.multibuffer
             .read(cx)
             .excerpt_paths()
-            .map(|key| key.path().to_string_lossy().to_string())
+            .map(|key| key.path().to_string())
             .collect()
     }
 }
@@ -1406,12 +1406,12 @@ mod tests {
 
         fs.set_head_for_repo(
             path!("/project/.git").as_ref(),
-            &[("foo.txt".into(), "foo\n".into())],
+            &[("foo.txt", "foo\n".into())],
             "deadbeef",
         );
         fs.set_index_for_repo(
             path!("/project/.git").as_ref(),
-            &[("foo.txt".into(), "foo\n".into())],
+            &[("foo.txt", "foo\n".into())],
         );
         cx.run_until_parked();
 
@@ -1461,16 +1461,13 @@ mod tests {
 
         fs.set_head_and_index_for_repo(
             path!("/project/.git").as_ref(),
-            &[
-                ("bar".into(), "bar\n".into()),
-                ("foo".into(), "foo\n".into()),
-            ],
+            &[("bar", "bar\n".into()), ("foo", "foo\n".into())],
         );
         cx.run_until_parked();
 
         let editor = cx.update_window_entity(&diff, |diff, window, cx| {
             diff.move_to_path(
-                PathKey::namespaced(TRACKED_NAMESPACE, Path::new("foo").into()),
+                PathKey::namespaced(TRACKED_NAMESPACE, "foo".into()),
                 window,
                 cx,
             );
@@ -1491,7 +1488,7 @@ mod tests {
 
         let editor = cx.update_window_entity(&diff, |diff, window, cx| {
             diff.move_to_path(
-                PathKey::namespaced(TRACKED_NAMESPACE, Path::new("bar").into()),
+                PathKey::namespaced(TRACKED_NAMESPACE, "bar".into()),
                 window,
                 cx,
             );
@@ -1543,7 +1540,7 @@ mod tests {
 
         fs.set_head_for_repo(
             path!("/project/.git").as_ref(),
-            &[("foo".into(), "original\n".into())],
+            &[("foo", "original\n".into())],
             "deadbeef",
         );
         cx.run_until_parked();
@@ -1646,12 +1643,12 @@ mod tests {
         )
         .await;
 
-        fs.set_git_content_for_repo(
+        fs.set_head_and_index_for_repo(
             Path::new("/a/.git"),
             &[
-                ("b.txt".into(), "before\n".to_string(), None),
-                ("c.txt".into(), "unchanged\n".to_string(), None),
-                ("d.txt".into(), "deleted\n".to_string(), None),
+                ("b.txt", "before\n".to_string()),
+                ("c.txt", "unchanged\n".to_string()),
+                ("d.txt", "deleted\n".to_string()),
             ],
         );
 
@@ -1764,9 +1761,9 @@ mod tests {
         )
         .await;
 
-        fs.set_git_content_for_repo(
+        fs.set_head_and_index_for_repo(
             Path::new("/a/.git"),
-            &[("main.rs".into(), git_contents.to_owned(), None)],
+            &[("main.rs", git_contents.to_owned())],
         );
 
         let project = Project::test(fs, [Path::new("/a")], cx).await;
@@ -1816,7 +1813,7 @@ mod tests {
         fs.set_status_for_repo(
             Path::new(path!("/project/.git")),
             &[(
-                Path::new("foo"),
+                "foo",
                 UnmergedStatus {
                     first_head: UnmergedStatusCode::Updated,
                     second_head: UnmergedStatusCode::Updated,

crates/go_to_line/src/go_to_line.rs 🔗

@@ -311,7 +311,7 @@ mod tests {
     use project::{FakeFs, Project};
     use serde_json::json;
     use std::{num::NonZeroU32, sync::Arc, time::Duration};
-    use util::path;
+    use util::{path, rel_path::rel_path};
     use workspace::{AppState, Workspace};
 
     #[gpui::test]
@@ -356,7 +356,7 @@ mod tests {
             .unwrap();
         let editor = workspace
             .update_in(cx, |workspace, window, cx| {
-                workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
+                workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
             })
             .await
             .unwrap()
@@ -460,7 +460,7 @@ mod tests {
             .unwrap();
         let editor = workspace
             .update_in(cx, |workspace, window, cx| {
-                workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
+                workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
             })
             .await
             .unwrap()
@@ -545,7 +545,7 @@ mod tests {
             .unwrap();
         let editor = workspace
             .update_in(cx, |workspace, window, cx| {
-                workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
+                workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
             })
             .await
             .unwrap()
@@ -623,7 +623,7 @@ mod tests {
             .unwrap();
         let editor = workspace
             .update_in(cx, |workspace, window, cx| {
-                workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
+                workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
             })
             .await
             .unwrap()

crates/image_viewer/src/image_viewer.rs 🔗

@@ -1,8 +1,6 @@
 mod image_info;
 mod image_viewer_settings;
 
-use std::path::PathBuf;
-
 use anyhow::Context as _;
 use editor::{EditorSettings, items::entry_git_aware_label_color};
 use file_icons::FileIcons;
@@ -144,7 +142,6 @@ impl Item for ImageView {
             .read(cx)
             .file
             .file_name(cx)
-            .to_string_lossy()
             .to_string()
             .into()
     }
@@ -198,20 +195,14 @@ impl Item for ImageView {
 }
 
 fn breadcrumbs_text_for_image(project: &Project, image: &ImageItem, cx: &App) -> String {
-    let path = image.file.file_name(cx);
-    if project.visible_worktrees(cx).count() <= 1 {
-        return path.to_string_lossy().to_string();
+    let mut path = image.file.path().clone();
+    if project.visible_worktrees(cx).count() > 1
+        && let Some(worktree) = project.worktree_for_id(image.project_path(cx).worktree_id, cx)
+    {
+        path = worktree.read(cx).root_name().join(&path);
     }
 
-    project
-        .worktree_for_id(image.project_path(cx).worktree_id, cx)
-        .map(|worktree| {
-            PathBuf::from(worktree.read(cx).root_name())
-                .join(path)
-                .to_string_lossy()
-                .to_string()
-        })
-        .unwrap_or_else(|| path.to_string_lossy().to_string())
+    path.display(project.path_style(cx)).to_string()
 }
 
 impl SerializableItem for ImageView {
@@ -242,7 +233,7 @@ impl SerializableItem for ImageView {
 
             let project_path = ProjectPath {
                 worktree_id,
-                path: relative_path.into(),
+                path: relative_path,
             };
 
             let image_item = project

crates/inspector_ui/src/div_inspector.rs 🔗

@@ -24,6 +24,7 @@ use std::path::Path;
 use std::rc::Rc;
 use std::sync::LazyLock;
 use ui::{Label, LabelSize, Tooltip, prelude::*, styled_ext_reflection, v_flex};
+use util::rel_path::RelPath;
 use util::split_str_with_ranges;
 
 /// Path used for unsaved buffer that contains style json. To support the json language server, this
@@ -466,7 +467,7 @@ impl DivInspector {
 
         let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
             worktree_id: worktree.id(),
-            path: Path::new("").into(),
+            path: RelPath::empty().into(),
         })?;
 
         let buffer = project

crates/journal/src/journal.rs 🔗

@@ -94,7 +94,7 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap
             break;
         }
         for directory in worktree.read(cx).directories(true, 1) {
-            let full_directory_path = worktree_root.join(&directory.path);
+            let full_directory_path = worktree_root.join(directory.path.as_std_path());
             if full_directory_path.ends_with(&journal_dir_clone) {
                 open_new_workspace = false;
                 break 'outer;

crates/language/src/buffer.rs 🔗

@@ -41,13 +41,12 @@ use std::{
     cell::Cell,
     cmp::{self, Ordering, Reverse},
     collections::{BTreeMap, BTreeSet},
-    ffi::OsStr,
     future::Future,
     iter::{self, Iterator, Peekable},
     mem,
     num::NonZeroU32,
     ops::{Deref, Range},
-    path::{Path, PathBuf},
+    path::PathBuf,
     rc,
     sync::{Arc, LazyLock},
     time::{Duration, Instant},
@@ -65,7 +64,7 @@ pub use text::{
 use theme::{ActiveTheme as _, SyntaxTheme};
 #[cfg(any(test, feature = "test-support"))]
 use util::RandomCharIter;
-use util::{RangeExt, debug_panic, maybe};
+use util::{RangeExt, debug_panic, maybe, paths::PathStyle, rel_path::RelPath};
 
 #[cfg(any(test, feature = "test-support"))]
 pub use {tree_sitter_python, tree_sitter_rust, tree_sitter_typescript};
@@ -349,15 +348,18 @@ pub trait File: Send + Sync + Any {
     fn disk_state(&self) -> DiskState;
 
     /// Returns the path of this file relative to the worktree's root directory.
-    fn path(&self) -> &Arc<Path>;
+    fn path(&self) -> &Arc<RelPath>;
 
     /// Returns the path of this file relative to the worktree's parent directory (this means it
     /// includes the name of the worktree's root folder).
     fn full_path(&self, cx: &App) -> PathBuf;
 
+    /// Returns the path style of this file.
+    fn path_style(&self, cx: &App) -> PathStyle;
+
     /// Returns the last component of this handle's absolute path. If this handle refers to the root
     /// of its worktree, then this method will return the name of the worktree itself.
-    fn file_name<'a>(&'a self, cx: &'a App) -> &'a OsStr;
+    fn file_name<'a>(&'a self, cx: &'a App) -> &'a str;
 
     /// Returns the id of the worktree to which this file belongs.
     ///
@@ -4626,13 +4628,12 @@ impl BufferSnapshot {
         self.file.as_ref()
     }
 
-    /// Resolves the file path (relative to the worktree root) associated with the underlying file.
     pub fn resolve_file_path(&self, cx: &App, include_root: bool) -> Option<PathBuf> {
         if let Some(file) = self.file() {
             if file.path().file_name().is_none() || include_root {
                 Some(file.full_path(cx))
             } else {
-                Some(file.path().to_path_buf())
+                Some(file.path().as_std_path().to_owned())
             }
         } else {
             None
@@ -5117,19 +5118,19 @@ impl IndentSize {
 
 #[cfg(any(test, feature = "test-support"))]
 pub struct TestFile {
-    pub path: Arc<Path>,
+    pub path: Arc<RelPath>,
     pub root_name: String,
     pub local_root: Option<PathBuf>,
 }
 
 #[cfg(any(test, feature = "test-support"))]
 impl File for TestFile {
-    fn path(&self) -> &Arc<Path> {
+    fn path(&self) -> &Arc<RelPath> {
         &self.path
     }
 
     fn full_path(&self, _: &gpui::App) -> PathBuf {
-        PathBuf::from(&self.root_name).join(self.path.as_ref())
+        PathBuf::from(self.root_name.clone()).join(self.path.as_std_path())
     }
 
     fn as_local(&self) -> Option<&dyn LocalFile> {
@@ -5144,7 +5145,7 @@ impl File for TestFile {
         unimplemented!()
     }
 
-    fn file_name<'a>(&'a self, _: &'a gpui::App) -> &'a std::ffi::OsStr {
+    fn file_name<'a>(&'a self, _: &'a gpui::App) -> &'a str {
         self.path().file_name().unwrap_or(self.root_name.as_ref())
     }
 
@@ -5159,6 +5160,10 @@ impl File for TestFile {
     fn is_private(&self) -> bool {
         false
     }
+
+    fn path_style(&self, _cx: &App) -> PathStyle {
+        PathStyle::local()
+    }
 }
 
 #[cfg(any(test, feature = "test-support"))]
@@ -5166,7 +5171,7 @@ impl LocalFile for TestFile {
     fn abs_path(&self, _cx: &App) -> PathBuf {
         PathBuf::from(self.local_root.as_ref().unwrap())
             .join(&self.root_name)
-            .join(self.path.as_ref())
+            .join(self.path.as_std_path())
     }
 
     fn load(&self, _cx: &App) -> Task<Result<String>> {

crates/language/src/buffer_tests.rs 🔗

@@ -24,6 +24,7 @@ use text::{BufferId, LineEnding};
 use text::{Point, ToPoint};
 use theme::ActiveTheme;
 use unindent::Unindent as _;
+use util::rel_path::rel_path;
 use util::test::marked_text_offsets;
 use util::{RandomCharIter, assert_set_eq, post_inc, test::marked_text_ranges};
 
@@ -380,7 +381,7 @@ async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext)
 
 fn file(path: &str) -> Arc<dyn File> {
     Arc::new(TestFile {
-        path: Path::new(path).into(),
+        path: Arc::from(rel_path(path)),
         root_name: "zed".into(),
         local_root: None,
     })

crates/language/src/language.rs 🔗

@@ -70,6 +70,7 @@ pub use toolchain::{
     ToolchainMetadata, ToolchainScope,
 };
 use tree_sitter::{self, Query, QueryCursor, WasmStore, wasmtime};
+use util::rel_path::RelPath;
 use util::serde::default_true;
 
 pub use buffer::Operation;
@@ -307,7 +308,7 @@ pub trait LspAdapterDelegate: Send + Sync {
     ) -> Result<Option<(PathBuf, String)>>;
     async fn which(&self, command: &OsStr) -> Option<PathBuf>;
     async fn shell_env(&self) -> HashMap<String, String>;
-    async fn read_text_file(&self, path: PathBuf) -> Result<String>;
+    async fn read_text_file(&self, path: &RelPath) -> Result<String>;
     async fn try_exec(&self, binary: LanguageServerBinary) -> Result<()>;
 }
 

crates/language/src/language_registry.rs 🔗

@@ -753,7 +753,7 @@ impl LanguageRegistry {
         content: Option<&Rope>,
         user_file_types: Option<&FxHashMap<Arc<str>, GlobSet>>,
     ) -> Option<AvailableLanguage> {
-        let filename = path.file_name().and_then(|name| name.to_str());
+        let filename = path.file_name().and_then(|filename| filename.to_str());
         // `Path.extension()` returns None for files with a leading '.'
         // and no other extension which is not the desired behavior here,
         // as we want `.zshrc` to result in extension being `Some("zshrc")`

crates/language/src/language_settings.rs 🔗

@@ -390,7 +390,7 @@ impl EditPredictionSettings {
                 file.as_local()
                     .is_some_and(|local| glob.matcher.is_match(local.abs_path(cx)))
             } else {
-                glob.matcher.is_match(file.path())
+                glob.matcher.is_match(file.path().as_std_path())
             }
         })
     }
@@ -798,6 +798,7 @@ pub struct JsxTagAutoCloseSettings {
 mod tests {
     use super::*;
     use gpui::TestAppContext;
+    use util::rel_path::rel_path;
 
     #[gpui::test]
     fn test_edit_predictions_enabled_for_file(cx: &mut TestAppContext) {
@@ -839,11 +840,11 @@ mod tests {
 
         const WORKTREE_NAME: &str = "project";
         let make_test_file = |segments: &[&str]| -> Arc<dyn File> {
-            let mut path_buf = PathBuf::new();
-            path_buf.extend(segments);
+            let path = segments.join("/");
+            let path = rel_path(&path);
 
             Arc::new(TestFile {
-                path: path_buf.as_path().into(),
+                path: path.into(),
                 root_name: WORKTREE_NAME.to_string(),
                 local_root: Some(PathBuf::from(if cfg!(windows) {
                     "C:\\absolute\\"
@@ -896,7 +897,7 @@ mod tests {
         assert!(!settings.enabled_for_file(&test_file, &cx));
 
         let test_file_root: Arc<dyn File> = Arc::new(TestFile {
-            path: PathBuf::from("file.rs").as_path().into(),
+            path: rel_path("file.rs").into(),
             root_name: WORKTREE_NAME.to_string(),
             local_root: Some(PathBuf::from("/absolute/")),
         });
@@ -928,8 +929,12 @@ mod tests {
 
         // Test tilde expansion
         let home = shellexpand::tilde("~").into_owned();
-        let home_file = make_test_file(&[&home, "test.rs"]);
-        let settings = build_settings(&["~/test.rs"]);
+        let home_file = Arc::new(TestFile {
+            path: rel_path("test.rs").into(),
+            root_name: "the-dir".to_string(),
+            local_root: Some(PathBuf::from(home)),
+        }) as Arc<dyn File>;
+        let settings = build_settings(&["~/the-dir/test.rs"]);
         assert!(!settings.enabled_for_file(&home_file, &cx));
     }
 

crates/language/src/manifest.rs 🔗

@@ -1,7 +1,8 @@
-use std::{borrow::Borrow, path::Path, sync::Arc};
+use std::{borrow::Borrow, sync::Arc};
 
 use gpui::SharedString;
 use settings::WorktreeId;
+use util::rel_path::RelPath;
 
 #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
 pub struct ManifestName(SharedString);
@@ -42,17 +43,17 @@ impl AsRef<SharedString> for ManifestName {
 /// For example, given a path like `foo/bar/baz`, a depth of 2 would explore `foo/bar/baz` and `foo/bar`, but not `foo`.
 pub struct ManifestQuery {
     /// Path to the file, relative to worktree root.
-    pub path: Arc<Path>,
+    pub path: Arc<RelPath>,
     pub depth: usize,
     pub delegate: Arc<dyn ManifestDelegate>,
 }
 
 pub trait ManifestProvider {
     fn name(&self) -> ManifestName;
-    fn search(&self, query: ManifestQuery) -> Option<Arc<Path>>;
+    fn search(&self, query: ManifestQuery) -> Option<Arc<RelPath>>;
 }
 
 pub trait ManifestDelegate: Send + Sync {
     fn worktree_id(&self) -> WorktreeId;
-    fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool;
+    fn exists(&self, path: &RelPath, is_dir: Option<bool>) -> bool;
 }

crates/language/src/toolchain.rs 🔗

@@ -4,10 +4,7 @@
 //! which is a set of tools used to interact with the projects written in said language.
 //! For example, a Python project can have an associated virtual environment; a Rust project can have a toolchain override.
 
-use std::{
-    path::{Path, PathBuf},
-    sync::Arc,
-};
+use std::{path::PathBuf, sync::Arc};
 
 use async_trait::async_trait;
 use collections::HashMap;
@@ -15,6 +12,7 @@ use fs::Fs;
 use gpui::{AsyncApp, SharedString};
 use settings::WorktreeId;
 use task::ShellKind;
+use util::rel_path::RelPath;
 
 use crate::{LanguageName, ManifestName};
 
@@ -23,6 +21,7 @@ use crate::{LanguageName, ManifestName};
 pub struct Toolchain {
     /// User-facing label
     pub name: SharedString,
+    /// Absolute path
     pub path: SharedString,
     pub language_name: LanguageName,
     /// Full toolchain data (including language-specific details)
@@ -37,7 +36,7 @@ pub struct Toolchain {
 /// - Only in the subproject they're currently in.
 #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
 pub enum ToolchainScope {
-    Subproject(WorktreeId, Arc<Path>),
+    Subproject(WorktreeId, Arc<RelPath>),
     Project,
     /// Available in all projects on this box. It wouldn't make sense to show suggestions across machines.
     Global,
@@ -97,7 +96,7 @@ pub trait ToolchainLister: Send + Sync + 'static {
     async fn list(
         &self,
         worktree_root: PathBuf,
-        subroot_relative_path: Arc<Path>,
+        subroot_relative_path: Arc<RelPath>,
         project_env: Option<HashMap<String, String>>,
     ) -> ToolchainList;
 
@@ -134,7 +133,7 @@ pub trait LanguageToolchainStore: Send + Sync + 'static {
     async fn active_toolchain(
         self: Arc<Self>,
         worktree_id: WorktreeId,
-        relative_path: Arc<Path>,
+        relative_path: Arc<RelPath>,
         language_name: LanguageName,
         cx: &mut AsyncApp,
     ) -> Option<Toolchain>;
@@ -144,7 +143,7 @@ pub trait LocalLanguageToolchainStore: Send + Sync + 'static {
     fn active_toolchain(
         self: Arc<Self>,
         worktree_id: WorktreeId,
-        relative_path: &Arc<Path>,
+        relative_path: &Arc<RelPath>,
         language_name: LanguageName,
         cx: &mut AsyncApp,
     ) -> Option<Toolchain>;
@@ -155,7 +154,7 @@ impl<T: LocalLanguageToolchainStore> LanguageToolchainStore for T {
     async fn active_toolchain(
         self: Arc<Self>,
         worktree_id: WorktreeId,
-        relative_path: Arc<Path>,
+        relative_path: Arc<RelPath>,
         language_name: LanguageName,
         cx: &mut AsyncApp,
     ) -> Option<Toolchain> {

crates/language_extension/src/extension_lsp_adapter.rs 🔗

@@ -19,7 +19,7 @@ use lsp::{
 };
 use serde::Serialize;
 use serde_json::Value;
-use util::{ResultExt, fs::make_file_executable, maybe};
+use util::{ResultExt, fs::make_file_executable, maybe, rel_path::RelPath};
 
 use crate::{LanguageServerRegistryProxy, LspAccess};
 
@@ -36,7 +36,7 @@ impl WorktreeDelegate for WorktreeDelegateAdapter {
         self.0.worktree_root_path().to_string_lossy().to_string()
     }
 
-    async fn read_text_file(&self, path: PathBuf) -> Result<String> {
+    async fn read_text_file(&self, path: &RelPath) -> Result<String> {
         self.0.read_text_file(path).await
     }
 

crates/language_tools/src/lsp_button.rs 🔗

@@ -21,6 +21,7 @@ use ui::{
     DocumentationSide, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, Window, prelude::*,
 };
 
+use util::{ResultExt, rel_path::RelPath};
 use workspace::{StatusItemView, Workspace};
 
 use crate::lsp_log_view;
@@ -148,6 +149,7 @@ impl LanguageServerState {
                                         return;
                                     };
                                     let project = workspace.read(cx).project().clone();
+                                    let path_style = project.read(cx).path_style(cx);
                                     let buffer_store = project.read(cx).buffer_store().clone();
                                     let buffers = state
                                         .read(cx)
@@ -159,6 +161,9 @@ impl LanguageServerState {
                                                 servers.worktree.as_ref()?.upgrade()?.read(cx);
                                             let relative_path =
                                                 abs_path.strip_prefix(&worktree.abs_path()).ok()?;
+                                            let relative_path =
+                                                RelPath::from_std_path(relative_path, path_style)
+                                                    .log_err()?;
                                             let entry = worktree.entry_for_path(&relative_path)?;
                                             let project_path =
                                                 project.read(cx).path_for_entry(entry.id, cx)?;
@@ -767,7 +772,7 @@ impl LspButton {
                 });
                 servers_with_health_checks.insert(&health.name);
                 let worktree_name =
-                    worktree.map(|worktree| SharedString::new(worktree.read(cx).root_name()));
+                    worktree.map(|worktree| SharedString::new(worktree.read(cx).root_name_str()));
 
                 let binary_status = state.language_servers.binary_statuses.get(&health.name);
                 let server_data = ServerData::WithHealthCheck {
@@ -826,7 +831,7 @@ impl LspButton {
                         {
                             Some((worktree, server_id)) => {
                                 let worktree_name =
-                                    SharedString::new(worktree.read(cx).root_name());
+                                    SharedString::new(worktree.read(cx).root_name_str());
                                 servers_per_worktree
                                     .entry(worktree_name.clone())
                                     .or_default()

crates/language_tools/src/lsp_log_view.rs 🔗

@@ -376,7 +376,7 @@ impl LspLogView {
                     let worktree_root_name = state
                         .worktree_id
                         .and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
-                        .map(|worktree| worktree.read(cx).root_name().to_string())
+                        .map(|worktree| worktree.read(cx).root_name_str().to_string())
                         .unwrap_or_else(|| "Unknown worktree".to_string());
 
                     LogMenuItem {

crates/language_tools/src/lsp_log_view_tests.rs 🔗

@@ -91,7 +91,7 @@ async fn test_lsp_log_view(cx: &mut TestAppContext) {
                     .next()
                     .unwrap()
                     .read(cx)
-                    .root_name()
+                    .root_name_str()
                     .to_string(),
                 rpc_trace_enabled: false,
                 selected_entry: LogKind::Logs,

crates/languages/src/json.rs 🔗

@@ -30,7 +30,10 @@ use std::{
 };
 use task::{AdapterSchemas, TaskTemplate, TaskTemplates, VariableName};
 use theme::ThemeRegistry;
-use util::{ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into};
+use util::{
+    ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into,
+    rel_path::RelPath,
+};
 
 use crate::PackageJsonData;
 
@@ -52,8 +55,8 @@ impl ContextProvider for JsonTaskProvider {
         let Some(file) = project::File::from_dyn(file.as_ref()).cloned() else {
             return Task::ready(None);
         };
-        let is_package_json = file.path.ends_with("package.json");
-        let is_composer_json = file.path.ends_with("composer.json");
+        let is_package_json = file.path.ends_with(RelPath::new("package.json").unwrap());
+        let is_composer_json = file.path.ends_with(RelPath::new("composer.json").unwrap());
         if !is_package_json && !is_composer_json {
             return Task::ready(None);
         }

crates/languages/src/python.rs 🔗

@@ -24,6 +24,7 @@ use smol::lock::OnceCell;
 use std::cmp::Ordering;
 use std::env::consts;
 use util::fs::{make_file_executable, remove_matching};
+use util::rel_path::RelPath;
 
 use parking_lot::Mutex;
 use std::str::FromStr;
@@ -52,9 +53,9 @@ impl ManifestProvider for PyprojectTomlManifestProvider {
             depth,
             delegate,
         }: ManifestQuery,
-    ) -> Option<Arc<Path>> {
+    ) -> Option<Arc<RelPath>> {
         for path in path.ancestors().take(depth) {
-            let p = path.join("pyproject.toml");
+            let p = path.join(RelPath::new("pyproject.toml").unwrap());
             if delegate.exists(&p, Some(false)) {
                 return Some(path.into());
             }
@@ -679,7 +680,7 @@ impl ContextProvider for PythonContextProvider {
                     .as_ref()
                     .and_then(|f| f.path().parent())
                     .map(Arc::from)
-                    .unwrap_or_else(|| Arc::from("".as_ref()));
+                    .unwrap_or_else(|| RelPath::empty().into());
 
                 toolchains
                     .active_toolchain(worktree_id, file_path, "Python".into(), cx)
@@ -1012,7 +1013,7 @@ impl ToolchainLister for PythonToolchainProvider {
     async fn list(
         &self,
         worktree_root: PathBuf,
-        subroot_relative_path: Arc<Path>,
+        subroot_relative_path: Arc<RelPath>,
         project_env: Option<HashMap<String, String>>,
     ) -> ToolchainList {
         let env = project_env.unwrap_or_default();
@@ -1024,7 +1025,6 @@ impl ToolchainLister for PythonToolchainProvider {
         );
         let mut config = Configuration::default();
 
-        debug_assert!(subroot_relative_path.is_relative());
         // `.ancestors()` will yield at least one path, so in case of empty `subroot_relative_path`, we'll just use
         // worktree root as the workspace directory.
         config.workspace_directories = Some(

crates/languages/src/rust.rs 🔗

@@ -23,6 +23,7 @@ use std::{
 use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
 use util::fs::{make_file_executable, remove_matching};
 use util::merge_json_value_into;
+use util::rel_path::RelPath;
 use util::{ResultExt, maybe};
 
 use crate::github_download::{GithubBinaryMetadata, download_server_binary};
@@ -88,10 +89,10 @@ impl ManifestProvider for CargoManifestProvider {
             depth,
             delegate,
         }: ManifestQuery,
-    ) -> Option<Arc<Path>> {
+    ) -> Option<Arc<RelPath>> {
         let mut outermost_cargo_toml = None;
         for path in path.ancestors().take(depth) {
-            let p = path.join("Cargo.toml");
+            let p = path.join(RelPath::new("Cargo.toml").unwrap());
             if delegate.exists(&p, Some(false)) {
                 outermost_cargo_toml = Some(Arc::from(path));
             }

crates/languages/src/typescript.rs 🔗

@@ -22,8 +22,8 @@ use std::{
     sync::{Arc, LazyLock},
 };
 use task::{TaskTemplate, TaskTemplates, VariableName};
-use util::merge_json_value_into;
 use util::{ResultExt, fs::remove_matching, maybe};
+use util::{merge_json_value_into, rel_path::RelPath};
 
 use crate::{PackageJson, PackageJsonData, github_download::download_server_binary};
 
@@ -264,7 +264,7 @@ impl TypeScriptContextProvider {
         &self,
         fs: Arc<dyn Fs>,
         worktree_root: &Path,
-        file_relative_path: &Path,
+        file_relative_path: &RelPath,
         cx: &App,
     ) -> Task<anyhow::Result<PackageJsonData>> {
         let new_json_data = file_relative_path
@@ -533,7 +533,7 @@ impl TypeScriptLspAdapter {
     }
     async fn tsdk_path(&self, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
         let is_yarn = adapter
-            .read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
+            .read_text_file(RelPath::new(".yarn/sdks/typescript/lib/typescript.js").unwrap())
             .await
             .is_ok();
 
@@ -1014,7 +1014,7 @@ mod tests {
     use serde_json::json;
     use task::TaskTemplates;
     use unindent::Unindent;
-    use util::path;
+    use util::{path, rel_path::rel_path};
 
     use crate::typescript::{
         PackageJsonData, TypeScriptContextProvider, replace_test_name_parameters,
@@ -1164,7 +1164,7 @@ mod tests {
                 provider.combined_package_json_data(
                     fs.clone(),
                     path!("/root").as_ref(),
-                    "sub/file1.js".as_ref(),
+                    rel_path("sub/file1.js"),
                     cx,
                 )
             })

crates/languages/src/vtsls.rs 🔗

@@ -12,7 +12,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::{ResultExt, maybe, merge_json_value_into};
+use util::{ResultExt, maybe, merge_json_value_into, rel_path::RelPath};
 
 fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
     vec![server_path.into(), "--stdio".into()]
@@ -36,7 +36,7 @@ impl VtslsLspAdapter {
 
     async fn tsdk_path(&self, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
         let is_yarn = adapter
-            .read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
+            .read_text_file(RelPath::new(".yarn/sdks/typescript/lib/typescript.js").unwrap())
             .await
             .is_ok();
 

crates/languages/src/yaml.rs 🔗

@@ -16,7 +16,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::{ResultExt, maybe, merge_json_value_into};
+use util::{ResultExt, maybe, merge_json_value_into, rel_path::RelPath};
 
 const SERVER_PATH: &str = "node_modules/yaml-language-server/bin/yaml-language-server";
 
@@ -141,7 +141,7 @@ impl LspAdapter for YamlLspAdapter {
     ) -> Result<Value> {
         let location = SettingsLocation {
             worktree_id: delegate.worktree_id(),
-            path: delegate.worktree_root_path(),
+            path: RelPath::empty(),
         };
 
         let tab_size = cx.update(|cx| {

crates/markdown/src/markdown.rs 🔗

@@ -334,7 +334,10 @@ impl Markdown {
                 }
 
                 for path in paths {
-                    if let Ok(language) = registry.language_for_file_path(&path).await {
+                    if let Ok(language) = registry
+                        .language_for_file_path(Path::new(path.as_ref()))
+                        .await
+                    {
                         languages_by_path.insert(path, language);
                     }
                 }
@@ -434,7 +437,7 @@ pub struct ParsedMarkdown {
     pub source: SharedString,
     pub events: Arc<[(Range<usize>, MarkdownEvent)]>,
     pub languages_by_name: TreeMap<SharedString, Arc<Language>>,
-    pub languages_by_path: TreeMap<Arc<Path>, Arc<Language>>,
+    pub languages_by_path: TreeMap<Arc<str>, Arc<Language>>,
 }
 
 impl ParsedMarkdown {

crates/markdown/src/parser.rs 🔗

@@ -4,7 +4,7 @@ pub use pulldown_cmark::TagEnd as MarkdownTagEnd;
 use pulldown_cmark::{
     Alignment, CowStr, HeadingLevel, LinkType, MetadataBlockKind, Options, Parser,
 };
-use std::{ops::Range, path::Path, sync::Arc};
+use std::{ops::Range, sync::Arc};
 
 use collections::HashSet;
 
@@ -25,7 +25,7 @@ pub fn parse_markdown(
 ) -> (
     Vec<(Range<usize>, MarkdownEvent)>,
     HashSet<SharedString>,
-    HashSet<Arc<Path>>,
+    HashSet<Arc<str>>,
 ) {
     let mut events = Vec::new();
     let mut language_names = HashSet::default();

crates/markdown/src/path_range.rs 🔗

@@ -1,8 +1,8 @@
-use std::{ops::Range, path::Path, sync::Arc};
+use std::{ops::Range, sync::Arc};
 
 #[derive(Debug, Clone, PartialEq)]
 pub struct PathWithRange {
-    pub path: Arc<Path>,
+    pub path: Arc<str>,
     pub range: Option<Range<LineCol>>,
 }
 
@@ -78,12 +78,12 @@ impl PathWithRange {
                 };
 
                 Self {
-                    path: Path::new(path).into(),
+                    path: path.into(),
                     range,
                 }
             }
             None => Self {
-                path: Path::new(str).into(),
+                path: str.into(),
                 range: None,
             },
         }
@@ -123,7 +123,7 @@ mod tests {
     #[test]
     fn test_pathrange_parsing() {
         let path_range = PathWithRange::new("file.rs#L10-L20");
-        assert_eq!(path_range.path.as_ref(), Path::new("file.rs"));
+        assert_eq!(path_range.path.as_ref(), "file.rs");
         assert!(path_range.range.is_some());
         if let Some(range) = path_range.range {
             assert_eq!(range.start.line, 10);
@@ -133,7 +133,7 @@ mod tests {
         }
 
         let single_line = PathWithRange::new("file.rs#L15");
-        assert_eq!(single_line.path.as_ref(), Path::new("file.rs"));
+        assert_eq!(single_line.path.as_ref(), "file.rs");
         assert!(single_line.range.is_some());
         if let Some(range) = single_line.range {
             assert_eq!(range.start.line, 15);
@@ -141,11 +141,11 @@ mod tests {
         }
 
         let no_range = PathWithRange::new("file.rs");
-        assert_eq!(no_range.path.as_ref(), Path::new("file.rs"));
+        assert_eq!(no_range.path.as_ref(), "file.rs");
         assert!(no_range.range.is_none());
 
         let lowercase = PathWithRange::new("file.rs#l5-l10");
-        assert_eq!(lowercase.path.as_ref(), Path::new("file.rs"));
+        assert_eq!(lowercase.path.as_ref(), "file.rs");
         assert!(lowercase.range.is_some());
         if let Some(range) = lowercase.range {
             assert_eq!(range.start.line, 5);
@@ -153,7 +153,7 @@ mod tests {
         }
 
         let complex = PathWithRange::new("src/path/to/file.rs#L100");
-        assert_eq!(complex.path.as_ref(), Path::new("src/path/to/file.rs"));
+        assert_eq!(complex.path.as_ref(), "src/path/to/file.rs");
         assert!(complex.range.is_some());
     }
 
@@ -161,7 +161,7 @@ mod tests {
     fn test_pathrange_from_str() {
         let with_range = PathWithRange::new("file.rs#L10-L20");
         assert!(with_range.range.is_some());
-        assert_eq!(with_range.path.as_ref(), Path::new("file.rs"));
+        assert_eq!(with_range.path.as_ref(), "file.rs");
 
         let without_range = PathWithRange::new("file.rs");
         assert!(without_range.range.is_none());
@@ -173,18 +173,18 @@ mod tests {
     #[test]
     fn test_pathrange_leading_text_trimming() {
         let with_language = PathWithRange::new("```rust file.rs#L10");
-        assert_eq!(with_language.path.as_ref(), Path::new("file.rs"));
+        assert_eq!(with_language.path.as_ref(), "file.rs");
         assert!(with_language.range.is_some());
         if let Some(range) = with_language.range {
             assert_eq!(range.start.line, 10);
         }
 
         let with_spaces = PathWithRange::new("```    file.rs#L10-L20");
-        assert_eq!(with_spaces.path.as_ref(), Path::new("file.rs"));
+        assert_eq!(with_spaces.path.as_ref(), "file.rs");
         assert!(with_spaces.range.is_some());
 
         let with_words = PathWithRange::new("```rust code example file.rs#L15:10");
-        assert_eq!(with_words.path.as_ref(), Path::new("file.rs"));
+        assert_eq!(with_words.path.as_ref(), "file.rs");
         assert!(with_words.range.is_some());
         if let Some(range) = with_words.range {
             assert_eq!(range.start.line, 15);
@@ -192,18 +192,18 @@ mod tests {
         }
 
         let with_whitespace = PathWithRange::new("  file.rs#L5");
-        assert_eq!(with_whitespace.path.as_ref(), Path::new("file.rs"));
+        assert_eq!(with_whitespace.path.as_ref(), "file.rs");
         assert!(with_whitespace.range.is_some());
 
         let no_leading = PathWithRange::new("file.rs#L10");
-        assert_eq!(no_leading.path.as_ref(), Path::new("file.rs"));
+        assert_eq!(no_leading.path.as_ref(), "file.rs");
         assert!(no_leading.range.is_some());
     }
 
     #[test]
     fn test_pathrange_with_line_and_column() {
         let line_and_col = PathWithRange::new("file.rs#L10:5");
-        assert_eq!(line_and_col.path.as_ref(), Path::new("file.rs"));
+        assert_eq!(line_and_col.path.as_ref(), "file.rs");
         assert!(line_and_col.range.is_some());
         if let Some(range) = line_and_col.range {
             assert_eq!(range.start.line, 10);
@@ -213,7 +213,7 @@ mod tests {
         }
 
         let full_range = PathWithRange::new("file.rs#L10:5-L20:15");
-        assert_eq!(full_range.path.as_ref(), Path::new("file.rs"));
+        assert_eq!(full_range.path.as_ref(), "file.rs");
         assert!(full_range.range.is_some());
         if let Some(range) = full_range.range {
             assert_eq!(range.start.line, 10);
@@ -223,7 +223,7 @@ mod tests {
         }
 
         let mixed_range1 = PathWithRange::new("file.rs#L10:5-L20");
-        assert_eq!(mixed_range1.path.as_ref(), Path::new("file.rs"));
+        assert_eq!(mixed_range1.path.as_ref(), "file.rs");
         assert!(mixed_range1.range.is_some());
         if let Some(range) = mixed_range1.range {
             assert_eq!(range.start.line, 10);
@@ -233,7 +233,7 @@ mod tests {
         }
 
         let mixed_range2 = PathWithRange::new("file.rs#L10-L20:15");
-        assert_eq!(mixed_range2.path.as_ref(), Path::new("file.rs"));
+        assert_eq!(mixed_range2.path.as_ref(), "file.rs");
         assert!(mixed_range2.range.is_some());
         if let Some(range) = mixed_range2.range {
             assert_eq!(range.start.line, 10);

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -37,7 +37,6 @@ use std::{
     iter::{self, FromIterator},
     mem,
     ops::{Range, RangeBounds, Sub},
-    path::{Path, PathBuf},
     rc::Rc,
     str,
     sync::Arc,
@@ -169,23 +168,23 @@ impl MultiBufferDiffHunk {
 #[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Hash, Debug)]
 pub struct PathKey {
     namespace: u32,
-    path: Arc<Path>,
+    path: Arc<str>,
 }
 
 impl PathKey {
-    pub fn namespaced(namespace: u32, path: Arc<Path>) -> Self {
+    pub fn namespaced(namespace: u32, path: Arc<str>) -> Self {
         Self { namespace, path }
     }
 
     pub fn for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Self {
         if let Some(file) = buffer.read(cx).file() {
-            Self::namespaced(1, Arc::from(file.full_path(cx)))
+            Self::namespaced(1, file.full_path(cx).to_string_lossy().to_string().into())
         } else {
-            Self::namespaced(0, Arc::from(PathBuf::from(buffer.entity_id().to_string())))
+            Self::namespaced(0, buffer.entity_id().to_string().into())
         }
     }
 
-    pub fn path(&self) -> &Arc<Path> {
+    pub fn path(&self) -> &Arc<str> {
         &self.path
     }
 }
@@ -2603,7 +2602,7 @@ impl MultiBuffer {
             let buffer = buffer.read(cx);
 
             if let Some(file) = buffer.file() {
-                return file.file_name(cx).to_string_lossy();
+                return file.file_name(cx).into();
             }
 
             if let Some(title) = self.buffer_content_title(buffer) {

crates/multi_buffer/src/multi_buffer_tests.rs 🔗

@@ -1524,7 +1524,7 @@ fn test_set_excerpts_for_buffer_ordering(cx: &mut TestAppContext) {
             cx,
         )
     });
-    let path1: PathKey = PathKey::namespaced(0, Path::new("/").into());
+    let path1: PathKey = PathKey::namespaced(0, "/".into());
 
     let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
     multibuffer.update(cx, |multibuffer, cx| {
@@ -1619,7 +1619,7 @@ fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) {
             cx,
         )
     });
-    let path1: PathKey = PathKey::namespaced(0, Path::new("/").into());
+    let path1: PathKey = PathKey::namespaced(0, "/".into());
     let buf2 = cx.new(|cx| {
         Buffer::local(
             indoc! {
@@ -1638,7 +1638,7 @@ fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) {
             cx,
         )
     });
-    let path2 = PathKey::namespaced(1, Path::new("/").into());
+    let path2 = PathKey::namespaced(1, "/".into());
 
     let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
     multibuffer.update(cx, |multibuffer, cx| {
@@ -1815,7 +1815,7 @@ fn test_set_excerpts_for_buffer_rename(cx: &mut TestAppContext) {
             cx,
         )
     });
-    let path: PathKey = PathKey::namespaced(0, Path::new("/").into());
+    let path: PathKey = PathKey::namespaced(0, "/".into());
     let buf2 = cx.new(|cx| {
         Buffer::local(
             indoc! {

crates/outline/src/outline.rs 🔗

@@ -389,7 +389,7 @@ mod tests {
     use language::{Language, LanguageConfig, LanguageMatcher};
     use project::{FakeFs, Project};
     use serde_json::json;
-    use util::path;
+    use util::{path, rel_path::rel_path};
     use workspace::{AppState, Workspace};
 
     #[gpui::test]
@@ -430,7 +430,7 @@ mod tests {
             .unwrap();
         let editor = workspace
             .update_in(cx, |workspace, window, cx| {
-                workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
+                workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
             })
             .await
             .unwrap()

crates/outline_panel/src/outline_panel.rs 🔗

@@ -29,7 +29,7 @@ use std::{
     collections::BTreeMap,
     hash::Hash,
     ops::Range,
-    path::{MAIN_SEPARATOR_STR, Path, PathBuf},
+    path::{Path, PathBuf},
     sync::{
         Arc, OnceLock,
         atomic::{self, AtomicBool},
@@ -51,7 +51,7 @@ use ui::{
     IndentGuideLayout, Label, LabelCommon, ListItem, ScrollAxes, Scrollbars, StyledExt,
     StyledTypography, Toggleable, Tooltip, WithScrollbar, h_flex, v_flex,
 };
-use util::{RangeExt, ResultExt, TryFutureExt, debug_panic};
+use util::{RangeExt, ResultExt, TryFutureExt, debug_panic, rel_path::RelPath};
 use workspace::{
     OpenInTerminal, WeakItemHandle, Workspace,
     dock::{DockPosition, Panel, PanelEvent},
@@ -107,7 +107,7 @@ pub struct OutlinePanel {
     pending_serialization: Task<Option<()>>,
     fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), usize>,
     fs_entries: Vec<FsEntry>,
-    fs_children_count: HashMap<WorktreeId, HashMap<Arc<Path>, FsChildren>>,
+    fs_children_count: HashMap<WorktreeId, HashMap<Arc<RelPath>, FsChildren>>,
     collapsed_entries: HashSet<CollapsedEntry>,
     unfolded_dirs: HashMap<WorktreeId, BTreeSet<ProjectEntryId>>,
     selected_entry: SelectedEntry,
@@ -1905,6 +1905,7 @@ impl OutlinePanel {
         _: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        let path_style = self.project.read(cx).path_style(cx);
         if let Some(clipboard_text) = self
             .selected_entry()
             .and_then(|entry| match entry {
@@ -1914,7 +1915,7 @@ impl OutlinePanel {
                 }
                 PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
             })
-            .map(|p| p.to_string_lossy().to_string())
+            .map(|p| p.display(path_style).to_string())
         {
             cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
         }
@@ -2272,7 +2273,7 @@ impl OutlinePanel {
                 let color =
                     entry_git_aware_label_color(entry.git_summary, entry.is_ignored, is_active);
                 let icon = if settings.file_icons {
-                    FileIcons::get_icon(&entry.path, cx)
+                    FileIcons::get_icon(entry.path.as_std_path(), cx)
                         .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
                 } else {
                     None
@@ -2303,7 +2304,7 @@ impl OutlinePanel {
                     is_active,
                 );
                 let icon = if settings.folder_icons {
-                    FileIcons::get_folder_icon(is_expanded, &directory.entry.path, cx)
+                    FileIcons::get_folder_icon(is_expanded, directory.entry.path.as_std_path(), cx)
                 } else {
                     FileIcons::get_chevron_icon(is_expanded, cx)
                 }
@@ -2329,13 +2330,13 @@ impl OutlinePanel {
                         Some(file) => {
                             let path = file.path();
                             let icon = if settings.file_icons {
-                                FileIcons::get_icon(path.as_ref(), cx)
+                                FileIcons::get_icon(path.as_std_path(), cx)
                             } else {
                                 None
                             }
                             .map(Icon::from_path)
                             .map(|icon| icon.color(color).into_any_element());
-                            (icon, file_name(path.as_ref()))
+                            (icon, file_name(path.as_std_path()))
                         }
                         None => (None, "Untitled".to_string()),
                     },
@@ -2615,19 +2616,17 @@ impl OutlinePanel {
                         if root_entry.id == entry.id {
                             file_name(worktree.abs_path().as_ref())
                         } else {
-                            let path = worktree.absolutize(entry.path.as_ref()).ok();
-                            let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
-                            file_name(path)
+                            let path = worktree.absolutize(entry.path.as_ref());
+                            file_name(&path)
                         }
                     }
                     None => {
-                        let path = worktree.absolutize(entry.path.as_ref()).ok();
-                        let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
-                        file_name(path)
+                        let path = worktree.absolutize(entry.path.as_ref());
+                        file_name(&path)
                     }
                 }
             }
-            None => file_name(entry.path.as_ref()),
+            None => file_name(entry.path.as_std_path()),
         }
     }
 
@@ -2842,7 +2841,7 @@ impl OutlinePanel {
                     }
 
                     let mut new_children_count =
-                        HashMap::<WorktreeId, HashMap<Arc<Path>, FsChildren>>::default();
+                        HashMap::<WorktreeId, HashMap<Arc<RelPath>, FsChildren>>::default();
 
                     let worktree_entries = new_worktree_entries
                         .into_iter()
@@ -3518,17 +3517,17 @@ impl OutlinePanel {
                 .buffer_snapshot_for_id(*buffer_id, cx)
                 .and_then(|buffer_snapshot| {
                     let file = File::from_dyn(buffer_snapshot.file())?;
-                    file.worktree.read(cx).absolutize(&file.path).ok()
+                    Some(file.worktree.read(cx).absolutize(&file.path))
                 }),
             PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
                 worktree_id, entry, ..
-            })) => self
-                .project
-                .read(cx)
-                .worktree_for_id(*worktree_id, cx)?
-                .read(cx)
-                .absolutize(&entry.path)
-                .ok(),
+            })) => Some(
+                self.project
+                    .read(cx)
+                    .worktree_for_id(*worktree_id, cx)?
+                    .read(cx)
+                    .absolutize(&entry.path),
+            ),
             PanelEntry::FoldedDirs(FoldedDirsEntry {
                 worktree_id,
                 entries: dirs,
@@ -3537,13 +3536,13 @@ impl OutlinePanel {
                 self.project
                     .read(cx)
                     .worktree_for_id(*worktree_id, cx)
-                    .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
+                    .map(|worktree| worktree.read(cx).absolutize(&entry.path))
             }),
             PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
         }
     }
 
-    fn relative_path(&self, entry: &FsEntry, cx: &App) -> Option<Arc<Path>> {
+    fn relative_path(&self, entry: &FsEntry, cx: &App) -> Option<Arc<RelPath>> {
         match entry {
             FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
                 let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?;
@@ -3627,7 +3626,7 @@ impl OutlinePanel {
 
                 #[derive(Debug)]
                 struct ParentStats {
-                    path: Arc<Path>,
+                    path: Arc<RelPath>,
                     folded: bool,
                     expanded: bool,
                     depth: usize,
@@ -4023,8 +4022,9 @@ impl OutlinePanel {
             let id = state.entries.len();
             match &entry {
                 PanelEntry::Fs(fs_entry) => {
-                    if let Some(file_name) =
-                        self.relative_path(fs_entry, cx).as_deref().map(file_name)
+                    if let Some(file_name) = self
+                        .relative_path(fs_entry, cx)
+                        .and_then(|path| Some(path.file_name()?.to_string()))
                     {
                         state
                             .match_candidates
@@ -4477,21 +4477,19 @@ impl OutlinePanel {
         let item_text_chars = match entry {
             PanelEntry::Fs(FsEntry::ExternalFile(external)) => self
                 .buffer_snapshot_for_id(external.buffer_id, cx)
-                .and_then(|snapshot| {
-                    Some(snapshot.file()?.path().file_name()?.to_string_lossy().len())
-                })
+                .and_then(|snapshot| Some(snapshot.file()?.path().file_name()?.len()))
                 .unwrap_or_default(),
             PanelEntry::Fs(FsEntry::Directory(directory)) => directory
                 .entry
                 .path
                 .file_name()
-                .map(|name| name.to_string_lossy().len())
+                .map(|name| name.len())
                 .unwrap_or_default(),
             PanelEntry::Fs(FsEntry::File(file)) => file
                 .entry
                 .path
                 .file_name()
-                .map(|name| name.to_string_lossy().len())
+                .map(|name| name.len())
                 .unwrap_or_default(),
             PanelEntry::FoldedDirs(folded_dirs) => {
                 folded_dirs
@@ -4500,11 +4498,11 @@ impl OutlinePanel {
                     .map(|dir| {
                         dir.path
                             .file_name()
-                            .map(|name| name.to_string_lossy().len())
+                            .map(|name| name.len())
                             .unwrap_or_default()
                     })
                     .sum::<usize>()
-                    + folded_dirs.entries.len().saturating_sub(1) * MAIN_SEPARATOR_STR.len()
+                    + folded_dirs.entries.len().saturating_sub(1) * "/".len()
             }
             PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self
                 .excerpt_label(excerpt.buffer_id, &excerpt.range, cx)
@@ -4799,7 +4797,7 @@ fn workspace_active_editor(
 }
 
 fn back_to_common_visited_parent(
-    visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
+    visited_dirs: &mut Vec<(ProjectEntryId, Arc<RelPath>)>,
     worktree_id: &WorktreeId,
     new_entry: &Entry,
 ) -> Option<(WorktreeId, ProjectEntryId)> {
@@ -5281,16 +5279,15 @@ mod tests {
                 });
         });
 
-        let all_matches = format!(
-            r#"{root}/
+        let all_matches = r#"rust-analyzer/
   crates/
     ide/src/
       inlay_hints/
         fn_lifetime_fn.rs
-          search: match config.param_names_for_lifetime_elision_hints {{
-          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{
-          search: Some(it) if config.param_names_for_lifetime_elision_hints => {{
-          search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }},
+          search: match config.param_names_for_lifetime_elision_hints {
+          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
+          search: Some(it) if config.param_names_for_lifetime_elision_hints => {
+          search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
       inlay_hints.rs
         search: pub param_names_for_lifetime_elision_hints: bool,
         search: param_names_for_lifetime_elision_hints: self
@@ -5302,7 +5299,7 @@ mod tests {
           search: param_names_for_lifetime_elision_hints: true,
       config.rs
         search: param_names_for_lifetime_elision_hints: self"#
-        );
+            .to_string();
 
         let select_first_in_all_matches = |line_to_select: &str| {
             assert!(all_matches.contains(line_to_select));
@@ -5360,7 +5357,7 @@ mod tests {
                     cx,
                 ),
                 format!(
-                    r#"{root}/
+                    r#"rust-analyzer/
   crates/
     ide/src/
       inlay_hints/
@@ -5430,7 +5427,7 @@ mod tests {
                     cx,
                 ),
                 format!(
-                    r#"{root}/
+                    r#"rust-analyzer/
   crates/
     ide/src/{SELECTED_MARKER}
     rust-analyzer/src/
@@ -5513,16 +5510,15 @@ mod tests {
                     );
                 });
         });
-        let all_matches = format!(
-            r#"{root}/
+        let all_matches = r#"rust-analyzer/
   crates/
     ide/src/
       inlay_hints/
         fn_lifetime_fn.rs
-          search: match config.param_names_for_lifetime_elision_hints {{
-          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{
-          search: Some(it) if config.param_names_for_lifetime_elision_hints => {{
-          search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }},
+          search: match config.param_names_for_lifetime_elision_hints {
+          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
+          search: Some(it) if config.param_names_for_lifetime_elision_hints => {
+          search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
       inlay_hints.rs
         search: pub param_names_for_lifetime_elision_hints: bool,
         search: param_names_for_lifetime_elision_hints: self
@@ -5534,7 +5530,7 @@ mod tests {
           search: param_names_for_lifetime_elision_hints: true,
       config.rs
         search: param_names_for_lifetime_elision_hints: self"#
-        );
+            .to_string();
 
         cx.executor()
             .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
@@ -5653,16 +5649,15 @@ mod tests {
                     );
                 });
         });
-        let all_matches = format!(
-            r#"{root}/
+        let all_matches = r#"rust-analyzer/
   crates/
     ide/src/
       inlay_hints/
         fn_lifetime_fn.rs
-          search: match config.param_names_for_lifetime_elision_hints {{
-          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{
-          search: Some(it) if config.param_names_for_lifetime_elision_hints => {{
-          search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }},
+          search: match config.param_names_for_lifetime_elision_hints {
+          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
+          search: Some(it) if config.param_names_for_lifetime_elision_hints => {
+          search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
       inlay_hints.rs
         search: pub param_names_for_lifetime_elision_hints: bool,
         search: param_names_for_lifetime_elision_hints: self
@@ -5674,7 +5669,7 @@ mod tests {
           search: param_names_for_lifetime_elision_hints: true,
       config.rs
         search: param_names_for_lifetime_elision_hints: self"#
-        );
+            .to_string();
         let select_first_in_all_matches = |line_to_select: &str| {
             assert!(all_matches.contains(line_to_select));
             all_matches.replacen(
@@ -5904,15 +5899,13 @@ mod tests {
                     cx,
                 ),
                 format!(
-                    r#"{}/
+                    r#"one/
   a.txt
     search: aaa aaa  <==== selected
     search: aaa aaa
-{}/
+two/
   b.txt
     search: a aaa"#,
-                    path!("/root/one"),
-                    path!("/root/two"),
                 ),
             );
         });
@@ -5934,13 +5927,11 @@ mod tests {
                     cx,
                 ),
                 format!(
-                    r#"{}/
+                    r#"one/
   a.txt  <==== selected
-{}/
+two/
   b.txt
     search: a aaa"#,
-                    path!("/root/one"),
-                    path!("/root/two"),
                 ),
             );
         });
@@ -5962,11 +5953,9 @@ mod tests {
                     cx,
                 ),
                 format!(
-                    r#"{}/
+                    r#"one/
   a.txt
-{}/  <==== selected"#,
-                    path!("/root/one"),
-                    path!("/root/two"),
+two/  <==== selected"#,
                 ),
             );
         });
@@ -5987,13 +5976,11 @@ mod tests {
                     cx,
                 ),
                 format!(
-                    r#"{}/
+                    r#"one/
   a.txt
-{}/  <==== selected
+two/  <==== selected
   b.txt
     search: a aaa"#,
-                    path!("/root/one"),
-                    path!("/root/two"),
                 )
             );
         });
@@ -6455,7 +6442,7 @@ outline: struct OutlineEntryExcerpt
                     cx,
                 ),
                 format!(
-                    r#"{root}/
+                    r#"frontend-project/
   public/lottie/
     syntax-tree.json
       search: {{ "something": "static" }}  <==== selected
@@ -6494,7 +6481,7 @@ outline: struct OutlineEntryExcerpt
                     cx,
                 ),
                 format!(
-                    r#"{root}/
+                    r#"frontend-project/
   public/lottie/
     syntax-tree.json
       search: {{ "something": "static" }}
@@ -6524,7 +6511,7 @@ outline: struct OutlineEntryExcerpt
                     cx,
                 ),
                 format!(
-                    r#"{root}/
+                    r#"frontend-project/
   public/lottie/
     syntax-tree.json
       search: {{ "something": "static" }}
@@ -6558,7 +6545,7 @@ outline: struct OutlineEntryExcerpt
                     cx,
                 ),
                 format!(
-                    r#"{root}/
+                    r#"frontend-project/
   public/lottie/
     syntax-tree.json
       search: {{ "something": "static" }}
@@ -6591,7 +6578,7 @@ outline: struct OutlineEntryExcerpt
                     cx,
                 ),
                 format!(
-                    r#"{root}/
+                    r#"frontend-project/
   public/lottie/
     syntax-tree.json
       search: {{ "something": "static" }}
@@ -6649,6 +6636,7 @@ outline: struct OutlineEntryExcerpt
         selected_entry: Option<&PanelEntry>,
         cx: &mut App,
     ) -> String {
+        let project = project.read(cx);
         let mut display_string = String::new();
         for entry in cached_entries {
             if !display_string.is_empty() {
@@ -6663,44 +6651,39 @@ outline: struct OutlineEntryExcerpt
                         panic!("Did not cover external files with tests")
                     }
                     FsEntry::Directory(directory) => {
-                        match project
-                            .read(cx)
+                        let path = if let Some(worktree) = project
                             .worktree_for_id(directory.worktree_id, cx)
-                            .and_then(|worktree| {
-                                if worktree.read(cx).root_entry() == Some(&directory.entry.entry) {
-                                    Some(worktree.read(cx).abs_path())
-                                } else {
-                                    None
-                                }
+                            .filter(|worktree| {
+                                worktree.read(cx).root_entry() == Some(&directory.entry.entry)
                             }) {
-                            Some(root_path) => format!(
-                                "{}/{}",
-                                root_path.display(),
-                                directory.entry.path.display(),
-                            ),
-                            None => format!(
-                                "{}/",
-                                directory
-                                    .entry
-                                    .path
-                                    .file_name()
-                                    .unwrap_or_default()
-                                    .to_string_lossy()
-                            ),
-                        }
+                            worktree
+                                .read(cx)
+                                .root_name()
+                                .join(&directory.entry.path)
+                                .as_str()
+                                .to_string()
+                        } else {
+                            directory
+                                .entry
+                                .path
+                                .file_name()
+                                .unwrap_or_default()
+                                .to_string()
+                        };
+                        format!("{path}/")
                     }
                     FsEntry::File(file) => file
                         .entry
                         .path
                         .file_name()
-                        .map(|name| name.to_string_lossy().to_string())
+                        .map(|name| name.to_string())
                         .unwrap_or_default(),
                 },
                 PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
                     .entries
                     .iter()
                     .filter_map(|dir| dir.path.file_name())
-                    .map(|name| name.to_string_lossy().to_string() + "/")
+                    .map(|name| name.to_string() + "/")
                     .collect(),
                 PanelEntry::Outline(outline_entry) => match outline_entry {
                     OutlineEntry::Excerpt(_) => continue,

crates/paths/src/paths.rs 🔗

@@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
 use std::sync::OnceLock;
 
 pub use util::paths::home_dir;
+use util::rel_path::RelPath;
 
 /// A default editorconfig file name to use when resolving project settings.
 pub const EDITORCONFIG_NAME: &str = ".editorconfig";
@@ -29,13 +30,13 @@ static CURRENT_DATA_DIR: OnceLock<PathBuf> = OnceLock::new();
 static CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
 
 /// Returns the relative path to the zed_server directory on the ssh host.
-pub fn remote_server_dir_relative() -> &'static Path {
-    Path::new(".zed_server")
+pub fn remote_server_dir_relative() -> &'static RelPath {
+    RelPath::new(".zed_server").unwrap()
 }
 
 /// Returns the relative path to the zed_wsl_server directory on the wsl host.
-pub fn remote_wsl_server_dir_relative() -> &'static Path {
-    Path::new(".zed_wsl_server")
+pub fn remote_wsl_server_dir_relative() -> &'static RelPath {
+    RelPath::new(".zed_wsl_server").unwrap()
 }
 
 /// Sets a custom directory for all user data, overriding the default data directory.
@@ -398,28 +399,28 @@ pub fn remote_servers_dir() -> &'static PathBuf {
 }
 
 /// Returns the relative path to a `.zed` folder within a project.
-pub fn local_settings_folder_relative_path() -> &'static Path {
-    Path::new(".zed")
+pub fn local_settings_folder_name() -> &'static str {
+    ".zed"
 }
 
 /// Returns the relative path to a `.vscode` folder within a project.
-pub fn local_vscode_folder_relative_path() -> &'static Path {
-    Path::new(".vscode")
+pub fn local_vscode_folder_name() -> &'static str {
+    ".vscode"
 }
 
 /// Returns the relative path to a `settings.json` file within a project.
-pub fn local_settings_file_relative_path() -> &'static Path {
-    Path::new(".zed/settings.json")
+pub fn local_settings_file_relative_path() -> &'static RelPath {
+    RelPath::new(".zed/settings.json").unwrap()
 }
 
 /// Returns the relative path to a `tasks.json` file within a project.
-pub fn local_tasks_file_relative_path() -> &'static Path {
-    Path::new(".zed/tasks.json")
+pub fn local_tasks_file_relative_path() -> &'static RelPath {
+    RelPath::new(".zed/tasks.json").unwrap()
 }
 
 /// Returns the relative path to a `.vscode/tasks.json` file within a project.
-pub fn local_vscode_tasks_file_relative_path() -> &'static Path {
-    Path::new(".vscode/tasks.json")
+pub fn local_vscode_tasks_file_relative_path() -> &'static RelPath {
+    RelPath::new(".vscode/tasks.json").unwrap()
 }
 
 pub fn debug_task_file_name() -> &'static str {
@@ -432,13 +433,13 @@ pub fn task_file_name() -> &'static str {
 
 /// Returns the relative path to a `debug.json` file within a project.
 /// .zed/debug.json
-pub fn local_debug_file_relative_path() -> &'static Path {
-    Path::new(".zed/debug.json")
+pub fn local_debug_file_relative_path() -> &'static RelPath {
+    RelPath::new(".zed/debug.json").unwrap()
 }
 
 /// Returns the relative path to a `.vscode/launch.json` file within a project.
-pub fn local_vscode_launch_file_relative_path() -> &'static Path {
-    Path::new(".vscode/launch.json")
+pub fn local_vscode_launch_file_relative_path() -> &'static RelPath {
+    RelPath::new(".vscode/launch.json").unwrap()
 }
 
 pub fn user_ssh_config_file() -> PathBuf {

crates/prettier/src/prettier.rs 🔗

@@ -12,7 +12,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::paths::PathMatcher;
+use util::paths::{PathMatcher, PathStyle};
 
 #[derive(Debug, Clone)]
 pub enum Prettier {
@@ -119,7 +119,7 @@ impl Prettier {
                                             None
                                         }
                                     }).any(|workspace_definition| {
-                                        workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]).ok().is_some_and(|path_matcher| path_matcher.is_match(subproject_path))
+                                        workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition], PathStyle::local()).ok().is_some_and(|path_matcher| path_matcher.is_match(subproject_path))
                                     }) {
                                         anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Path {path_to_check:?} is the workspace root for project in {closest_package_json_path:?}, but it has no prettier installed");
                                         log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {closest_package_json_path:?}");
@@ -215,11 +215,14 @@ impl Prettier {
                                 })
                                 .any(|workspace_definition| {
                                     workspace_definition == subproject_path.to_string_lossy()
-                                        || PathMatcher::new(&[workspace_definition])
-                                            .ok()
-                                            .is_some_and(|path_matcher| {
-                                                path_matcher.is_match(subproject_path)
-                                            })
+                                        || PathMatcher::new(
+                                            &[workspace_definition],
+                                            PathStyle::local(),
+                                        )
+                                        .ok()
+                                        .is_some_and(
+                                            |path_matcher| path_matcher.is_match(subproject_path),
+                                        )
                                 })
                             {
                                 let workspace_ignore = path_to_check.join(".prettierignore");

crates/project/Cargo.toml 🔗

@@ -58,7 +58,6 @@ lsp.workspace = true
 markdown.workspace = true
 node_runtime.workspace = true
 parking_lot.workspace = true
-pathdiff.workspace = true
 paths.workspace = true
 postage.workspace = true
 prettier.workspace = true

crates/project/src/agent_server_store.rs 🔗

@@ -16,10 +16,7 @@ use gpui::{
 };
 use node_runtime::NodeRuntime;
 use remote::RemoteClient;
-use rpc::{
-    AnyProtoClient, TypedEnvelope,
-    proto::{self, ToProto},
-};
+use rpc::{AnyProtoClient, TypedEnvelope, proto};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{SettingsContent, SettingsStore};
@@ -845,7 +842,7 @@ impl ExternalAgentServer for LocalGemini {
 
             // Gemini CLI doesn't seem to have a dedicated invocation for logging in--we just run it normally without any arguments.
             let login = task::SpawnInTerminal {
-                command: Some(command.path.clone().to_proto()),
+                command: Some(command.path.to_string_lossy().to_string()),
                 args: command.args.clone(),
                 env: command.env.clone().unwrap_or_default(),
                 label: "gemini /auth".into(),
@@ -854,7 +851,7 @@ impl ExternalAgentServer for LocalGemini {
 
             command.env.get_or_insert_default().extend(extra_env);
             command.args.push("--experimental-acp".into());
-            Ok((command, root_dir.to_proto(), Some(login)))
+            Ok((command, root_dir.to_string_lossy().to_string(), Some(login)))
         })
     }
 
@@ -922,7 +919,7 @@ impl ExternalAgentServer for LocalClaudeCode {
                         path.strip_suffix("/@zed-industries/claude-code-acp/dist/index.js")
                     })
                     .map(|path_prefix| task::SpawnInTerminal {
-                        command: Some(command.path.clone().to_proto()),
+                        command: Some(command.path.to_string_lossy().to_string()),
                         args: vec![
                             Path::new(path_prefix)
                                 .join("@anthropic-ai/claude-code/cli.js")
@@ -938,7 +935,7 @@ impl ExternalAgentServer for LocalClaudeCode {
             };
 
             command.env.get_or_insert_default().extend(extra_env);
-            Ok((command, root_dir.to_proto(), login))
+            Ok((command, root_dir.to_string_lossy().to_string(), login))
         })
     }
 
@@ -977,7 +974,7 @@ impl ExternalAgentServer for LocalCustomAgent {
             env.extend(command.env.unwrap_or_default());
             env.extend(extra_env);
             command.env = Some(env);
-            Ok((command, root_dir.to_proto(), None))
+            Ok((command, root_dir.to_string_lossy().to_string(), None))
         })
     }
 

crates/project/src/buffer_store.rs 🔗

@@ -21,12 +21,12 @@ use language::{
 };
 use rpc::{
     AnyProtoClient, ErrorCode, ErrorExt as _, TypedEnvelope,
-    proto::{self, ToProto},
+    proto::{self},
 };
 use smol::channel::Receiver;
-use std::{io, path::Path, pin::pin, sync::Arc, time::Instant};
+use std::{io, pin::pin, sync::Arc, time::Instant};
 use text::BufferId;
-use util::{ResultExt as _, TryFutureExt, debug_panic, maybe};
+use util::{ResultExt as _, TryFutureExt, debug_panic, maybe, rel_path::RelPath};
 use worktree::{File, PathChange, ProjectEntryId, Worktree, WorktreeId};
 
 /// A set of open buffers.
@@ -292,7 +292,7 @@ impl RemoteBufferStore {
 
     fn open_buffer(
         &self,
-        path: Arc<Path>,
+        path: Arc<RelPath>,
         worktree: Entity<Worktree>,
         cx: &mut Context<BufferStore>,
     ) -> Task<Result<Entity<Buffer>>> {
@@ -370,7 +370,7 @@ impl LocalBufferStore {
         &self,
         buffer_handle: Entity<Buffer>,
         worktree: Entity<Worktree>,
-        path: Arc<Path>,
+        path: Arc<RelPath>,
         mut has_changed_file: bool,
         cx: &mut Context<BufferStore>,
     ) -> Task<Result<()>> {
@@ -389,7 +389,7 @@ impl LocalBufferStore {
         }
 
         let save = worktree.update(cx, |worktree, cx| {
-            worktree.write_file(path.as_ref(), text, line_ending, cx)
+            worktree.write_file(path, text, line_ending, cx)
         });
 
         cx.spawn(async move |this, cx| {
@@ -443,7 +443,7 @@ impl LocalBufferStore {
     fn local_worktree_entries_changed(
         this: &mut BufferStore,
         worktree_handle: &Entity<Worktree>,
-        changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
+        changes: &[(Arc<RelPath>, ProjectEntryId, PathChange)],
         cx: &mut Context<BufferStore>,
     ) {
         let snapshot = worktree_handle.read(cx).snapshot();
@@ -462,7 +462,7 @@ impl LocalBufferStore {
     fn local_worktree_entry_changed(
         this: &mut BufferStore,
         entry_id: ProjectEntryId,
-        path: &Arc<Path>,
+        path: &Arc<RelPath>,
         worktree: &Entity<worktree::Worktree>,
         snapshot: &worktree::Snapshot,
         cx: &mut Context<BufferStore>,
@@ -615,7 +615,7 @@ impl LocalBufferStore {
 
     fn open_buffer(
         &self,
-        path: Arc<Path>,
+        path: Arc<RelPath>,
         worktree: Entity<Worktree>,
         cx: &mut Context<BufferStore>,
     ) -> Task<Result<Entity<Buffer>>> {
@@ -1402,8 +1402,9 @@ impl BufferStore {
             .await?;
         let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?;
 
-        if let Some(new_path) = envelope.payload.new_path {
-            let new_path = ProjectPath::from_proto(new_path);
+        if let Some(new_path) = envelope.payload.new_path
+            && let Some(new_path) = ProjectPath::from_proto(new_path)
+        {
             this.update(&mut cx, |this, cx| {
                 this.save_buffer_as(buffer.clone(), new_path, cx)
             })?

crates/project/src/context_server_store.rs 🔗

@@ -1,7 +1,7 @@
 pub mod extension;
 pub mod registry;
 
-use std::{path::Path, sync::Arc};
+use std::sync::Arc;
 
 use anyhow::{Context as _, Result};
 use collections::{HashMap, HashSet};
@@ -10,7 +10,7 @@ use futures::{FutureExt as _, future::join_all};
 use gpui::{App, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, actions};
 use registry::ContextServerDescriptorRegistry;
 use settings::{Settings as _, SettingsStore};
-use util::ResultExt as _;
+use util::{ResultExt as _, rel_path::RelPath};
 
 use crate::{
     Project,
@@ -510,7 +510,7 @@ impl ContextServerStore {
             .next()
             .map(|worktree| settings::SettingsLocation {
                 worktree_id: worktree.read(cx).id(),
-                path: Path::new(""),
+                path: RelPath::empty(),
             });
         &ProjectSettings::get(location, cx).context_servers
     }

crates/project/src/debugger/breakpoint_store.rs 🔗

@@ -387,7 +387,7 @@ impl BreakpointStore {
 
     pub fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
         worktree::File::from_dyn(buffer.read(cx).file())
-            .and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok())
+            .map(|file| file.worktree.read(cx).absolutize(&file.path))
             .map(Arc::<Path>::from)
     }
 
@@ -794,7 +794,7 @@ impl BreakpointStore {
                         .update(cx, |this, cx| {
                             let path = ProjectPath {
                                 worktree_id: worktree.read(cx).id(),
-                                path: relative_path.into(),
+                                path: relative_path,
                             };
                             this.open_buffer(path, cx)
                         })?

crates/project/src/debugger/dap_store.rs 🔗

@@ -50,7 +50,7 @@ use std::{
     sync::{Arc, Once},
 };
 use task::{DebugScenario, SpawnInTerminal, TaskContext, TaskTemplate};
-use util::ResultExt as _;
+use util::{ResultExt as _, rel_path::RelPath};
 use worktree::Worktree;
 
 #[derive(Debug)]
@@ -206,7 +206,7 @@ impl DapStore {
 
                 let settings_location = SettingsLocation {
                     worktree_id: worktree.read(cx).id(),
-                    path: Path::new(""),
+                    path: RelPath::empty(),
                 };
                 let dap_settings = ProjectSettings::get(Some(settings_location), cx)
                     .dap
@@ -943,15 +943,13 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate {
     fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
         self.toolchain_store.clone()
     }
-    async fn read_text_file(&self, path: PathBuf) -> Result<String> {
+
+    async fn read_text_file(&self, path: &RelPath) -> Result<String> {
         let entry = self
             .worktree
-            .entry_for_path(&path)
+            .entry_for_path(path)
             .with_context(|| format!("no worktree entry for path {path:?}"))?;
-        let abs_path = self
-            .worktree
-            .absolutize(&entry.path)
-            .with_context(|| format!("cannot absolutize path {path:?}"))?;
+        let abs_path = self.worktree.absolutize(&entry.path);
 
         self.fs.load(&abs_path).await
     }

crates/project/src/git_store.rs 🔗

@@ -20,7 +20,7 @@ use futures::{
     stream::FuturesOrdered,
 };
 use git::{
-    BuildPermalinkParams, GitHostingProviderRegistry, Oid, WORK_DIRECTORY_REPO_PATH,
+    BuildPermalinkParams, GitHostingProviderRegistry, Oid,
     blame::Blame,
     parse_git_remote_url,
     repository::{
@@ -45,7 +45,7 @@ use parking_lot::Mutex;
 use postage::stream::Stream as _;
 use rpc::{
     AnyProtoClient, TypedEnvelope,
-    proto::{self, FromProto, ToProto, git_reset, split_repository_update},
+    proto::{self, git_reset, split_repository_update},
 };
 use serde::Deserialize;
 use std::{
@@ -63,7 +63,12 @@ use std::{
 };
 use sum_tree::{Edit, SumTree, TreeSet};
 use text::{Bias, BufferId};
-use util::{ResultExt, debug_panic, paths::SanitizedPath, post_inc};
+use util::{
+    ResultExt, debug_panic,
+    paths::{PathStyle, SanitizedPath},
+    post_inc,
+    rel_path::RelPath,
+};
 use worktree::{
     File, PathChange, PathKey, PathProgress, PathSummary, PathTarget, ProjectEntryId,
     UpdatedGitRepositoriesSet, UpdatedGitRepository, Worktree,
@@ -189,7 +194,7 @@ impl StatusEntry {
         };
 
         proto::StatusEntry {
-            repo_path: self.repo_path.as_ref().to_proto(),
+            repo_path: self.repo_path.to_proto(),
             simple_status,
             status: Some(status_to_proto(self.status)),
         }
@@ -200,7 +205,7 @@ impl TryFrom<proto::StatusEntry> for StatusEntry {
     type Error = anyhow::Error;
 
     fn try_from(value: proto::StatusEntry) -> Result<Self, Self::Error> {
-        let repo_path = RepoPath(Arc::<Path>::from_proto(value.repo_path));
+        let repo_path = RepoPath::from_proto(&value.repo_path).context("invalid repo path")?;
         let status = status_from_proto(value.simple_status, value.status)?;
         Ok(Self { repo_path, status })
     }
@@ -240,6 +245,7 @@ pub struct RepositorySnapshot {
     pub id: RepositoryId,
     pub statuses_by_path: SumTree<StatusEntry>,
     pub work_directory_abs_path: Arc<Path>,
+    pub path_style: PathStyle,
     pub branch: Option<Branch>,
     pub head_commit: Option<CommitDetails>,
     pub scan_id: u64,
@@ -947,9 +953,7 @@ impl GitStore {
             {
                 return Task::ready(Err(anyhow!("no permalink available")));
             }
-            let Some(file_path) = file.worktree.read(cx).absolutize(&file.path).ok() else {
-                return Task::ready(Err(anyhow!("no permalink available")));
-            };
+            let file_path = file.worktree.read(cx).absolutize(&file.path);
             return cx.spawn(async move |cx| {
                 let provider_registry = cx.update(GitHostingProviderRegistry::default_global)?;
                 get_permalink_in_rust_registry_src(provider_registry, file_path, selection)
@@ -985,9 +989,7 @@ impl GitStore {
                             parse_git_remote_url(provider_registry, &origin_url)
                                 .context("parsing Git remote URL")?;
 
-                        let path = repo_path.to_str().with_context(|| {
-                            format!("converting repo path {repo_path:?} to string")
-                        })?;
+                        let path = repo_path.as_str();
 
                         Ok(provider.build_permalink(
                             remote,
@@ -1313,7 +1315,7 @@ impl GitStore {
                 });
                 if let Some((repo, path)) = self.repository_and_path_for_buffer_id(buffer_id, cx) {
                     let recv = repo.update(cx, |repo, cx| {
-                        log::debug!("hunks changed for {}", path.display());
+                        log::debug!("hunks changed for {}", path.as_str());
                         repo.spawn_set_index_text_job(
                             path,
                             new_index_text.as_ref().map(|rope| rope.to_string()),
@@ -1475,6 +1477,7 @@ impl GitStore {
         mut cx: AsyncApp,
     ) -> Result<()> {
         this.update(&mut cx, |this, cx| {
+            let path_style = this.worktree_store.read(cx).path_style();
             let mut update = envelope.payload;
 
             let id = RepositoryId::from_proto(update.id);
@@ -1488,6 +1491,7 @@ impl GitStore {
                     Repository::remote(
                         id,
                         Path::new(&update.abs_path).into(),
+                        path_style,
                         ProjectId(update.project_id),
                         client,
                         git_store,
@@ -1681,9 +1685,8 @@ impl GitStore {
             .payload
             .paths
             .into_iter()
-            .map(PathBuf::from)
-            .map(RepoPath::new)
-            .collect();
+            .map(|path| RepoPath::new(&path))
+            .collect::<Result<Vec<_>>>()?;
 
         repository_handle
             .update(&mut cx, |repository_handle, cx| {
@@ -1705,9 +1708,8 @@ impl GitStore {
             .payload
             .paths
             .into_iter()
-            .map(PathBuf::from)
-            .map(RepoPath::new)
-            .collect();
+            .map(|path| RepoPath::new(&path))
+            .collect::<Result<Vec<_>>>()?;
 
         repository_handle
             .update(&mut cx, |repository_handle, cx| {
@@ -1730,9 +1732,8 @@ impl GitStore {
             .payload
             .paths
             .into_iter()
-            .map(PathBuf::from)
-            .map(RepoPath::new)
-            .collect();
+            .map(|path| RepoPath::new(&path))
+            .collect::<Result<Vec<_>>>()?;
 
         repository_handle
             .update(&mut cx, |repository_handle, cx| {
@@ -1804,7 +1805,7 @@ impl GitStore {
     ) -> Result<proto::Ack> {
         let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
         let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
-        let repo_path = RepoPath::from_str(&envelope.payload.path);
+        let repo_path = RepoPath::from_proto(&envelope.payload.path)?;
 
         repository_handle
             .update(&mut cx, |repository_handle, cx| {
@@ -2005,7 +2006,7 @@ impl GitStore {
                 .files
                 .into_iter()
                 .map(|file| proto::CommitFile {
-                    path: file.path.to_string(),
+                    path: file.path.to_proto(),
                     old_text: file.old_text,
                     new_text: file.new_text,
                 })
@@ -2045,8 +2046,8 @@ impl GitStore {
             .payload
             .paths
             .iter()
-            .map(|s| RepoPath::from_str(s))
-            .collect();
+            .map(|s| RepoPath::from_proto(s))
+            .collect::<Result<Vec<_>>>()?;
 
         repository_handle
             .update(&mut cx, |repository_handle, cx| {
@@ -2332,9 +2333,10 @@ impl GitStore {
     fn process_updated_entries(
         &self,
         worktree: &Entity<Worktree>,
-        updated_entries: &[(Arc<Path>, ProjectEntryId, PathChange)],
+        updated_entries: &[(Arc<RelPath>, ProjectEntryId, PathChange)],
         cx: &mut App,
     ) -> Task<HashMap<Entity<Repository>, Vec<RepoPath>>> {
+        let path_style = worktree.read(cx).path_style();
         let mut repo_paths = self
             .repositories
             .values()
@@ -2349,7 +2351,7 @@ impl GitStore {
 
         let entries = entries
             .into_iter()
-            .filter_map(|path| worktree.absolutize(&path).ok())
+            .map(|path| worktree.absolutize(&path))
             .collect::<Arc<[_]>>();
 
         let executor = cx.background_executor().clone();
@@ -2369,8 +2371,9 @@ impl GitStore {
                     let mut paths = Vec::new();
                     // All paths prefixed by a given repo will constitute a continuous range.
                     while let Some(path) = entries.get(ix)
-                        && let Some(repo_path) =
-                            RepositorySnapshot::abs_path_to_repo_path_inner(&repo_path, path)
+                        && let Some(repo_path) = RepositorySnapshot::abs_path_to_repo_path_inner(
+                            &repo_path, path, path_style,
+                        )
                     {
                         paths.push((repo_path, ix));
                         ix += 1;
@@ -2764,7 +2767,7 @@ impl RepositoryId {
 }
 
 impl RepositorySnapshot {
-    fn empty(id: RepositoryId, work_directory_abs_path: Arc<Path>) -> Self {
+    fn empty(id: RepositoryId, work_directory_abs_path: Arc<Path>, path_style: PathStyle) -> Self {
         Self {
             id,
             statuses_by_path: Default::default(),
@@ -2776,6 +2779,7 @@ impl RepositorySnapshot {
             remote_origin_url: None,
             remote_upstream_url: None,
             stash_entries: Default::default(),
+            path_style,
         }
     }
 
@@ -2798,7 +2802,7 @@ impl RepositorySnapshot {
             merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()),
             project_id,
             id: self.id.to_proto(),
-            abs_path: self.work_directory_abs_path.to_proto(),
+            abs_path: self.work_directory_abs_path.to_string_lossy().to_string(),
             entry_ids: vec![self.id.to_proto()],
             scan_id: self.scan_id,
             is_last_update: true,
@@ -2836,13 +2840,13 @@ impl RepositorySnapshot {
                             current_new_entry = new_statuses.next();
                         }
                         Ordering::Greater => {
-                            removed_statuses.push(old_entry.repo_path.as_ref().to_proto());
+                            removed_statuses.push(old_entry.repo_path.to_proto());
                             current_old_entry = old_statuses.next();
                         }
                     }
                 }
                 (None, Some(old_entry)) => {
-                    removed_statuses.push(old_entry.repo_path.as_ref().to_proto());
+                    removed_statuses.push(old_entry.repo_path.to_proto());
                     current_old_entry = old_statuses.next();
                 }
                 (Some(new_entry), None) => {
@@ -2862,12 +2866,12 @@ impl RepositorySnapshot {
                 .merge
                 .conflicted_paths
                 .iter()
-                .map(|path| path.as_ref().to_proto())
+                .map(|path| path.to_proto())
                 .collect(),
             merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()),
             project_id,
             id: self.id.to_proto(),
-            abs_path: self.work_directory_abs_path.to_proto(),
+            abs_path: self.work_directory_abs_path.to_string_lossy().to_string(),
             entry_ids: vec![],
             scan_id: self.scan_id,
             is_last_update: true,
@@ -2895,18 +2899,19 @@ impl RepositorySnapshot {
     }
 
     pub fn abs_path_to_repo_path(&self, abs_path: &Path) -> Option<RepoPath> {
-        Self::abs_path_to_repo_path_inner(&self.work_directory_abs_path, abs_path)
+        Self::abs_path_to_repo_path_inner(&self.work_directory_abs_path, abs_path, self.path_style)
     }
 
     #[inline]
     fn abs_path_to_repo_path_inner(
         work_directory_abs_path: &Path,
         abs_path: &Path,
+        path_style: PathStyle,
     ) -> Option<RepoPath> {
         abs_path
             .strip_prefix(&work_directory_abs_path)
-            .map(RepoPath::from)
             .ok()
+            .and_then(|path| RepoPath::from_std_path(path, path_style).ok())
     }
 
     pub fn had_conflict_on_last_merge_head_change(&self, repo_path: &RepoPath) -> bool {
@@ -3032,7 +3037,8 @@ impl Repository {
         git_store: WeakEntity<GitStore>,
         cx: &mut Context<Self>,
     ) -> Self {
-        let snapshot = RepositorySnapshot::empty(id, work_directory_abs_path.clone());
+        let snapshot =
+            RepositorySnapshot::empty(id, work_directory_abs_path.clone(), PathStyle::local());
         Repository {
             this: cx.weak_entity(),
             git_store,
@@ -3058,12 +3064,13 @@ impl Repository {
     fn remote(
         id: RepositoryId,
         work_directory_abs_path: Arc<Path>,
+        path_style: PathStyle,
         project_id: ProjectId,
         client: AnyProtoClient,
         git_store: WeakEntity<GitStore>,
         cx: &mut Context<Self>,
     ) -> Self {
-        let snapshot = RepositorySnapshot::empty(id, work_directory_abs_path);
+        let snapshot = RepositorySnapshot::empty(id, work_directory_abs_path, path_style);
         Self {
             this: cx.weak_entity(),
             snapshot,
@@ -3107,12 +3114,11 @@ impl Repository {
                                 let buffer_store = git_store.buffer_store.read(cx);
                                 let buffer = buffer_store.get(*buffer_id)?;
                                 let file = File::from_dyn(buffer.read(cx).file())?;
-                                let abs_path =
-                                    file.worktree.read(cx).absolutize(&file.path).ok()?;
+                                let abs_path = file.worktree.read(cx).absolutize(&file.path);
                                 let repo_path = this.abs_path_to_repo_path(&abs_path)?;
                                 log::debug!(
                                     "start reload diff bases for repo path {}",
-                                    repo_path.0.display()
+                                    repo_path.as_str()
                                 );
                                 diff_state.update(cx, |diff_state, _| {
                                     let has_unstaged_diff = diff_state
@@ -3335,12 +3341,15 @@ impl Repository {
     pub fn repo_path_to_project_path(&self, path: &RepoPath, cx: &App) -> Option<ProjectPath> {
         let git_store = self.git_store.upgrade()?;
         let worktree_store = git_store.read(cx).worktree_store.read(cx);
-        let abs_path = self.snapshot.work_directory_abs_path.join(&path.0);
+        let abs_path = self
+            .snapshot
+            .work_directory_abs_path
+            .join(path.as_std_path());
         let abs_path = SanitizedPath::new(&abs_path);
         let (worktree, relative_path) = worktree_store.find_worktree(abs_path, cx)?;
         Some(ProjectPath {
             worktree_id: worktree.read(cx).id(),
-            path: relative_path.into(),
+            path: relative_path,
         })
     }
 
@@ -3464,10 +3473,7 @@ impl Repository {
                                 project_id: project_id.0,
                                 repository_id: id.to_proto(),
                                 commit,
-                                paths: paths
-                                    .into_iter()
-                                    .map(|p| p.to_string_lossy().to_string())
-                                    .collect(),
+                                paths: paths.into_iter().map(|p| p.to_proto()).collect(),
                             })
                             .await?;
 
@@ -3557,12 +3563,14 @@ impl Repository {
                         files: response
                             .files
                             .into_iter()
-                            .map(|file| CommitFile {
-                                path: Path::new(&file.path).into(),
-                                old_text: file.old_text,
-                                new_text: file.new_text,
+                            .map(|file| {
+                                Ok(CommitFile {
+                                    path: RepoPath::from_proto(&file.path)?,
+                                    old_text: file.old_text,
+                                    new_text: file.new_text,
+                                })
                             })
-                            .collect(),
+                            .collect::<Result<Vec<_>>>()?,
                     })
                 }
             }
@@ -3622,7 +3630,7 @@ impl Repository {
                                     repository_id: id.to_proto(),
                                     paths: entries
                                         .into_iter()
-                                        .map(|repo_path| repo_path.as_ref().to_proto())
+                                        .map(|repo_path| repo_path.to_proto())
                                         .collect(),
                                 })
                                 .await
@@ -3688,7 +3696,7 @@ impl Repository {
                                     repository_id: id.to_proto(),
                                     paths: entries
                                         .into_iter()
-                                        .map(|repo_path| repo_path.as_ref().to_proto())
+                                        .map(|repo_path| repo_path.to_proto())
                                         .collect(),
                                 })
                                 .await
@@ -3752,7 +3760,7 @@ impl Repository {
                                     repository_id: id.to_proto(),
                                     paths: entries
                                         .into_iter()
-                                        .map(|repo_path| repo_path.as_ref().to_proto())
+                                        .map(|repo_path| repo_path.to_proto())
                                         .collect(),
                                 })
                                 .await
@@ -4154,7 +4162,7 @@ impl Repository {
             Some(GitJobKey::WriteIndex(path.clone())),
             None,
             move |git_repo, mut cx| async move {
-                log::debug!("start updating index text for buffer {}", path.display());
+                log::debug!("start updating index text for buffer {}", path.as_str());
                 match git_repo {
                     RepositoryState::Local {
                         backend,
@@ -4170,13 +4178,13 @@ impl Repository {
                             .request(proto::SetIndexText {
                                 project_id: project_id.0,
                                 repository_id: id.to_proto(),
-                                path: path.as_ref().to_proto(),
+                                path: path.to_proto(),
                                 text: content,
                             })
                             .await?;
                     }
                 }
-                log::debug!("finish updating index text for buffer {}", path.display());
+                log::debug!("finish updating index text for buffer {}", path.as_str());
 
                 if let Some(hunk_staging_operation_count) = hunk_staging_operation_count {
                     let project_path = this
@@ -4439,7 +4447,7 @@ impl Repository {
             update
                 .current_merge_conflicts
                 .into_iter()
-                .map(|path| RepoPath(Path::new(&path).into())),
+                .filter_map(|path| RepoPath::from_proto(&path).log_err()),
         );
         self.snapshot.branch = update.branch_summary.as_ref().map(proto_to_branch);
         self.snapshot.head_commit = update
@@ -4460,7 +4468,11 @@ impl Repository {
         let edits = update
             .removed_statuses
             .into_iter()
-            .map(|path| sum_tree::Edit::Remove(PathKey(FromProto::from_proto(path))))
+            .filter_map(|path| {
+                Some(sum_tree::Edit::Remove(PathKey(
+                    RelPath::from_proto(&path).log_err()?,
+                )))
+            })
             .chain(
                 update
                     .updated_statuses
@@ -5060,9 +5072,7 @@ async fn compute_snapshot(
     let mut events = Vec::new();
     let branches = backend.branches().await?;
     let branch = branches.into_iter().find(|branch| branch.is_head);
-    let statuses = backend
-        .status(std::slice::from_ref(&WORK_DIRECTORY_REPO_PATH))
-        .await?;
+    let statuses = backend.status(&[RelPath::empty().into()]).await?;
     let stash_entries = backend.stash_entries().await?;
     let statuses_by_path = SumTree::from_iter(
         statuses
@@ -5108,6 +5118,7 @@ async fn compute_snapshot(
         id,
         statuses_by_path,
         work_directory_abs_path,
+        path_style: prev_snapshot.path_style,
         scan_id: prev_snapshot.scan_id + 1,
         branch,
         head_commit,

crates/project/src/git_store/conflict_set.rs 🔗

@@ -255,20 +255,23 @@ impl EventEmitter<ConflictSetUpdate> for ConflictSet {}
 
 #[cfg(test)]
 mod tests {
-    use std::{path::Path, sync::mpsc};
+    use std::sync::mpsc;
 
     use crate::Project;
 
     use super::*;
     use fs::FakeFs;
-    use git::status::{UnmergedStatus, UnmergedStatusCode};
+    use git::{
+        repository::repo_path,
+        status::{UnmergedStatus, UnmergedStatusCode},
+    };
     use gpui::{BackgroundExecutor, TestAppContext};
     use language::language_settings::AllLanguageSettings;
     use serde_json::json;
     use settings::Settings as _;
     use text::{Buffer, BufferId, Point, ToOffset as _};
     use unindent::Unindent as _;
-    use util::path;
+    use util::{path, rel_path::rel_path};
     use worktree::WorktreeSettings;
 
     #[test]
@@ -543,7 +546,7 @@ mod tests {
 
         fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
             state.unmerged_paths.insert(
-                "a.txt".into(),
+                repo_path("a.txt"),
                 UnmergedStatus {
                     first_head: UnmergedStatusCode::Updated,
                     second_head: UnmergedStatusCode::Updated,
@@ -621,7 +624,7 @@ mod tests {
         cx.run_until_parked();
         fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
             state.unmerged_paths.insert(
-                "a.txt".into(),
+                rel_path("a.txt").into(),
                 UnmergedStatus {
                     first_head: UnmergedStatusCode::Updated,
                     second_head: UnmergedStatusCode::Updated,
@@ -647,7 +650,7 @@ mod tests {
 
         // Simulate the conflict being removed by e.g. staging the file.
         fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
-            state.unmerged_paths.remove(Path::new("a.txt"))
+            state.unmerged_paths.remove(&repo_path("a.txt"))
         })
         .unwrap();
 
@@ -660,7 +663,7 @@ mod tests {
         // Simulate the conflict being re-added.
         fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
             state.unmerged_paths.insert(
-                "a.txt".into(),
+                repo_path("a.txt"),
                 UnmergedStatus {
                     first_head: UnmergedStatusCode::Updated,
                     second_head: UnmergedStatusCode::Updated,

crates/project/src/git_store/git_traversal.rs 🔗

@@ -3,6 +3,7 @@ use git::{repository::RepoPath, status::GitSummary};
 use std::{collections::BTreeMap, ops::Deref, path::Path};
 use sum_tree::Cursor;
 use text::Bias;
+use util::rel_path::RelPath;
 use worktree::{Entry, PathProgress, PathTarget, Traversal};
 
 use super::{RepositoryId, RepositorySnapshot, StatusEntry};
@@ -70,10 +71,7 @@ impl<'a> GitTraversal<'a> {
             return;
         };
 
-        let Ok(abs_path) = self.traversal.snapshot().absolutize(&entry.path) else {
-            self.repo_location = None;
-            return;
-        };
+        let abs_path = self.traversal.snapshot().absolutize(&entry.path);
 
         let Some((repo, repo_path)) = self.repo_root_for_path(&abs_path) else {
             self.repo_location = None;
@@ -97,13 +95,13 @@ impl<'a> GitTraversal<'a> {
 
         if entry.is_dir() {
             let mut statuses = statuses.clone();
-            statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left);
-            let summary = statuses.summary(&PathTarget::Successor(repo_path.as_ref()), Bias::Left);
+            statuses.seek_forward(&PathTarget::Path(&repo_path), Bias::Left);
+            let summary = statuses.summary(&PathTarget::Successor(&repo_path), Bias::Left);
 
             self.current_entry_summary = Some(summary);
         } else if entry.is_file() {
             // For a file entry, park the cursor on the corresponding status
-            if statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left) {
+            if statuses.seek_forward(&PathTarget::Path(&repo_path), Bias::Left) {
                 // TODO: Investigate statuses.item() being None here.
                 self.current_entry_summary = statuses.item().map(|item| item.status.into());
             } else {
@@ -159,7 +157,7 @@ impl<'a> Iterator for GitTraversal<'a> {
 }
 
 pub struct ChildEntriesGitIter<'a> {
-    parent_path: &'a Path,
+    parent_path: &'a RelPath,
     traversal: GitTraversal<'a>,
 }
 
@@ -167,7 +165,7 @@ impl<'a> ChildEntriesGitIter<'a> {
     pub fn new(
         repo_snapshots: &'a HashMap<RepositoryId, RepositorySnapshot>,
         worktree_snapshot: &'a worktree::Snapshot,
-        parent_path: &'a Path,
+        parent_path: &'a RelPath,
     ) -> Self {
         let mut traversal = GitTraversal::new(
             repo_snapshots,
@@ -265,7 +263,7 @@ mod tests {
     use gpui::TestAppContext;
     use serde_json::json;
     use settings::SettingsStore;
-    use util::path;
+    use util::{path, rel_path::rel_path};
 
     const CONFLICT: FileStatus = FileStatus::Unmerged(UnmergedStatus {
         first_head: UnmergedStatusCode::Updated,
@@ -312,17 +310,14 @@ mod tests {
         fs.set_status_for_repo(
             Path::new(path!("/root/x/.git")),
             &[
-                (Path::new("x2.txt"), StatusCode::Modified.index()),
-                (Path::new("z.txt"), StatusCode::Added.index()),
+                ("x2.txt", StatusCode::Modified.index()),
+                ("z.txt", StatusCode::Added.index()),
             ],
         );
-        fs.set_status_for_repo(
-            Path::new(path!("/root/x/y/.git")),
-            &[(Path::new("y1.txt"), CONFLICT)],
-        );
+        fs.set_status_for_repo(Path::new(path!("/root/x/y/.git")), &[("y1.txt", CONFLICT)]);
         fs.set_status_for_repo(
             Path::new(path!("/root/z/.git")),
-            &[(Path::new("z2.txt"), StatusCode::Added.index())],
+            &[("z2.txt", StatusCode::Added.index())],
         );
 
         let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
@@ -337,7 +332,7 @@ mod tests {
 
         let traversal = GitTraversal::new(
             &repo_snapshots,
-            worktree_snapshot.traverse_from_path(true, false, true, Path::new("x")),
+            worktree_snapshot.traverse_from_path(true, false, true, RelPath::new("x").unwrap()),
         );
         let entries = traversal
             .map(|entry| (entry.path.clone(), entry.git_summary))
@@ -345,13 +340,13 @@ mod tests {
         pretty_assertions::assert_eq!(
             entries,
             [
-                (Path::new("x/x1.txt").into(), GitSummary::UNCHANGED),
-                (Path::new("x/x2.txt").into(), MODIFIED),
-                (Path::new("x/y/y1.txt").into(), GitSummary::CONFLICT),
-                (Path::new("x/y/y2.txt").into(), GitSummary::UNCHANGED),
-                (Path::new("x/z.txt").into(), ADDED),
-                (Path::new("z/z1.txt").into(), GitSummary::UNCHANGED),
-                (Path::new("z/z2.txt").into(), ADDED),
+                (rel_path("x/x1.txt").into(), GitSummary::UNCHANGED),
+                (rel_path("x/x2.txt").into(), MODIFIED),
+                (rel_path("x/y/y1.txt").into(), GitSummary::CONFLICT),
+                (rel_path("x/y/y2.txt").into(), GitSummary::UNCHANGED),
+                (rel_path("x/z.txt").into(), ADDED),
+                (rel_path("z/z1.txt").into(), GitSummary::UNCHANGED),
+                (rel_path("z/z2.txt").into(), ADDED),
             ]
         )
     }
@@ -386,18 +381,15 @@ mod tests {
         fs.set_status_for_repo(
             Path::new(path!("/root/x/.git")),
             &[
-                (Path::new("x2.txt"), StatusCode::Modified.index()),
-                (Path::new("z.txt"), StatusCode::Added.index()),
+                ("x2.txt", StatusCode::Modified.index()),
+                ("z.txt", StatusCode::Added.index()),
             ],
         );
-        fs.set_status_for_repo(
-            Path::new(path!("/root/x/y/.git")),
-            &[(Path::new("y1.txt"), CONFLICT)],
-        );
+        fs.set_status_for_repo(Path::new(path!("/root/x/y/.git")), &[("y1.txt", CONFLICT)]);
 
         fs.set_status_for_repo(
             Path::new(path!("/root/z/.git")),
-            &[(Path::new("z2.txt"), StatusCode::Added.index())],
+            &[("z2.txt", StatusCode::Added.index())],
         );
 
         let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
@@ -415,18 +407,18 @@ mod tests {
             &repo_snapshots,
             &worktree_snapshot,
             &[
-                (Path::new("x/y"), GitSummary::CONFLICT),
-                (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
-                (Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
+                ("x/y", GitSummary::CONFLICT),
+                ("x/y/y1.txt", GitSummary::CONFLICT),
+                ("x/y/y2.txt", GitSummary::UNCHANGED),
             ],
         );
         check_git_statuses(
             &repo_snapshots,
             &worktree_snapshot,
             &[
-                (Path::new("z"), ADDED),
-                (Path::new("z/z1.txt"), GitSummary::UNCHANGED),
-                (Path::new("z/z2.txt"), ADDED),
+                ("z", ADDED),
+                ("z/z1.txt", GitSummary::UNCHANGED),
+                ("z/z2.txt", ADDED),
             ],
         );
 
@@ -435,9 +427,9 @@ mod tests {
             &repo_snapshots,
             &worktree_snapshot,
             &[
-                (Path::new("x"), MODIFIED + ADDED),
-                (Path::new("x/y"), GitSummary::CONFLICT),
-                (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
+                ("x", MODIFIED + ADDED),
+                ("x/y", GitSummary::CONFLICT),
+                ("x/y/y1.txt", GitSummary::CONFLICT),
             ],
         );
 
@@ -446,13 +438,13 @@ mod tests {
             &repo_snapshots,
             &worktree_snapshot,
             &[
-                (Path::new("x"), MODIFIED + ADDED),
-                (Path::new("x/x1.txt"), GitSummary::UNCHANGED),
-                (Path::new("x/x2.txt"), MODIFIED),
-                (Path::new("x/y"), GitSummary::CONFLICT),
-                (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
-                (Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
-                (Path::new("x/z.txt"), ADDED),
+                ("x", MODIFIED + ADDED),
+                ("x/x1.txt", GitSummary::UNCHANGED),
+                ("x/x2.txt", MODIFIED),
+                ("x/y", GitSummary::CONFLICT),
+                ("x/y/y1.txt", GitSummary::CONFLICT),
+                ("x/y/y2.txt", GitSummary::UNCHANGED),
+                ("x/z.txt", ADDED),
             ],
         );
 
@@ -461,9 +453,9 @@ mod tests {
             &repo_snapshots,
             &worktree_snapshot,
             &[
-                (Path::new(""), GitSummary::UNCHANGED),
-                (Path::new("x"), MODIFIED + ADDED),
-                (Path::new("x/x1.txt"), GitSummary::UNCHANGED),
+                ("", GitSummary::UNCHANGED),
+                ("x", MODIFIED + ADDED),
+                ("x/x1.txt", GitSummary::UNCHANGED),
             ],
         );
 
@@ -472,17 +464,17 @@ mod tests {
             &repo_snapshots,
             &worktree_snapshot,
             &[
-                (Path::new(""), GitSummary::UNCHANGED),
-                (Path::new("x"), MODIFIED + ADDED),
-                (Path::new("x/x1.txt"), GitSummary::UNCHANGED),
-                (Path::new("x/x2.txt"), MODIFIED),
-                (Path::new("x/y"), GitSummary::CONFLICT),
-                (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
-                (Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
-                (Path::new("x/z.txt"), ADDED),
-                (Path::new("z"), ADDED),
-                (Path::new("z/z1.txt"), GitSummary::UNCHANGED),
-                (Path::new("z/z2.txt"), ADDED),
+                ("", GitSummary::UNCHANGED),
+                ("x", MODIFIED + ADDED),
+                ("x/x1.txt", GitSummary::UNCHANGED),
+                ("x/x2.txt", MODIFIED),
+                ("x/y", GitSummary::CONFLICT),
+                ("x/y/y1.txt", GitSummary::CONFLICT),
+                ("x/y/y2.txt", GitSummary::UNCHANGED),
+                ("x/z.txt", ADDED),
+                ("z", ADDED),
+                ("z/z1.txt", GitSummary::UNCHANGED),
+                ("z/z2.txt", ADDED),
             ],
         );
     }
@@ -520,9 +512,9 @@ mod tests {
         fs.set_status_for_repo(
             Path::new(path!("/root/.git")),
             &[
-                (Path::new("a/b/c1.txt"), StatusCode::Added.index()),
-                (Path::new("a/d/e2.txt"), StatusCode::Modified.index()),
-                (Path::new("g/h2.txt"), CONFLICT),
+                ("a/b/c1.txt", StatusCode::Added.index()),
+                ("a/d/e2.txt", StatusCode::Modified.index()),
+                ("g/h2.txt", CONFLICT),
             ],
         );
 
@@ -540,9 +532,9 @@ mod tests {
             &repo_snapshots,
             &worktree_snapshot,
             &[
-                (Path::new(""), GitSummary::CONFLICT + MODIFIED + ADDED),
-                (Path::new("g"), GitSummary::CONFLICT),
-                (Path::new("g/h2.txt"), GitSummary::CONFLICT),
+                ("", GitSummary::CONFLICT + MODIFIED + ADDED),
+                ("g", GitSummary::CONFLICT),
+                ("g/h2.txt", GitSummary::CONFLICT),
             ],
         );
 
@@ -550,17 +542,17 @@ mod tests {
             &repo_snapshots,
             &worktree_snapshot,
             &[
-                (Path::new(""), GitSummary::CONFLICT + ADDED + MODIFIED),
-                (Path::new("a"), ADDED + MODIFIED),
-                (Path::new("a/b"), ADDED),
-                (Path::new("a/b/c1.txt"), ADDED),
-                (Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
-                (Path::new("a/d"), MODIFIED),
-                (Path::new("a/d/e2.txt"), MODIFIED),
-                (Path::new("f"), GitSummary::UNCHANGED),
-                (Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
-                (Path::new("g"), GitSummary::CONFLICT),
-                (Path::new("g/h2.txt"), GitSummary::CONFLICT),
+                ("", GitSummary::CONFLICT + ADDED + MODIFIED),
+                ("a", ADDED + MODIFIED),
+                ("a/b", ADDED),
+                ("a/b/c1.txt", ADDED),
+                ("a/b/c2.txt", GitSummary::UNCHANGED),
+                ("a/d", MODIFIED),
+                ("a/d/e2.txt", MODIFIED),
+                ("f", GitSummary::UNCHANGED),
+                ("f/no-status.txt", GitSummary::UNCHANGED),
+                ("g", GitSummary::CONFLICT),
+                ("g/h2.txt", GitSummary::CONFLICT),
             ],
         );
 
@@ -568,15 +560,15 @@ mod tests {
             &repo_snapshots,
             &worktree_snapshot,
             &[
-                (Path::new("a/b"), ADDED),
-                (Path::new("a/b/c1.txt"), ADDED),
-                (Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
-                (Path::new("a/d"), MODIFIED),
-                (Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
-                (Path::new("a/d/e2.txt"), MODIFIED),
-                (Path::new("f"), GitSummary::UNCHANGED),
-                (Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
-                (Path::new("g"), GitSummary::CONFLICT),
+                ("a/b", ADDED),
+                ("a/b/c1.txt", ADDED),
+                ("a/b/c2.txt", GitSummary::UNCHANGED),
+                ("a/d", MODIFIED),
+                ("a/d/e1.txt", GitSummary::UNCHANGED),
+                ("a/d/e2.txt", MODIFIED),
+                ("f", GitSummary::UNCHANGED),
+                ("f/no-status.txt", GitSummary::UNCHANGED),
+                ("g", GitSummary::CONFLICT),
             ],
         );
 
@@ -584,11 +576,11 @@ mod tests {
             &repo_snapshots,
             &worktree_snapshot,
             &[
-                (Path::new("a/b/c1.txt"), ADDED),
-                (Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
-                (Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
-                (Path::new("a/d/e2.txt"), MODIFIED),
-                (Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
+                ("a/b/c1.txt", ADDED),
+                ("a/b/c2.txt", GitSummary::UNCHANGED),
+                ("a/d/e1.txt", GitSummary::UNCHANGED),
+                ("a/d/e2.txt", MODIFIED),
+                ("f/no-status.txt", GitSummary::UNCHANGED),
             ],
         );
     }
@@ -621,18 +613,18 @@ mod tests {
 
         fs.set_status_for_repo(
             Path::new(path!("/root/x/.git")),
-            &[(Path::new("x1.txt"), StatusCode::Added.index())],
+            &[("x1.txt", StatusCode::Added.index())],
         );
         fs.set_status_for_repo(
             Path::new(path!("/root/y/.git")),
             &[
-                (Path::new("y1.txt"), CONFLICT),
-                (Path::new("y2.txt"), StatusCode::Modified.index()),
+                ("y1.txt", CONFLICT),
+                ("y2.txt", StatusCode::Modified.index()),
             ],
         );
         fs.set_status_for_repo(
             Path::new(path!("/root/z/.git")),
-            &[(Path::new("z2.txt"), StatusCode::Modified.index())],
+            &[("z2.txt", StatusCode::Modified.index())],
         );
 
         let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
@@ -648,47 +640,44 @@ mod tests {
         check_git_statuses(
             &repo_snapshots,
             &worktree_snapshot,
-            &[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
+            &[("x", ADDED), ("x/x1.txt", ADDED)],
         );
 
         check_git_statuses(
             &repo_snapshots,
             &worktree_snapshot,
             &[
-                (Path::new("y"), GitSummary::CONFLICT + MODIFIED),
-                (Path::new("y/y1.txt"), GitSummary::CONFLICT),
-                (Path::new("y/y2.txt"), MODIFIED),
+                ("y", GitSummary::CONFLICT + MODIFIED),
+                ("y/y1.txt", GitSummary::CONFLICT),
+                ("y/y2.txt", MODIFIED),
             ],
         );
 
         check_git_statuses(
             &repo_snapshots,
             &worktree_snapshot,
-            &[
-                (Path::new("z"), MODIFIED),
-                (Path::new("z/z2.txt"), MODIFIED),
-            ],
+            &[("z", MODIFIED), ("z/z2.txt", MODIFIED)],
         );
 
         check_git_statuses(
             &repo_snapshots,
             &worktree_snapshot,
-            &[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
+            &[("x", ADDED), ("x/x1.txt", ADDED)],
         );
 
         check_git_statuses(
             &repo_snapshots,
             &worktree_snapshot,
             &[
-                (Path::new("x"), ADDED),
-                (Path::new("x/x1.txt"), ADDED),
-                (Path::new("x/x2.txt"), GitSummary::UNCHANGED),
-                (Path::new("y"), GitSummary::CONFLICT + MODIFIED),
-                (Path::new("y/y1.txt"), GitSummary::CONFLICT),
-                (Path::new("y/y2.txt"), MODIFIED),
-                (Path::new("z"), MODIFIED),
-                (Path::new("z/z1.txt"), GitSummary::UNCHANGED),
-                (Path::new("z/z2.txt"), MODIFIED),
+                ("x", ADDED),
+                ("x/x1.txt", ADDED),
+                ("x/x2.txt", GitSummary::UNCHANGED),
+                ("y", GitSummary::CONFLICT + MODIFIED),
+                ("y/y1.txt", GitSummary::CONFLICT),
+                ("y/y2.txt", MODIFIED),
+                ("z", MODIFIED),
+                ("z/z1.txt", GitSummary::UNCHANGED),
+                ("z/z2.txt", MODIFIED),
             ],
         );
     }
@@ -722,7 +711,7 @@ mod tests {
         .await;
         fs.set_head_and_index_for_repo(
             path!("/root/.git").as_ref(),
-            &[("a.txt".into(), "".into()), ("b/c.txt".into(), "".into())],
+            &[("a.txt", "".into()), ("b/c.txt", "".into())],
         );
         cx.run_until_parked();
 
@@ -757,10 +746,7 @@ mod tests {
         // detected.
         fs.set_head_for_repo(
             path!("/root/.git").as_ref(),
-            &[
-                ("a.txt".into(), "".into()),
-                ("b/c.txt".into(), "something-else".into()),
-            ],
+            &[("a.txt", "".into()), ("b/c.txt", "something-else".into())],
             "deadbeef",
         );
         cx.executor().run_until_parked();
@@ -777,9 +763,9 @@ mod tests {
             &repo_snapshots,
             &worktree_snapshot,
             &[
-                (Path::new(""), MODIFIED),
-                (Path::new("a.txt"), GitSummary::UNCHANGED),
-                (Path::new("b/c.txt"), MODIFIED),
+                ("", MODIFIED),
+                ("a.txt", GitSummary::UNCHANGED),
+                ("b/c.txt", MODIFIED),
             ],
         );
     }
@@ -788,17 +774,17 @@ mod tests {
     fn check_git_statuses(
         repo_snapshots: &HashMap<RepositoryId, RepositorySnapshot>,
         worktree_snapshot: &worktree::Snapshot,
-        expected_statuses: &[(&Path, GitSummary)],
+        expected_statuses: &[(&str, GitSummary)],
     ) {
         let mut traversal = GitTraversal::new(
             repo_snapshots,
-            worktree_snapshot.traverse_from_path(true, true, false, "".as_ref()),
+            worktree_snapshot.traverse_from_path(true, true, false, RelPath::empty()),
         );
         let found_statuses = expected_statuses
             .iter()
             .map(|&(path, _)| {
                 let git_entry = traversal
-                    .find(|git_entry| &*git_entry.path == path)
+                    .find(|git_entry| git_entry.path.as_ref() == rel_path(path))
                     .unwrap_or_else(|| panic!("Traversal has no entry for {path:?}"));
                 (path, git_entry.git_summary)
             })

crates/project/src/image_store.rs 🔗

@@ -13,10 +13,9 @@ use image::{ExtendedColorType, GenericImageView, ImageReader};
 use language::{DiskState, File};
 use rpc::{AnyProtoClient, ErrorExt as _};
 use std::num::NonZeroU64;
-use std::path::Path;
+use std::path::PathBuf;
 use std::sync::Arc;
-use std::{ffi::OsStr, path::PathBuf};
-use util::ResultExt;
+use util::{ResultExt, rel_path::RelPath};
 use worktree::{LoadedBinaryFile, PathChange, Worktree};
 
 #[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd, Ord, Eq)]
@@ -207,8 +206,7 @@ pub fn is_image_file(project: &Entity<Project>, path: &ProjectPath, cx: &App) ->
             .abs_path();
         path.path
             .extension()
-            .or_else(|| worktree_abs_path.extension())
-            .and_then(OsStr::to_str)
+            .or_else(|| worktree_abs_path.extension()?.to_str())
             .map(str::to_lowercase)
     });
 
@@ -255,7 +253,7 @@ impl ProjectItem for ImageItem {
 trait ImageStoreImpl {
     fn open_image(
         &self,
-        path: Arc<Path>,
+        path: Arc<RelPath>,
         worktree: Entity<Worktree>,
         cx: &mut Context<ImageStore>,
     ) -> Task<Result<Entity<ImageItem>>>;
@@ -458,7 +456,7 @@ impl ImageStore {
 impl ImageStoreImpl for Entity<LocalImageStore> {
     fn open_image(
         &self,
-        path: Arc<Path>,
+        path: Arc<RelPath>,
         worktree: Entity<Worktree>,
         cx: &mut Context<ImageStore>,
     ) -> Task<Result<Entity<ImageItem>>> {
@@ -539,7 +537,7 @@ impl LocalImageStore {
     fn local_worktree_entries_changed(
         &mut self,
         worktree_handle: &Entity<Worktree>,
-        changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
+        changes: &[(Arc<RelPath>, ProjectEntryId, PathChange)],
         cx: &mut Context<Self>,
     ) {
         let snapshot = worktree_handle.read(cx).snapshot();
@@ -551,7 +549,7 @@ impl LocalImageStore {
     fn local_worktree_entry_changed(
         &mut self,
         entry_id: ProjectEntryId,
-        path: &Arc<Path>,
+        path: &Arc<RelPath>,
         worktree: &Entity<worktree::Worktree>,
         snapshot: &worktree::Snapshot,
         cx: &mut Context<Self>,
@@ -698,7 +696,7 @@ fn create_gpui_image(content: Vec<u8>) -> anyhow::Result<Arc<gpui::Image>> {
 impl ImageStoreImpl for Entity<RemoteImageStore> {
     fn open_image(
         &self,
-        _path: Arc<Path>,
+        _path: Arc<RelPath>,
         _worktree: Entity<Worktree>,
         _cx: &mut Context<ImageStore>,
     ) -> Task<Result<Entity<ImageItem>>> {
@@ -729,7 +727,7 @@ mod tests {
     use gpui::TestAppContext;
     use serde_json::json;
     use settings::SettingsStore;
-    use std::path::PathBuf;
+    use util::rel_path::rel_path;
 
     pub fn init_test(cx: &mut TestAppContext) {
         zlog::init_test();
@@ -768,7 +766,7 @@ mod tests {
 
         let project_path = ProjectPath {
             worktree_id,
-            path: PathBuf::from("image_1.png").into(),
+            path: rel_path("image_1.png").into(),
         };
 
         let (task1, task2) = project.update(cx, |project, cx| {

crates/project/src/lsp_store.rs 🔗

@@ -33,7 +33,6 @@ use crate::{
     },
     prettier_store::{self, PrettierStore, PrettierStoreEvent},
     project_settings::{LspSettings, ProjectSettings},
-    relativize_path, resolve_path,
     toolchain_store::{LocalToolchainStore, ToolchainStoreEvent},
     worktree_store::{WorktreeStore, WorktreeStoreEvent},
     yarn::YarnPathStore,
@@ -88,7 +87,7 @@ use postage::{mpsc, sink::Sink, stream::Stream, watch};
 use rand::prelude::*;
 use rpc::{
     AnyProtoClient,
-    proto::{FromProto, LspRequestId, LspRequestMessage as _, ToProto},
+    proto::{LspRequestId, LspRequestMessage as _},
 };
 use serde::Serialize;
 use settings::{Settings, SettingsLocation, SettingsStore};
@@ -116,8 +115,9 @@ use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, ToPoint as _};
 
 use util::{
     ConnectionResult, ResultExt as _, debug_panic, defer, maybe, merge_json_value_into,
-    paths::{PathExt, SanitizedPath},
+    paths::{PathStyle, SanitizedPath},
     post_inc,
+    rel_path::RelPath,
 };
 
 pub use fs::*;
@@ -158,7 +158,7 @@ impl FormatTrigger {
 #[derive(Clone)]
 struct UnifiedLanguageServer {
     id: LanguageServerId,
-    project_roots: HashSet<Arc<Path>>,
+    project_roots: HashSet<Arc<RelPath>>,
 }
 
 #[derive(Clone, Hash, PartialEq, Eq)]
@@ -209,7 +209,7 @@ pub struct LocalLspStore {
     diagnostics: HashMap<
         WorktreeId,
         HashMap<
-            Arc<Path>,
+            Arc<RelPath>,
             Vec<(
                 LanguageServerId,
                 Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
@@ -1086,7 +1086,7 @@ impl LocalLspStore {
         if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) {
             let worktree_id = file.worktree_id(cx);
 
-            let path: Arc<Path> = file
+            let path: Arc<RelPath> = file
                 .path()
                 .parent()
                 .map(Arc::from)
@@ -1842,17 +1842,19 @@ impl LocalLspStore {
                             }
 
                             if !project_transaction_command.0.is_empty() {
-                                let extra_buffers = project_transaction_command
-                                    .0
-                                    .keys()
-                                    .filter_map(|buffer_handle| {
-                                        buffer_handle
-                                            .read_with(cx, |b, cx| b.project_path(cx))
-                                            .ok()
-                                            .flatten()
-                                    })
-                                    .map(|p| p.path.to_sanitized_string())
-                                    .join(", ");
+                                let mut extra_buffers = String::new();
+                                for buffer in project_transaction_command.0.keys() {
+                                    buffer
+                                        .read_with(cx, |b, cx| {
+                                            if let Some(path) = b.project_path(cx) {
+                                                if !extra_buffers.is_empty() {
+                                                    extra_buffers.push_str(", ");
+                                                }
+                                                extra_buffers.push_str(path.path.as_str());
+                                            }
+                                        })
+                                        .ok();
+                                }
                                 zlog::warn!(
                                     logger =>
                                     "Unexpected edits to buffers other than the buffer actively being formatted due to command {}. Impacted buffers: [{}].",
@@ -2347,7 +2349,7 @@ impl LocalLspStore {
         let Some(language) = buffer.language().cloned() else {
             return;
         };
-        let path: Arc<Path> = file
+        let path: Arc<RelPath> = file
             .path()
             .parent()
             .map(Arc::from)
@@ -2403,8 +2405,7 @@ impl LocalLspStore {
                     let path = &disposition.path;
 
                     {
-                        let uri =
-                            Uri::from_file_path(worktree.read(cx).abs_path().join(&path.path));
+                        let uri = Uri::from_file_path(worktree.read(cx).absolutize(&path.path));
 
                         let server_id = self.get_or_insert_language_server(
                             &worktree,
@@ -3172,7 +3173,7 @@ impl LocalLspStore {
                     if let Some((tree, glob)) =
                         worktree.as_local_mut().zip(Glob::new(&pattern).log_err())
                     {
-                        tree.add_path_prefix_to_scan(literal_prefix.into());
+                        tree.add_path_prefix_to_scan(literal_prefix);
                         worktree_globs
                             .entry(tree.id())
                             .or_insert_with(GlobSetBuilder::new)
@@ -3268,10 +3269,11 @@ impl LocalLspStore {
         worktrees: &[Entity<Worktree>],
         watcher: &FileSystemWatcher,
         cx: &App,
-    ) -> Option<(Entity<Worktree>, PathBuf, String)> {
+    ) -> Option<(Entity<Worktree>, Arc<RelPath>, String)> {
         worktrees.iter().find_map(|worktree| {
             let tree = worktree.read(cx);
             let worktree_root_path = tree.abs_path();
+            let path_style = tree.path_style();
             match &watcher.glob_pattern {
                 lsp::GlobPattern::String(s) => {
                     let watcher_path = SanitizedPath::new(s);
@@ -3282,7 +3284,7 @@ impl LocalLspStore {
                     let literal_prefix = glob_literal_prefix(relative);
                     Some((
                         worktree.clone(),
-                        literal_prefix,
+                        RelPath::from_std_path(&literal_prefix, path_style).ok()?,
                         relative.to_string_lossy().to_string(),
                     ))
                 }
@@ -3296,7 +3298,11 @@ impl LocalLspStore {
                     let relative = base_uri.strip_prefix(&worktree_root_path).ok()?;
                     let mut literal_prefix = relative.to_owned();
                     literal_prefix.push(glob_literal_prefix(Path::new(&rp.pattern)));
-                    Some((worktree.clone(), literal_prefix, rp.pattern.clone()))
+                    Some((
+                        worktree.clone(),
+                        RelPath::from_std_path(&literal_prefix, path_style).ok()?,
+                        rp.pattern.clone(),
+                    ))
                 }
             }
         })
@@ -3483,7 +3489,7 @@ pub struct LspStore {
     _maintain_workspace_config: (Task<Result<()>>, watch::Sender<()>),
     _maintain_buffer_languages: Task<()>,
     diagnostic_summaries:
-        HashMap<WorktreeId, HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>>,
+        HashMap<WorktreeId, HashMap<Arc<RelPath>, HashMap<LanguageServerId, DiagnosticSummary>>>,
     pub lsp_server_capabilities: HashMap<LanguageServerId, lsp::ServerCapabilities>,
     lsp_document_colors: HashMap<BufferId, DocumentColorData>,
     lsp_code_lens: HashMap<BufferId, CodeLensData>,
@@ -3569,11 +3575,28 @@ struct CoreSymbol {
     pub language_server_name: LanguageServerName,
     pub source_worktree_id: WorktreeId,
     pub source_language_server_id: LanguageServerId,
-    pub path: ProjectPath,
+    pub path: SymbolLocation,
     pub name: String,
     pub kind: lsp::SymbolKind,
     pub range: Range<Unclipped<PointUtf16>>,
-    pub signature: [u8; 32],
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum SymbolLocation {
+    InProject(ProjectPath),
+    OutsideProject {
+        abs_path: Arc<Path>,
+        signature: [u8; 32],
+    },
+}
+
+impl SymbolLocation {
+    fn file_name(&self) -> Option<&str> {
+        match self {
+            Self::InProject(path) => path.path.file_name(),
+            Self::OutsideProject { abs_path, .. } => abs_path.file_name()?.to_str(),
+        }
+    }
 }
 
 impl LspStore {
@@ -4353,7 +4376,7 @@ impl LspStore {
             let mut summaries = diangostic_summaries.iter().flat_map(|(path, summaries)| {
                 summaries
                     .iter()
-                    .map(|(server_id, summary)| summary.to_proto(*server_id, path))
+                    .map(|(server_id, summary)| summary.to_proto(*server_id, path.as_ref()))
             });
             if let Some(summary) = summaries.next() {
                 client
@@ -4655,7 +4678,6 @@ impl LspStore {
                         .unwrap_or_else(|| file.path().clone());
                     let worktree_path = ProjectPath { worktree_id, path };
                     let abs_path = file.abs_path(cx);
-                    let worktree_root = worktree.read(cx).abs_path();
                     let nodes = rebase
                         .walk(
                             worktree_path,
@@ -4668,7 +4690,7 @@ impl LspStore {
                     for node in nodes {
                         let server_id = node.server_id_or_init(|disposition| {
                             let path = &disposition.path;
-                            let uri = Uri::from_file_path(worktree_root.join(&path.path));
+                            let uri = Uri::from_file_path(worktree.read(cx).absolutize(&path.path));
                             let key = LanguageServerSeed {
                                 worktree_id,
                                 name: disposition.server_name.clone(),
@@ -6965,7 +6987,6 @@ impl LspStore {
                 server_id: LanguageServerId,
                 lsp_adapter: Arc<CachedLspAdapter>,
                 worktree: WeakEntity<Worktree>,
-                worktree_abs_path: Arc<Path>,
                 lsp_symbols: Vec<(String, SymbolKind, lsp::Location)>,
             }
 
@@ -7004,7 +7025,6 @@ impl LspStore {
                 if !supports_workspace_symbol_request {
                     continue;
                 }
-                let worktree_abs_path = worktree.abs_path().clone();
                 let worktree_handle = worktree_handle.clone();
                 let server_id = server.server_id();
                 requests.push(
@@ -7044,7 +7064,6 @@ impl LspStore {
                                     server_id,
                                     lsp_adapter,
                                     worktree: worktree_handle.downgrade(),
-                                    worktree_abs_path,
                                     lsp_symbols,
                                 }
                             }),
@@ -7069,33 +7088,29 @@ impl LspStore {
                                 let source_worktree = result.worktree.upgrade()?;
                                 let source_worktree_id = source_worktree.read(cx).id();
 
-                                let path;
-                                let worktree;
-                                if let Some((tree, rel_path)) =
+                                let path = if let Some((tree, rel_path)) =
                                     this.worktree_store.read(cx).find_worktree(&abs_path, cx)
                                 {
-                                    worktree = tree;
-                                    path = rel_path;
+                                    let worktree_id = tree.read(cx).id();
+                                    SymbolLocation::InProject(ProjectPath {
+                                        worktree_id,
+                                        path: rel_path,
+                                    })
                                 } else {
-                                    worktree = source_worktree;
-                                    path = relativize_path(&result.worktree_abs_path, &abs_path);
-                                }
-
-                                let worktree_id = worktree.read(cx).id();
-                                let project_path = ProjectPath {
-                                    worktree_id,
-                                    path: path.into(),
+                                    SymbolLocation::OutsideProject {
+                                        signature: this.symbol_signature(&abs_path),
+                                        abs_path: abs_path.into(),
+                                    }
                                 };
-                                let signature = this.symbol_signature(&project_path);
+
                                 Some(CoreSymbol {
                                     source_language_server_id: result.server_id,
                                     language_server_name: result.lsp_adapter.name.clone(),
                                     source_worktree_id,
-                                    path: project_path,
+                                    path,
                                     kind: symbol_kind,
                                     name: symbol_name,
                                     range: range_from_lsp(symbol_location.range),
-                                    signature,
                                 })
                             })
                             .collect()
@@ -7638,7 +7653,7 @@ impl LspStore {
             let worktree_id = worktree.read(cx).id();
             let project_path = ProjectPath {
                 worktree_id,
-                path: relative_path.into(),
+                path: relative_path,
             };
 
             if let Some(buffer_handle) = self.buffer_store.read(cx).get_by_path(&project_path) {
@@ -7735,7 +7750,7 @@ impl LspStore {
         &mut self,
         worktree_id: WorktreeId,
         server_id: LanguageServerId,
-        path_in_worktree: Arc<Path>,
+        path_in_worktree: Arc<RelPath>,
         diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
         _: &mut Context<Worktree>,
     ) -> Result<ControlFlow<(), Option<(u64, proto::DiagnosticSummary)>>> {
@@ -7827,18 +7842,21 @@ impl LspStore {
                 )));
             };
 
-            let worktree_abs_path = if let Some(worktree_abs_path) = self
-                .worktree_store
-                .read(cx)
-                .worktree_for_id(symbol.path.worktree_id, cx)
-                .map(|worktree| worktree.read(cx).abs_path())
-            {
-                worktree_abs_path
-            } else {
-                return Task::ready(Err(anyhow!("worktree not found for symbol")));
+            let symbol_abs_path = match &symbol.path {
+                SymbolLocation::InProject(project_path) => self
+                    .worktree_store
+                    .read(cx)
+                    .absolutize(&project_path, cx)
+                    .context("no such worktree"),
+                SymbolLocation::OutsideProject {
+                    abs_path,
+                    signature: _,
+                } => Ok(abs_path.to_path_buf()),
+            };
+            let symbol_abs_path = match symbol_abs_path {
+                Ok(abs_path) => abs_path,
+                Err(err) => return Task::ready(Err(err)),
             };
-
-            let symbol_abs_path = resolve_path(&worktree_abs_path, &symbol.path.path);
             let symbol_uri = if let Ok(uri) = lsp::Uri::from_file_path(symbol_abs_path) {
                 uri
             } else {
@@ -7891,8 +7909,7 @@ impl LspStore {
                         worktree_store.find_worktree(&worktree_root_target, cx)
                     })
                 })? {
-                let relative_path =
-                    known_relative_path.unwrap_or_else(|| Arc::<Path>::from(result.1));
+                let relative_path = known_relative_path.unwrap_or_else(|| result.1.clone());
                 (result.0, relative_path)
             } else {
                 let worktree = lsp_store
@@ -7919,7 +7936,11 @@ impl LspStore {
                 let relative_path = if let Some(known_path) = known_relative_path {
                     known_path
                 } else {
-                    abs_path.strip_prefix(worktree_root)?.into()
+                    RelPath::from_std_path(
+                        abs_path.strip_prefix(worktree_root)?,
+                        PathStyle::local(),
+                    )
+                    .context("failed to create relative path")?
                 };
                 (worktree, relative_path)
             };
@@ -8326,39 +8347,56 @@ impl LspStore {
         mut cx: AsyncApp,
     ) -> Result<proto::ProjectEntryResponse> {
         let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
-        let (worktree_id, worktree, old_path, is_dir) = this
+        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")?;
+
+        let (worktree_store, old_worktree, new_worktree, old_entry) = this
             .update(&mut cx, |this, cx| {
-                this.worktree_store
+                let (worktree, entry) = this
+                    .worktree_store
                     .read(cx)
-                    .worktree_and_entry_for_id(entry_id, cx)
-                    .map(|(worktree, entry)| {
-                        (
-                            worktree.read(cx).id(),
-                            worktree,
-                            entry.path.clone(),
-                            entry.is_dir(),
-                        )
-                    })
+                    .worktree_and_entry_for_id(entry_id, cx)?;
+                let new_worktree = this
+                    .worktree_store
+                    .read(cx)
+                    .worktree_for_id(new_worktree_id, cx)?;
+                Some((
+                    this.worktree_store.clone(),
+                    worktree,
+                    new_worktree,
+                    entry.clone(),
+                ))
             })?
             .context("worktree not found")?;
-        let (old_abs_path, new_abs_path) = {
-            let root_path = worktree.read_with(&cx, |this, _| this.abs_path())?;
-            let new_path = PathBuf::from_proto(envelope.payload.new_path.clone());
-            (root_path.join(&old_path), root_path.join(&new_path))
-        };
+        let (old_abs_path, old_worktree_id) = old_worktree.read_with(&cx, |worktree, _| {
+            (worktree.absolutize(&old_entry.path), worktree.id())
+        })?;
+        let new_abs_path =
+            new_worktree.read_with(&cx, |worktree, _| worktree.absolutize(&new_path))?;
 
         let _transaction = Self::will_rename_entry(
             this.downgrade(),
-            worktree_id,
+            old_worktree_id,
             &old_abs_path,
             &new_abs_path,
-            is_dir,
+            old_entry.is_dir(),
+            cx.clone(),
+        )
+        .await;
+        let response = WorktreeStore::handle_rename_project_entry(
+            worktree_store,
+            envelope.payload,
             cx.clone(),
         )
         .await;
-        let response = Worktree::handle_rename_entry(worktree, envelope.payload, cx.clone()).await;
         this.read_with(&cx, |this, _| {
-            this.did_rename_entry(worktree_id, &old_abs_path, &new_abs_path, is_dir);
+            this.did_rename_entry(
+                old_worktree_id,
+                &old_abs_path,
+                &new_abs_path,
+                old_entry.is_dir(),
+            );
         })
         .ok();
         response
@@ -8381,7 +8419,7 @@ impl LspStore {
             {
                 let project_path = ProjectPath {
                     worktree_id,
-                    path: Arc::<Path>::from_proto(message_summary.path),
+                    path: RelPath::from_proto(&message_summary.path).context("invalid path")?,
                 };
                 let path = project_path.path.clone();
                 let server_id = LanguageServerId(message_summary.language_server_id as usize);
@@ -9436,10 +9474,16 @@ impl LspStore {
         let peer_id = envelope.original_sender_id().unwrap_or_default();
         let symbol = envelope.payload.symbol.context("invalid symbol")?;
         let symbol = Self::deserialize_symbol(symbol)?;
-        let symbol = this.read_with(&cx, |this, _| {
-            let signature = this.symbol_signature(&symbol.path);
-            anyhow::ensure!(signature == symbol.signature, "invalid symbol signature");
-            Ok(symbol)
+        this.read_with(&cx, |this, _| {
+            if let SymbolLocation::OutsideProject {
+                abs_path,
+                signature,
+            } = &symbol.path
+            {
+                let new_signature = this.symbol_signature(&abs_path);
+                anyhow::ensure!(&new_signature == signature, "invalid symbol signature");
+            }
+            Ok(())
         })??;
         let buffer = this
             .update(&mut cx, |this, cx| {
@@ -9452,7 +9496,6 @@ impl LspStore {
                         name: symbol.name,
                         kind: symbol.kind,
                         range: symbol.range,
-                        signature: symbol.signature,
                         label: CodeLabel {
                             text: Default::default(),
                             runs: Default::default(),
@@ -9484,10 +9527,9 @@ impl LspStore {
         })?
     }
 
-    fn symbol_signature(&self, project_path: &ProjectPath) -> [u8; 32] {
+    fn symbol_signature(&self, abs_path: &Path) -> [u8; 32] {
         let mut hasher = Sha256::new();
-        hasher.update(project_path.worktree_id.to_proto().to_be_bytes());
-        hasher.update(project_path.path.to_string_lossy().as_bytes());
+        hasher.update(abs_path.to_string_lossy().as_bytes());
         hasher.update(self.nonce.to_be_bytes());
         hasher.finalize().as_slice().try_into().unwrap()
     }
@@ -10233,7 +10275,7 @@ impl LspStore {
 
         let project_path = ProjectPath {
             worktree_id: worktree.read(cx).id(),
-            path: relative_path.into(),
+            path: relative_path,
         };
 
         Some(
@@ -10799,7 +10841,7 @@ impl LspStore {
     pub(super) fn update_local_worktree_language_servers(
         &mut self,
         worktree_handle: &Entity<Worktree>,
-        changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
+        changes: &[(Arc<RelPath>, ProjectEntryId, PathChange)],
         cx: &mut Context<Self>,
     ) {
         if changes.is_empty() {
@@ -10821,7 +10863,7 @@ impl LspStore {
         language_server_ids.sort();
         language_server_ids.dedup();
 
-        let abs_path = worktree_handle.read(cx).abs_path();
+        // let abs_path = worktree_handle.read(cx).abs_path();
         for server_id in &language_server_ids {
             if let Some(LanguageServerState::Running { server, .. }) =
                 local.language_servers.get(server_id)
@@ -10834,7 +10876,7 @@ impl LspStore {
                     changes: changes
                         .iter()
                         .filter_map(|(path, _, change)| {
-                            if !watched_paths.is_match(path) {
+                            if !watched_paths.is_match(path.as_std_path()) {
                                 return None;
                             }
                             let typ = match change {
@@ -10844,10 +10886,11 @@ impl LspStore {
                                 PathChange::Updated => lsp::FileChangeType::CHANGED,
                                 PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED,
                             };
-                            Some(lsp::FileEvent {
-                                uri: lsp::Uri::from_file_path(abs_path.join(path)).unwrap(),
-                                typ,
-                            })
+                            let uri = lsp::Uri::from_file_path(
+                                worktree_handle.read(cx).absolutize(&path),
+                            )
+                            .ok()?;
+                            Some(lsp::FileEvent { uri, typ })
                         })
                         .collect(),
                 };
@@ -10859,7 +10902,7 @@ impl LspStore {
             }
         }
         for (path, _, _) in changes {
-            if let Some(file_name) = path.file_name().and_then(|file_name| file_name.to_str())
+            if let Some(file_name) = path.file_name()
                 && local.watched_manifest_filenames.contains(file_name)
             {
                 self.request_workspace_config_refresh();
@@ -10879,12 +10922,10 @@ impl LspStore {
     }
 
     fn serialize_symbol(symbol: &Symbol) -> proto::Symbol {
-        proto::Symbol {
+        let mut result = proto::Symbol {
             language_server_name: symbol.language_server_name.0.to_string(),
             source_worktree_id: symbol.source_worktree_id.to_proto(),
             language_server_id: symbol.source_language_server_id.to_proto(),
-            worktree_id: symbol.path.worktree_id.to_proto(),
-            path: symbol.path.path.as_ref().to_proto(),
             name: symbol.name.clone(),
             kind: unsafe { mem::transmute::<lsp::SymbolKind, i32>(symbol.kind) },
             start: Some(proto::PointUtf16 {
@@ -10895,17 +10936,45 @@ impl LspStore {
                 row: symbol.range.end.0.row,
                 column: symbol.range.end.0.column,
             }),
-            signature: symbol.signature.to_vec(),
+            worktree_id: Default::default(),
+            path: Default::default(),
+            signature: Default::default(),
+        };
+        match &symbol.path {
+            SymbolLocation::InProject(path) => {
+                result.worktree_id = path.worktree_id.to_proto();
+                result.path = path.path.to_proto();
+            }
+            SymbolLocation::OutsideProject {
+                abs_path,
+                signature,
+            } => {
+                result.path = abs_path.to_string_lossy().to_string();
+                result.signature = signature.to_vec();
+            }
         }
+        result
     }
 
     fn deserialize_symbol(serialized_symbol: proto::Symbol) -> Result<CoreSymbol> {
         let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id);
         let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id);
         let kind = unsafe { mem::transmute::<i32, lsp::SymbolKind>(serialized_symbol.kind) };
-        let path = ProjectPath {
-            worktree_id,
-            path: Arc::<Path>::from_proto(serialized_symbol.path),
+
+        let path = if serialized_symbol.signature.is_empty() {
+            SymbolLocation::InProject(ProjectPath {
+                worktree_id,
+                path: RelPath::from_proto(&serialized_symbol.path)
+                    .context("invalid symbol path")?,
+            })
+        } else {
+            SymbolLocation::OutsideProject {
+                abs_path: Path::new(&serialized_symbol.path).into(),
+                signature: serialized_symbol
+                    .signature
+                    .try_into()
+                    .map_err(|_| anyhow!("invalid signature"))?,
+            }
         };
 
         let start = serialized_symbol.start.context("invalid start")?;
@@ -10921,10 +10990,6 @@ impl LspStore {
             range: Unclipped(PointUtf16::new(start.row, start.column))
                 ..Unclipped(PointUtf16::new(end.row, end.column)),
             kind,
-            signature: serialized_symbol
-                .signature
-                .try_into()
-                .map_err(|_| anyhow!("invalid signature"))?,
         })
     }
 
@@ -12484,7 +12549,7 @@ impl DiagnosticSummary {
     pub fn to_proto(
         self,
         language_server_id: LanguageServerId,
-        path: &Path,
+        path: &RelPath,
     ) -> proto::DiagnosticSummary {
         proto::DiagnosticSummary {
             path: path.to_proto(),
@@ -12657,7 +12722,7 @@ pub fn language_server_settings<'a>(
     language_server_settings_for(
         SettingsLocation {
             worktree_id: delegate.worktree_id(),
-            path: delegate.worktree_root_path(),
+            path: RelPath::empty(),
         },
         language,
         cx,
@@ -12847,16 +12912,12 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate {
         Some(dir)
     }
 
-    async fn read_text_file(&self, path: PathBuf) -> Result<String> {
+    async fn read_text_file(&self, path: &RelPath) -> Result<String> {
         let entry = self
             .worktree
-            .entry_for_path(&path)
+            .entry_for_path(path)
             .with_context(|| format!("no worktree entry for path {path:?}"))?;
-        let abs_path = self
-            .worktree
-            .absolutize(&entry.path)
-            .with_context(|| format!("cannot absolutize path {path:?}"))?;
-
+        let abs_path = self.worktree.absolutize(&entry.path);
         self.fs.load(&abs_path).await
     }
 }
@@ -12870,14 +12931,17 @@ async fn populate_labels_for_symbols(
     #[allow(clippy::mutable_key_type)]
     let mut symbols_by_language = HashMap::<Option<Arc<Language>>, Vec<CoreSymbol>>::default();
 
-    let mut unknown_paths = BTreeSet::new();
+    let mut unknown_paths = BTreeSet::<Arc<str>>::new();
     for symbol in symbols {
+        let Some(file_name) = symbol.path.file_name() else {
+            continue;
+        };
         let language = language_registry
-            .language_for_file_path(&symbol.path.path)
+            .language_for_file_path(Path::new(file_name))
             .await
             .ok()
             .or_else(|| {
-                unknown_paths.insert(symbol.path.path.clone());
+                unknown_paths.insert(file_name.into());
                 None
             });
         symbols_by_language
@@ -12887,10 +12951,7 @@ async fn populate_labels_for_symbols(
     }
 
     for unknown_path in unknown_paths {
-        log::info!(
-            "no language found for symbol path {}",
-            unknown_path.display()
-        );
+        log::info!("no language found for symbol in file {unknown_path:?}");
     }
 
     let mut label_params = Vec::new();
@@ -12933,7 +12994,6 @@ async fn populate_labels_for_symbols(
                 name,
                 kind: symbol.kind,
                 range: symbol.range,
-                signature: symbol.signature,
             });
         }
     }

crates/project/src/manifest_tree.rs 🔗

@@ -7,7 +7,7 @@ mod manifest_store;
 mod path_trie;
 mod server_tree;
 
-use std::{borrow::Borrow, collections::hash_map::Entry, ops::ControlFlow, path::Path, sync::Arc};
+use std::{borrow::Borrow, collections::hash_map::Entry, ops::ControlFlow, sync::Arc};
 
 use collections::HashMap;
 use gpui::{App, AppContext as _, Context, Entity, Subscription};
@@ -15,6 +15,7 @@ use language::{ManifestDelegate, ManifestName, ManifestQuery};
 pub use manifest_store::ManifestProvidersStore;
 use path_trie::{LabelPresence, RootPathTrie, TriePath};
 use settings::{SettingsStore, WorktreeId};
+use util::rel_path::RelPath;
 use worktree::{Event as WorktreeEvent, Snapshot, Worktree};
 
 use crate::{
@@ -184,7 +185,7 @@ impl ManifestTree {
             .and_then(|manifest_name| self.root_for_path(project_path, manifest_name, delegate, cx))
             .unwrap_or_else(|| ProjectPath {
                 worktree_id,
-                path: Arc::from(Path::new("")),
+                path: RelPath::empty().into(),
             })
     }
 
@@ -211,7 +212,7 @@ impl ManifestQueryDelegate {
 }
 
 impl ManifestDelegate for ManifestQueryDelegate {
-    fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool {
+    fn exists(&self, path: &RelPath, is_dir: Option<bool>) -> bool {
         self.worktree.entry_for_path(path).is_some_and(|entry| {
             is_dir.is_none_or(|is_required_to_be_dir| is_required_to_be_dir == entry.is_dir())
         })

crates/project/src/manifest_tree/path_trie.rs 🔗

@@ -1,11 +1,11 @@
 use std::{
     collections::{BTreeMap, btree_map::Entry},
-    ffi::OsStr,
     ops::ControlFlow,
-    path::{Path, PathBuf},
     sync::Arc,
 };
 
+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.
 /// It also determines how much of a given path is unexplored, thus letting callers fill in that gap if needed.
 /// Conceptually, it allows one to annotate Worktree entries with arbitrary extra metadata and run closest-ancestor searches.
@@ -14,9 +14,9 @@ use std::{
 /// For example, if there's a project root at path `python/project` and we query for a path `python/project/subdir/another_subdir/file.py`, there is
 /// a known root at `python/project` and the unexplored part is `subdir/another_subdir` - we need to run a scan on these 2 directories.
 pub(super) struct RootPathTrie<Label> {
-    worktree_relative_path: Arc<Path>,
+    worktree_relative_path: Arc<RelPath>,
     labels: BTreeMap<Label, LabelPresence>,
-    children: BTreeMap<Arc<OsStr>, RootPathTrie<Label>>,
+    children: BTreeMap<Arc<str>, RootPathTrie<Label>>,
 }
 
 /// Label presence is a marker that allows to optimize searches within [RootPathTrie]; node label can be:
@@ -39,15 +39,17 @@ pub(super) enum LabelPresence {
 
 impl<Label: Ord + Clone> RootPathTrie<Label> {
     pub(super) fn new() -> Self {
-        Self::new_with_key(Arc::from(Path::new("")))
+        Self::new_with_key(Arc::from(RelPath::empty()))
     }
-    fn new_with_key(worktree_relative_path: Arc<Path>) -> Self {
+
+    fn new_with_key(worktree_relative_path: Arc<RelPath>) -> Self {
         RootPathTrie {
             worktree_relative_path,
             labels: Default::default(),
             children: Default::default(),
         }
     }
+
     // Internal implementation of inner that allows one to visit descendants of insertion point for a node.
     fn insert_inner(
         &mut self,
@@ -57,12 +59,13 @@ impl<Label: Ord + Clone> RootPathTrie<Label> {
     ) -> &mut Self {
         let mut current = self;
 
-        let mut path_so_far = PathBuf::new();
+        let mut path_so_far = <Arc<RelPath>>::from(RelPath::empty());
         for key in path.0.iter() {
-            path_so_far.push(Path::new(key));
+            path_so_far = path_so_far.join(RelPath::new(key).unwrap());
             current = match current.children.entry(key.clone()) {
-                Entry::Vacant(vacant_entry) => vacant_entry
-                    .insert(RootPathTrie::new_with_key(Arc::from(path_so_far.as_path()))),
+                Entry::Vacant(vacant_entry) => {
+                    vacant_entry.insert(RootPathTrie::new_with_key(path_so_far.clone()))
+                }
                 Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
             };
         }
@@ -70,6 +73,7 @@ impl<Label: Ord + Clone> RootPathTrie<Label> {
         debug_assert_eq!(_previous_value, None);
         current
     }
+
     pub(super) fn insert(&mut self, path: &TriePath, value: Label, presence: LabelPresence) {
         self.insert_inner(path, value, presence);
     }
@@ -78,7 +82,7 @@ impl<Label: Ord + Clone> RootPathTrie<Label> {
         &'a self,
         path: &TriePath,
         callback: &mut dyn for<'b> FnMut(
-            &'b Arc<Path>,
+            &'b Arc<RelPath>,
             &'a BTreeMap<Label, LabelPresence>,
         ) -> ControlFlow<()>,
     ) {
@@ -115,11 +119,22 @@ impl<Label: Ord + Clone> RootPathTrie<Label> {
 
 /// [TriePath] is a [Path] preprocessed for amortizing the cost of doing multiple lookups in distinct [RootPathTrie]s.
 #[derive(Clone)]
-pub(super) struct TriePath(Arc<[Arc<OsStr>]>);
+pub(super) struct TriePath(Arc<[Arc<str>]>);
 
-impl From<&Path> for TriePath {
-    fn from(value: &Path) -> Self {
-        TriePath(value.components().map(|c| c.as_os_str().into()).collect())
+impl TriePath {
+    fn new(value: &RelPath) -> Self {
+        TriePath(
+            value
+                .components()
+                .map(|component| component.into())
+                .collect(),
+        )
+    }
+}
+
+impl From<&RelPath> for TriePath {
+    fn from(value: &RelPath) -> Self {
+        Self::new(value)
     }
 }
 
@@ -127,39 +142,38 @@ impl From<&Path> for TriePath {
 mod tests {
     use std::collections::BTreeSet;
 
+    use util::rel_path::rel_path;
+
     use super::*;
 
     #[test]
     fn test_insert_and_lookup() {
         let mut trie = RootPathTrie::<()>::new();
         trie.insert(
-            &TriePath::from(Path::new("a/b/c")),
+            &TriePath::new(rel_path("a/b/c")),
             (),
             LabelPresence::Present,
         );
 
-        trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| {
+        trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, nodes| {
             assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
-            assert_eq!(path.as_ref(), Path::new("a/b/c"));
+            assert_eq!(path.as_str(), "a/b/c");
             ControlFlow::Continue(())
         });
         // Now let's annotate a parent with "Known missing" node.
         trie.insert(
-            &TriePath::from(Path::new("a")),
+            &TriePath::new(rel_path("a")),
             (),
             LabelPresence::KnownAbsent,
         );
 
         // Ensure that we walk from the root to the leaf.
         let mut visited_paths = BTreeSet::new();
-        trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| {
-            if path.as_ref() == Path::new("a/b/c") {
-                assert_eq!(
-                    visited_paths,
-                    BTreeSet::from_iter([Arc::from(Path::new("a/"))])
-                );
+        trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, nodes| {
+            if path.as_str() == "a/b/c" {
+                assert_eq!(visited_paths, BTreeSet::from_iter([rel_path("a").into()]));
                 assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
-            } else if path.as_ref() == Path::new("a/") {
+            } else if path.as_str() == "a" {
                 assert!(visited_paths.is_empty());
                 assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
             } else {
@@ -173,15 +187,12 @@ mod tests {
         // One can also pass a path whose prefix is in the tree, but not that path itself.
         let mut visited_paths = BTreeSet::new();
         trie.walk(
-            &TriePath::from(Path::new("a/b/c/d/e/f/g")),
+            &TriePath::new(rel_path("a/b/c/d/e/f/g")),
             &mut |path, nodes| {
-                if path.as_ref() == Path::new("a/b/c") {
-                    assert_eq!(
-                        visited_paths,
-                        BTreeSet::from_iter([Arc::from(Path::new("a/"))])
-                    );
+                if path.as_str() == "a/b/c" {
+                    assert_eq!(visited_paths, BTreeSet::from_iter([rel_path("a").into()]));
                     assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
-                } else if path.as_ref() == Path::new("a/") {
+                } else if path.as_str() == "a" {
                     assert!(visited_paths.is_empty());
                     assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
                 } else {
@@ -195,8 +206,8 @@ mod tests {
 
         // Test breaking from the tree-walk.
         let mut visited_paths = BTreeSet::new();
-        trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| {
-            if path.as_ref() == Path::new("a/") {
+        trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, nodes| {
+            if path.as_str() == "a" {
                 assert!(visited_paths.is_empty());
                 assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
             } else {
@@ -210,45 +221,41 @@ mod tests {
 
         // Entry removal.
         trie.insert(
-            &TriePath::from(Path::new("a/b")),
+            &TriePath::new(rel_path("a/b")),
             (),
             LabelPresence::KnownAbsent,
         );
         let mut visited_paths = BTreeSet::new();
-        trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, _nodes| {
+        trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, _nodes| {
             // Assert that we only ever visit a path once.
             assert!(visited_paths.insert(path.clone()));
             ControlFlow::Continue(())
         });
         assert_eq!(visited_paths.len(), 3);
-        trie.remove(&TriePath::from(Path::new("a/b/")));
+        trie.remove(&TriePath::new(rel_path("a/b")));
         let mut visited_paths = BTreeSet::new();
-        trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, _nodes| {
+        trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, _nodes| {
             // Assert that we only ever visit a path once.
             assert!(visited_paths.insert(path.clone()));
             ControlFlow::Continue(())
         });
         assert_eq!(visited_paths.len(), 1);
         assert_eq!(
-            visited_paths.into_iter().next().unwrap().as_ref(),
-            Path::new("a/")
+            visited_paths.into_iter().next().unwrap(),
+            rel_path("a").into()
         );
     }
 
     #[test]
     fn path_to_a_root_can_contain_multiple_known_nodes() {
         let mut trie = RootPathTrie::<()>::new();
-        trie.insert(
-            &TriePath::from(Path::new("a/b")),
-            (),
-            LabelPresence::Present,
-        );
-        trie.insert(&TriePath::from(Path::new("a")), (), LabelPresence::Present);
+        trie.insert(&TriePath::new(rel_path("a/b")), (), LabelPresence::Present);
+        trie.insert(&TriePath::new(rel_path("a")), (), LabelPresence::Present);
         let mut visited_paths = BTreeSet::new();
-        trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| {
+        trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, nodes| {
             assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
-            if path.as_ref() != Path::new("a") && path.as_ref() != Path::new("a/b") {
-                panic!("Unexpected path: {}", path.as_ref().display());
+            if path.as_str() != "a" && path.as_str() != "a/b" {
+                panic!("Unexpected path: {}", path.as_str());
             }
             assert!(visited_paths.insert(path.clone()));
             ControlFlow::Continue(())

crates/project/src/manifest_tree/server_tree.rs 🔗

@@ -8,7 +8,6 @@
 
 use std::{
     collections::{BTreeMap, BTreeSet},
-    path::Path,
     sync::{Arc, Weak},
 };
 
@@ -21,6 +20,7 @@ use language::{
 use lsp::LanguageServerName;
 use settings::{Settings, SettingsLocation, WorktreeId};
 use std::sync::OnceLock;
+use util::rel_path::RelPath;
 
 use crate::{
     LanguageServerId, ProjectPath, project_settings::LspSettings,
@@ -32,7 +32,7 @@ use super::ManifestTree;
 #[derive(Clone, Debug, Default)]
 pub(crate) struct ServersForWorktree {
     pub(crate) roots: BTreeMap<
-        Arc<Path>,
+        Arc<RelPath>,
         BTreeMap<LanguageServerName, (Arc<InnerTreeNode>, BTreeSet<LanguageName>)>,
     >,
 }
@@ -338,7 +338,7 @@ impl LanguageServerTree {
             .entry(worktree_id)
             .or_default()
             .roots
-            .entry(Arc::from(Path::new("")))
+            .entry(RelPath::empty().into())
             .or_default()
             .entry(node.disposition.server_name.clone())
             .or_insert_with(|| (node, BTreeSet::new()))

crates/project/src/prettier_store.rs 🔗

@@ -23,7 +23,7 @@ use node_runtime::NodeRuntime;
 use paths::default_prettier_dir;
 use prettier::Prettier;
 use smol::stream::StreamExt;
-use util::{ResultExt, TryFutureExt};
+use util::{ResultExt, TryFutureExt, rel_path::RelPath};
 
 use crate::{
     File, PathChange, ProjectEntryId, Worktree, lsp_store::WorktreeId,
@@ -442,12 +442,12 @@ impl PrettierStore {
     pub fn update_prettier_settings(
         &self,
         worktree: &Entity<Worktree>,
-        changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
+        changes: &[(Arc<RelPath>, ProjectEntryId, PathChange)],
         cx: &mut Context<Self>,
     ) {
         let prettier_config_files = Prettier::CONFIG_FILE_NAMES
             .iter()
-            .map(Path::new)
+            .map(|name| RelPath::new(name).unwrap())
             .collect::<HashSet<_>>();
 
         let prettier_config_file_changed = changes
@@ -456,7 +456,7 @@ impl PrettierStore {
             .filter(|(path, _, _)| {
                 !path
                     .components()
-                    .any(|component| component.as_os_str().to_string_lossy() == "node_modules")
+                    .any(|component| component == "node_modules")
             })
             .find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
         let current_worktree_id = worktree.read(cx).id();

crates/project/src/project.rs 🔗

@@ -37,7 +37,7 @@ use dap::inline_value::{InlineValueLocation, VariableLookupKind, VariableScope};
 use crate::{
     agent_server_store::{AgentServerStore, AllAgentServersSettings},
     git_store::GitStore,
-    lsp_store::log_store::LogKind,
+    lsp_store::{SymbolLocation, log_store::LogKind},
 };
 pub use git_store::{
     ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate,
@@ -96,7 +96,7 @@ use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent}
 use remote::{RemoteClient, RemoteConnectionOptions};
 use rpc::{
     AnyProtoClient, ErrorCode,
-    proto::{FromProto, LanguageServerPromptResponse, REMOTE_SERVER_PROJECT_ID, ToProto},
+    proto::{LanguageServerPromptResponse, REMOTE_SERVER_PROJECT_ID},
 };
 use search::{SearchInputKind, SearchQuery, SearchResult};
 use search_history::SearchHistory;
@@ -108,7 +108,7 @@ use std::{
     borrow::Cow,
     collections::BTreeMap,
     ops::Range,
-    path::{Component, Path, PathBuf},
+    path::{Path, PathBuf},
     pin::pin,
     str,
     sync::Arc,
@@ -121,7 +121,8 @@ use text::{Anchor, BufferId, OffsetRangeExt, Point, Rope};
 use toolchain_store::EmptyToolchainStore;
 use util::{
     ResultExt as _, maybe,
-    paths::{PathStyle, RemotePathBuf, SanitizedPath, compare_paths},
+    paths::{PathStyle, SanitizedPath, compare_paths, is_absolute},
+    rel_path::RelPath,
 };
 use worktree::{CreatedEntry, Snapshot, Traversal};
 pub use worktree::{
@@ -353,7 +354,7 @@ pub enum DebugAdapterClientState {
 #[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
 pub struct ProjectPath {
     pub worktree_id: WorktreeId,
-    pub path: Arc<Path>,
+    pub path: Arc<RelPath>,
 }
 
 impl ProjectPath {
@@ -364,11 +365,11 @@ impl ProjectPath {
         }
     }
 
-    pub fn from_proto(p: proto::ProjectPath) -> Self {
-        Self {
+    pub fn from_proto(p: proto::ProjectPath) -> Option<Self> {
+        Some(Self {
             worktree_id: WorktreeId::from_proto(p.worktree_id),
-            path: Arc::<Path>::from_proto(p.path),
-        }
+            path: RelPath::from_proto(&p.path).log_err()?,
+        })
     }
 
     pub fn to_proto(&self) -> proto::ProjectPath {
@@ -381,7 +382,7 @@ impl ProjectPath {
     pub fn root_path(worktree_id: WorktreeId) -> Self {
         Self {
             worktree_id,
-            path: Path::new("").into(),
+            path: RelPath::empty().into(),
         }
     }
 
@@ -743,12 +744,11 @@ pub struct Symbol {
     pub language_server_name: LanguageServerName,
     pub source_worktree_id: WorktreeId,
     pub source_language_server_id: LanguageServerId,
-    pub path: ProjectPath,
+    pub path: SymbolLocation,
     pub label: CodeLabel,
     pub name: String,
     pub kind: lsp::SymbolKind,
     pub range: Range<Unclipped<PointUtf16>>,
-    pub signature: [u8; 32],
 }
 
 #[derive(Clone, Debug)]
@@ -882,28 +882,29 @@ impl DirectoryLister {
     }
 
     pub fn default_query(&self, cx: &mut App) -> String {
-        let separator = std::path::MAIN_SEPARATOR_STR;
-        match self {
+        let project = match self {
             DirectoryLister::Project(project) => project,
             DirectoryLister::Local(project, _) => project,
         }
-        .read(cx)
-        .visible_worktrees(cx)
-        .next()
-        .map(|worktree| worktree.read(cx).abs_path())
-        .map(|dir| dir.to_string_lossy().to_string())
-        .or_else(|| std::env::home_dir().map(|dir| dir.to_string_lossy().to_string()))
-        .map(|mut s| {
-            s.push_str(separator);
-            s
-        })
-        .unwrap_or_else(|| {
-            if cfg!(target_os = "windows") {
-                format!("C:{separator}")
-            } else {
-                format!("~{separator}")
-            }
-        })
+        .read(cx);
+        let path_style = project.path_style(cx);
+        project
+            .visible_worktrees(cx)
+            .next()
+            .map(|worktree| worktree.read(cx).abs_path().to_string_lossy().to_string())
+            .or_else(|| std::env::home_dir().map(|dir| dir.to_string_lossy().to_string()))
+            .map(|mut s| {
+                s.push_str(path_style.separator());
+                s
+            })
+            .unwrap_or_else(|| {
+                if path_style.is_windows() {
+                    "C:\\"
+                } else {
+                    "~/"
+                }
+                .to_string()
+            })
     }
 
     pub fn list_directory(&self, path: String, cx: &mut App) -> Task<Result<Vec<DirectoryItem>>> {
@@ -1469,14 +1470,18 @@ impl Project {
         let remote_id = response.payload.project_id;
         let role = response.payload.role();
 
-        // todo(zjk)
-        // Set the proper path style based on the remote
+        let path_style = if response.payload.windows_paths {
+            PathStyle::Windows
+        } else {
+            PathStyle::Posix
+        };
+
         let worktree_store = cx.new(|_| {
             WorktreeStore::remote(
                 true,
                 client.clone().into(),
                 response.payload.project_id,
-                PathStyle::Posix,
+                path_style,
             )
         })?;
         let buffer_store = cx.new(|cx| {
@@ -1548,10 +1553,9 @@ impl Project {
         })?;
 
         let agent_server_store = cx.new(|cx| AgentServerStore::collab(cx))?;
+        let replica_id = response.payload.replica_id as ReplicaId;
 
         let project = cx.new(|cx| {
-            let replica_id = response.payload.replica_id as ReplicaId;
-
             let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx);
 
             let weak_self = cx.weak_entity();
@@ -1560,8 +1564,14 @@ impl Project {
 
             let mut worktrees = Vec::new();
             for worktree in response.payload.worktrees {
-                let worktree =
-                    Worktree::remote(remote_id, replica_id, worktree, client.clone().into(), cx);
+                let worktree = Worktree::remote(
+                    remote_id,
+                    replica_id,
+                    worktree,
+                    client.clone().into(),
+                    path_style,
+                    cx,
+                );
                 worktrees.push(worktree);
             }
 
@@ -2022,7 +2032,7 @@ impl Project {
 
     pub fn worktree_root_names<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a str> {
         self.visible_worktrees(cx)
-            .map(|tree| tree.read(cx).root_name())
+            .map(|tree| tree.read(cx).root_name().as_str())
     }
 
     pub fn worktree_for_id(&self, id: WorktreeId, cx: &App) -> Option<Entity<Worktree>> {
@@ -2120,15 +2130,11 @@ impl Project {
     pub fn copy_entry(
         &mut self,
         entry_id: ProjectEntryId,
-        relative_worktree_source_path: Option<PathBuf>,
-        new_path: impl Into<Arc<Path>>,
+        new_project_path: ProjectPath,
         cx: &mut Context<Self>,
     ) -> Task<Result<Option<Entry>>> {
-        let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
-            return Task::ready(Ok(None));
-        };
-        worktree.update(cx, |worktree, cx| {
-            worktree.copy_entry(entry_id, relative_worktree_source_path, new_path, cx)
+        self.worktree_store.update(cx, |worktree_store, cx| {
+            worktree_store.copy_entry(entry_id, new_project_path, cx)
         })
     }
 
@@ -2139,12 +2145,12 @@ impl Project {
     pub fn rename_entry(
         &mut self,
         entry_id: ProjectEntryId,
-        new_path: impl Into<Arc<Path>>,
+        new_path: ProjectPath,
         cx: &mut Context<Self>,
     ) -> Task<Result<CreatedEntry>> {
-        let worktree_store = self.worktree_store.read(cx);
-        let new_path = new_path.into();
+        let worktree_store = self.worktree_store.clone();
         let Some((worktree, old_path, is_dir)) = worktree_store
+            .read(cx)
             .worktree_and_entry_for_id(entry_id, cx)
             .map(|(worktree, entry)| (worktree, entry.path.clone(), entry.is_dir()))
         else {
@@ -2159,11 +2165,14 @@ impl Project {
             let (old_abs_path, new_abs_path) = {
                 let root_path = worktree.read_with(cx, |this, _| this.abs_path())?;
                 let new_abs_path = if is_root_entry {
-                    root_path.parent().unwrap().join(&new_path)
+                    root_path
+                        .parent()
+                        .unwrap()
+                        .join(new_path.path.as_std_path())
                 } else {
-                    root_path.join(&new_path)
+                    root_path.join(&new_path.path.as_std_path())
                 };
-                (root_path.join(&old_path), new_abs_path)
+                (root_path.join(old_path.as_std_path()), new_abs_path)
             };
             let transaction = LspStore::will_rename_entry(
                 lsp_store.clone(),
@@ -2175,9 +2184,9 @@ impl Project {
             )
             .await;
 
-            let entry = worktree
-                .update(cx, |worktree, cx| {
-                    worktree.rename_entry(entry_id, new_path.clone(), cx)
+            let entry = worktree_store
+                .update(cx, |worktree_store, cx| {
+                    worktree_store.rename_entry(entry_id, new_path.clone(), cx)
                 })?
                 .await?;
 
@@ -4012,7 +4021,7 @@ impl Project {
             .filter(|buffer| {
                 let b = buffer.read(cx);
                 if let Some(file) = b.file() {
-                    if !search_query.match_path(file.path()) {
+                    if !search_query.match_path(file.path().as_std_path()) {
                         return false;
                     }
                     if let Some(entry) = b
@@ -4032,7 +4041,10 @@ impl Project {
             (None, None) => a.read(cx).remote_id().cmp(&b.read(cx).remote_id()),
             (None, Some(_)) => std::cmp::Ordering::Less,
             (Some(_), None) => std::cmp::Ordering::Greater,
-            (Some(a), Some(b)) => compare_paths((a.path(), true), (b.path(), true)),
+            (Some(a), Some(b)) => compare_paths(
+                (a.path().as_std_path(), true),
+                (b.path().as_std_path(), true),
+            ),
         });
         for buffer in buffers {
             tx.send_blocking(buffer.clone()).unwrap()
@@ -4139,13 +4151,17 @@ impl Project {
         abs_path: impl AsRef<Path>,
         visible: bool,
         cx: &mut Context<Self>,
-    ) -> Task<Result<(Entity<Worktree>, PathBuf)>> {
+    ) -> Task<Result<(Entity<Worktree>, Arc<RelPath>)>> {
         self.worktree_store.update(cx, |worktree_store, cx| {
             worktree_store.find_or_create_worktree(abs_path, visible, cx)
         })
     }
 
-    pub fn find_worktree(&self, abs_path: &Path, cx: &App) -> Option<(Entity<Worktree>, PathBuf)> {
+    pub fn find_worktree(
+        &self,
+        abs_path: &Path,
+        cx: &App,
+    ) -> Option<(Entity<Worktree>, Arc<RelPath>)> {
         self.worktree_store.read(cx).find_worktree(abs_path, cx)
     }
 
@@ -4164,11 +4180,10 @@ impl Project {
         buffer: &Entity<Buffer>,
         cx: &mut Context<Self>,
     ) -> Task<Option<ResolvedPath>> {
-        let path_buf = PathBuf::from(path);
-        if path_buf.is_absolute() || path.starts_with("~") {
+        if util::paths::is_absolute(path, self.path_style(cx)) || path.starts_with("~") {
             self.resolve_abs_path(path, cx)
         } else {
-            self.resolve_path_in_worktrees(path_buf, buffer, cx)
+            self.resolve_path_in_worktrees(path, buffer, cx)
         }
     }
 
@@ -4189,29 +4204,26 @@ impl Project {
             let expanded = PathBuf::from(shellexpand::tilde(&path).into_owned());
             let fs = self.fs.clone();
             cx.background_spawn(async move {
-                let path = expanded.as_path();
-                let metadata = fs.metadata(path).await.ok().flatten();
+                let metadata = fs.metadata(&expanded).await.ok().flatten();
 
                 metadata.map(|metadata| ResolvedPath::AbsPath {
-                    path: expanded,
+                    path: expanded.to_string_lossy().to_string(),
                     is_dir: metadata.is_dir,
                 })
             })
         } else if let Some(ssh_client) = self.remote_client.as_ref() {
-            let path_style = ssh_client.read(cx).path_style();
-            let request_path = RemotePathBuf::from_str(path, path_style);
             let request = ssh_client
                 .read(cx)
                 .proto_client()
                 .request(proto::GetPathMetadata {
                     project_id: REMOTE_SERVER_PROJECT_ID,
-                    path: request_path.to_proto(),
+                    path: path.into(),
                 });
             cx.background_spawn(async move {
                 let response = request.await.log_err()?;
                 if response.exists {
                     Some(ResolvedPath::AbsPath {
-                        path: PathBuf::from_proto(response.path),
+                        path: response.path,
                         is_dir: response.is_dir,
                     })
                 } else {
@@ -4225,17 +4237,26 @@ impl Project {
 
     fn resolve_path_in_worktrees(
         &self,
-        path: PathBuf,
+        path: &str,
         buffer: &Entity<Buffer>,
         cx: &mut Context<Self>,
     ) -> Task<Option<ResolvedPath>> {
-        let mut candidates = vec![path.clone()];
+        let mut candidates = vec![];
+        if let Ok(path) = RelPath::from_std_path(Path::new(path), self.path_style(cx)) {
+            candidates.push(path);
+        }
 
         if let Some(file) = buffer.read(cx).file()
             && let Some(dir) = file.path().parent()
         {
-            let joined = dir.to_path_buf().join(path);
-            candidates.push(joined);
+            if let Some(joined) = self
+                .path_style(cx)
+                .join(&*dir.display(self.path_style(cx)), path)
+                && let Some(joined) =
+                    RelPath::from_std_path(Path::new(&joined), self.path_style(cx)).ok()
+            {
+                candidates.push(joined);
+            }
         }
 
         let buffer_worktree_id = buffer.read(cx).file().map(|file| file.worktree_id(cx));
@@ -4275,15 +4296,12 @@ impl Project {
 
     fn resolve_path_in_worktree(
         worktree: &Entity<Worktree>,
-        path: &PathBuf,
+        path: &RelPath,
         cx: &mut AsyncApp,
     ) -> Option<ResolvedPath> {
         worktree
             .read_with(cx, |worktree, _| {
-                let root_entry_path = &worktree.root_entry()?.path;
-                let resolved = resolve_path(root_entry_path, path);
-                let stripped = resolved.strip_prefix(root_entry_path).unwrap_or(&resolved);
-                worktree.entry_for_path(stripped).map(|entry| {
+                worktree.entry_for_path(path).map(|entry| {
                     let project_path = ProjectPath {
                         worktree_id: worktree.id(),
                         path: entry.path.clone(),
@@ -4305,10 +4323,9 @@ impl Project {
         if self.is_local() {
             DirectoryLister::Local(cx.entity(), self.fs.clone()).list_directory(query, cx)
         } else if let Some(session) = self.remote_client.as_ref() {
-            let path_buf = PathBuf::from(query);
             let request = proto::ListRemoteDirectory {
                 dev_server_id: REMOTE_SERVER_PROJECT_ID,
-                path: path_buf.to_proto(),
+                path: query,
                 config: Some(proto::ListRemoteDirectoryConfig { is_dir: true }),
             };
 
@@ -4358,7 +4375,7 @@ impl Project {
     pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut Context<Self>) {
         let new_active_entry = entry.and_then(|project_path| {
             let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
-            let entry = worktree.read(cx).entry_for_path(project_path.path)?;
+            let entry = worktree.read(cx).entry_for_path(&project_path.path)?;
             Some(entry.id)
         });
         if new_active_entry != self.active_entry {
@@ -4419,10 +4436,11 @@ impl Project {
     }
 
     pub fn absolute_path(&self, project_path: &ProjectPath, cx: &App) -> Option<PathBuf> {
-        self.worktree_for_id(project_path.worktree_id, cx)?
-            .read(cx)
-            .absolutize(&project_path.path)
-            .ok()
+        Some(
+            self.worktree_for_id(project_path.worktree_id, cx)?
+                .read(cx)
+                .absolutize(&project_path.path),
+        )
     }
 
     /// Attempts to find a `ProjectPath` corresponding to the given path. If the path
@@ -4435,7 +4453,7 @@ impl Project {
     ///
     /// # Arguments
     ///
-    /// * `path` - A full path that starts with a worktree root name, or alternatively a
+    /// * `path` - An absolute path, or a full path that starts with a worktree root name, or a
     ///   relative path within a visible worktree.
     /// * `cx` - A reference to the `AppContext`.
     ///
@@ -4443,34 +4461,41 @@ impl Project {
     ///
     /// Returns `Some(ProjectPath)` if a matching worktree is found, otherwise `None`.
     pub fn find_project_path(&self, path: impl AsRef<Path>, cx: &App) -> Option<ProjectPath> {
+        let path_style = self.path_style(cx);
         let path = path.as_ref();
         let worktree_store = self.worktree_store.read(cx);
 
-        if path.is_absolute() {
+        if is_absolute(&path.to_string_lossy(), path_style) {
             for worktree in worktree_store.visible_worktrees(cx) {
                 let worktree_abs_path = worktree.read(cx).abs_path();
 
-                if let Ok(relative_path) = path.strip_prefix(worktree_abs_path) {
+                if let Ok(relative_path) = path.strip_prefix(worktree_abs_path)
+                    && let Ok(path) = RelPath::from_std_path(relative_path, path_style)
+                {
                     return Some(ProjectPath {
                         worktree_id: worktree.read(cx).id(),
-                        path: relative_path.into(),
+                        path,
                     });
                 }
             }
         } else {
             for worktree in worktree_store.visible_worktrees(cx) {
                 let worktree_root_name = worktree.read(cx).root_name();
-                if let Ok(relative_path) = path.strip_prefix(worktree_root_name) {
+                if let Ok(relative_path) = path.strip_prefix(worktree_root_name)
+                    && let Ok(path) = RelPath::from_std_path(relative_path, path_style)
+                {
                     return Some(ProjectPath {
                         worktree_id: worktree.read(cx).id(),
-                        path: relative_path.into(),
+                        path,
                     });
                 }
             }
 
             for worktree in worktree_store.visible_worktrees(cx) {
                 let worktree = worktree.read(cx);
-                if let Some(entry) = worktree.entry_for_path(path) {
+                if let Ok(path) = RelPath::from_std_path(path, path_style)
+                    && let Some(entry) = worktree.entry_for_path(&path)
+                {
                     return Some(ProjectPath {
                         worktree_id: worktree.id(),
                         path: entry.path.clone(),
@@ -4489,13 +4514,18 @@ impl Project {
         &self,
         project_path: &ProjectPath,
         cx: &App,
-    ) -> Option<PathBuf> {
+    ) -> Option<String> {
+        let path_style = self.path_style(cx);
         if self.visible_worktrees(cx).take(2).count() < 2 {
-            return Some(project_path.path.to_path_buf());
+            return Some(project_path.path.display(path_style).to_string());
         }
         self.worktree_for_id(project_path.worktree_id, cx)
-            .and_then(|worktree| {
-                Some(Path::new(worktree.read(cx).abs_path().file_name()?).join(&project_path.path))
+            .map(|worktree| {
+                let worktree_name = worktree.read(cx).root_name();
+                worktree_name
+                    .join(&project_path.path)
+                    .display(path_style)
+                    .to_string()
             })
     }
 
@@ -4503,7 +4533,7 @@ impl Project {
         self.find_worktree(abs_path, cx)
             .map(|(worktree, relative_path)| ProjectPath {
                 worktree_id: worktree.read(cx).id(),
-                path: relative_path.into(),
+                path: relative_path,
             })
     }
 
@@ -4869,7 +4899,9 @@ impl Project {
     ) -> Result<proto::FindSearchCandidatesResponse> {
         let peer_id = envelope.original_sender_id()?;
         let message = envelope.payload;
-        let query = SearchQuery::from_proto(message.query.context("missing query field")?)?;
+        let path_style = this.read_with(&cx, |this, cx| this.path_style(cx))?;
+        let query =
+            SearchQuery::from_proto(message.query.context("missing query field")?, path_style)?;
         let results = this.update(&mut cx, |this, cx| {
             this.find_search_candidate_buffers(&query, message.limit as _, cx)
         })?;
@@ -4908,18 +4940,13 @@ impl Project {
     ) -> Result<proto::OpenBufferResponse> {
         let peer_id = envelope.original_sender_id()?;
         let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
-        let open_buffer = this.update(&mut cx, |this, cx| {
-            this.open_buffer(
-                ProjectPath {
-                    worktree_id,
-                    path: Arc::<Path>::from_proto(envelope.payload.path),
-                },
-                cx,
-            )
-        })?;
-
-        let buffer = open_buffer.await?;
-        Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx)
+        let path = RelPath::from_proto(&envelope.payload.path)?;
+        let open_buffer = this
+            .update(&mut cx, |this, cx| {
+                this.open_buffer(ProjectPath { worktree_id, path }, cx)
+            })?
+            .await?;
+        Project::respond_to_open_buffer_request(this, open_buffer, peer_id, &mut cx)
     }
 
     async fn handle_open_new_buffer(
@@ -5263,6 +5290,10 @@ impl Project {
     pub fn agent_location(&self) -> Option<AgentLocation> {
         self.agent_location.clone()
     }
+
+    pub fn path_style(&self, cx: &App) -> PathStyle {
+        self.worktree_store.read(cx).path_style()
+    }
 }
 
 pub struct PathMatchCandidateSet {
@@ -5316,16 +5347,22 @@ impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet {
         }
     }
 
-    fn prefix(&self) -> Arc<str> {
-        if self.snapshot.root_entry().is_some_and(|e| e.is_file()) {
+    fn prefix(&self) -> Arc<RelPath> {
+        if self.snapshot.root_entry().is_some_and(|e| e.is_file()) || self.include_root_name {
             self.snapshot.root_name().into()
-        } else if self.include_root_name {
-            format!("{}{}", self.snapshot.root_name(), std::path::MAIN_SEPARATOR).into()
         } else {
-            Arc::default()
+            RelPath::empty().into()
         }
     }
 
+    fn root_is_file(&self) -> bool {
+        self.snapshot.root_entry().is_some_and(|f| f.is_file())
+    }
+
+    fn path_style(&self) -> PathStyle {
+        self.snapshot.path_style()
+    }
+
     fn candidates(&'a self, start: usize) -> Self::Candidates {
         PathMatchCandidateSetIter {
             traversal: match self.candidates {
@@ -5366,56 +5403,13 @@ impl<'a> From<&'a ProjectPath> for SettingsLocation<'a> {
     }
 }
 
-impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
+impl<P: Into<Arc<RelPath>>> From<(WorktreeId, P)> for ProjectPath {
     fn from((worktree_id, path): (WorktreeId, P)) -> Self {
         Self {
             worktree_id,
-            path: path.as_ref().into(),
-        }
-    }
-}
-
-pub fn relativize_path(base: &Path, path: &Path) -> PathBuf {
-    let mut path_components = path.components();
-    let mut base_components = base.components();
-    let mut components: Vec<Component> = Vec::new();
-    loop {
-        match (path_components.next(), base_components.next()) {
-            (None, None) => break,
-            (Some(a), None) => {
-                components.push(a);
-                components.extend(path_components.by_ref());
-                break;
-            }
-            (None, _) => components.push(Component::ParentDir),
-            (Some(a), Some(b)) if components.is_empty() && a == b => (),
-            (Some(a), Some(Component::CurDir)) => components.push(a),
-            (Some(a), Some(_)) => {
-                components.push(Component::ParentDir);
-                for _ in base_components {
-                    components.push(Component::ParentDir);
-                }
-                components.push(a);
-                components.extend(path_components.by_ref());
-                break;
-            }
+            path: path.into(),
         }
     }
-    components.iter().map(|c| c.as_os_str()).collect()
-}
-
-fn resolve_path(base: &Path, path: &Path) -> PathBuf {
-    let mut result = base.to_path_buf();
-    for component in path.components() {
-        match component {
-            Component::ParentDir => {
-                result.pop();
-            }
-            Component::CurDir => (),
-            _ => result.push(component),
-        }
-    }
-    result
 }
 
 /// ResolvedPath is a path that has been resolved to either a ProjectPath
@@ -5427,20 +5421,20 @@ pub enum ResolvedPath {
         is_dir: bool,
     },
     AbsPath {
-        path: PathBuf,
+        path: String,
         is_dir: bool,
     },
 }
 
 impl ResolvedPath {
-    pub fn abs_path(&self) -> Option<&Path> {
+    pub fn abs_path(&self) -> Option<&str> {
         match self {
-            Self::AbsPath { path, .. } => Some(path.as_path()),
+            Self::AbsPath { path, .. } => Some(path),
             _ => None,
         }
     }
 
-    pub fn into_abs_path(self) -> Option<PathBuf> {
+    pub fn into_abs_path(self) -> Option<String> {
         match self {
             Self::AbsPath { path, .. } => Some(path),
             _ => None,
@@ -5550,8 +5544,8 @@ pub fn sort_worktree_entries(entries: &mut [impl AsRef<Entry>]) {
         let entry_a = entry_a.as_ref();
         let entry_b = entry_b.as_ref();
         compare_paths(
-            (&entry_a.path, entry_a.is_file()),
-            (&entry_b.path, entry_b.is_file()),
+            (entry_a.path.as_std_path(), entry_a.is_file()),
+            (entry_b.path.as_std_path(), entry_b.is_file()),
         )
     });
 }

crates/project/src/project_settings.rs 🔗

@@ -13,7 +13,7 @@ use paths::{
 };
 use rpc::{
     AnyProtoClient, TypedEnvelope,
-    proto::{self, FromProto, REMOTE_SERVER_PROJECT_ID, ToProto},
+    proto::{self, REMOTE_SERVER_PROJECT_ID},
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -23,13 +23,9 @@ use settings::{
     DapSettingsContent, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation,
     SettingsStore, parse_json_with_comments, watch_config_file,
 };
-use std::{
-    path::{Path, PathBuf},
-    sync::Arc,
-    time::Duration,
-};
+use std::{path::PathBuf, sync::Arc, time::Duration};
 use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
-use util::{ResultExt, serde::default_true};
+use util::{ResultExt, rel_path::RelPath, serde::default_true};
 use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
 
 use crate::{
@@ -742,6 +738,7 @@ impl SettingsObserver {
                 .with_context(|| format!("unknown kind {kind}"))?,
             None => proto::LocalSettingsKind::Settings,
         };
+        let path = RelPath::from_proto(&envelope.payload.path)?;
         this.update(&mut cx, |this, cx| {
             let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
             let Some(worktree) = this
@@ -755,7 +752,7 @@ impl SettingsObserver {
             this.update_settings(
                 worktree,
                 [(
-                    Arc::<Path>::from_proto(envelope.payload.path.clone()),
+                    path,
                     local_settings_kind_from_proto(kind),
                     envelope.payload.content,
                 )],
@@ -808,61 +805,61 @@ impl SettingsObserver {
         let mut settings_contents = Vec::new();
         for (path, _, change) in changes.iter() {
             let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
-                let settings_dir = Arc::<Path>::from(
-                    path.ancestors()
-                        .nth(local_settings_file_relative_path().components().count())
-                        .unwrap(),
-                );
+                let settings_dir = path
+                    .ancestors()
+                    .nth(local_settings_file_relative_path().components().count())
+                    .unwrap()
+                    .into();
                 (settings_dir, LocalSettingsKind::Settings)
             } else if path.ends_with(local_tasks_file_relative_path()) {
-                let settings_dir = Arc::<Path>::from(
-                    path.ancestors()
-                        .nth(
-                            local_tasks_file_relative_path()
-                                .components()
-                                .count()
-                                .saturating_sub(1),
-                        )
-                        .unwrap(),
-                );
+                let settings_dir = path
+                    .ancestors()
+                    .nth(
+                        local_tasks_file_relative_path()
+                            .components()
+                            .count()
+                            .saturating_sub(1),
+                    )
+                    .unwrap()
+                    .into();
                 (settings_dir, LocalSettingsKind::Tasks)
             } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
-                let settings_dir = Arc::<Path>::from(
-                    path.ancestors()
-                        .nth(
-                            local_vscode_tasks_file_relative_path()
-                                .components()
-                                .count()
-                                .saturating_sub(1),
-                        )
-                        .unwrap(),
-                );
+                let settings_dir = path
+                    .ancestors()
+                    .nth(
+                        local_vscode_tasks_file_relative_path()
+                            .components()
+                            .count()
+                            .saturating_sub(1),
+                    )
+                    .unwrap()
+                    .into();
                 (settings_dir, LocalSettingsKind::Tasks)
             } else if path.ends_with(local_debug_file_relative_path()) {
-                let settings_dir = Arc::<Path>::from(
-                    path.ancestors()
-                        .nth(
-                            local_debug_file_relative_path()
-                                .components()
-                                .count()
-                                .saturating_sub(1),
-                        )
-                        .unwrap(),
-                );
+                let settings_dir = path
+                    .ancestors()
+                    .nth(
+                        local_debug_file_relative_path()
+                            .components()
+                            .count()
+                            .saturating_sub(1),
+                    )
+                    .unwrap()
+                    .into();
                 (settings_dir, LocalSettingsKind::Debug)
             } else if path.ends_with(local_vscode_launch_file_relative_path()) {
-                let settings_dir = Arc::<Path>::from(
-                    path.ancestors()
-                        .nth(
-                            local_vscode_tasks_file_relative_path()
-                                .components()
-                                .count()
-                                .saturating_sub(1),
-                        )
-                        .unwrap(),
-                );
+                let settings_dir = path
+                    .ancestors()
+                    .nth(
+                        local_vscode_tasks_file_relative_path()
+                            .components()
+                            .count()
+                            .saturating_sub(1),
+                    )
+                    .unwrap()
+                    .into();
                 (settings_dir, LocalSettingsKind::Debug)
-            } else if path.ends_with(EDITORCONFIG_NAME) {
+            } else if path.ends_with(RelPath::new(EDITORCONFIG_NAME).unwrap()) {
                 let Some(settings_dir) = path.parent().map(Arc::from) else {
                     continue;
                 };
@@ -873,13 +870,7 @@ impl SettingsObserver {
 
             let removed = change == &PathChange::Removed;
             let fs = fs.clone();
-            let abs_path = match worktree.read(cx).absolutize(path) {
-                Ok(abs_path) => abs_path,
-                Err(e) => {
-                    log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
-                    continue;
-                }
-            };
+            let abs_path = worktree.read(cx).absolutize(path);
             settings_contents.push(async move {
                 (
                     settings_dir,
@@ -941,7 +932,7 @@ impl SettingsObserver {
 
         let worktree = worktree.clone();
         cx.spawn(async move |this, cx| {
-            let settings_contents: Vec<(Arc<Path>, _, _)> =
+            let settings_contents: Vec<(Arc<RelPath>, _, _)> =
                 futures::future::join_all(settings_contents).await;
             cx.update(|cx| {
                 this.update(cx, |this, cx| {
@@ -961,7 +952,7 @@ impl SettingsObserver {
     fn update_settings(
         &mut self,
         worktree: Entity<Worktree>,
-        settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
+        settings_contents: impl IntoIterator<Item = (Arc<RelPath>, LocalSettingsKind, Option<String>)>,
         cx: &mut Context<Self>,
     ) {
         let worktree_id = worktree.read(cx).id();
@@ -991,9 +982,9 @@ impl SettingsObserver {
                                 log::error!("Failed to set local settings: {e}");
                             }
                             Ok(()) => {
-                                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
-                                    directory.join(local_settings_file_relative_path())
-                                )));
+                                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(directory
+                                    .as_std_path()
+                                    .join(local_settings_file_relative_path()))));
                             }
                         }
                     }),
@@ -1020,9 +1011,9 @@ impl SettingsObserver {
                             log::error!("Failed to set local tasks: {e}");
                         }
                         Ok(()) => {
-                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
-                                directory.join(task_file_name())
-                            )));
+                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
+                                .as_std_path()
+                                .join(RelPath::new(task_file_name()).unwrap()))));
                         }
                     }
                 }
@@ -1051,9 +1042,9 @@ impl SettingsObserver {
                             log::error!("Failed to set local tasks: {e}");
                         }
                         Ok(()) => {
-                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
-                                directory.join(task_file_name())
-                            )));
+                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
+                                .as_std_path()
+                                .join(RelPath::new(task_file_name()).unwrap()))));
                         }
                     }
                 }

crates/project/src/project_tests.rs 🔗

@@ -13,7 +13,7 @@ use fs::FakeFs;
 use futures::{StreamExt, future};
 use git::{
     GitHostingProviderRegistry,
-    repository::RepoPath,
+    repository::{RepoPath, repo_path},
     status::{StatusCode, TrackedStatus},
 };
 use git2::RepositoryInitOptions;
@@ -44,6 +44,7 @@ use unindent::Unindent as _;
 use util::{
     TryFutureExt as _, assert_set_eq, maybe, path,
     paths::PathMatcher,
+    rel_path::rel_path,
     test::{TempTree, marked_text_offsets},
     uri,
 };
@@ -122,8 +123,10 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
         let tree = project.worktrees(cx).next().unwrap().read(cx);
         assert_eq!(tree.file_count(), 5);
         assert_eq!(
-            tree.inode_for_path("fennel/grape"),
-            tree.inode_for_path("finnochio/grape")
+            tree.entry_for_path(rel_path("fennel/grape")).unwrap().inode,
+            tree.entry_for_path(rel_path("finnochio/grape"))
+                .unwrap()
+                .inode
         );
     });
 }
@@ -186,12 +189,12 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) {
     cx.update(|cx| {
         let tree = worktree.read(cx);
         let settings_for = |path: &str| {
-            let file_entry = tree.entry_for_path(path).unwrap().clone();
+            let file_entry = tree.entry_for_path(rel_path(path)).unwrap().clone();
             let file = File::for_entry(file_entry, worktree.clone());
             let file_language = project
                 .read(cx)
                 .languages()
-                .language_for_file_path(file.path.as_ref());
+                .language_for_file_path(file.path.as_std_path());
             let file_language = cx
                 .background_executor()
                 .block(file_language)
@@ -343,7 +346,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
 
     let topmost_local_task_source_kind = TaskSourceKind::Worktree {
         id: worktree_id,
-        directory_in_worktree: PathBuf::from(".zed"),
+        directory_in_worktree: rel_path(".zed").into(),
         id_base: "local worktree tasks from directory \".zed\"".into(),
     };
 
@@ -352,12 +355,12 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
             let tree = worktree.read(cx);
 
             let file_a = File::for_entry(
-                tree.entry_for_path("a/a.rs").unwrap().clone(),
+                tree.entry_for_path(rel_path("a/a.rs")).unwrap().clone(),
                 worktree.clone(),
             ) as _;
             let settings_a = language_settings(None, Some(&file_a), cx);
             let file_b = File::for_entry(
-                tree.entry_for_path("b/b.rs").unwrap().clone(),
+                tree.entry_for_path(rel_path("b/b.rs")).unwrap().clone(),
                 worktree.clone(),
             ) as _;
             let settings_b = language_settings(None, Some(&file_b), cx);
@@ -385,12 +388,8 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
             (
                 TaskSourceKind::Worktree {
                     id: worktree_id,
-                    directory_in_worktree: PathBuf::from(path!("b/.zed")),
-                    id_base: if cfg!(windows) {
-                        "local worktree tasks from directory \"b\\\\.zed\"".into()
-                    } else {
-                        "local worktree tasks from directory \"b/.zed\"".into()
-                    },
+                    directory_in_worktree: rel_path("b/.zed").into(),
+                    id_base: "local worktree tasks from directory \"b/.zed\"".into()
                 },
                 "cargo check".to_string(),
                 vec!["check".to_string()],
@@ -470,12 +469,8 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
             (
                 TaskSourceKind::Worktree {
                     id: worktree_id,
-                    directory_in_worktree: PathBuf::from(path!("b/.zed")),
-                    id_base: if cfg!(windows) {
-                        "local worktree tasks from directory \"b\\\\.zed\"".into()
-                    } else {
-                        "local worktree tasks from directory \"b/.zed\"".into()
-                    },
+                    directory_in_worktree: rel_path("b/.zed").into(),
+                    id_base: "local worktree tasks from directory \"b/.zed\"".into()
                 },
                 "cargo check".to_string(),
                 vec!["check".to_string()],
@@ -585,12 +580,8 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) {
         vec![(
             TaskSourceKind::Worktree {
                 id: worktree_id,
-                directory_in_worktree: PathBuf::from(path!(".zed")),
-                id_base: if cfg!(windows) {
-                    "local worktree tasks from directory \".zed\"".into()
-                } else {
-                    "local worktree tasks from directory \".zed\"".into()
-                },
+                directory_in_worktree: rel_path(".zed").into(),
+                id_base: "local worktree tasks from directory \".zed\"".into(),
             },
             "echo /dir".to_string(),
         )]
@@ -615,9 +606,9 @@ async fn test_running_multiple_instances_of_a_single_server_in_one_worktree(
                 depth,
                 delegate,
             }: ManifestQuery,
-        ) -> Option<Arc<Path>> {
+        ) -> Option<Arc<RelPath>> {
             for path in path.ancestors().take(depth) {
-                let p = path.join("pyproject.toml");
+                let p = path.join(RelPath::new("pyproject.toml").unwrap());
                 if delegate.exists(&p, Some(false)) {
                     return Some(path.into());
                 }
@@ -738,7 +729,7 @@ async fn test_running_multiple_instances_of_a_single_server_in_one_worktree(
             this.available_toolchains(
                 ProjectPath {
                     worktree_id,
-                    path: Arc::from("project-b/source_file.py".as_ref()),
+                    path: rel_path("project-b/source_file.py").into(),
                 },
                 LanguageName::new("Python"),
                 cx,
@@ -746,7 +737,7 @@ async fn test_running_multiple_instances_of_a_single_server_in_one_worktree(
         })
         .await
         .expect("A toolchain to be discovered");
-    assert_eq!(root_path.as_ref(), Path::new("project-b"));
+    assert_eq!(root_path.as_ref(), RelPath::new("project-b").unwrap());
     assert_eq!(available_toolchains_for_b.toolchains().len(), 1);
     let currently_active_toolchain = project
         .update(cx, |this, cx| {
@@ -754,7 +745,7 @@ async fn test_running_multiple_instances_of_a_single_server_in_one_worktree(
             this.active_toolchain(
                 ProjectPath {
                     worktree_id,
-                    path: Arc::from("project-b/source_file.py".as_ref()),
+                    path: rel_path("project-b/source_file.py").into(),
                 },
                 LanguageName::new("Python"),
                 cx,
@@ -1294,16 +1285,16 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
                 .read(cx)
                 .snapshot()
                 .entries(true, 0)
-                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+                .map(|entry| (entry.path.as_str(), entry.is_ignored))
                 .collect::<Vec<_>>(),
             &[
-                (Path::new(""), false),
-                (Path::new(".gitignore"), false),
-                (Path::new("Cargo.lock"), false),
-                (Path::new("src"), false),
-                (Path::new("src/a.rs"), false),
-                (Path::new("src/b.rs"), false),
-                (Path::new("target"), true),
+                ("", false),
+                (".gitignore", false),
+                ("Cargo.lock", false),
+                ("src", false),
+                ("src/a.rs", false),
+                ("src/b.rs", false),
+                ("target", true),
             ]
         );
     });
@@ -1412,21 +1403,21 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
                 .read(cx)
                 .snapshot()
                 .entries(true, 0)
-                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+                .map(|entry| (entry.path.as_str(), entry.is_ignored))
                 .collect::<Vec<_>>(),
             &[
-                (Path::new(""), false),
-                (Path::new(".gitignore"), false),
-                (Path::new("Cargo.lock"), false),
-                (Path::new("src"), false),
-                (Path::new("src/a.rs"), false),
-                (Path::new("src/b.rs"), false),
-                (Path::new("target"), true),
-                (Path::new("target/x"), true),
-                (Path::new("target/y"), true),
-                (Path::new("target/y/out"), true),
-                (Path::new("target/y/out/y.rs"), true),
-                (Path::new("target/z"), true),
+                ("", false),
+                (".gitignore", false),
+                ("Cargo.lock", false),
+                ("src", false),
+                ("src/a.rs", false),
+                ("src/b.rs", false),
+                ("target", true),
+                ("target/x", true),
+                ("target/y", true),
+                ("target/y/out", true),
+                ("target/y/out/y.rs", true),
+                ("target/z", true),
             ]
         );
     });
@@ -1694,7 +1685,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
 
     let main_ignored_buffer = project
         .update(cx, |project, cx| {
-            project.open_buffer((main_worktree_id, "b.rs"), cx)
+            project.open_buffer((main_worktree_id, rel_path("b.rs")), cx)
         })
         .await
         .unwrap();
@@ -1715,7 +1706,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
     });
     let other_buffer = project
         .update(cx, |project, cx| {
-            project.open_buffer((other_worktree_id, ""), cx)
+            project.open_buffer((other_worktree_id, rel_path("")), cx)
         })
         .await
         .unwrap();
@@ -1742,7 +1733,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
             vec![(
                 ProjectPath {
                     worktree_id: main_worktree_id,
-                    path: Arc::from(Path::new("b.rs")),
+                    path: rel_path("b.rs").into(),
                 },
                 server_id,
                 DiagnosticSummary {
@@ -1832,7 +1823,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
         events.next().await.unwrap(),
         Event::DiagnosticsUpdated {
             language_server_id: LanguageServerId(0),
-            paths: vec![(worktree_id, Path::new("a.rs")).into()],
+            paths: vec![(worktree_id, rel_path("a.rs")).into()],
         }
     );
 
@@ -1880,7 +1871,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
         events.next().await.unwrap(),
         Event::DiagnosticsUpdated {
             language_server_id: LanguageServerId(0),
-            paths: vec![(worktree_id, Path::new("a.rs")).into()],
+            paths: vec![(worktree_id, rel_path("a.rs")).into()],
         }
     );
 
@@ -3808,6 +3799,114 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_rename_file_to_new_directory(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+    let fs = FakeFs::new(cx.background_executor.clone());
+    let expected_contents = "content";
+    fs.as_fake()
+        .insert_tree(
+            "/root",
+            json!({
+                "test.txt": expected_contents
+            }),
+        )
+        .await;
+
+    let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
+
+    let (worktree, entry_id) = project.read_with(cx, |project, cx| {
+        let worktree = project.worktrees(cx).next().unwrap();
+        let entry_id = worktree
+            .read(cx)
+            .entry_for_path(rel_path("test.txt"))
+            .unwrap()
+            .id;
+        (worktree, entry_id)
+    });
+    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
+    let _result = project
+        .update(cx, |project, cx| {
+            project.rename_entry(
+                entry_id,
+                (worktree_id, rel_path("dir1/dir2/dir3/test.txt")).into(),
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+    worktree.read_with(cx, |worktree, _| {
+        assert!(
+            worktree.entry_for_path(rel_path("test.txt")).is_none(),
+            "Old file should have been removed"
+        );
+        assert!(
+            worktree
+                .entry_for_path(rel_path("dir1/dir2/dir3/test.txt"))
+                .is_some(),
+            "Whole directory hierarchy and the new file should have been created"
+        );
+    });
+    assert_eq!(
+        worktree
+            .update(cx, |worktree, cx| {
+                worktree.load_file(rel_path("dir1/dir2/dir3/test.txt"), cx)
+            })
+            .await
+            .unwrap()
+            .text,
+        expected_contents,
+        "Moved file's contents should be preserved"
+    );
+
+    let entry_id = worktree.read_with(cx, |worktree, _| {
+        worktree
+            .entry_for_path(rel_path("dir1/dir2/dir3/test.txt"))
+            .unwrap()
+            .id
+    });
+
+    let _result = project
+        .update(cx, |project, cx| {
+            project.rename_entry(
+                entry_id,
+                (worktree_id, rel_path("dir1/dir2/test.txt")).into(),
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+    worktree.read_with(cx, |worktree, _| {
+        assert!(
+            worktree.entry_for_path(rel_path("test.txt")).is_none(),
+            "First file should not reappear"
+        );
+        assert!(
+            worktree
+                .entry_for_path(rel_path("dir1/dir2/dir3/test.txt"))
+                .is_none(),
+            "Old file should have been removed"
+        );
+        assert!(
+            worktree
+                .entry_for_path(rel_path("dir1/dir2/test.txt"))
+                .is_some(),
+            "No error should have occurred after moving into existing directory"
+        );
+    });
+    assert_eq!(
+        worktree
+            .update(cx, |worktree, cx| {
+                worktree.load_file(rel_path("dir1/dir2/test.txt"), cx)
+            })
+            .await
+            .unwrap()
+            .text,
+        expected_contents,
+        "Moved file's contents should be preserved"
+    );
+}
+
 #[gpui::test(iterations = 10)]
 async fn test_save_file(cx: &mut gpui::TestAppContext) {
     init_test(cx);
@@ -3895,7 +3994,7 @@ async fn test_save_file_spawns_language_server(cx: &mut gpui::TestAppContext) {
                 buffer.clone(),
                 ProjectPath {
                     worktree_id,
-                    path: Arc::from("file.rs".as_ref()),
+                    path: rel_path("file.rs").into(),
                 },
                 cx,
             )
@@ -4104,7 +4203,7 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) {
             let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
             let path = ProjectPath {
                 worktree_id,
-                path: Arc::from(Path::new("file1.rs")),
+                path: rel_path("file1.rs").into(),
             };
             project.save_buffer_as(buffer.clone(), path, cx)
         })
@@ -4163,7 +4262,7 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
         project.update(cx, |project, cx| {
             let tree = project.worktrees(cx).next().unwrap();
             tree.read(cx)
-                .entry_for_path(path)
+                .entry_for_path(rel_path(path))
                 .unwrap_or_else(|| panic!("no entry for path {}", path))
                 .id
         })
@@ -4191,8 +4290,16 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
         });
     });
 
-    let remote =
-        cx.update(|cx| Worktree::remote(0, 1, metadata, project.read(cx).client().into(), cx));
+    let remote = cx.update(|cx| {
+        Worktree::remote(
+            0,
+            1,
+            metadata,
+            project.read(cx).client().into(),
+            project.read(cx).path_style(cx),
+            cx,
+        )
+    });
 
     cx.executor().run_until_parked();
 
@@ -4213,18 +4320,15 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
 
     cx.update(|app| {
         assert_eq!(
-            tree.read(app)
-                .paths()
-                .map(|p| p.to_str().unwrap())
-                .collect::<Vec<_>>(),
+            tree.read(app).paths().collect::<Vec<_>>(),
             vec![
-                "a",
-                path!("a/file1"),
-                path!("a/file2.new"),
-                "b",
-                "d",
-                path!("d/file3"),
-                path!("d/file4"),
+                rel_path("a"),
+                rel_path("a/file1"),
+                rel_path("a/file2.new"),
+                rel_path("b"),
+                rel_path("d"),
+                rel_path("d/file3"),
+                rel_path("d/file4"),
             ]
         );
     });
@@ -4236,19 +4340,19 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
     cx.update(|cx| {
         assert_eq!(
             buffer2.read(cx).file().unwrap().path().as_ref(),
-            Path::new("a/file2.new")
+            rel_path("a/file2.new")
         );
         assert_eq!(
             buffer3.read(cx).file().unwrap().path().as_ref(),
-            Path::new("d/file3")
+            rel_path("d/file3")
         );
         assert_eq!(
             buffer4.read(cx).file().unwrap().path().as_ref(),
-            Path::new("d/file4")
+            rel_path("d/file4")
         );
         assert_eq!(
             buffer5.read(cx).file().unwrap().path().as_ref(),
-            Path::new("b/c/file5")
+            rel_path("b/c/file5")
         );
 
         assert_matches!(
@@ -4281,18 +4385,15 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
     cx.executor().run_until_parked();
     remote.update(cx, |remote, _| {
         assert_eq!(
-            remote
-                .paths()
-                .map(|p| p.to_str().unwrap())
-                .collect::<Vec<_>>(),
+            remote.paths().collect::<Vec<_>>(),
             vec![
-                "a",
-                path!("a/file1"),
-                path!("a/file2.new"),
-                "b",
-                "d",
-                path!("d/file3"),
-                path!("d/file4"),
+                rel_path("a"),
+                rel_path("a/file1"),
+                rel_path("a/file2.new"),
+                rel_path("b"),
+                rel_path("d"),
+                rel_path("d/file3"),
+                rel_path("d/file4"),
             ]
         );
     });
@@ -4321,7 +4422,7 @@ async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) {
         project.update(cx, |project, cx| {
             let tree = project.worktrees(cx).next().unwrap();
             tree.read(cx)
-                .entry_for_path(path)
+                .entry_for_path(rel_path(path))
                 .unwrap_or_else(|| panic!("no entry for path {}", path))
                 .id
         })
@@ -4330,14 +4431,16 @@ async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) {
     let dir_id = id_for_path("a", cx);
     let file_id = id_for_path("a/file1", cx);
     let buffer = project
-        .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx))
+        .update(cx, |p, cx| {
+            p.open_buffer((tree_id, rel_path("a/file1")), cx)
+        })
         .await
         .unwrap();
     buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
 
     project
         .update(cx, |project, cx| {
-            project.rename_entry(dir_id, Path::new("b"), cx)
+            project.rename_entry(dir_id, (tree_id, rel_path("b")).into(), cx)
         })
         .unwrap()
         .await
@@ -5051,8 +5154,15 @@ async fn test_lsp_rename_notifications(cx: &mut gpui::TestAppContext) {
     let fake_server = fake_servers.next().await.unwrap();
     let response = project.update(cx, |project, cx| {
         let worktree = project.worktrees(cx).next().unwrap();
-        let entry = worktree.read(cx).entry_for_path("one.rs").unwrap();
-        project.rename_entry(entry.id, "three.rs".as_ref(), cx)
+        let entry = worktree
+            .read(cx)
+            .entry_for_path(rel_path("one.rs"))
+            .unwrap();
+        project.rename_entry(
+            entry.id,
+            (worktree.read(cx).id(), rel_path("three.rs")).into(),
+            cx,
+        )
     });
     let expected_edit = lsp::WorkspaceEdit {
         changes: None,
@@ -5356,7 +5466,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
                 false,
                 true,
                 false,
-                PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
+                PathMatcher::new(&["*.odd".to_owned()], PathStyle::local()).unwrap(),
                 Default::default(),
                 false,
                 None
@@ -5378,7 +5488,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
                 false,
                 true,
                 false,
-                PathMatcher::new(&["*.rs".to_owned()]).unwrap(),
+                PathMatcher::new(&["*.rs".to_owned()], PathStyle::local()).unwrap(),
                 Default::default(),
                 false,
                 None
@@ -5403,7 +5513,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
                 false,
                 true,
                 false,
-                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
+                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], PathStyle::local())
+                    .unwrap(),
                 Default::default(),
                 false,
                 None,
@@ -5428,8 +5539,11 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
                 false,
                 true,
                 false,
-                PathMatcher::new(&["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()])
-                    .unwrap(),
+                PathMatcher::new(
+                    &["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()],
+                    PathStyle::local()
+                )
+                .unwrap(),
                 Default::default(),
                 false,
                 None,
@@ -5477,7 +5591,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
                 true,
                 false,
                 Default::default(),
-                PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
+                PathMatcher::new(&["*.odd".to_owned()], PathStyle::local()).unwrap(),
                 false,
                 None,
             )
@@ -5504,7 +5618,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
                 true,
                 false,
                 Default::default(),
-                PathMatcher::new(&["*.rs".to_owned()]).unwrap(),
+                PathMatcher::new(&["*.rs".to_owned()], PathStyle::local()).unwrap(),
                 false,
                 None,
             )
@@ -5529,7 +5643,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
                 true,
                 false,
                 Default::default(),
-                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
+                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], PathStyle::local())
+                    .unwrap(),
                 false,
                 None,
             )
@@ -5554,8 +5669,11 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
                 true,
                 false,
                 Default::default(),
-                PathMatcher::new(&["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()])
-                    .unwrap(),
+                PathMatcher::new(
+                    &["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()],
+                    PathStyle::local(),
+                )
+                .unwrap(),
                 false,
                 None,
             )
@@ -5588,6 +5706,7 @@ async fn test_search_with_buffer_exclusions(cx: &mut gpui::TestAppContext) {
     .await;
 
     let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
+    let path_style = PathStyle::local();
     let _buffer = project.update(cx, |project, cx| {
         project.create_local_buffer("file", None, false, cx)
     });
@@ -5601,7 +5720,7 @@ async fn test_search_with_buffer_exclusions(cx: &mut gpui::TestAppContext) {
                 true,
                 false,
                 Default::default(),
-                PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
+                PathMatcher::new(&["*.odd".to_owned()], path_style).unwrap(),
                 false,
                 None,
             )
@@ -5628,7 +5747,7 @@ async fn test_search_with_buffer_exclusions(cx: &mut gpui::TestAppContext) {
                 true,
                 false,
                 Default::default(),
-                PathMatcher::new(&["*.rs".to_owned()]).unwrap(),
+                PathMatcher::new(&["*.rs".to_owned()], path_style).unwrap(),
                 false,
                 None,
             )
@@ -5653,7 +5772,7 @@ async fn test_search_with_buffer_exclusions(cx: &mut gpui::TestAppContext) {
                 true,
                 false,
                 Default::default(),
-                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
+                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], path_style).unwrap(),
                 false,
                 None,
             )
@@ -5678,8 +5797,11 @@ async fn test_search_with_buffer_exclusions(cx: &mut gpui::TestAppContext) {
                 true,
                 false,
                 Default::default(),
-                PathMatcher::new(&["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()])
-                    .unwrap(),
+                PathMatcher::new(
+                    &["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()],
+                    PathStyle::local(),
+                )
+                .unwrap(),
                 false,
                 None,
             )
@@ -5711,7 +5833,6 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
     )
     .await;
     let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
-
     assert!(
         search(
             &project,
@@ -5720,8 +5841,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
                 false,
                 true,
                 false,
-                PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
-                PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
+                PathMatcher::new(&["*.odd".to_owned()], PathStyle::local()).unwrap(),
+                PathMatcher::new(&["*.odd".to_owned()], PathStyle::local()).unwrap(),
                 false,
                 None,
             )
@@ -5742,8 +5863,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
                 false,
                 true,
                 false,
-                PathMatcher::new(&["*.ts".to_owned()]).unwrap(),
-                PathMatcher::new(&["*.ts".to_owned()]).unwrap(),
+                PathMatcher::new(&["*.ts".to_owned()], PathStyle::local()).unwrap(),
+                PathMatcher::new(&["*.ts".to_owned()], PathStyle::local()).unwrap(),
                 false,
                 None,
             )
@@ -5764,8 +5885,10 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
                 false,
                 true,
                 false,
-                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
-                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
+                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], PathStyle::local())
+                    .unwrap(),
+                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], PathStyle::local())
+                    .unwrap(),
                 false,
                 None,
             )
@@ -5786,8 +5909,10 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
                 false,
                 true,
                 false,
-                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
-                PathMatcher::new(&["*.rs".to_owned(), "*.odd".to_owned()]).unwrap(),
+                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], PathStyle::local())
+                    .unwrap(),
+                PathMatcher::new(&["*.rs".to_owned(), "*.odd".to_owned()], PathStyle::local())
+                    .unwrap(),
                 false,
                 None,
             )
@@ -5826,6 +5951,7 @@ async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppCo
     )
     .await;
 
+    let path_style = PathStyle::local();
     let project = Project::test(
         fs.clone(),
         [path!("/worktree-a").as_ref(), path!("/worktree-b").as_ref()],
@@ -5841,7 +5967,7 @@ async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppCo
                 false,
                 true,
                 false,
-                PathMatcher::new(&["worktree-a/*.rs".to_owned()]).unwrap(),
+                PathMatcher::new(&["worktree-a/*.rs".to_owned()], path_style).unwrap(),
                 Default::default(),
                 true,
                 None,
@@ -5862,7 +5988,7 @@ async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppCo
                 false,
                 true,
                 false,
-                PathMatcher::new(&["worktree-b/*.rs".to_owned()]).unwrap(),
+                PathMatcher::new(&["worktree-b/*.rs".to_owned()], path_style).unwrap(),
                 Default::default(),
                 true,
                 None,
@@ -5884,7 +6010,7 @@ async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppCo
                 false,
                 true,
                 false,
-                PathMatcher::new(&["*.ts".to_owned()]).unwrap(),
+                PathMatcher::new(&["*.ts".to_owned()], path_style).unwrap(),
                 Default::default(),
                 false,
                 None,
@@ -5955,6 +6081,7 @@ async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) {
     );
 
     let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
+    let path_style = PathStyle::local();
     assert_eq!(
         search(
             &project,
@@ -5996,8 +6123,9 @@ async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) {
         "Unrestricted search with ignored directories should find every file with the query"
     );
 
-    let files_to_include = PathMatcher::new(&["node_modules/prettier/**".to_owned()]).unwrap();
-    let files_to_exclude = PathMatcher::new(&["*.ts".to_owned()]).unwrap();
+    let files_to_include =
+        PathMatcher::new(&["node_modules/prettier/**".to_owned()], path_style).unwrap();
+    let files_to_exclude = PathMatcher::new(&["*.ts".to_owned()], path_style).unwrap();
     let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
     assert_eq!(
         search(
@@ -6040,7 +6168,6 @@ async fn test_search_with_unicode(cx: &mut gpui::TestAppContext) {
     )
     .await;
     let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
-
     let unicode_case_sensitive_query = SearchQuery::text(
         "привет",
         false,
@@ -6130,31 +6257,13 @@ async fn test_create_entry(cx: &mut gpui::TestAppContext) {
     project
         .update(cx, |project, cx| {
             let id = project.worktrees(cx).next().unwrap().read(cx).id();
-            project.create_entry((id, "b.."), true, cx)
+            project.create_entry((id, rel_path("b..")), true, cx)
         })
         .await
         .unwrap()
         .into_included()
         .unwrap();
 
-    // Can't create paths outside the project
-    let result = project
-        .update(cx, |project, cx| {
-            let id = project.worktrees(cx).next().unwrap().read(cx).id();
-            project.create_entry((id, "../../boop"), true, cx)
-        })
-        .await;
-    assert!(result.is_err());
-
-    // Can't create paths with '..'
-    let result = project
-        .update(cx, |project, cx| {
-            let id = project.worktrees(cx).next().unwrap().read(cx).id();
-            project.create_entry((id, "four/../beep"), true, cx)
-        })
-        .await;
-    assert!(result.is_err());
-
     assert_eq!(
         fs.paths(true),
         vec![
@@ -6168,15 +6277,6 @@ async fn test_create_entry(cx: &mut gpui::TestAppContext) {
             PathBuf::from(path!("/one/two/three/four")),
         ]
     );
-
-    // And we cannot open buffers with '..'
-    let result = project
-        .update(cx, |project, cx| {
-            let id = project.worktrees(cx).next().unwrap().read(cx).id();
-            project.open_buffer((id, "../c.rs"), cx)
-        })
-        .await;
-    assert!(result.is_err())
 }
 
 #[gpui::test]
@@ -6875,10 +6975,7 @@ async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) {
     )
     .await;
 
-    fs.set_index_for_repo(
-        Path::new("/dir/.git"),
-        &[("src/main.rs".into(), staged_contents)],
-    );
+    fs.set_index_for_repo(Path::new("/dir/.git"), &[("src/main.rs", staged_contents)]);
 
     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
 
@@ -6921,10 +7018,7 @@ async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) {
     "#
     .unindent();
 
-    fs.set_index_for_repo(
-        Path::new("/dir/.git"),
-        &[("src/main.rs".into(), staged_contents)],
-    );
+    fs.set_index_for_repo(Path::new("/dir/.git"), &[("src/main.rs", staged_contents)]);
 
     cx.run_until_parked();
     unstaged_diff.update(cx, |unstaged_diff, cx| {
@@ -6982,16 +7076,16 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
     fs.set_head_for_repo(
         Path::new("/dir/.git"),
         &[
-            ("src/modification.rs".into(), committed_contents),
-            ("src/deletion.rs".into(), "// the-deleted-contents\n".into()),
+            ("src/modification.rs", committed_contents),
+            ("src/deletion.rs", "// the-deleted-contents\n".into()),
         ],
         "deadbeef",
     );
     fs.set_index_for_repo(
         Path::new("/dir/.git"),
         &[
-            ("src/modification.rs".into(), staged_contents),
-            ("src/deletion.rs".into(), "// the-deleted-contents\n".into()),
+            ("src/modification.rs", staged_contents),
+            ("src/deletion.rs", "// the-deleted-contents\n".into()),
         ],
     );
 
@@ -7049,8 +7143,8 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
     fs.set_head_for_repo(
         Path::new("/dir/.git"),
         &[
-            ("src/modification.rs".into(), committed_contents.clone()),
-            ("src/deletion.rs".into(), "// the-deleted-contents\n".into()),
+            ("src/modification.rs", committed_contents.clone()),
+            ("src/deletion.rs", "// the-deleted-contents\n".into()),
         ],
         "deadbeef",
     );
@@ -7104,7 +7198,7 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
     // Stage the deletion of this file
     fs.set_index_for_repo(
         Path::new("/dir/.git"),
-        &[("src/modification.rs".into(), committed_contents.clone())],
+        &[("src/modification.rs", committed_contents.clone())],
     );
     cx.run_until_parked();
     diff_2.update(cx, |diff, cx| {
@@ -7157,8 +7251,8 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
     .await;
 
     fs.set_head_and_index_for_repo(
-        "/dir/.git".as_ref(),
-        &[("file.txt".into(), committed_contents.clone())],
+        path!("/dir/.git").as_ref(),
+        &[("file.txt", committed_contents.clone())],
     );
 
     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
@@ -7498,12 +7592,12 @@ async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext)
 
     fs.set_head_for_repo(
         "/dir/.git".as_ref(),
-        &[("file.txt".into(), committed_contents.clone())],
+        &[("file.txt", committed_contents.clone())],
         "deadbeef",
     );
     fs.set_index_for_repo(
         "/dir/.git".as_ref(),
-        &[("file.txt".into(), committed_contents.clone())],
+        &[("file.txt", committed_contents.clone())],
     );
 
     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
@@ -7695,12 +7789,12 @@ async fn test_staging_random_hunks(
     .await;
     fs.set_head_for_repo(
         path!("/dir/.git").as_ref(),
-        &[("file.txt".into(), committed_text.clone())],
+        &[("file.txt", committed_text.clone())],
         "deadbeef",
     );
     fs.set_index_for_repo(
         path!("/dir/.git").as_ref(),
-        &[("file.txt".into(), index_text.clone())],
+        &[("file.txt", index_text.clone())],
     );
     let repo = fs.open_repo(path!("/dir/.git").as_ref()).unwrap();
 
@@ -7760,7 +7854,9 @@ async fn test_staging_random_hunks(
 
     log::info!(
         "index text:\n{}",
-        repo.load_index_text("file.txt".into()).await.unwrap()
+        repo.load_index_text(rel_path("file.txt").into())
+            .await
+            .unwrap()
     );
 
     uncommitted_diff.update(cx, |diff, cx| {
@@ -7807,12 +7903,12 @@ async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
 
     fs.set_head_for_repo(
         Path::new("/dir/.git"),
-        &[("src/main.rs".into(), committed_contents.clone())],
+        &[("src/main.rs", committed_contents.clone())],
         "deadbeef",
     );
     fs.set_index_for_repo(
         Path::new("/dir/.git"),
-        &[("src/main.rs".into(), committed_contents.clone())],
+        &[("src/main.rs", committed_contents.clone())],
     );
 
     let project = Project::test(fs.clone(), ["/dir/src/main.rs".as_ref()], cx).await;
@@ -7903,7 +7999,7 @@ async fn test_repository_and_path_for_project_path(
                 (
                     path,
                     result.map(|(repo, repo_path)| {
-                        (Path::new(repo).into(), RepoPath::from(repo_path))
+                        (Path::new(repo).into(), RepoPath::new(repo_path).unwrap())
                     }),
                 )
             })
@@ -7911,7 +8007,7 @@ async fn test_repository_and_path_for_project_path(
         let actual = pairs
             .iter()
             .map(|(path, _)| {
-                let project_path = (tree_id, Path::new(path)).into();
+                let project_path = (tree_id, rel_path(path)).into();
                 let result = maybe!({
                     let (repo, repo_path) =
                         git_store.repository_and_path_for_project_path(&project_path, cx)?;
@@ -7932,7 +8028,7 @@ async fn test_repository_and_path_for_project_path(
         let git_store = project.git_store().read(cx);
         assert_eq!(
             git_store.repository_and_path_for_project_path(
-                &(tree_id, Path::new("dir1/src/b.txt")).into(),
+                &(tree_id, rel_path("dir1/src/b.txt")).into(),
                 cx
             ),
             None

crates/project/src/search.rs 🔗

@@ -13,7 +13,7 @@ use std::{
     sync::{Arc, LazyLock},
 };
 use text::Anchor;
-use util::paths::PathMatcher;
+use util::paths::{PathMatcher, PathStyle};
 
 #[derive(Debug)]
 pub enum SearchResult {
@@ -238,7 +238,7 @@ impl SearchQuery {
         is_case_sensitive.map(|c| (c, new_query))
     }
 
-    pub fn from_proto(message: proto::SearchQuery) -> Result<Self> {
+    pub fn from_proto(message: proto::SearchQuery, path_style: PathStyle) -> Result<Self> {
         let files_to_include = if message.files_to_include.is_empty() {
             message
                 .files_to_include_legacy
@@ -270,8 +270,8 @@ impl SearchQuery {
                 message.case_sensitive,
                 message.include_ignored,
                 false,
-                PathMatcher::new(files_to_include)?,
-                PathMatcher::new(files_to_exclude)?,
+                PathMatcher::new(files_to_include, path_style)?,
+                PathMatcher::new(files_to_exclude, path_style)?,
                 message.match_full_paths,
                 None, // search opened only don't need search remote
             )
@@ -281,8 +281,8 @@ impl SearchQuery {
                 message.whole_word,
                 message.case_sensitive,
                 message.include_ignored,
-                PathMatcher::new(files_to_include)?,
-                PathMatcher::new(files_to_exclude)?,
+                PathMatcher::new(files_to_include, path_style)?,
+                PathMatcher::new(files_to_exclude, path_style)?,
                 false,
                 None, // search opened only don't need search remote
             )
@@ -610,9 +610,10 @@ mod tests {
             "dir/[a-z].txt",
             "../dir/filé",
         ] {
-            let path_matcher = PathMatcher::new(&[valid_path.to_owned()]).unwrap_or_else(|e| {
-                panic!("Valid path {valid_path} should be accepted, but got: {e}")
-            });
+            let path_matcher = PathMatcher::new(&[valid_path.to_owned()], PathStyle::local())
+                .unwrap_or_else(|e| {
+                    panic!("Valid path {valid_path} should be accepted, but got: {e}")
+                });
             assert!(
                 path_matcher.is_match(valid_path),
                 "Path matcher for valid path {valid_path} should match itself"
@@ -623,7 +624,7 @@ mod tests {
     #[test]
     fn path_matcher_creation_for_globs() {
         for invalid_glob in ["dir/[].txt", "dir/[a-z.txt", "dir/{file"] {
-            match PathMatcher::new(&[invalid_glob.to_owned()]) {
+            match PathMatcher::new(&[invalid_glob.to_owned()], PathStyle::local()) {
                 Ok(_) => panic!("Invalid glob {invalid_glob} should not be accepted"),
                 Err(_expected) => {}
             }
@@ -636,7 +637,7 @@ mod tests {
             "dir/[a-z].txt",
             "{dir,file}",
         ] {
-            match PathMatcher::new(&[valid_glob.to_owned()]) {
+            match PathMatcher::new(&[valid_glob.to_owned()], PathStyle::local()) {
                 Ok(_expected) => {}
                 Err(e) => panic!("Valid glob should be accepted, but got: {e}"),
             }

crates/project/src/task_inventory.rs 🔗

@@ -4,7 +4,7 @@ use std::{
     borrow::Cow,
     cmp::{self, Reverse},
     collections::hash_map,
-    path::{Path, PathBuf},
+    path::PathBuf,
     sync::Arc,
 };
 
@@ -25,7 +25,7 @@ use task::{
     VariableName,
 };
 use text::{BufferId, Point, ToPoint};
-use util::{NumericPrefixWithSuffix, ResultExt as _, paths::PathExt as _, post_inc};
+use util::{NumericPrefixWithSuffix, ResultExt as _, post_inc, rel_path::RelPath};
 use worktree::WorktreeId;
 
 use crate::{task_store::TaskSettingsLocation, worktree_store::WorktreeStore};
@@ -76,7 +76,7 @@ impl InventoryContents for DebugScenario {
 #[derive(Debug)]
 struct InventoryFor<T> {
     global: HashMap<PathBuf, Vec<T>>,
-    worktree: HashMap<WorktreeId, HashMap<Arc<Path>, Vec<T>>>,
+    worktree: HashMap<WorktreeId, HashMap<Arc<RelPath>, Vec<T>>>,
 }
 
 impl<T: InventoryContents> InventoryFor<T> {
@@ -95,7 +95,7 @@ impl<T: InventoryContents> InventoryFor<T> {
                 (
                     TaskSourceKind::Worktree {
                         id: worktree,
-                        directory_in_worktree: directory.to_path_buf(),
+                        directory_in_worktree: directory.clone(),
                         id_base: Cow::Owned(format!(
                             "local worktree {} from directory {directory:?}",
                             T::LABEL
@@ -138,7 +138,7 @@ pub enum TaskSourceKind {
     /// Tasks from the worktree's .zed/task.json
     Worktree {
         id: WorktreeId,
-        directory_in_worktree: PathBuf,
+        directory_in_worktree: Arc<RelPath>,
         id_base: Cow<'static, str>,
     },
     /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path
@@ -228,7 +228,7 @@ impl TaskSourceKind {
                 id_base,
                 directory_in_worktree,
             } => {
-                format!("{id_base}_{id}_{}", directory_in_worktree.display())
+                format!("{id_base}_{id}_{}", directory_in_worktree.as_str())
             }
             Self::Language { name } => format!("language_{name}"),
             Self::Lsp {
@@ -653,7 +653,7 @@ impl Inventory {
                     path: match location {
                         TaskSettingsLocation::Global(path) => path.to_owned(),
                         TaskSettingsLocation::Worktree(settings_location) => {
-                            settings_location.path.join(task_file_name())
+                            settings_location.path.as_std_path().join(task_file_name())
                         }
                     },
                     message: format!("Failed to parse tasks file content as a JSON array: {e}"),
@@ -701,7 +701,8 @@ impl Inventory {
                         ..
                     } = kind
                     {
-                        *id != location.worktree_id || directory_in_worktree != location.path
+                        *id != location.worktree_id
+                            || directory_in_worktree.as_ref() != location.path
                     } else {
                         true
                     }
@@ -729,9 +730,10 @@ impl Inventory {
                 return Err(InvalidSettingsError::Debug {
                     path: match location {
                         TaskSettingsLocation::Global(path) => path.to_owned(),
-                        TaskSettingsLocation::Worktree(settings_location) => {
-                            settings_location.path.join(debug_task_file_name())
-                        }
+                        TaskSettingsLocation::Worktree(settings_location) => settings_location
+                            .path
+                            .as_std_path()
+                            .join(debug_task_file_name()),
                     },
                     message: format!("Failed to parse tasks file content as a JSON array: {e}"),
                 });
@@ -969,6 +971,7 @@ impl BasicContextProvider {
         Self { worktree_store }
     }
 }
+
 impl ContextProvider for BasicContextProvider {
     fn build_context(
         &self,
@@ -991,10 +994,7 @@ impl ContextProvider for BasicContextProvider {
             symbol.text[range].to_string()
         });
 
-        let current_file = buffer
-            .file()
-            .and_then(|file| file.as_local())
-            .map(|file| file.abs_path(cx).to_sanitized_string());
+        let current_file = buffer.file().and_then(|file| file.as_local());
         let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
         let row = row + 1;
         let column = column + 1;
@@ -1013,44 +1013,43 @@ impl ContextProvider for BasicContextProvider {
         if !selected_text.trim().is_empty() {
             task_variables.insert(VariableName::SelectedText, selected_text);
         }
-        let worktree_root_dir =
-            buffer
-                .file()
-                .map(|file| file.worktree_id(cx))
-                .and_then(|worktree_id| {
-                    self.worktree_store
-                        .read(cx)
-                        .worktree_for_id(worktree_id, cx)
-                        .and_then(|worktree| worktree.read(cx).root_dir())
-                });
-        if let Some(worktree_path) = worktree_root_dir {
+        let worktree = buffer
+            .file()
+            .map(|file| file.worktree_id(cx))
+            .and_then(|worktree_id| {
+                self.worktree_store
+                    .read(cx)
+                    .worktree_for_id(worktree_id, cx)
+            });
+
+        if let Some(worktree) = worktree {
+            let worktree = worktree.read(cx);
+            let path_style = worktree.path_style();
             task_variables.insert(
                 VariableName::WorktreeRoot,
-                worktree_path.to_sanitized_string(),
+                worktree.abs_path().to_string_lossy().to_string(),
             );
-            if let Some(full_path) = current_file.as_ref() {
-                let relative_path = pathdiff::diff_paths(full_path, worktree_path);
-                if let Some(relative_file) = relative_path {
+            if let Some(current_file) = current_file.as_ref() {
+                let relative_path = current_file.path();
+                task_variables.insert(
+                    VariableName::RelativeFile,
+                    relative_path.display(path_style).to_string(),
+                );
+                if let Some(relative_dir) = relative_path.parent() {
                     task_variables.insert(
-                        VariableName::RelativeFile,
-                        relative_file.to_sanitized_string(),
+                        VariableName::RelativeDir,
+                        if relative_dir.is_empty() {
+                            String::from(".")
+                        } else {
+                            relative_dir.display(path_style).to_string()
+                        },
                     );
-                    if let Some(relative_dir) = relative_file.parent() {
-                        task_variables.insert(
-                            VariableName::RelativeDir,
-                            if relative_dir.as_os_str().is_empty() {
-                                String::from(".")
-                            } else {
-                                relative_dir.to_sanitized_string()
-                            },
-                        );
-                    }
                 }
             }
         }
 
-        if let Some(path_as_string) = current_file {
-            let path = Path::new(&path_as_string);
+        if let Some(current_file) = current_file {
+            let path = current_file.abs_path(cx);
             if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
                 task_variables.insert(VariableName::Filename, String::from(filename));
             }
@@ -1063,7 +1062,7 @@ impl ContextProvider for BasicContextProvider {
                 task_variables.insert(VariableName::Dirname, dirname.into());
             }
 
-            task_variables.insert(VariableName::File, path_as_string);
+            task_variables.insert(VariableName::File, path.to_string_lossy().to_string());
         }
 
         Task::ready(Ok(task_variables))
@@ -1096,6 +1095,8 @@ mod tests {
     use pretty_assertions::assert_eq;
     use serde_json::json;
     use settings::SettingsLocation;
+    use std::path::Path;
+    use util::rel_path::rel_path;
 
     use crate::task_store::TaskStore;
 
@@ -1181,7 +1182,7 @@ mod tests {
         let worktree_id = WorktreeId::from_usize(0);
         let local_worktree_location = SettingsLocation {
             worktree_id,
-            path: Path::new("foo"),
+            path: RelPath::new("foo").unwrap(),
         };
         inventory.update(cx, |inventory, _| {
             inventory
@@ -1427,7 +1428,7 @@ mod tests {
             (
                 TaskSourceKind::Worktree {
                     id: worktree_1,
-                    directory_in_worktree: PathBuf::from(".zed"),
+                    directory_in_worktree: rel_path(".zed").into(),
                     id_base: "local worktree tasks from directory \".zed\"".into(),
                 },
                 common_name.to_string(),
@@ -1435,7 +1436,7 @@ mod tests {
             (
                 TaskSourceKind::Worktree {
                     id: worktree_1,
-                    directory_in_worktree: PathBuf::from(".zed"),
+                    directory_in_worktree: rel_path(".zed").into(),
                     id_base: "local worktree tasks from directory \".zed\"".into(),
                 },
                 "worktree_1".to_string(),
@@ -1445,7 +1446,7 @@ mod tests {
             (
                 TaskSourceKind::Worktree {
                     id: worktree_2,
-                    directory_in_worktree: PathBuf::from(".zed"),
+                    directory_in_worktree: rel_path(".zed").into(),
                     id_base: "local worktree tasks from directory \".zed\"".into(),
                 },
                 common_name.to_string(),
@@ -1453,7 +1454,7 @@ mod tests {
             (
                 TaskSourceKind::Worktree {
                     id: worktree_2,
-                    directory_in_worktree: PathBuf::from(".zed"),
+                    directory_in_worktree: rel_path(".zed").into(),
                     id_base: "local worktree tasks from directory \".zed\"".into(),
                 },
                 "worktree_2".to_string(),
@@ -1475,7 +1476,7 @@ mod tests {
                 .update_file_based_tasks(
                     TaskSettingsLocation::Worktree(SettingsLocation {
                         worktree_id: worktree_1,
-                        path: Path::new(".zed"),
+                        path: RelPath::new(".zed").unwrap(),
                     }),
                     Some(&mock_tasks_from_names(
                         worktree_1_tasks.iter().map(|(_, name)| name.as_str()),
@@ -1486,7 +1487,7 @@ mod tests {
                 .update_file_based_tasks(
                     TaskSettingsLocation::Worktree(SettingsLocation {
                         worktree_id: worktree_2,
-                        path: Path::new(".zed"),
+                        path: RelPath::new(".zed").unwrap(),
                     }),
                     Some(&mock_tasks_from_names(
                         worktree_2_tasks.iter().map(|(_, name)| name.as_str()),

crates/project/src/task_store.rs 🔗

@@ -438,7 +438,7 @@ fn worktree_root(
             if !root_entry.is_dir() {
                 return None;
             }
-            worktree.absolutize(&root_entry.path).ok()
+            Some(worktree.absolutize(&root_entry.path))
         })
 }
 

crates/project/src/terminals.rs 🔗

@@ -16,7 +16,7 @@ use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal};
 use terminal::{
     TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings,
 };
-use util::{get_default_system_shell, get_system_shell, maybe};
+use util::{get_default_system_shell, get_system_shell, maybe, rel_path::RelPath};
 
 use crate::{Project, ProjectPath};
 
@@ -68,7 +68,7 @@ impl Project {
         {
             settings_location = Some(SettingsLocation {
                 worktree_id: worktree.read(cx).id(),
-                path,
+                path: RelPath::empty(),
             });
         }
         let settings = TerminalSettings::get(settings_location, cx).clone();
@@ -118,7 +118,7 @@ impl Project {
                     .map(|wt| wt.read(cx).id())
                     .map(|worktree_id| ProjectPath {
                         worktree_id,
-                        path: Arc::from(Path::new("")),
+                        path: Arc::from(RelPath::empty()),
                     }),
             );
         let toolchains = project_path_contexts
@@ -298,7 +298,7 @@ impl Project {
         {
             settings_location = Some(SettingsLocation {
                 worktree_id: worktree.read(cx).id(),
-                path,
+                path: RelPath::empty(),
             });
         }
         let settings = TerminalSettings::get(settings_location, cx).clone();
@@ -325,7 +325,7 @@ impl Project {
                     .map(|wt| wt.read(cx).id())
                     .map(|worktree_id| ProjectPath {
                         worktree_id,
-                        path: Arc::from(Path::new("")),
+                        path: RelPath::empty().into(),
                     }),
             );
         let toolchains = project_path_contexts
@@ -464,7 +464,7 @@ impl Project {
         {
             settings_location = Some(SettingsLocation {
                 worktree_id: worktree.read(cx).id(),
-                path,
+                path: RelPath::empty(),
             });
         }
         TerminalSettings::get(settings_location, cx)

crates/project/src/toolchain_store.rs 🔗

@@ -1,8 +1,4 @@
-use std::{
-    path::{Path, PathBuf},
-    str::FromStr,
-    sync::Arc,
-};
+use std::{path::PathBuf, str::FromStr, sync::Arc};
 
 use anyhow::{Context as _, Result, bail};
 
@@ -18,12 +14,12 @@ use language::{
 use rpc::{
     AnyProtoClient, TypedEnvelope,
     proto::{
-        self, FromProto, ResolveToolchainResponse, ToProto,
+        self, ResolveToolchainResponse,
         resolve_toolchain_response::Response as ResolveResponsePayload,
     },
 };
 use settings::WorktreeId;
-use util::ResultExt as _;
+use util::{ResultExt as _, rel_path::RelPath};
 
 use crate::{
     ProjectEnvironment, ProjectPath,
@@ -46,7 +42,7 @@ pub struct Toolchains {
     /// Auto-detected toolchains.
     pub toolchains: ToolchainList,
     /// Path of the project root at which we ran the automatic toolchain detection.
-    pub root_path: Arc<Path>,
+    pub root_path: Arc<RelPath>,
     pub user_toolchains: BTreeMap<ToolchainScope, IndexSet<Toolchain>>,
 }
 impl EventEmitter<ToolchainStoreEvent> for ToolchainStore {}
@@ -241,15 +237,15 @@ impl ToolchainStore {
                 name: toolchain.name.into(),
                 // todo(windows)
                 // Do we need to convert path to native string?
-                path: PathBuf::from(toolchain.path).to_proto().into(),
+                path: toolchain.path.into(),
                 as_json: serde_json::Value::from_str(&toolchain.raw_json)?,
                 language_name,
             };
             let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
-            let path: Arc<Path> = if let Some(path) = envelope.payload.path {
-                Arc::from(path.as_ref())
+            let path = if let Some(path) = envelope.payload.path {
+                RelPath::from_proto(&path)?
             } else {
-                Arc::from("".as_ref())
+                RelPath::empty().into()
             };
             Ok(this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx))
         })??
@@ -261,6 +257,7 @@ impl ToolchainStore {
         envelope: TypedEnvelope<proto::ActiveToolchain>,
         mut cx: AsyncApp,
     ) -> Result<proto::ActiveToolchainResponse> {
+        let path = RelPath::new(envelope.payload.path.as_deref().unwrap_or(""))?;
         let toolchain = this
             .update(&mut cx, |this, cx| {
                 let language_name = LanguageName::from_proto(envelope.payload.language_name);
@@ -268,7 +265,7 @@ impl ToolchainStore {
                 this.active_toolchain(
                     ProjectPath {
                         worktree_id,
-                        path: Arc::from(envelope.payload.path.as_deref().unwrap_or("").as_ref()),
+                        path: Arc::from(path),
                     },
                     language_name,
                     cx,
@@ -281,7 +278,7 @@ impl ToolchainStore {
                 let path = PathBuf::from(toolchain.path.to_string());
                 proto::Toolchain {
                     name: toolchain.name.into(),
-                    path: path.to_proto(),
+                    path: path.to_string_lossy().to_string(),
                     raw_json: toolchain.as_json.to_string(),
                 }
             }),
@@ -297,9 +294,13 @@ impl ToolchainStore {
             .update(&mut cx, |this, cx| {
                 let language_name = LanguageName::from_proto(envelope.payload.language_name);
                 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
-                let path = Arc::from(envelope.payload.path.as_deref().unwrap_or("").as_ref());
-                this.list_toolchains(ProjectPath { worktree_id, path }, language_name, cx)
-            })?
+                let path = RelPath::from_proto(envelope.payload.path.as_deref().unwrap_or(""))?;
+                anyhow::Ok(this.list_toolchains(
+                    ProjectPath { worktree_id, path },
+                    language_name,
+                    cx,
+                ))
+            })??
             .await;
         let has_values = toolchains.is_some();
         let groups = if let Some(Toolchains { toolchains, .. }) = &toolchains {
@@ -329,21 +330,21 @@ impl ToolchainStore {
                     let path = PathBuf::from(toolchain.path.to_string());
                     proto::Toolchain {
                         name: toolchain.name.to_string(),
-                        path: path.to_proto(),
+                        path: path.to_string_lossy().to_string(),
                         raw_json: toolchain.as_json.to_string(),
                     }
                 })
                 .collect::<Vec<_>>();
             (toolchains, relative_path)
         } else {
-            (vec![], Arc::from(Path::new("")))
+            (vec![], Arc::from(RelPath::empty()))
         };
 
         Ok(proto::ListToolchainsResponse {
             has_values,
             toolchains,
             groups,
-            relative_worktree_path: Some(relative_path.to_string_lossy().into_owned()),
+            relative_worktree_path: Some(relative_path.to_proto()),
         })
     }
 
@@ -393,7 +394,7 @@ pub struct LocalToolchainStore {
     languages: Arc<LanguageRegistry>,
     worktree_store: Entity<WorktreeStore>,
     project_environment: Entity<ProjectEnvironment>,
-    active_toolchains: BTreeMap<(WorktreeId, LanguageName), BTreeMap<Arc<Path>, Toolchain>>,
+    active_toolchains: BTreeMap<(WorktreeId, LanguageName), BTreeMap<Arc<RelPath>, Toolchain>>,
     manifest_tree: Entity<ManifestTree>,
 }
 
@@ -402,7 +403,7 @@ impl language::LocalLanguageToolchainStore for LocalStore {
     fn active_toolchain(
         self: Arc<Self>,
         worktree_id: WorktreeId,
-        path: &Arc<Path>,
+        path: &Arc<RelPath>,
         language_name: LanguageName,
         cx: &mut AsyncApp,
     ) -> Option<Toolchain> {
@@ -419,7 +420,7 @@ impl language::LanguageToolchainStore for RemoteStore {
     async fn active_toolchain(
         self: Arc<Self>,
         worktree_id: WorktreeId,
-        path: Arc<Path>,
+        path: Arc<RelPath>,
         language_name: LanguageName,
         cx: &mut AsyncApp,
     ) -> Option<Toolchain> {
@@ -437,7 +438,7 @@ impl language::LocalLanguageToolchainStore for EmptyToolchainStore {
     fn active_toolchain(
         self: Arc<Self>,
         _: WorktreeId,
-        _: &Arc<Path>,
+        _: &Arc<RelPath>,
         _: LanguageName,
         _: &mut AsyncApp,
     ) -> Option<Toolchain> {
@@ -479,7 +480,7 @@ impl LocalToolchainStore {
         path: ProjectPath,
         language_name: LanguageName,
         cx: &mut Context<Self>,
-    ) -> Task<Option<(ToolchainList, Arc<Path>)>> {
+    ) -> Task<Option<(ToolchainList, Arc<RelPath>)>> {
         let registry = self.languages.clone();
 
         let manifest_tree = self.manifest_tree.downgrade();
@@ -511,13 +512,12 @@ impl LocalToolchainStore {
                 })
                 .ok()?
                 .unwrap_or_else(|| ProjectPath {
-                    path: Arc::from(Path::new("")),
+                    path: Arc::from(RelPath::empty()),
                     worktree_id,
                 });
             let abs_path = worktree
-                .update(cx, |this, _| this.absolutize(&relative_path.path).ok())
-                .ok()
-                .flatten()?;
+                .update(cx, |this, _| this.absolutize(&relative_path.path))
+                .ok()?;
 
             let project_env = environment
                 .update(cx, |environment, cx| {
@@ -540,7 +540,7 @@ impl LocalToolchainStore {
     pub(crate) fn active_toolchain(
         &self,
         worktree_id: WorktreeId,
-        relative_path: &Arc<Path>,
+        relative_path: &Arc<RelPath>,
         language_name: LanguageName,
     ) -> Option<Toolchain> {
         let ancestors = relative_path.ancestors();
@@ -609,10 +609,10 @@ impl RemoteToolchainStore {
                             language_name: toolchain.language_name.into(),
                             toolchain: Some(proto::Toolchain {
                                 name: toolchain.name.into(),
-                                path: path.to_proto(),
+                                path: path.to_string_lossy().to_string(),
                                 raw_json: toolchain.as_json.to_string(),
                             }),
-                            path: Some(project_path.path.to_string_lossy().into_owned()),
+                            path: Some(project_path.path.to_proto()),
                         })
                         .await
                         .log_err()?;
@@ -633,7 +633,7 @@ impl RemoteToolchainStore {
         path: ProjectPath,
         language_name: LanguageName,
         cx: &App,
-    ) -> Task<Option<(ToolchainList, Arc<Path>)>> {
+    ) -> Task<Option<(ToolchainList, Arc<RelPath>)>> {
         let project_id = self.project_id;
         let client = self.client.clone();
         cx.background_spawn(async move {
@@ -642,7 +642,7 @@ impl RemoteToolchainStore {
                     project_id,
                     worktree_id: path.worktree_id.to_proto(),
                     language_name: language_name.clone().into(),
-                    path: Some(path.path.to_string_lossy().into_owned()),
+                    path: Some(path.path.to_proto()),
                 })
                 .await
                 .log_err()?;
@@ -656,12 +656,7 @@ impl RemoteToolchainStore {
                     Some(Toolchain {
                         language_name: language_name.clone(),
                         name: toolchain.name.into(),
-                        // todo(windows)
-                        // Do we need to convert path to native string?
-                        path: PathBuf::from_proto(toolchain.path)
-                            .to_string_lossy()
-                            .to_string()
-                            .into(),
+                        path: toolchain.path.into(),
                         as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?,
                     })
                 })
@@ -673,12 +668,13 @@ impl RemoteToolchainStore {
                     Some((usize::try_from(group.start_index).ok()?, group.name.into()))
                 })
                 .collect();
-            let relative_path = Arc::from(Path::new(
+            let relative_path = RelPath::from_proto(
                 response
                     .relative_worktree_path
                     .as_deref()
                     .unwrap_or_default(),
-            ));
+            )
+            .log_err()?;
             Some((
                 ToolchainList {
                     toolchains,
@@ -703,7 +699,7 @@ impl RemoteToolchainStore {
                     project_id,
                     worktree_id: path.worktree_id.to_proto(),
                     language_name: language_name.clone().into(),
-                    path: Some(path.path.to_string_lossy().into_owned()),
+                    path: Some(path.path.to_proto()),
                 })
                 .await
                 .log_err()?;
@@ -712,12 +708,7 @@ impl RemoteToolchainStore {
                 Some(Toolchain {
                     language_name: language_name.clone(),
                     name: toolchain.name.into(),
-                    // todo(windows)
-                    // Do we need to convert path to native string?
-                    path: PathBuf::from_proto(toolchain.path)
-                        .to_string_lossy()
-                        .to_string()
-                        .into(),
+                    path: toolchain.path.into(),
                     as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?,
                 })
             })
@@ -746,20 +737,13 @@ impl RemoteToolchainStore {
                 .context("Failed to resolve toolchain via RPC")?;
             use proto::resolve_toolchain_response::Response;
             match response {
-                Response::Toolchain(toolchain) => {
-                    Ok(Toolchain {
-                        language_name: language_name.clone(),
-                        name: toolchain.name.into(),
-                        // todo(windows)
-                        // Do we need to convert path to native string?
-                        path: PathBuf::from_proto(toolchain.path)
-                            .to_string_lossy()
-                            .to_string()
-                            .into(),
-                        as_json: serde_json::Value::from_str(&toolchain.raw_json)
-                            .context("Deserializing ResolveToolchain LSP response")?,
-                    })
-                }
+                Response::Toolchain(toolchain) => Ok(Toolchain {
+                    language_name: language_name.clone(),
+                    name: toolchain.name.into(),
+                    path: toolchain.path.into(),
+                    as_json: serde_json::Value::from_str(&toolchain.raw_json)
+                        .context("Deserializing ResolveToolchain LSP response")?,
+                }),
                 Response::Error(error) => {
                     anyhow::bail!("{error}");
                 }

crates/project/src/worktree_store.rs 🔗

@@ -7,7 +7,7 @@ use std::{
 
 use anyhow::{Context as _, Result, anyhow};
 use collections::{HashMap, HashSet};
-use fs::Fs;
+use fs::{Fs, copy_recursive};
 use futures::{
     FutureExt, SinkExt,
     future::{BoxFuture, Shared},
@@ -18,7 +18,7 @@ use gpui::{
 use postage::oneshot;
 use rpc::{
     AnyProtoClient, ErrorExt, TypedEnvelope,
-    proto::{self, FromProto, REMOTE_SERVER_PROJECT_ID, ToProto},
+    proto::{self, REMOTE_SERVER_PROJECT_ID},
 };
 use smol::{
     channel::{Receiver, Sender},
@@ -28,16 +28,17 @@ use text::ReplicaId;
 use util::{
     ResultExt,
     paths::{PathStyle, RemotePathBuf, SanitizedPath},
+    rel_path::RelPath,
 };
 use worktree::{
-    Entry, ProjectEntryId, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId,
-    WorktreeSettings,
+    CreatedEntry, Entry, ProjectEntryId, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree,
+    WorktreeId, WorktreeSettings,
 };
 
 use crate::{ProjectPath, search::SearchQuery};
 
 struct MatchingEntry {
-    worktree_path: Arc<Path>,
+    worktree_root: Arc<Path>,
     path: ProjectPath,
     respond: oneshot::Sender<ProjectPath>,
 }
@@ -155,11 +156,14 @@ impl WorktreeStore {
         &self,
         abs_path: impl AsRef<Path>,
         cx: &App,
-    ) -> Option<(Entity<Worktree>, PathBuf)> {
-        let abs_path = SanitizedPath::new(&abs_path);
+    ) -> Option<(Entity<Worktree>, Arc<RelPath>)> {
+        let abs_path = SanitizedPath::new(abs_path.as_ref());
         for tree in self.worktrees() {
-            if let Ok(relative_path) = abs_path.as_path().strip_prefix(tree.read(cx).abs_path()) {
-                return Some((tree.clone(), relative_path.into()));
+            let path_style = tree.read(cx).path_style();
+            if let Ok(relative_path) = abs_path.as_ref().strip_prefix(tree.read(cx).abs_path())
+                && let Ok(relative_path) = RelPath::from_std_path(relative_path, path_style)
+            {
+                return Some((tree.clone(), relative_path));
             }
         }
         None
@@ -167,7 +171,14 @@ impl WorktreeStore {
 
     pub fn absolutize(&self, project_path: &ProjectPath, cx: &App) -> Option<PathBuf> {
         let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
-        worktree.read(cx).absolutize(&project_path.path).ok()
+        Some(worktree.read(cx).absolutize(&project_path.path))
+    }
+
+    pub fn path_style(&self) -> PathStyle {
+        match &self.state {
+            WorktreeStoreState::Local { .. } => PathStyle::local(),
+            WorktreeStoreState::Remote { path_style, .. } => *path_style,
+        }
     }
 
     pub fn find_or_create_worktree(
@@ -175,13 +186,13 @@ impl WorktreeStore {
         abs_path: impl AsRef<Path>,
         visible: bool,
         cx: &mut Context<Self>,
-    ) -> Task<Result<(Entity<Worktree>, PathBuf)>> {
+    ) -> Task<Result<(Entity<Worktree>, Arc<RelPath>)>> {
         let abs_path = abs_path.as_ref();
         if let Some((tree, relative_path)) = self.find_worktree(abs_path, cx) {
             Task::ready(Ok((tree, relative_path)))
         } else {
             let worktree = self.create_worktree(abs_path, visible, cx);
-            cx.background_spawn(async move { Ok((worktree.await?, PathBuf::new())) })
+            cx.background_spawn(async move { Ok((worktree.await?, RelPath::empty().into())) })
         }
     }
 
@@ -209,6 +220,240 @@ impl WorktreeStore {
             .entry_for_path(&path.path)
     }
 
+    pub fn copy_entry(
+        &mut self,
+        entry_id: ProjectEntryId,
+        new_project_path: ProjectPath,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<Option<Entry>>> {
+        let Some(old_worktree) = self.worktree_for_entry(entry_id, cx) else {
+            return Task::ready(Err(anyhow!("no such worktree")));
+        };
+        let Some(old_entry) = old_worktree.read(cx).entry_for_id(entry_id) else {
+            return Task::ready(Err(anyhow!("no such entry")));
+        };
+        let Some(new_worktree) = self.worktree_for_id(new_project_path.worktree_id, cx) else {
+            return Task::ready(Err(anyhow!("no such worktree")));
+        };
+
+        match &self.state {
+            WorktreeStoreState::Local { fs } => {
+                let old_abs_path = old_worktree.read(cx).absolutize(&old_entry.path);
+                let new_abs_path = new_worktree.read(cx).absolutize(&new_project_path.path);
+                let fs = fs.clone();
+                let copy = cx.background_spawn(async move {
+                    copy_recursive(
+                        fs.as_ref(),
+                        &old_abs_path,
+                        &new_abs_path,
+                        Default::default(),
+                    )
+                    .await
+                });
+
+                cx.spawn(async move |_, cx| {
+                    copy.await?;
+                    new_worktree
+                        .update(cx, |this, cx| {
+                            this.as_local_mut().unwrap().refresh_entry(
+                                new_project_path.path,
+                                None,
+                                cx,
+                            )
+                        })?
+                        .await
+                })
+            }
+            WorktreeStoreState::Remote {
+                upstream_client,
+                upstream_project_id,
+                ..
+            } => {
+                let response = upstream_client.request(proto::CopyProjectEntry {
+                    project_id: *upstream_project_id,
+                    entry_id: entry_id.to_proto(),
+                    new_path: new_project_path.path.to_proto(),
+                    new_worktree_id: new_project_path.worktree_id.to_proto(),
+                });
+                cx.spawn(async move |_, cx| {
+                    let response = response.await?;
+                    match response.entry {
+                        Some(entry) => new_worktree
+                            .update(cx, |worktree, cx| {
+                                worktree.as_remote_mut().unwrap().insert_entry(
+                                    entry,
+                                    response.worktree_scan_id as usize,
+                                    cx,
+                                )
+                            })?
+                            .await
+                            .map(Some),
+                        None => Ok(None),
+                    }
+                })
+            }
+        }
+    }
+
+    pub fn rename_entry(
+        &mut self,
+        entry_id: ProjectEntryId,
+        new_project_path: ProjectPath,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<CreatedEntry>> {
+        let Some(old_worktree) = self.worktree_for_entry(entry_id, cx) else {
+            return Task::ready(Err(anyhow!("no such worktree")));
+        };
+        let Some(old_entry) = old_worktree.read(cx).entry_for_id(entry_id).cloned() else {
+            return Task::ready(Err(anyhow!("no such entry")));
+        };
+        let Some(new_worktree) = self.worktree_for_id(new_project_path.worktree_id, cx) else {
+            return Task::ready(Err(anyhow!("no such worktree")));
+        };
+
+        match &self.state {
+            WorktreeStoreState::Local { fs } => {
+                let abs_old_path = old_worktree.read(cx).absolutize(&old_entry.path);
+                let new_worktree_ref = new_worktree.read(cx);
+                let is_root_entry = new_worktree_ref
+                    .root_entry()
+                    .is_some_and(|e| e.id == entry_id);
+                let abs_new_path = if is_root_entry {
+                    let abs_path = new_worktree_ref.abs_path();
+                    let Some(root_parent_path) = abs_path.parent() else {
+                        return Task::ready(Err(anyhow!("no parent for path {:?}", abs_path)));
+                    };
+                    root_parent_path.join(new_project_path.path.as_std_path())
+                } else {
+                    new_worktree_ref.absolutize(&new_project_path.path)
+                };
+
+                let fs = fs.clone();
+                let case_sensitive = new_worktree
+                    .read(cx)
+                    .as_local()
+                    .unwrap()
+                    .fs_is_case_sensitive();
+
+                let do_rename =
+                    async move |fs: &dyn Fs, old_path: &Path, new_path: &Path, overwrite| {
+                        fs.rename(
+                            &old_path,
+                            &new_path,
+                            fs::RenameOptions {
+                                overwrite,
+                                ..fs::RenameOptions::default()
+                            },
+                        )
+                        .await
+                        .with_context(|| format!("renaming {old_path:?} into {new_path:?}"))
+                    };
+
+                let rename = cx.background_spawn({
+                    let abs_new_path = abs_new_path.clone();
+                    async move {
+                        // If we're on a case-insensitive FS and we're doing a case-only rename (i.e. `foobar` to `FOOBAR`)
+                        // we want to overwrite, because otherwise we run into a file-already-exists error.
+                        let overwrite = !case_sensitive
+                            && abs_old_path != abs_new_path
+                            && abs_old_path.to_str().map(|p| p.to_lowercase())
+                                == abs_new_path.to_str().map(|p| p.to_lowercase());
+
+                        // The directory we're renaming into might not exist yet
+                        if let Err(e) =
+                            do_rename(fs.as_ref(), &abs_old_path, &abs_new_path, overwrite).await
+                        {
+                            if let Some(err) = e.downcast_ref::<std::io::Error>()
+                                && err.kind() == std::io::ErrorKind::NotFound
+                            {
+                                if let Some(parent) = abs_new_path.parent() {
+                                    fs.create_dir(parent).await.with_context(|| {
+                                        format!("creating parent directory {parent:?}")
+                                    })?;
+                                    return do_rename(
+                                        fs.as_ref(),
+                                        &abs_old_path,
+                                        &abs_new_path,
+                                        overwrite,
+                                    )
+                                    .await;
+                                }
+                            }
+                            return Err(e);
+                        }
+                        Ok(())
+                    }
+                });
+
+                cx.spawn(async move |_, cx| {
+                    rename.await?;
+                    Ok(new_worktree
+                        .update(cx, |this, cx| {
+                            let local = this.as_local_mut().unwrap();
+                            if is_root_entry {
+                                // We eagerly update `abs_path` and refresh this worktree.
+                                // Otherwise, the FS watcher would do it on the `RootUpdated` event,
+                                // but with a noticeable delay, so we handle it proactively.
+                                local.update_abs_path_and_refresh(
+                                    Some(SanitizedPath::new_arc(&abs_new_path)),
+                                    cx,
+                                );
+                                Task::ready(Ok(this.root_entry().cloned()))
+                            } else {
+                                // First refresh the parent directory (in case it was newly created)
+                                if let Some(parent) = new_project_path.path.parent() {
+                                    let _ = local.refresh_entries_for_paths(vec![parent.into()]);
+                                }
+                                // Then refresh the new path
+                                local.refresh_entry(
+                                    new_project_path.path.clone(),
+                                    Some(old_entry.path),
+                                    cx,
+                                )
+                            }
+                        })?
+                        .await?
+                        .map(CreatedEntry::Included)
+                        .unwrap_or_else(|| CreatedEntry::Excluded {
+                            abs_path: abs_new_path,
+                        }))
+                })
+            }
+            WorktreeStoreState::Remote {
+                upstream_client,
+                upstream_project_id,
+                ..
+            } => {
+                let response = upstream_client.request(proto::RenameProjectEntry {
+                    project_id: *upstream_project_id,
+                    entry_id: entry_id.to_proto(),
+                    new_path: new_project_path.path.to_proto(),
+                    new_worktree_id: new_project_path.worktree_id.to_proto(),
+                });
+                cx.spawn(async move |_, cx| {
+                    let response = response.await?;
+                    match response.entry {
+                        Some(entry) => new_worktree
+                            .update(cx, |worktree, cx| {
+                                worktree.as_remote_mut().unwrap().insert_entry(
+                                    entry,
+                                    response.worktree_scan_id as usize,
+                                    cx,
+                                )
+                            })?
+                            .await
+                            .map(CreatedEntry::Included),
+                        None => {
+                            let abs_path = new_worktree.read_with(cx, |worktree, _| {
+                                worktree.absolutize(&new_project_path.path)
+                            })?;
+                            Ok(CreatedEntry::Excluded { abs_path })
+                        }
+                    }
+                })
+            }
+        }
+    }
     pub fn create_worktree(
         &mut self,
         abs_path: impl AsRef<Path>,
@@ -226,7 +471,7 @@ impl WorktreeStore {
                     if upstream_client.is_via_collab() {
                         Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab"))))
                     } else {
-                        let abs_path = RemotePathBuf::new(abs_path.to_path_buf(), *path_style);
+                        let abs_path = RemotePathBuf::new(abs_path.to_string(), *path_style);
                         self.create_remote_worktree(upstream_client.clone(), abs_path, visible, cx)
                     }
                 }
@@ -273,7 +518,7 @@ impl WorktreeStore {
         cx.spawn(async move |this, cx| {
             let this = this.upgrade().context("Dropped worktree store")?;
 
-            let path = RemotePathBuf::new(abs_path.into(), path_style);
+            let path = RemotePathBuf::new(abs_path, path_style);
             let response = client
                 .request(proto::AddWorktree {
                     project_id: REMOTE_SERVER_PROJECT_ID,
@@ -288,7 +533,7 @@ impl WorktreeStore {
                 return Ok(existing_worktree);
             }
 
-            let root_path_buf = PathBuf::from_proto(response.canonicalized_path.clone());
+            let root_path_buf = PathBuf::from(response.canonicalized_path.clone());
             let root_name = root_path_buf
                 .file_name()
                 .map(|n| n.to_string_lossy().to_string())
@@ -305,6 +550,7 @@ impl WorktreeStore {
                         abs_path: response.canonicalized_path,
                     },
                     client,
+                    path_style,
                     cx,
                 )
             })?;
@@ -477,7 +723,14 @@ impl WorktreeStore {
                 self.worktrees.push(handle);
             } else {
                 self.add(
-                    &Worktree::remote(project_id, replica_id, worktree, client.clone(), cx),
+                    &Worktree::remote(
+                        project_id,
+                        replica_id,
+                        worktree,
+                        client.clone(),
+                        self.path_style(),
+                        cx,
+                    ),
                     cx,
                 );
             }
@@ -605,9 +858,9 @@ impl WorktreeStore {
                 let worktree = worktree.read(cx);
                 proto::WorktreeMetadata {
                     id: worktree.id().to_proto(),
-                    root_name: worktree.root_name().into(),
+                    root_name: worktree.root_name_str().to_owned(),
                     visible: worktree.is_visible(),
-                    abs_path: worktree.abs_path().to_proto(),
+                    abs_path: worktree.abs_path().to_string_lossy().to_string(),
                 }
             })
             .collect()
@@ -740,13 +993,13 @@ impl WorktreeStore {
     fn scan_ignored_dir<'a>(
         fs: &'a Arc<dyn Fs>,
         snapshot: &'a worktree::Snapshot,
-        path: &'a Path,
+        path: &'a RelPath,
         query: &'a SearchQuery,
         filter_tx: &'a Sender<MatchingEntry>,
         output_tx: &'a Sender<oneshot::Receiver<ProjectPath>>,
     ) -> BoxFuture<'a, Result<()>> {
         async move {
-            let abs_path = snapshot.abs_path().join(path);
+            let abs_path = snapshot.absolutize(path);
             let Some(mut files) = fs
                 .read_dir(&abs_path)
                 .await
@@ -771,21 +1024,21 @@ impl WorktreeStore {
                 if metadata.is_symlink || metadata.is_fifo {
                     continue;
                 }
-                results.push((
-                    file.strip_prefix(snapshot.abs_path())?.to_path_buf(),
-                    !metadata.is_dir,
-                ))
+                let relative_path = file.strip_prefix(snapshot.abs_path())?;
+                let relative_path = RelPath::from_std_path(&relative_path, snapshot.path_style())
+                    .context("getting relative path")?;
+                results.push((relative_path, !metadata.is_dir))
             }
             results.sort_by(|(a_path, _), (b_path, _)| a_path.cmp(b_path));
             for (path, is_file) in results {
                 if is_file {
                     if query.filters_path() {
                         let matched_path = if query.match_full_paths() {
-                            let mut full_path = PathBuf::from(snapshot.root_name());
-                            full_path.push(&path);
+                            let mut full_path = snapshot.root_name().as_std_path().to_owned();
+                            full_path.push(path.as_std_path());
                             query.match_path(&full_path)
                         } else {
-                            query.match_path(&path)
+                            query.match_path(&path.as_std_path())
                         };
                         if !matched_path {
                             continue;
@@ -796,10 +1049,10 @@ impl WorktreeStore {
                     filter_tx
                         .send(MatchingEntry {
                             respond: tx,
-                            worktree_path: snapshot.abs_path().clone(),
+                            worktree_root: snapshot.abs_path().clone(),
                             path: ProjectPath {
                                 worktree_id: snapshot.id(),
-                                path: Arc::from(path),
+                                path,
                             },
                         })
                         .await?;
@@ -844,11 +1097,11 @@ impl WorktreeStore {
 
                 if query.filters_path() {
                     let matched_path = if query.match_full_paths() {
-                        let mut full_path = PathBuf::from(snapshot.root_name());
-                        full_path.push(&entry.path);
+                        let mut full_path = snapshot.root_name().as_std_path().to_owned();
+                        full_path.push(entry.path.as_std_path());
                         query.match_path(&full_path)
                     } else {
-                        query.match_path(&entry.path)
+                        query.match_path(entry.path.as_std_path())
                     };
                     if !matched_path {
                         continue;
@@ -867,7 +1120,7 @@ impl WorktreeStore {
                     filter_tx
                         .send(MatchingEntry {
                             respond: tx,
-                            worktree_path: snapshot.abs_path().clone(),
+                            worktree_root: snapshot.abs_path().clone(),
                             path: ProjectPath {
                                 worktree_id: snapshot.id(),
                                 path: entry.path.clone(),
@@ -889,7 +1142,7 @@ impl WorktreeStore {
     ) -> Result<()> {
         let mut input = pin!(input);
         while let Some(mut entry) = input.next().await {
-            let abs_path = entry.worktree_path.join(&entry.path.path);
+            let abs_path = entry.worktree_root.join(entry.path.path.as_std_path());
             let Some(file) = fs.open_sync(&abs_path).await.log_err() else {
                 continue;
             };
@@ -935,11 +1188,26 @@ impl WorktreeStore {
         mut cx: AsyncApp,
     ) -> Result<proto::ProjectEntryResponse> {
         let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
-        let worktree = this.update(&mut cx, |this, cx| {
-            this.worktree_for_entry(entry_id, cx)
-                .context("worktree not found")
+        let new_worktree_id = WorktreeId::from_proto(envelope.payload.new_worktree_id);
+        let new_project_path = (
+            new_worktree_id,
+            RelPath::from_proto(&envelope.payload.new_path)?,
+        );
+        let (scan_id, entry) = this.update(&mut cx, |this, cx| {
+            let new_worktree = this
+                .worktree_for_id(new_worktree_id, cx)
+                .context("no such worktree")?;
+            let scan_id = new_worktree.read(cx).scan_id();
+            anyhow::Ok((
+                scan_id,
+                this.copy_entry(entry_id, new_project_path.into(), cx),
+            ))
         })??;
-        Worktree::handle_copy_entry(worktree, envelope.payload, cx).await
+        let entry = entry.await?;
+        Ok(proto::ProjectEntryResponse {
+            entry: entry.as_ref().map(|entry| entry.into()),
+            worktree_scan_id: scan_id as u64,
+        })
     }
 
     pub async fn handle_delete_project_entry(
@@ -955,6 +1223,35 @@ impl WorktreeStore {
         Worktree::handle_delete_entry(worktree, envelope.payload, cx).await
     }
 
+    pub async fn handle_rename_project_entry(
+        this: Entity<Self>,
+        request: proto::RenameProjectEntry,
+        mut cx: AsyncApp,
+    ) -> Result<proto::ProjectEntryResponse> {
+        let entry_id = ProjectEntryId::from_proto(request.entry_id);
+        let new_worktree_id = WorktreeId::from_proto(request.new_worktree_id);
+        let rel_path = RelPath::from_proto(&request.new_path)
+            .with_context(|| format!("received invalid relative path {:?}", &request.new_path))?;
+
+        let (scan_id, task) = this.update(&mut cx, |this, cx| {
+            let worktree = this
+                .worktree_for_entry(entry_id, cx)
+                .context("no such worktree")?;
+            let scan_id = worktree.read(cx).scan_id();
+            anyhow::Ok((
+                scan_id,
+                this.rename_entry(entry_id, (new_worktree_id, rel_path).into(), cx),
+            ))
+        })??;
+        Ok(proto::ProjectEntryResponse {
+            entry: match &task.await? {
+                CreatedEntry::Included(entry) => Some(entry.into()),
+                CreatedEntry::Excluded { .. } => None,
+            },
+            worktree_scan_id: scan_id as u64,
+        })
+    }
+
     pub async fn handle_expand_project_entry(
         this: Entity<Self>,
         envelope: TypedEnvelope<proto::ExpandProjectEntry>,

crates/project/src/yarn.rs 🔗

@@ -15,7 +15,7 @@ use anyhow::Result;
 use collections::HashMap;
 use fs::Fs;
 use gpui::{App, AppContext as _, Context, Entity, Task};
-use util::{ResultExt, archive::extract_zip};
+use util::{ResultExt, archive::extract_zip, paths::PathStyle, rel_path::RelPath};
 
 pub(crate) struct YarnPathStore {
     temp_dirs: HashMap<Arc<Path>, tempfile::TempDir>,
@@ -63,12 +63,13 @@ impl YarnPathStore {
             fs,
         })
     }
+
     pub(crate) fn process_path(
         &mut self,
         path: &Path,
         protocol: &str,
         cx: &Context<Self>,
-    ) -> Task<Option<(Arc<Path>, Arc<Path>)>> {
+    ) -> Task<Option<(Arc<Path>, Arc<RelPath>)>> {
         let mut is_zip = protocol.eq("zip");
 
         let path: &Path = if let Some(non_zip_part) = path
@@ -112,7 +113,9 @@ impl YarnPathStore {
                     new_path
                 };
                 // Rebase zip-path onto new temp path.
-                let as_relative = path.strip_prefix(zip_file).ok()?.into();
+                let as_relative =
+                    RelPath::from_std_path(path.strip_prefix(zip_file).ok()?, PathStyle::local())
+                        .ok()?;
                 Some((zip_root.into(), as_relative))
             })
         } else {

crates/project_panel/Cargo.toml 🔗

@@ -20,7 +20,6 @@ db.workspace = true
 editor.workspace = true
 file_icons.workspace = true
 git_ui.workspace = true
-indexmap.workspace = true
 git.workspace = true
 gpui.workspace = true
 menu.workspace = true

crates/project_panel/src/project_panel.rs 🔗

@@ -17,16 +17,14 @@ use file_icons::FileIcons;
 use git::status::GitSummary;
 use git_ui::file_diff_view::FileDiffView;
 use gpui::{
-    Action, AnyElement, App, ArcCow, AsyncWindowContext, Bounds, ClipboardItem, Context,
-    CursorStyle, DismissEvent, Div, DragMoveEvent, Entity, EventEmitter, ExternalPaths,
-    FocusHandle, Focusable, Hsla, InteractiveElement, KeyContext, ListHorizontalSizingBehavior,
-    ListSizingBehavior, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
-    ParentElement, Pixels, Point, PromptLevel, Render, ScrollStrategy, Stateful, Styled,
-    Subscription, Task, UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred,
-    div, hsla, linear_color_stop, linear_gradient, point, px, size, transparent_white,
-    uniform_list,
+    Action, AnyElement, App, AsyncWindowContext, Bounds, ClipboardItem, Context, CursorStyle,
+    DismissEvent, Div, DragMoveEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable,
+    Hsla, InteractiveElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior,
+    Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
+    PromptLevel, Render, ScrollStrategy, Stateful, Styled, Subscription, Task,
+    UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, div, hsla,
+    linear_color_stop, linear_gradient, point, px, size, transparent_white, uniform_list,
 };
-use indexmap::IndexMap;
 use language::DiagnosticSeverity;
 use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
 use project::{
@@ -34,7 +32,6 @@ use project::{
     ProjectPath, Worktree, WorktreeId,
     git_store::{GitStoreEvent, git_traversal::ChildEntriesGitIter},
     project_settings::GoToDiagnosticSeverityFilter,
-    relativize_path,
 };
 use project_panel_settings::ProjectPanelSettings;
 use schemars::JsonSchema;
@@ -49,7 +46,6 @@ use std::{
     cell::OnceCell,
     cmp,
     collections::HashSet,
-    ffi::OsStr,
     ops::Range,
     path::{Path, PathBuf},
     sync::Arc,
@@ -62,7 +58,7 @@ use ui::{
     ScrollAxes, ScrollableHandle, Scrollbars, StickyCandidate, Tooltip, WithScrollbar, prelude::*,
     v_flex,
 };
-use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths};
+use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths, rel_path::RelPath};
 use workspace::{
     DraggedSelection, OpenInTerminal, OpenOptions, OpenVisible, PreviewTabsSettings, SelectedEntry,
     SplitDirection, Workspace,
@@ -78,7 +74,7 @@ const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
 struct VisibleEntriesForWorktree {
     worktree_id: WorktreeId,
     entries: Vec<GitEntry>,
-    index: OnceCell<HashSet<Arc<Path>>>,
+    index: OnceCell<HashSet<Arc<RelPath>>>,
 }
 
 pub struct ProjectPanel {
@@ -110,7 +106,7 @@ pub struct ProjectPanel {
     workspace: WeakEntity<Workspace>,
     width: Option<Pixels>,
     pending_serialization: Task<Option<()>>,
-    diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>,
+    diagnostics: HashMap<(WorktreeId, Arc<RelPath>), DiagnosticSeverity>,
     max_width_item_index: Option<usize>,
     diagnostic_summary_update: Task<()>,
     // We keep track of the mouse down state on entries so we don't flash the UI
@@ -156,7 +152,7 @@ struct EditState {
     leaf_entry_id: Option<ProjectEntryId>,
     is_dir: bool,
     depth: usize,
-    processing_filename: Option<String>,
+    processing_filename: Option<Arc<RelPath>>,
     previously_focused: Option<SelectedEntry>,
     validation_state: ValidationState,
 }
@@ -177,7 +173,7 @@ enum ClipboardEntry {
 struct EntryDetails {
     filename: String,
     icon: Option<SharedString>,
-    path: Arc<Path>,
+    path: Arc<RelPath>,
     depth: usize,
     kind: EntryKind,
     is_ignored: bool,
@@ -459,6 +455,7 @@ impl ProjectPanel {
     ) -> Entity<Self> {
         let project = workspace.project().clone();
         let git_store = project.read(cx).git_store().clone();
+        let path_style = project.read(cx).path_style(cx);
         let project_panel = cx.new(|cx| {
             let focus_handle = cx.focus_handle();
             cx.on_focus(&focus_handle, window, Self::focus_in).detach();
@@ -705,7 +702,7 @@ impl ProjectPanel {
                                         },
                                         ErrorCode::UnsharedItem => Some(format!(
                                             "{} is not shared by the host. This could be because it has been marked as `private`",
-                                            file_path.display()
+                                            file_path.display(path_style)
                                         )),
                                         // See note in worktree.rs where this error originates. Returning Some in this case prevents
                                         // the error popup from saying "Try Again", which is a red herring in this case
@@ -795,7 +792,7 @@ impl ProjectPanel {
     }
 
     fn update_diagnostics(&mut self, cx: &mut Context<Self>) {
-        let mut diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity> =
+        let mut diagnostics: HashMap<(WorktreeId, Arc<RelPath>), DiagnosticSeverity> =
             Default::default();
         let show_diagnostics_setting = ProjectPanelSettings::get_global(cx).show_diagnostics;
 
@@ -815,20 +812,12 @@ impl ProjectPanel {
                     }
                 })
                 .for_each(|(project_path, diagnostic_severity)| {
-                    let mut path_buffer = PathBuf::new();
-                    Self::update_strongest_diagnostic_severity(
-                        &mut diagnostics,
-                        &project_path,
-                        path_buffer.clone(),
-                        diagnostic_severity,
-                    );
-
-                    for component in project_path.path.components() {
-                        path_buffer.push(component);
+                    let ancestors = project_path.path.ancestors().collect::<Vec<_>>();
+                    for path in ancestors.into_iter().rev() {
                         Self::update_strongest_diagnostic_severity(
                             &mut diagnostics,
                             &project_path,
-                            path_buffer.clone(),
+                            path.into(),
                             diagnostic_severity,
                         );
                     }
@@ -838,9 +827,9 @@ impl ProjectPanel {
     }
 
     fn update_strongest_diagnostic_severity(
-        diagnostics: &mut HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>,
+        diagnostics: &mut HashMap<(WorktreeId, Arc<RelPath>), DiagnosticSeverity>,
         project_path: &ProjectPath,
-        path_buffer: PathBuf,
+        path_buffer: Arc<RelPath>,
         diagnostic_severity: DiagnosticSeverity,
     ) {
         diagnostics
@@ -1419,6 +1408,31 @@ impl ProjectPanel {
         };
         let filename = self.filename_editor.read(cx).text(cx);
         if !filename.is_empty() {
+            if filename.is_empty() {
+                edit_state.validation_state =
+                    ValidationState::Error("File or directory name cannot be empty.".to_string());
+                cx.notify();
+                return;
+            }
+
+            let trimmed_filename = filename.trim();
+            if trimmed_filename != filename {
+                edit_state.validation_state = ValidationState::Warning(
+                    "File or directory name contains leading or trailing whitespace.".to_string(),
+                );
+                cx.notify();
+                return;
+            }
+            let trimmed_filename = trimmed_filename.trim_start_matches('/');
+
+            let Ok(filename) = RelPath::new(trimmed_filename) else {
+                edit_state.validation_state = ValidationState::Warning(
+                    "File or directory name contains leading or trailing whitespace.".to_string(),
+                );
+                cx.notify();
+                return;
+            };
+
             if let Some(worktree) = self
                 .project
                 .read(cx)
@@ -1427,21 +1441,17 @@ impl ProjectPanel {
             {
                 let mut already_exists = false;
                 if edit_state.is_new_entry() {
-                    let new_path = entry.path.join(filename.trim_start_matches('/'));
-                    if worktree
-                        .read(cx)
-                        .entry_for_path(new_path.as_path())
-                        .is_some()
-                    {
+                    let new_path = entry.path.join(filename);
+                    if worktree.read(cx).entry_for_path(&new_path).is_some() {
                         already_exists = true;
                     }
                 } else {
                     let new_path = if let Some(parent) = entry.path.clone().parent() {
                         parent.join(&filename)
                     } else {
-                        filename.clone().into()
+                        filename.into()
                     };
-                    if let Some(existing) = worktree.read(cx).entry_for_path(new_path.as_path())
+                    if let Some(existing) = worktree.read(cx).entry_for_path(&new_path)
                         && existing.id != entry.id
                     {
                         already_exists = true;
@@ -1450,26 +1460,12 @@ impl ProjectPanel {
                 if already_exists {
                     edit_state.validation_state = ValidationState::Error(format!(
                         "File or directory '{}' already exists at location. Please choose a different name.",
-                        filename
+                        filename.as_str()
                     ));
                     cx.notify();
                     return;
                 }
             }
-            let trimmed_filename = filename.trim();
-            if trimmed_filename.is_empty() {
-                edit_state.validation_state =
-                    ValidationState::Error("File or directory name cannot be empty.".to_string());
-                cx.notify();
-                return;
-            }
-            if trimmed_filename != filename {
-                edit_state.validation_state = ValidationState::Warning(
-                    "File or directory name contains leading or trailing whitespace.".to_string(),
-                );
-                cx.notify();
-                return;
-            }
         }
         edit_state.validation_state = ValidationState::None;
         cx.notify();
@@ -1487,11 +1483,20 @@ impl ProjectPanel {
         if filename.trim().is_empty() {
             return None;
         }
-        #[cfg(not(target_os = "windows"))]
-        let filename_indicates_dir = filename.ends_with("/");
-        // On Windows, path separator could be either `/` or `\`.
-        #[cfg(target_os = "windows")]
-        let filename_indicates_dir = filename.ends_with("/") || filename.ends_with("\\");
+
+        let path_style = self.project.read(cx).path_style(cx);
+        let filename_indicates_dir = if path_style.is_windows() {
+            filename.ends_with('/') || filename.ends_with('\\')
+        } else {
+            filename.ends_with('/')
+        };
+        let filename = if path_style.is_windows() {
+            filename.trim_start_matches(&['/', '\\'])
+        } else {
+            filename.trim_start_matches('/')
+        };
+        let filename = RelPath::from_std_path(filename.as_ref(), path_style).ok()?;
+
         edit_state.is_dir =
             edit_state.is_dir || (edit_state.is_new_entry() && filename_indicates_dir);
         let is_dir = edit_state.is_dir;
@@ -1505,26 +1510,22 @@ impl ProjectPanel {
                 worktree_id,
                 entry_id: NEW_ENTRY_ID,
             });
-            let new_path = entry.path.join(filename.trim_start_matches('/'));
-            if worktree
-                .read(cx)
-                .entry_for_path(new_path.as_path())
-                .is_some()
-            {
+            let new_path = entry.path.join(&filename);
+            if worktree.read(cx).entry_for_path(&new_path).is_some() {
                 return None;
             }
 
             edited_entry_id = NEW_ENTRY_ID;
             edit_task = self.project.update(cx, |project, cx| {
-                project.create_entry((worktree_id, &new_path), is_dir, cx)
+                project.create_entry((worktree_id, new_path), is_dir, cx)
             });
         } else {
             let new_path = if let Some(parent) = entry.path.clone().parent() {
                 parent.join(&filename)
             } else {
-                filename.clone().into()
+                filename.clone()
             };
-            if let Some(existing) = worktree.read(cx).entry_for_path(new_path.as_path()) {
+            if let Some(existing) = worktree.read(cx).entry_for_path(&new_path) {
                 if existing.id == entry.id {
                     window.focus(&self.focus_handle);
                 }
@@ -1532,7 +1533,7 @@ impl ProjectPanel {
             }
             edited_entry_id = entry.id;
             edit_task = self.project.update(cx, |project, cx| {
-                project.rename_entry(entry.id, new_path.as_path(), cx)
+                project.rename_entry(entry.id, (worktree_id, new_path).into(), cx)
             });
         };
 
@@ -1793,14 +1794,9 @@ impl ProjectPanel {
                     depth: 0,
                     validation_state: ValidationState::None,
                 });
-                let file_name = entry
-                    .path
-                    .file_name()
-                    .map(|s| s.to_string_lossy())
-                    .unwrap_or_default()
-                    .to_string();
+                let file_name = entry.path.file_name().unwrap_or_default().to_string();
                 let selection = selection.unwrap_or_else(|| {
-                    let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy());
+                    let file_stem = entry.path.file_stem().map(|s| s.to_string());
                     let selection_end =
                         file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
                     0..selection_end
@@ -1854,11 +1850,7 @@ impl ProjectPanel {
                         project.dirty_buffers(cx).any(|path| path == project_path) as usize;
                     Some((
                         selection.entry_id,
-                        project_path
-                            .path
-                            .file_name()?
-                            .to_string_lossy()
-                            .into_owned(),
+                        project_path.path.file_name()?.to_string(),
                     ))
                 })
                 .collect::<Vec<_>>();
@@ -1977,9 +1969,10 @@ impl ProjectPanel {
                     worktree.entry_for_id(a.entry_id),
                     worktree.entry_for_id(b.entry_id),
                 ) {
-                    (Some(a), Some(b)) => {
-                        compare_paths((&a.path, a.is_file()), (&b.path, b.is_file()))
-                    }
+                    (Some(a), Some(b)) => compare_paths(
+                        (a.path.as_std_path(), a.is_file()),
+                        (b.path.as_std_path(), b.is_file()),
+                    ),
                     _ => cmp::Ordering::Equal,
                 }
             })
@@ -2161,7 +2154,7 @@ impl ProjectPanel {
                     && entry.is_file()
                     && self
                         .diagnostics
-                        .get(&(worktree_id, entry.path.to_path_buf()))
+                        .get(&(worktree_id, entry.path.clone()))
                         .is_some_and(|severity| action.severity.matches(*severity))
             },
             cx,
@@ -2197,7 +2190,7 @@ impl ProjectPanel {
                     && entry.is_file()
                     && self
                         .diagnostics
-                        .get(&(worktree_id, entry.path.to_path_buf()))
+                        .get(&(worktree_id, entry.path.clone()))
                         .is_some_and(|severity| action.severity.matches(*severity))
             },
             cx,
@@ -2432,8 +2425,8 @@ impl ProjectPanel {
         source: &SelectedEntry,
         (worktree, target_entry): (Entity<Worktree>, &Entry),
         cx: &App,
-    ) -> Option<(PathBuf, Option<Range<usize>>)> {
-        let mut new_path = target_entry.path.to_path_buf();
+    ) -> Option<(Arc<RelPath>, Option<Range<usize>>)> {
+        let mut new_path = target_entry.path.to_rel_path_buf();
         // If we're pasting into a file, or a directory into itself, go up one level.
         if target_entry.is_file() || (target_entry.is_dir() && target_entry.id == source.entry_id) {
             new_path.pop();
@@ -2444,11 +2437,11 @@ impl ProjectPanel {
             .path_for_entry(source.entry_id, cx)?
             .path
             .file_name()?
-            .to_os_string();
-        new_path.push(&clipboard_entry_file_name);
-        let extension = new_path.extension().map(|e| e.to_os_string());
-        let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?;
-        let file_name_len = file_name_without_extension.to_string_lossy().len();
+            .to_string();
+        new_path.push(RelPath::new(&clipboard_entry_file_name).unwrap());
+        let extension = new_path.extension().map(|s| s.to_string());
+        let file_name_without_extension = new_path.file_stem()?.to_string();
+        let file_name_len = file_name_without_extension.len();
         let mut disambiguation_range = None;
         let mut ix = 0;
         {
@@ -2456,30 +2449,30 @@ impl ProjectPanel {
             while worktree.entry_for_path(&new_path).is_some() {
                 new_path.pop();
 
-                let mut new_file_name = file_name_without_extension.to_os_string();
+                let mut new_file_name = file_name_without_extension.to_string();
 
                 let disambiguation = " copy";
                 let mut disambiguation_len = disambiguation.len();
 
-                new_file_name.push(disambiguation);
+                new_file_name.push_str(disambiguation);
 
                 if ix > 0 {
                     let extra_disambiguation = format!(" {}", ix);
                     disambiguation_len += extra_disambiguation.len();
-
-                    new_file_name.push(extra_disambiguation);
+                    new_file_name.push_str(&extra_disambiguation);
                 }
                 if let Some(extension) = extension.as_ref() {
-                    new_file_name.push(".");
-                    new_file_name.push(extension);
+                    new_file_name.push_str(".");
+                    new_file_name.push_str(extension);
                 }
 
-                new_path.push(new_file_name);
+                new_path.push(RelPath::new(&new_file_name).unwrap());
+
                 disambiguation_range = Some(file_name_len..(file_name_len + disambiguation_len));
                 ix += 1;
             }
         }
-        Some((new_path, disambiguation_range))
+        Some((new_path.as_rel_path().into(), disambiguation_range))
     }
 
     fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
@@ -2491,61 +2484,39 @@ impl ProjectPanel {
                 .clipboard
                 .as_ref()
                 .filter(|clipboard| !clipboard.items().is_empty())?;
+
             enum PasteTask {
                 Rename(Task<Result<CreatedEntry>>),
                 Copy(Task<Result<Option<Entry>>>),
             }
-            let mut paste_entry_tasks: IndexMap<(ProjectEntryId, bool), PasteTask> =
-                IndexMap::default();
+
+            let mut paste_tasks = Vec::new();
             let mut disambiguation_range = None;
             let clip_is_cut = clipboard_entries.is_cut();
             for clipboard_entry in clipboard_entries.items() {
                 let (new_path, new_disambiguation_range) =
                     self.create_paste_path(clipboard_entry, self.selected_sub_entry(cx)?, cx)?;
                 let clip_entry_id = clipboard_entry.entry_id;
-                let is_same_worktree = clipboard_entry.worktree_id == worktree_id;
-                let relative_worktree_source_path = if !is_same_worktree {
-                    let target_base_path = worktree.read(cx).abs_path();
-                    let clipboard_project_path =
-                        self.project.read(cx).path_for_entry(clip_entry_id, cx)?;
-                    let clipboard_abs_path = self
-                        .project
-                        .read(cx)
-                        .absolute_path(&clipboard_project_path, cx)?;
-                    Some(relativize_path(
-                        &target_base_path,
-                        clipboard_abs_path.as_path(),
-                    ))
-                } else {
-                    None
-                };
-                let task = if clip_is_cut && is_same_worktree {
+                let task = if clipboard_entries.is_cut() {
                     let task = self.project.update(cx, |project, cx| {
-                        project.rename_entry(clip_entry_id, new_path, cx)
+                        project.rename_entry(clip_entry_id, (worktree_id, new_path).into(), cx)
                     });
                     PasteTask::Rename(task)
                 } else {
-                    let entry_id = if is_same_worktree {
-                        clip_entry_id
-                    } else {
-                        entry.id
-                    };
                     let task = self.project.update(cx, |project, cx| {
-                        project.copy_entry(entry_id, relative_worktree_source_path, new_path, cx)
+                        project.copy_entry(clip_entry_id, (worktree_id, new_path).into(), cx)
                     });
                     PasteTask::Copy(task)
                 };
-                let needs_delete = !is_same_worktree && clip_is_cut;
-                paste_entry_tasks.insert((clip_entry_id, needs_delete), task);
+                paste_tasks.push(task);
                 disambiguation_range = new_disambiguation_range.or(disambiguation_range);
             }
 
-            let item_count = paste_entry_tasks.len();
+            let item_count = paste_tasks.len();
 
             cx.spawn_in(window, async move |project_panel, cx| {
                 let mut last_succeed = None;
-                let mut need_delete_ids = Vec::new();
-                for ((entry_id, need_delete), task) in paste_entry_tasks.into_iter() {
+                for task in paste_tasks {
                     match task {
                         PasteTask::Rename(task) => {
                             if let Some(CreatedEntry::Included(entry)) = task.await.log_err() {
@@ -2555,24 +2526,10 @@ impl ProjectPanel {
                         PasteTask::Copy(task) => {
                             if let Some(Some(entry)) = task.await.log_err() {
                                 last_succeed = Some(entry);
-                                if need_delete {
-                                    need_delete_ids.push(entry_id);
-                                }
                             }
                         }
                     }
                 }
-                // remove entry for cut in difference worktree
-                for entry_id in need_delete_ids {
-                    project_panel
-                        .update(cx, |project_panel, cx| {
-                            project_panel
-                                .project
-                                .update(cx, |project, cx| project.delete_entry(entry_id, true, cx))
-                                .context("no such entry")
-                        })??
-                        .await?;
-                }
                 // update selection
                 if let Some(entry) = last_succeed {
                     project_panel
@@ -2639,8 +2596,7 @@ impl ProjectPanel {
                         project
                             .worktree_for_id(entry.worktree_id, cx)?
                             .read(cx)
-                            .abs_path()
-                            .join(entry_path)
+                            .absolutize(&entry_path)
                             .to_string_lossy()
                             .to_string(),
                     )
@@ -2658,6 +2614,7 @@ impl ProjectPanel {
         _: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        let path_style = self.project.read(cx).path_style(cx);
         let file_paths = {
             let project = self.project.read(cx);
             self.effective_entries()
@@ -2667,8 +2624,8 @@ impl ProjectPanel {
                         project
                             .path_for_entry(entry.entry_id, cx)?
                             .path
-                            .to_string_lossy()
-                            .to_string(),
+                            .display(path_style)
+                            .into_owned(),
                     )
                 })
                 .collect::<Vec<_>>()
@@ -2685,7 +2642,7 @@ impl ProjectPanel {
         cx: &mut Context<Self>,
     ) {
         if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
-            cx.reveal_path(&worktree.read(cx).abs_path().join(&entry.path));
+            cx.reveal_path(&worktree.read(cx).absolutize(&entry.path));
         }
     }
 
@@ -2713,7 +2670,7 @@ impl ProjectPanel {
                 if !entry.is_file() {
                     return None;
                 }
-                worktree.read(cx).absolutize(&entry.path).ok()
+                Some(worktree.read(cx).absolutize(&entry.path))
             })
             .rev();
 
@@ -2741,7 +2698,7 @@ impl ProjectPanel {
 
     fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context<Self>) {
         if let Some((worktree, entry)) = self.selected_entry(cx) {
-            let abs_path = worktree.abs_path().join(&entry.path);
+            let abs_path = worktree.absolutize(&entry.path);
             cx.open_with_system(&abs_path);
         }
     }
@@ -2754,14 +2711,14 @@ impl ProjectPanel {
     ) {
         if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
             let abs_path = match &entry.canonical_path {
-                Some(canonical_path) => Some(canonical_path.to_path_buf()),
-                None => worktree.read(cx).absolutize(&entry.path).ok(),
+                Some(canonical_path) => canonical_path.to_path_buf(),
+                None => worktree.read(cx).absolutize(&entry.path),
             };
 
             let working_directory = if entry.is_dir() {
-                abs_path
+                Some(abs_path)
             } else {
-                abs_path.and_then(|path| Some(path.parent()?.to_path_buf()))
+                abs_path.parent().map(|path| path.to_path_buf())
             };
             if let Some(working_directory) = working_directory {
                 window.dispatch_action(
@@ -2791,7 +2748,7 @@ impl ProjectPanel {
                             .update(cx, |workspace, cx| {
                                 search::ProjectSearchView::new_search_in_directory(
                                     workspace,
-                                    Path::new(""),
+                                    RelPath::empty(),
                                     window,
                                     cx,
                                 );
@@ -2804,9 +2761,7 @@ impl ProjectPanel {
 
             let include_root = self.project.read(cx).visible_worktrees(cx).count() > 1;
             let dir_path = if include_root {
-                let mut full_path = PathBuf::from(worktree.read(cx).root_name());
-                full_path.push(&dir_path);
-                Arc::from(full_path)
+                worktree.read(cx).root_name().join(&dir_path)
             } else {
                 dir_path
             };
@@ -2865,35 +2820,40 @@ impl ProjectPanel {
     fn move_worktree_entry(
         &mut self,
         entry_to_move: ProjectEntryId,
-        destination: ProjectEntryId,
+        destination_entry: ProjectEntryId,
         destination_is_file: bool,
         cx: &mut Context<Self>,
     ) {
-        if entry_to_move == destination {
+        if entry_to_move == destination_entry {
             return;
         }
 
         let destination_worktree = self.project.update(cx, |project, cx| {
-            let entry_path = project.path_for_entry(entry_to_move, cx)?;
-            let destination_entry_path = project.path_for_entry(destination, cx)?.path;
+            let source_path = project.path_for_entry(entry_to_move, cx)?;
+            let destination_path = project.path_for_entry(destination_entry, cx)?;
+            let destination_worktree_id = destination_path.worktree_id;
 
-            let mut destination_path = destination_entry_path.as_ref();
+            let mut destination_path = destination_path.path.as_ref();
             if destination_is_file {
                 destination_path = destination_path.parent()?;
             }
 
-            let mut new_path = destination_path.to_path_buf();
-            new_path.push(entry_path.path.file_name()?);
-            if new_path != entry_path.path.as_ref() {
-                let task = project.rename_entry(entry_to_move, new_path, cx);
+            let mut new_path = destination_path.to_rel_path_buf();
+            new_path.push(RelPath::new(source_path.path.file_name()?).unwrap());
+            if new_path.as_rel_path() != source_path.path.as_ref() {
+                let task = project.rename_entry(
+                    entry_to_move,
+                    (destination_worktree_id, new_path).into(),
+                    cx,
+                );
                 cx.foreground_executor().spawn(task).detach_and_log_err(cx);
             }
 
-            project.worktree_id_for_entry(destination, cx)
+            project.worktree_id_for_entry(destination_entry, cx)
         });
 
         if let Some(destination_worktree) = destination_worktree {
-            self.expand_entry(destination_worktree, destination, cx);
+            self.expand_entry(destination_worktree, destination_entry, cx);
         }
     }
 
@@ -3047,7 +3007,7 @@ impl ProjectPanel {
             entry: Entry {
                 id: NEW_ENTRY_ID,
                 kind: new_entry_kind,
-                path: parent_entry.path.join("\0").into(),
+                path: parent_entry.path.join(RelPath::new("\0").unwrap()),
                 inode: 0,
                 mtime: parent_entry.mtime,
                 size: parent_entry.size,
@@ -3193,55 +3153,57 @@ impl ProjectPanel {
                     ));
                 }
                 let worktree_abs_path = worktree.read(cx).abs_path();
-                let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
-                    let Some(path_name) = worktree_abs_path.file_name() else {
-                        continue;
-                    };
-                    let path = ArcCow::Borrowed(Path::new(path_name));
-                    let depth = 0;
-                    (depth, path)
-                } else if entry.is_file() {
-                    let Some(path_name) = entry
-                        .path
-                        .file_name()
-                        .with_context(|| format!("Non-root entry has no file name: {entry:?}"))
-                        .log_err()
-                    else {
-                        continue;
-                    };
-                    let path = ArcCow::Borrowed(Path::new(path_name));
-                    let depth = entry.path.ancestors().count() - 1;
-                    (depth, path)
-                } else {
-                    let path = self
-                        .ancestors
-                        .get(&entry.id)
-                        .and_then(|ancestors| {
-                            let outermost_ancestor = ancestors.ancestors.last()?;
-                            let root_folded_entry = worktree
-                                .read(cx)
-                                .entry_for_id(*outermost_ancestor)?
-                                .path
-                                .as_ref();
-                            entry
-                                .path
-                                .strip_prefix(root_folded_entry)
-                                .ok()
-                                .and_then(|suffix| {
-                                    let full_path = Path::new(root_folded_entry.file_name()?);
-                                    Some(ArcCow::Owned(Arc::<Path>::from(full_path.join(suffix))))
+                let (depth, chars) =
+                    if Some(entry.entry) == worktree.read(cx).root_entry() {
+                        let Some(path_name) = worktree_abs_path.file_name() else {
+                            continue;
+                        };
+                        let depth = 0;
+                        (depth, path_name.to_string_lossy().chars().count())
+                    } else if entry.is_file() {
+                        let Some(path_name) = entry
+                            .path
+                            .file_name()
+                            .with_context(|| format!("Non-root entry has no file name: {entry:?}"))
+                            .log_err()
+                        else {
+                            continue;
+                        };
+                        let depth = entry.path.ancestors().count() - 1;
+                        (depth, path_name.chars().count())
+                    } else {
+                        let path =
+                            self.ancestors
+                                .get(&entry.id)
+                                .and_then(|ancestors| {
+                                    let outermost_ancestor = ancestors.ancestors.last()?;
+                                    let root_folded_entry = worktree
+                                        .read(cx)
+                                        .entry_for_id(*outermost_ancestor)?
+                                        .path
+                                        .as_ref();
+                                    entry.path.strip_prefix(root_folded_entry).ok().and_then(
+                                        |suffix| {
+                                            Some(
+                                                RelPath::new(root_folded_entry.file_name()?)
+                                                    .unwrap()
+                                                    .join(suffix),
+                                            )
+                                        },
+                                    )
                                 })
-                        })
-                        .or_else(|| entry.path.file_name().map(Path::new).map(ArcCow::Borrowed))
-                        .unwrap_or_else(|| ArcCow::Owned(entry.path.clone()));
-                    let depth = path.components().count();
-                    (depth, path)
-                };
-                let width_estimate = item_width_estimate(
-                    depth,
-                    path.to_string_lossy().chars().count(),
-                    entry.canonical_path.is_some(),
-                );
+                                .or_else(|| {
+                                    entry
+                                        .path
+                                        .file_name()
+                                        .map(|file_name| RelPath::new(file_name).unwrap().into())
+                                })
+                                .unwrap_or_else(|| entry.path.clone());
+                        let depth = path.components().count();
+                        (depth, path.as_str().chars().count())
+                    };
+                let width_estimate =
+                    item_width_estimate(depth, chars, entry.canonical_path.is_some());
 
                 match max_width_item.as_mut() {
                     Some((id, worktree_id, width)) => {
@@ -3361,9 +3323,9 @@ impl ProjectPanel {
             let entry = worktree.read(cx).entry_for_id(entry_id)?;
             let path = entry.path.clone();
             let target_directory = if entry.is_dir() {
-                path.to_path_buf()
+                path
             } else {
-                path.parent()?.to_path_buf()
+                path.parent()?.into()
             };
             Some((target_directory, worktree, fs))
         }) else {
@@ -3372,11 +3334,12 @@ impl ProjectPanel {
 
         let mut paths_to_replace = Vec::new();
         for path in &paths {
-            if let Some(name) = path.file_name() {
-                let mut target_path = target_directory.clone();
-                target_path.push(name);
-                if target_path.exists() {
-                    paths_to_replace.push((name.to_string_lossy().to_string(), path.clone()));
+            if let Some(name) = path.file_name()
+                && let Some(name) = name.to_str()
+            {
+                let target_path = target_directory.join(RelPath::new(name).unwrap());
+                if worktree.read(cx).entry_for_path(&target_path).is_some() {
+                    paths_to_replace.push((name.to_string(), path.clone()));
                 }
             }
         }
@@ -3406,7 +3369,7 @@ impl ProjectPanel {
                 }
 
                 let task = worktree.update( cx, |worktree, cx| {
-                    worktree.copy_external_entries(target_directory.into(), paths, fs, cx)
+                    worktree.copy_external_entries(target_directory, paths, fs, cx)
                 })?;
 
                 let opened_entries = task.await.with_context(|| "failed to copy external paths")?;
@@ -3472,7 +3435,7 @@ impl ProjectPanel {
                     )?;
 
                     let task = self.project.update(cx, |project, cx| {
-                        project.copy_entry(selection.entry_id, None, new_path, cx)
+                        project.copy_entry(selection.entry_id, (worktree_id, new_path).into(), cx)
                     });
                     copy_tasks.push(task);
                     disambiguation_range = new_disambiguation_range.or(disambiguation_range);
@@ -3559,7 +3522,7 @@ impl ProjectPanel {
         mut callback: impl FnMut(
             &Entry,
             usize,
-            &HashSet<Arc<Path>>,
+            &HashSet<Arc<RelPath>>,
             &mut Window,
             &mut Context<ProjectPanel>,
         ),
@@ -3618,7 +3581,7 @@ impl ProjectPanel {
                 .worktree_for_id(visible.worktree_id, cx)
             {
                 let snapshot = worktree.read(cx).snapshot();
-                let root_name = OsStr::new(snapshot.root_name());
+                let root_name = snapshot.root_name();
 
                 let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
                 let entries = visible
@@ -3672,7 +3635,7 @@ impl ProjectPanel {
                                         .take(prefix_components)
                                         .collect::<PathBuf>();
                                     if let Some(last_component) =
-                                        Path::new(processing_filename).components().next_back()
+                                        processing_filename.components().next_back()
                                     {
                                         new_path.push(last_component);
                                         previous_components.next();
@@ -3687,7 +3650,7 @@ impl ProjectPanel {
                                     }
                                 } else {
                                     details.filename.clear();
-                                    details.filename.push_str(processing_filename);
+                                    details.filename.push_str(processing_filename.as_str());
                                 }
                             } else {
                                 if edit_state.is_new_entry() {
@@ -3953,7 +3916,7 @@ impl ProjectPanel {
 
     fn calculate_depth_and_difference(
         entry: &Entry,
-        visible_worktree_entries: &HashSet<Arc<Path>>,
+        visible_worktree_entries: &HashSet<Arc<RelPath>>,
     ) -> (usize, usize) {
         let (depth, difference) = entry
             .path
@@ -4119,6 +4082,7 @@ impl ProjectPanel {
             .canonical_path
             .as_ref()
             .map(|f| f.to_string_lossy().to_string());
+        let path_style = self.project.read(cx).path_style(cx);
         let path = details.path.clone();
         let path_for_external_paths = path.clone();
         let path_for_dragged_selection = path.clone();
@@ -4578,8 +4542,7 @@ impl ProjectPanel {
                                         .collect::<Vec<_>>();
                                     let active_index = folded_ancestors.active_index();
                                     let components_len = components.len();
-                                        const DELIMITER: SharedString =
-                                        SharedString::new_static(std::path::MAIN_SEPARATOR_STR);
+                                    let delimiter = SharedString::new(path_style.separator());
                                     for (index, component) in components.iter().enumerate() {
                                         if index != 0 {
                                                 let delimiter_target_index = index - 1;
@@ -4623,7 +4586,7 @@ impl ProjectPanel {
                                                         )))
                                                     })
                                                     .child(
-                                                        Label::new(DELIMITER.clone())
+                                                        Label::new(delimiter.clone())
                                                             .single_line()
                                                             .color(filename_text_color)
                                                     )
@@ -4769,8 +4732,8 @@ impl ProjectPanel {
         &self,
         entry: &Entry,
         worktree_id: WorktreeId,
-        root_name: &OsStr,
-        entries_paths: &HashSet<Arc<Path>>,
+        root_name: &RelPath,
+        entries_paths: &HashSet<Arc<RelPath>>,
         git_status: GitSummary,
         sticky: Option<StickyDetails>,
         _window: &mut Window,
@@ -4791,37 +4754,37 @@ impl ProjectPanel {
         let icon = match entry.kind {
             EntryKind::File => {
                 if show_file_icons {
-                    FileIcons::get_icon(&entry.path, cx)
+                    FileIcons::get_icon(entry.path.as_std_path(), cx)
                 } else {
                     None
                 }
             }
             _ => {
                 if show_folder_icons {
-                    FileIcons::get_folder_icon(is_expanded, &entry.path, cx)
+                    FileIcons::get_folder_icon(is_expanded, entry.path.as_std_path(), cx)
                 } else {
                     FileIcons::get_chevron_icon(is_expanded, cx)
                 }
             }
         };
 
+        let path_style = self.project.read(cx).path_style(cx);
         let (depth, difference) =
             ProjectPanel::calculate_depth_and_difference(entry, entries_paths);
 
-        let filename = match difference {
-            diff if diff > 1 => entry
+        let filename = if difference > 1 {
+            entry
                 .path
-                .iter()
-                .skip(entry.path.components().count() - diff)
-                .collect::<PathBuf>()
-                .to_str()
-                .unwrap_or_default()
-                .to_string(),
-            _ => entry
+                .last_n_components(difference)
+                .map_or(String::new(), |suffix| {
+                    suffix.display(path_style).to_string()
+                })
+        } else {
+            entry
                 .path
                 .file_name()
-                .map(|name| name.to_string_lossy().into_owned())
-                .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
+                .map(|name| name.to_string())
+                .unwrap_or_else(|| root_name.as_str().to_string())
         };
 
         let selection = SelectedEntry {
@@ -4833,7 +4796,7 @@ impl ProjectPanel {
 
         let diagnostic_severity = self
             .diagnostics
-            .get(&(worktree_id, entry.path.to_path_buf()))
+            .get(&(worktree_id, entry.path.clone()))
             .cloned();
 
         let filename_text_color =
@@ -5048,7 +5011,7 @@ impl ProjectPanel {
 
         let panel_settings = ProjectPanelSettings::get_global(cx);
         let git_status_enabled = panel_settings.git_status;
-        let root_name = OsStr::new(worktree.root_name());
+        let root_name = worktree.root_name();
 
         let git_summaries_by_id = if git_status_enabled {
             visible

crates/project_panel/src/project_panel_tests.rs 🔗

@@ -6,7 +6,7 @@ use project::FakeFs;
 use serde_json::json;
 use settings::SettingsStore;
 use std::path::{Path, PathBuf};
-use util::path;
+use util::{path, paths::PathStyle, rel_path::rel_path};
 use workspace::{
     AppState, ItemHandle, Pane,
     item::{Item, ProjectItem},
@@ -978,7 +978,7 @@ async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
             "    > a",
             "    > b",
             "    > C",
-            "      [PROCESSING: '/bdir1/dir2/the-new-filename']  <== selected",
+            "      [PROCESSING: 'bdir1/dir2/the-new-filename']  <== selected",
             "      .dockerignore",
             "v root2",
             "    > d",
@@ -1068,7 +1068,7 @@ async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) {
         &[
             "v root1",
             "    > .git",
-            "      [PROCESSING: 'new_dir/']  <== selected",
+            "      [PROCESSING: 'new_dir']  <== selected",
             "      .dockerignore",
         ]
     );
@@ -1992,7 +1992,7 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
         })
         .unwrap();
 
-    select_path(&panel, "src/", cx);
+    select_path(&panel, "src", cx);
     panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
     cx.executor().run_until_parked();
     assert_eq!(
@@ -2045,7 +2045,7 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
         "File list should be unchanged after failed folder create confirmation"
     );
 
-    select_path(&panel, "src/test/", cx);
+    select_path(&panel, "src/test", cx);
     panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
     cx.executor().run_until_parked();
     assert_eq!(
@@ -2193,20 +2193,20 @@ async fn test_select_git_entry(cx: &mut gpui::TestAppContext) {
     .await;
 
     // Mark files as git modified
-    fs.set_git_content_for_repo(
+    fs.set_head_and_index_for_repo(
         path!("/root/tree1/.git").as_ref(),
         &[
-            ("dir1/modified1.txt".into(), "modified".into(), None),
-            ("dir1/modified2.txt".into(), "modified".into(), None),
-            ("modified4.txt".into(), "modified".into(), None),
-            ("dir2/modified3.txt".into(), "modified".into(), None),
+            ("dir1/modified1.txt", "modified".into()),
+            ("dir1/modified2.txt", "modified".into()),
+            ("modified4.txt", "modified".into()),
+            ("dir2/modified3.txt", "modified".into()),
         ],
     );
-    fs.set_git_content_for_repo(
+    fs.set_head_and_index_for_repo(
         path!("/root/tree2/.git").as_ref(),
         &[
-            ("dir3/modified5.txt".into(), "modified".into(), None),
-            ("modified6.txt".into(), "modified".into(), None),
+            ("dir3/modified5.txt", "modified".into()),
+            ("modified6.txt", "modified".into()),
         ],
     );
 
@@ -3178,7 +3178,7 @@ async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
             let target_entry = this
                 .project
                 .read(cx)
-                .entry_for_path(&(worktree_id, "").into(), cx)
+                .entry_for_path(&(worktree_id, rel_path("")).into(), cx)
                 .unwrap();
             this.drag_onto(&drag, target_entry.id, false, window, cx);
         });
@@ -3322,7 +3322,7 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
             .next()
             .unwrap()
             .read(cx)
-            .entry_for_path("target_destination")
+            .entry_for_path(rel_path("target_destination"))
             .unwrap();
         panel.drag_onto(&drag, target_entry.id, false, window, cx);
     });
@@ -3355,7 +3355,7 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
             .next()
             .unwrap()
             .read(cx)
-            .entry_for_path("a/b/c")
+            .entry_for_path(rel_path("a/b/c"))
             .unwrap();
         panel.drag_onto(&drag, target_entry.id, false, window, cx);
     });
@@ -3378,7 +3378,7 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
             .next()
             .unwrap()
             .read(cx)
-            .entry_for_path("target_destination")
+            .entry_for_path(rel_path("target_destination"))
             .unwrap();
         panel.drag_onto(&drag, target_entry.id, false, window, cx);
     });
@@ -3407,7 +3407,7 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
             .next()
             .unwrap()
             .read(cx)
-            .entry_for_path("a")
+            .entry_for_path(rel_path("a"))
             .unwrap();
         panel.drag_onto(&drag, target_entry.id, false, window, cx);
     });
@@ -3430,7 +3430,7 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
             .next()
             .unwrap()
             .read(cx)
-            .entry_for_path("target_destination")
+            .entry_for_path(rel_path("target_destination"))
             .unwrap();
         panel.drag_onto(&drag, target_entry.id, false, window, cx);
     });
@@ -4098,7 +4098,7 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
                 .clone();
             assert_eq!(
                 active_entry_path.path.as_ref(),
-                Path::new(excluded_file_path),
+                rel_path(excluded_file_path),
                 "Should open the excluded file"
             );
 
@@ -4228,7 +4228,7 @@ async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppC
         })
         .unwrap();
 
-    select_path(&panel, "src/", cx);
+    select_path(&panel, "src", cx);
     panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
     cx.executor().run_until_parked();
     assert_eq!(
@@ -5126,12 +5126,8 @@ async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestApp
     );
 }
 
-fn toggle_expand_dir(
-    panel: &Entity<ProjectPanel>,
-    path: impl AsRef<Path>,
-    cx: &mut VisualTestContext,
-) {
-    let path = path.as_ref();
+fn toggle_expand_dir(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
+    let path = rel_path(path);
     panel.update_in(cx, |panel, window, cx| {
         for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
             let worktree = worktree.read(cx);
@@ -5764,7 +5760,7 @@ async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) {
         let worktree = worktree.read(cx);
 
         // Test 1: Target is a directory, should highlight the directory itself
-        let dir_entry = worktree.entry_for_path("dir1").unwrap();
+        let dir_entry = worktree.entry_for_path(rel_path("dir1")).unwrap();
         let result = panel.highlight_entry_for_external_drag(dir_entry, worktree);
         assert_eq!(
             result,
@@ -5773,8 +5769,10 @@ async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) {
         );
 
         // Test 2: Target is nested file, should highlight immediate parent
-        let nested_file = worktree.entry_for_path("dir1/dir2/file2.txt").unwrap();
-        let nested_parent = worktree.entry_for_path("dir1/dir2").unwrap();
+        let nested_file = worktree
+            .entry_for_path(rel_path("dir1/dir2/file2.txt"))
+            .unwrap();
+        let nested_parent = worktree.entry_for_path(rel_path("dir1/dir2")).unwrap();
         let result = panel.highlight_entry_for_external_drag(nested_file, worktree);
         assert_eq!(
             result,
@@ -5783,7 +5781,7 @@ async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) {
         );
 
         // Test 3: Target is root level file, should highlight root
-        let root_file = worktree.entry_for_path("file3.txt").unwrap();
+        let root_file = worktree.entry_for_path(rel_path("file3.txt")).unwrap();
         let result = panel.highlight_entry_for_external_drag(root_file, worktree);
         assert_eq!(
             result,
@@ -5835,16 +5833,20 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext)
         let worktree_id = worktree.read(cx).id();
         let worktree = worktree.read(cx);
 
-        let parent_dir = worktree.entry_for_path("parent_dir").unwrap();
+        let parent_dir = worktree.entry_for_path(rel_path("parent_dir")).unwrap();
         let child_file = worktree
-            .entry_for_path("parent_dir/child_file.txt")
+            .entry_for_path(rel_path("parent_dir/child_file.txt"))
             .unwrap();
         let sibling_file = worktree
-            .entry_for_path("parent_dir/sibling_file.txt")
+            .entry_for_path(rel_path("parent_dir/sibling_file.txt"))
+            .unwrap();
+        let child_dir = worktree
+            .entry_for_path(rel_path("parent_dir/child_dir"))
+            .unwrap();
+        let other_dir = worktree.entry_for_path(rel_path("other_dir")).unwrap();
+        let other_file = worktree
+            .entry_for_path(rel_path("other_dir/other_file.txt"))
             .unwrap();
-        let child_dir = worktree.entry_for_path("parent_dir/child_dir").unwrap();
-        let other_dir = worktree.entry_for_path("other_dir").unwrap();
-        let other_file = worktree.entry_for_path("other_dir/other_file.txt").unwrap();
 
         // Test 1: Single item drag, don't highlight parent directory
         let dragged_selection = DraggedSelection {
@@ -5969,11 +5971,17 @@ async fn test_highlight_entry_for_selection_drag_cross_worktree(cx: &mut gpui::T
         let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
 
         let worktree_a = &worktrees[0];
-        let main_rs_from_a = worktree_a.read(cx).entry_for_path("src/main.rs").unwrap();
+        let main_rs_from_a = worktree_a
+            .read(cx)
+            .entry_for_path(rel_path("src/main.rs"))
+            .unwrap();
 
         let worktree_b = &worktrees[1];
-        let src_dir_from_b = worktree_b.read(cx).entry_for_path("src").unwrap();
-        let main_rs_from_b = worktree_b.read(cx).entry_for_path("src/main.rs").unwrap();
+        let src_dir_from_b = worktree_b.read(cx).entry_for_path(rel_path("src")).unwrap();
+        let main_rs_from_b = worktree_b
+            .read(cx)
+            .entry_for_path(rel_path("src/main.rs"))
+            .unwrap();
 
         // Test dragging file from worktree A onto parent of file with same relative path in worktree B
         let dragged_selection = DraggedSelection {
@@ -6058,14 +6066,14 @@ async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::Test
 
         let root1_entry = worktree1.root_entry().unwrap();
         let root2_entry = worktree2.root_entry().unwrap();
-        let _parent_dir = worktree1.entry_for_path("parent_dir").unwrap();
+        let _parent_dir = worktree1.entry_for_path(rel_path("parent_dir")).unwrap();
         let child_file = worktree1
-            .entry_for_path("parent_dir/child_file.txt")
+            .entry_for_path(rel_path("parent_dir/child_file.txt"))
             .unwrap();
         let nested_file = worktree1
-            .entry_for_path("parent_dir/nested_dir/nested_file.txt")
+            .entry_for_path(rel_path("parent_dir/nested_dir/nested_file.txt"))
             .unwrap();
-        let root_file = worktree1.entry_for_path("root_file.txt").unwrap();
+        let root_file = worktree1.entry_for_path(rel_path("root_file.txt")).unwrap();
 
         // Test 1: Multiple entries - should always highlight background
         let multiple_dragged_selection = DraggedSelection {
@@ -6368,8 +6376,8 @@ async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) {
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
     let panel = workspace.update(cx, ProjectPanel::new).unwrap();
 
-    let file1_path = path!("root/file1.txt");
-    let file2_path = path!("root/file2.txt");
+    let file1_path = "root/file1.txt";
+    let file2_path = "root/file2.txt";
     select_path_with_mark(&panel, file1_path, cx);
     select_path_with_mark(&panel, file2_path, cx);
 
@@ -6395,7 +6403,11 @@ async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) {
             assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt");
             assert_eq!(
                 diff_view.tab_tooltip_text(cx).unwrap(),
-                format!("{} ↔ {}", file1_path, file2_path)
+                format!(
+                    "{} ↔ {}",
+                    rel_path(file1_path).display(PathStyle::local()),
+                    rel_path(file2_path).display(PathStyle::local())
+                )
             );
         })
         .unwrap();
@@ -6526,8 +6538,8 @@ async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) {
     }
 }
 
-fn select_path(panel: &Entity<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
-    let path = path.as_ref();
+fn select_path(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
+    let path = rel_path(path);
     panel.update(cx, |panel, cx| {
         for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
             let worktree = worktree.read(cx);
@@ -6544,12 +6556,8 @@ fn select_path(panel: &Entity<ProjectPanel>, path: impl AsRef<Path>, cx: &mut Vi
     });
 }
 
-fn select_path_with_mark(
-    panel: &Entity<ProjectPanel>,
-    path: impl AsRef<Path>,
-    cx: &mut VisualTestContext,
-) {
-    let path = path.as_ref();
+fn select_path_with_mark(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
+    let path = rel_path(path);
     panel.update(cx, |panel, cx| {
         for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
             let worktree = worktree.read(cx);
@@ -6572,10 +6580,10 @@ fn select_path_with_mark(
 
 fn find_project_entry(
     panel: &Entity<ProjectPanel>,
-    path: impl AsRef<Path>,
+    path: &str,
     cx: &mut VisualTestContext,
 ) -> Option<ProjectEntryId> {
-    let path = path.as_ref();
+    let path = rel_path(path);
     panel.update(cx, |panel, cx| {
         for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
             let worktree = worktree.read(cx);
@@ -6713,7 +6721,7 @@ fn ensure_single_file_is_opened(
                 open_project_paths,
                 vec![ProjectPath {
                     worktree_id,
-                    path: Arc::from(Path::new(expected_path))
+                    path: Arc::from(rel_path(expected_path))
                 }],
                 "Should have opened file, selected in project panel"
             );

crates/project_symbols/src/project_symbols.rs 🔗

@@ -6,9 +6,9 @@ use gpui::{
 };
 use ordered_float::OrderedFloat;
 use picker::{Picker, PickerDelegate};
-use project::{Project, Symbol};
+use project::{Project, Symbol, lsp_store::SymbolLocation};
 use settings::Settings;
-use std::{borrow::Cow, cmp::Reverse, sync::Arc};
+use std::{cmp::Reverse, sync::Arc};
 use theme::{ActiveTheme, ThemeSettings};
 use util::ResultExt;
 use workspace::{
@@ -195,9 +195,13 @@ impl PickerDelegate for ProjectSymbolsDelegate {
                             StringMatchCandidate::new(id, symbol.label.filter_text())
                         })
                         .partition(|candidate| {
-                            project
-                                .entry_for_path(&symbols[candidate.id].path, cx)
-                                .is_some_and(|e| !e.is_ignored)
+                            if let SymbolLocation::InProject(path) = &symbols[candidate.id].path {
+                                project
+                                    .entry_for_path(path, cx)
+                                    .is_some_and(|e| !e.is_ignored)
+                            } else {
+                                false
+                            }
                         });
 
                     delegate.visible_match_candidates = visible_match_candidates;
@@ -217,22 +221,27 @@ impl PickerDelegate for ProjectSymbolsDelegate {
         _window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
+        let path_style = self.project.read(cx).path_style(cx);
         let string_match = &self.matches.get(ix)?;
         let symbol = &self.symbols.get(string_match.candidate_id)?;
         let syntax_runs = styled_runs_for_code_label(&symbol.label, cx.theme().syntax());
 
-        let mut path = symbol.path.path.to_string_lossy();
-        if self.show_worktree_root_name {
-            let project = self.project.read(cx);
-            if let Some(worktree) = project.worktree_for_id(symbol.path.worktree_id, cx) {
-                path = Cow::Owned(format!(
-                    "{}{}{}",
-                    worktree.read(cx).root_name(),
-                    std::path::MAIN_SEPARATOR,
-                    path.as_ref()
-                ));
+        let path = match &symbol.path {
+            SymbolLocation::InProject(project_path) => {
+                let project = self.project.read(cx);
+                let mut path = project_path.path.clone();
+                if self.show_worktree_root_name
+                    && let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
+                {
+                    path = worktree.read(cx).root_name().join(&path);
+                }
+                path.display(path_style).into_owned().into()
             }
-        }
+            SymbolLocation::OutsideProject {
+                abs_path,
+                signature: _,
+            } => abs_path.to_string_lossy(),
+        };
         let label = symbol.label.text.clone();
         let path = path.to_string();
 

crates/prompt_store/src/prompts.rs 🔗

@@ -14,7 +14,7 @@ use std::{
     time::Duration,
 };
 use text::LineEnding;
-use util::{ResultExt, get_system_shell};
+use util::{ResultExt, get_system_shell, rel_path::RelPath};
 
 use crate::UserPromptId;
 
@@ -80,7 +80,7 @@ pub struct WorktreeContext {
 
 #[derive(Debug, Clone, Eq, PartialEq, Serialize)]
 pub struct RulesFileContext {
-    pub path_in_worktree: Arc<Path>,
+    pub path_in_worktree: Arc<RelPath>,
     pub text: String,
     // This used for opening rules files. TODO: Since it isn't related to prompt templating, this
     // should be moved elsewhere.
@@ -447,6 +447,7 @@ impl PromptBuilder {
 mod test {
     use super::*;
     use serde_json;
+    use util::rel_path::rel_path;
     use uuid::Uuid;
 
     #[test]
@@ -455,7 +456,7 @@ mod test {
             root_name: "path".into(),
             abs_path: Path::new("/path/to/root").into(),
             rules_file: Some(RulesFileContext {
-                path_in_worktree: Path::new(".rules").into(),
+                path_in_worktree: rel_path(".rules").into(),
                 text: "".into(),
                 project_entry_id: 0,
             }),

crates/proto/proto/call.proto 🔗

@@ -173,6 +173,7 @@ message ShareProject {
     repeated WorktreeMetadata worktrees = 2;
     reserved 3;
     bool is_ssh_project = 4;
+    optional bool windows_paths = 5;
 }
 
 message ShareProjectResponse {
@@ -202,6 +203,7 @@ message JoinProjectResponse {
     repeated LanguageServer language_servers = 4;
     repeated string language_server_capabilities = 8;
     ChannelRole role = 6;
+    bool windows_paths = 9;
     reserved 7;
 }
 

crates/proto/proto/worktree.proto 🔗

@@ -98,13 +98,15 @@ message RenameProjectEntry {
     uint64 project_id = 1;
     uint64 entry_id = 2;
     string new_path = 3;
+    uint64 new_worktree_id = 4;
 }
 
 message CopyProjectEntry {
     uint64 project_id = 1;
     uint64 entry_id = 2;
     string new_path = 3;
-    optional string relative_worktree_source_path = 4;
+    uint64 new_worktree_id = 5;
+    reserved 4;
 }
 
 message DeleteProjectEntry {

crates/proto/src/proto.rs 🔗

@@ -873,24 +873,4 @@ mod tests {
         };
         assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id);
     }
-
-    #[test]
-    #[cfg(target_os = "windows")]
-    fn test_proto() {
-        use std::path::PathBuf;
-
-        fn generate_proto_path(path: PathBuf) -> PathBuf {
-            let proto = path.to_proto();
-            PathBuf::from_proto(proto)
-        }
-
-        let path = PathBuf::from("C:\\foo\\bar");
-        assert_eq!(path, generate_proto_path(path.clone()));
-
-        let path = PathBuf::from("C:/foo/bar/");
-        assert_eq!(path, generate_proto_path(path.clone()));
-
-        let path = PathBuf::from("C:/foo\\bar\\");
-        assert_eq!(path, generate_proto_path(path.clone()));
-    }
 }

crates/proto/src/typed_envelope.rs 🔗

@@ -5,8 +5,6 @@ use std::{
     any::{Any, TypeId},
     cmp,
     fmt::{self, Debug},
-    path::{Path, PathBuf},
-    sync::Arc,
 };
 use std::{marker::PhantomData, time::Instant};
 
@@ -171,57 +169,6 @@ impl fmt::Display for PeerId {
     }
 }
 
-pub trait FromProto {
-    fn from_proto(proto: String) -> Self;
-}
-
-pub trait ToProto {
-    fn to_proto(self) -> String;
-}
-
-#[inline]
-fn from_proto_path(proto: String) -> PathBuf {
-    #[cfg(target_os = "windows")]
-    let proto = proto.replace('/', "\\");
-
-    PathBuf::from(proto)
-}
-
-#[inline]
-fn to_proto_path(path: &Path) -> String {
-    #[cfg(target_os = "windows")]
-    let proto = path.to_string_lossy().replace('\\', "/");
-
-    #[cfg(not(target_os = "windows"))]
-    let proto = path.to_string_lossy().to_string();
-
-    proto
-}
-
-impl FromProto for PathBuf {
-    fn from_proto(proto: String) -> Self {
-        from_proto_path(proto)
-    }
-}
-
-impl FromProto for Arc<Path> {
-    fn from_proto(proto: String) -> Self {
-        from_proto_path(proto).into()
-    }
-}
-
-impl ToProto for PathBuf {
-    fn to_proto(self) -> String {
-        to_proto_path(&self)
-    }
-}
-
-impl ToProto for &Path {
-    fn to_proto(self) -> String {
-        to_proto_path(self)
-    }
-}
-
 pub struct Receipt<T> {
     pub sender_id: PeerId,
     pub message_id: u32,
@@ -261,103 +208,3 @@ impl<T: RequestMessage> TypedEnvelope<T> {
         }
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use typed_path::{UnixPath, UnixPathBuf, WindowsPath, WindowsPathBuf};
-
-    fn windows_path_from_proto(proto: String) -> WindowsPathBuf {
-        let proto = proto.replace('/', "\\");
-        WindowsPathBuf::from(proto)
-    }
-
-    fn unix_path_from_proto(proto: String) -> UnixPathBuf {
-        UnixPathBuf::from(proto)
-    }
-
-    fn windows_path_to_proto(path: &WindowsPath) -> String {
-        path.to_string_lossy().replace('\\', "/")
-    }
-
-    fn unix_path_to_proto(path: &UnixPath) -> String {
-        path.to_string_lossy().to_string()
-    }
-
-    #[test]
-    fn test_path_proto_interop() {
-        const WINDOWS_PATHS: &[&str] = &[
-            "C:\\Users\\User\\Documents\\file.txt",
-            "C:/Program Files/App/app.exe",
-            "projects\\zed\\crates\\proto\\src\\typed_envelope.rs",
-            "projects/my project/src/main.rs",
-        ];
-        const UNIX_PATHS: &[&str] = &[
-            "/home/user/documents/file.txt",
-            "/usr/local/bin/my app/app",
-            "projects/zed/crates/proto/src/typed_envelope.rs",
-            "projects/my project/src/main.rs",
-        ];
-
-        // Windows path to proto and back
-        for &windows_path_str in WINDOWS_PATHS {
-            let windows_path = WindowsPathBuf::from(windows_path_str);
-            let proto = windows_path_to_proto(&windows_path);
-            let recovered_path = windows_path_from_proto(proto);
-            assert_eq!(windows_path, recovered_path);
-            assert_eq!(
-                recovered_path.to_string_lossy(),
-                windows_path_str.replace('/', "\\")
-            );
-        }
-        // Unix path to proto and back
-        for &unix_path_str in UNIX_PATHS {
-            let unix_path = UnixPathBuf::from(unix_path_str);
-            let proto = unix_path_to_proto(&unix_path);
-            let recovered_path = unix_path_from_proto(proto);
-            assert_eq!(unix_path, recovered_path);
-            assert_eq!(recovered_path.to_string_lossy(), unix_path_str);
-        }
-        // Windows host, Unix client, host sends Windows path to client
-        for &windows_path_str in WINDOWS_PATHS {
-            let windows_host_path = WindowsPathBuf::from(windows_path_str);
-            let proto = windows_path_to_proto(&windows_host_path);
-            let unix_client_received_path = unix_path_from_proto(proto);
-            let proto = unix_path_to_proto(&unix_client_received_path);
-            let windows_host_recovered_path = windows_path_from_proto(proto);
-            assert_eq!(windows_host_path, windows_host_recovered_path);
-            assert_eq!(
-                windows_host_recovered_path.to_string_lossy(),
-                windows_path_str.replace('/', "\\")
-            );
-        }
-        // Unix host, Windows client, host sends Unix path to client
-        for &unix_path_str in UNIX_PATHS {
-            let unix_host_path = UnixPathBuf::from(unix_path_str);
-            let proto = unix_path_to_proto(&unix_host_path);
-            let windows_client_received_path = windows_path_from_proto(proto);
-            let proto = windows_path_to_proto(&windows_client_received_path);
-            let unix_host_recovered_path = unix_path_from_proto(proto);
-            assert_eq!(unix_host_path, unix_host_recovered_path);
-            assert_eq!(unix_host_recovered_path.to_string_lossy(), unix_path_str);
-        }
-    }
-
-    // todo(zjk)
-    #[test]
-    fn test_unsolved_case() {
-        // Unix host, Windows client
-        // The Windows client receives a Unix path with backslashes in it, then
-        // sends it back to the host.
-        // This currently fails.
-        let unix_path = UnixPathBuf::from("/home/user/projects/my\\project/src/main.rs");
-        let proto = unix_path_to_proto(&unix_path);
-        let windows_client_received_path = windows_path_from_proto(proto);
-        let proto = windows_path_to_proto(&windows_client_received_path);
-        let unix_host_recovered_path = unix_path_from_proto(proto);
-        assert_ne!(unix_path, unix_host_recovered_path);
-        assert_eq!(
-            unix_host_recovered_path.to_string_lossy(),
-            "/home/user/projects/my/project/src/main.rs"
-        );
-    }
-}

crates/remote/src/transport.rs 🔗

@@ -194,7 +194,6 @@ async fn build_remote_server_from_source(
         )
         .await?;
     } else if build_remote_server.contains("cross") {
-        #[cfg(target_os = "windows")]
         use util::paths::SanitizedPath;
 
         delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx);
@@ -216,11 +215,7 @@ async fn build_remote_server_from_source(
         );
         log::info!("building remote server binary from source for {}", &triple);
 
-        // On Windows, the binding needs to be set to the canonical path
-        #[cfg(target_os = "windows")]
-        let src = SanitizedPath::new(&smol::fs::canonicalize("./target").await?).to_glob_string();
-        #[cfg(not(target_os = "windows"))]
-        let src = "./target";
+        let src = SanitizedPath::new(&smol::fs::canonicalize("target").await?).to_string();
 
         run_cmd(
             Command::new("cross")

crates/remote/src/transport/ssh.rs 🔗

@@ -13,6 +13,7 @@ use futures::{
 use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task};
 use itertools::Itertools;
 use parking_lot::Mutex;
+use paths::remote_server_dir_relative;
 use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
 use rpc::proto::Envelope;
 pub use settings::SshPortForwardOption;
@@ -27,12 +28,15 @@ use std::{
     time::Instant,
 };
 use tempfile::TempDir;
-use util::paths::{PathStyle, RemotePathBuf};
+use util::{
+    paths::{PathStyle, RemotePathBuf},
+    rel_path::RelPath,
+};
 
 pub(crate) struct SshRemoteConnection {
     socket: SshSocket,
     master_process: Mutex<Option<Child>>,
-    remote_binary_path: Option<RemotePathBuf>,
+    remote_binary_path: Option<Arc<RelPath>>,
     ssh_platform: RemotePlatform,
     ssh_path_style: PathStyle,
     ssh_shell: String,
@@ -204,7 +208,7 @@ impl RemoteConnection for SshRemoteConnection {
 
         let mut start_proxy_command = shell_script!(
             "exec {binary_path} proxy --identifier {identifier}",
-            binary_path = &remote_binary_path.to_string(),
+            binary_path = &remote_binary_path.display(self.path_style()),
             identifier = &unique_identifier,
         );
 
@@ -400,7 +404,7 @@ impl SshRemoteConnection {
         version: SemanticVersion,
         commit: Option<AppCommitSha>,
         cx: &mut AsyncApp,
-    ) -> Result<RemotePathBuf> {
+    ) -> Result<Arc<RelPath>> {
         let version_str = match release_channel {
             ReleaseChannel::Nightly => {
                 let commit = commit.map(|s| s.full()).unwrap_or_default();
@@ -414,23 +418,21 @@ impl SshRemoteConnection {
             release_channel.dev_name(),
             version_str
         );
-        let dst_path = RemotePathBuf::new(
-            paths::remote_server_dir_relative().join(binary_name),
-            self.ssh_path_style,
-        );
+        let dst_path =
+            paths::remote_server_dir_relative().join(RelPath::new(&binary_name).unwrap());
 
         #[cfg(debug_assertions)]
         if let Some(remote_server_path) =
             super::build_remote_server_from_source(&self.ssh_platform, delegate.as_ref(), cx)
                 .await?
         {
-            let tmp_path = RemotePathBuf::new(
-                paths::remote_server_dir_relative().join(format!(
+            let tmp_path = paths::remote_server_dir_relative().join(
+                RelPath::new(&format!(
                     "download-{}-{}",
                     std::process::id(),
                     remote_server_path.file_name().unwrap().to_string_lossy()
-                )),
-                self.ssh_path_style,
+                ))
+                .unwrap(),
             );
             self.upload_local_server_binary(&remote_server_path, &tmp_path, delegate, cx)
                 .await?;
@@ -441,7 +443,7 @@ impl SshRemoteConnection {
 
         if self
             .socket
-            .run_command(&dst_path.to_string(), &["version"])
+            .run_command(&dst_path.display(self.path_style()), &["version"])
             .await
             .is_ok()
         {
@@ -459,9 +461,13 @@ impl SshRemoteConnection {
             _ => Ok(Some(AppVersion::global(cx))),
         })??;
 
-        let tmp_path_gz = RemotePathBuf::new(
-            PathBuf::from(format!("{}-download-{}.gz", dst_path, std::process::id())),
-            self.ssh_path_style,
+        let tmp_path_gz = remote_server_dir_relative().join(
+            RelPath::new(&format!(
+                "{}-download-{}.gz",
+                binary_name,
+                std::process::id()
+            ))
+            .unwrap(),
         );
         if !self.socket.connection_options.upload_binary_over_ssh
             && let Some((url, body)) = delegate
@@ -500,7 +506,7 @@ impl SshRemoteConnection {
         &self,
         url: &str,
         body: &str,
-        tmp_path_gz: &RemotePathBuf,
+        tmp_path_gz: &RelPath,
         delegate: &Arc<dyn RemoteClientDelegate>,
         cx: &mut AsyncApp,
     ) -> Result<()> {
@@ -510,7 +516,10 @@ impl SshRemoteConnection {
                     "sh",
                     &[
                         "-c",
-                        &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
+                        &shell_script!(
+                            "mkdir -p {parent}",
+                            parent = parent.display(self.path_style()).as_ref()
+                        ),
                     ],
                 )
                 .await?;
@@ -533,7 +542,7 @@ impl SshRemoteConnection {
                     body,
                     url,
                     "-o",
-                    &tmp_path_gz.to_string(),
+                    &tmp_path_gz.display(self.path_style()),
                 ],
             )
             .await
@@ -555,7 +564,7 @@ impl SshRemoteConnection {
                             body,
                             url,
                             "-O",
-                            &tmp_path_gz.to_string(),
+                            &tmp_path_gz.display(self.path_style()),
                         ],
                     )
                     .await
@@ -578,7 +587,7 @@ impl SshRemoteConnection {
     async fn upload_local_server_binary(
         &self,
         src_path: &Path,
-        tmp_path_gz: &RemotePathBuf,
+        tmp_path_gz: &RelPath,
         delegate: &Arc<dyn RemoteClientDelegate>,
         cx: &mut AsyncApp,
     ) -> Result<()> {
@@ -588,7 +597,10 @@ impl SshRemoteConnection {
                     "sh",
                     &[
                         "-c",
-                        &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
+                        &shell_script!(
+                            "mkdir -p {parent}",
+                            parent = parent.display(self.path_style()).as_ref()
+                        ),
                     ],
                 )
                 .await?;
@@ -613,33 +625,33 @@ impl SshRemoteConnection {
 
     async fn extract_server_binary(
         &self,
-        dst_path: &RemotePathBuf,
-        tmp_path: &RemotePathBuf,
+        dst_path: &RelPath,
+        tmp_path: &RelPath,
         delegate: &Arc<dyn RemoteClientDelegate>,
         cx: &mut AsyncApp,
     ) -> Result<()> {
         delegate.set_status(Some("Extracting remote development server"), cx);
         let server_mode = 0o755;
 
-        let orig_tmp_path = tmp_path.to_string();
+        let orig_tmp_path = tmp_path.display(self.path_style());
         let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") {
             shell_script!(
                 "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}",
                 server_mode = &format!("{:o}", server_mode),
-                dst_path = &dst_path.to_string(),
+                dst_path = &dst_path.display(self.path_style()),
             )
         } else {
             shell_script!(
                 "chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}",
                 server_mode = &format!("{:o}", server_mode),
-                dst_path = &dst_path.to_string()
+                dst_path = &dst_path.display(self.path_style())
             )
         };
         self.socket.run_command("sh", &["-c", &script]).await?;
         Ok(())
     }
 
-    async fn upload_file(&self, src_path: &Path, dest_path: &RemotePathBuf) -> Result<()> {
+    async fn upload_file(&self, src_path: &Path, dest_path: &RelPath) -> Result<()> {
         log::debug!("uploading file {:?} to {:?}", src_path, dest_path);
         let mut command = util::command::new_smol_command("scp");
         let output = self
@@ -656,7 +668,7 @@ impl SshRemoteConnection {
             .arg(format!(
                 "{}:{}",
                 self.socket.connection_options.scp_url(),
-                dest_path
+                dest_path.display(self.path_style())
             ))
             .output()
             .await?;
@@ -665,7 +677,7 @@ impl SshRemoteConnection {
             output.status.success(),
             "failed to upload file {} -> {}: {}",
             src_path.display(),
-            dest_path.to_string(),
+            dest_path.display(self.path_style()),
             String::from_utf8_lossy(&output.stderr)
         );
         Ok(())
@@ -1040,7 +1052,7 @@ fn build_command(
 
     let mut exec = String::from("exec env -C ");
     if let Some(working_dir) = working_dir {
-        let working_dir = RemotePathBuf::new(working_dir.into(), ssh_path_style).to_string();
+        let working_dir = RemotePathBuf::new(working_dir, ssh_path_style).to_string();
 
         // shlex will wrap the command in single quotes (''), disabling ~ expansion,
         // replace with with something that works

crates/remote/src/transport/wsl.rs 🔗

@@ -17,7 +17,10 @@ use std::{
     sync::Arc,
     time::Instant,
 };
-use util::paths::{PathStyle, RemotePathBuf};
+use util::{
+    paths::{PathStyle, RemotePathBuf},
+    rel_path::RelPath,
+};
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct WslConnectionOptions {
@@ -35,7 +38,7 @@ impl From<settings::WslConnection> for WslConnectionOptions {
 }
 
 pub(crate) struct WslRemoteConnection {
-    remote_binary_path: Option<RemotePathBuf>,
+    remote_binary_path: Option<Arc<RelPath>>,
     platform: RemotePlatform,
     shell: String,
     default_system_shell: String,
@@ -122,7 +125,7 @@ impl WslRemoteConnection {
         version: SemanticVersion,
         commit: Option<AppCommitSha>,
         cx: &mut AsyncApp,
-    ) -> Result<RemotePathBuf> {
+    ) -> Result<Arc<RelPath>> {
         let version_str = match release_channel {
             ReleaseChannel::Nightly => {
                 let commit = commit.map(|s| s.full()).unwrap_or_default();
@@ -138,13 +141,11 @@ impl WslRemoteConnection {
             version_str
         );
 
-        let dst_path = RemotePathBuf::new(
-            paths::remote_wsl_server_dir_relative().join(binary_name),
-            PathStyle::Posix,
-        );
+        let dst_path =
+            paths::remote_wsl_server_dir_relative().join(RelPath::new(&binary_name).unwrap());
 
         if let Some(parent) = dst_path.parent() {
-            self.run_wsl_command("mkdir", &["-p", &parent.to_string()])
+            self.run_wsl_command("mkdir", &["-p", &parent.display(PathStyle::Posix)])
                 .await
                 .map_err(|e| anyhow!("Failed to create directory: {}", e))?;
         }
@@ -153,13 +154,13 @@ impl WslRemoteConnection {
         if let Some(remote_server_path) =
             super::build_remote_server_from_source(&self.platform, delegate.as_ref(), cx).await?
         {
-            let tmp_path = RemotePathBuf::new(
-                paths::remote_wsl_server_dir_relative().join(format!(
+            let tmp_path = paths::remote_wsl_server_dir_relative().join(
+                &RelPath::new(&format!(
                     "download-{}-{}",
                     std::process::id(),
                     remote_server_path.file_name().unwrap().to_string_lossy()
-                )),
-                PathStyle::Posix,
+                ))
+                .unwrap(),
             );
             self.upload_file(&remote_server_path, &tmp_path, delegate, cx)
                 .await?;
@@ -169,7 +170,7 @@ impl WslRemoteConnection {
         }
 
         if self
-            .run_wsl_command(&dst_path.to_string(), &["version"])
+            .run_wsl_command(&dst_path.display(PathStyle::Posix), &["version"])
             .await
             .is_ok()
         {
@@ -187,10 +188,12 @@ impl WslRemoteConnection {
             .download_server_binary_locally(self.platform, release_channel, wanted_version, cx)
             .await?;
 
-        let tmp_path = RemotePathBuf::new(
-            PathBuf::from(format!("{}.{}.gz", dst_path, std::process::id())),
-            PathStyle::Posix,
+        let tmp_path = format!(
+            "{}.{}.gz",
+            dst_path.display(PathStyle::Posix),
+            std::process::id()
         );
+        let tmp_path = RelPath::new(&tmp_path).unwrap();
 
         self.upload_file(&src_path, &tmp_path, delegate, cx).await?;
         self.extract_and_install(&tmp_path, &dst_path, delegate, cx)
@@ -202,14 +205,14 @@ impl WslRemoteConnection {
     async fn upload_file(
         &self,
         src_path: &Path,
-        dst_path: &RemotePathBuf,
+        dst_path: &RelPath,
         delegate: &Arc<dyn RemoteClientDelegate>,
         cx: &mut AsyncApp,
     ) -> Result<()> {
         delegate.set_status(Some("Uploading remote server to WSL"), cx);
 
         if let Some(parent) = dst_path.parent() {
-            self.run_wsl_command("mkdir", &["-p", &parent.to_string()])
+            self.run_wsl_command("mkdir", &["-p", &parent.display(PathStyle::Posix)])
                 .await
                 .map_err(|e| anyhow!("Failed to create directory when uploading file: {}", e))?;
         }
@@ -224,17 +227,20 @@ impl WslRemoteConnection {
         );
 
         let src_path_in_wsl = self.windows_path_to_wsl_path(src_path).await?;
-        self.run_wsl_command("cp", &["-f", &src_path_in_wsl, &dst_path.to_string()])
-            .await
-            .map_err(|e| {
-                anyhow!(
-                    "Failed to copy file {}({}) to WSL {:?}: {}",
-                    src_path.display(),
-                    src_path_in_wsl,
-                    dst_path,
-                    e
-                )
-            })?;
+        self.run_wsl_command(
+            "cp",
+            &["-f", &src_path_in_wsl, &dst_path.display(PathStyle::Posix)],
+        )
+        .await
+        .map_err(|e| {
+            anyhow!(
+                "Failed to copy file {}({}) to WSL {:?}: {}",
+                src_path.display(),
+                src_path_in_wsl,
+                dst_path,
+                e
+            )
+        })?;
 
         log::info!("uploaded remote server in {:?}", t0.elapsed());
         Ok(())
@@ -242,15 +248,15 @@ impl WslRemoteConnection {
 
     async fn extract_and_install(
         &self,
-        tmp_path: &RemotePathBuf,
-        dst_path: &RemotePathBuf,
+        tmp_path: &RelPath,
+        dst_path: &RelPath,
         delegate: &Arc<dyn RemoteClientDelegate>,
         cx: &mut AsyncApp,
     ) -> Result<()> {
         delegate.set_status(Some("Extracting remote server"), cx);
 
-        let tmp_path_str = tmp_path.to_string();
-        let dst_path_str = dst_path.to_string();
+        let tmp_path_str = tmp_path.display(PathStyle::Posix);
+        let dst_path_str = dst_path.display(PathStyle::Posix);
 
         // Build extraction script with proper error handling
         let script = if tmp_path_str.ends_with(".gz") {
@@ -293,7 +299,8 @@ impl RemoteConnection for WslRemoteConnection {
 
         let mut proxy_command = format!(
             "exec {} proxy --identifier {}",
-            remote_binary_path, unique_identifier
+            remote_binary_path.display(PathStyle::Posix),
+            unique_identifier
         );
 
         if reconnect {
@@ -377,7 +384,7 @@ impl RemoteConnection for WslRemoteConnection {
         }
 
         let working_dir = working_dir
-            .map(|working_dir| RemotePathBuf::new(working_dir.into(), PathStyle::Posix).to_string())
+            .map(|working_dir| RemotePathBuf::new(working_dir, PathStyle::Posix).to_string())
             .unwrap_or("~".to_string());
 
         let mut script = String::new();

crates/remote_server/src/headless_project.rs 🔗

@@ -1,4 +1,3 @@
-use ::proto::{FromProto, ToProto};
 use anyhow::{Context as _, Result, anyhow};
 use lsp::LanguageServerId;
 
@@ -34,7 +33,7 @@ use std::{
     sync::{Arc, atomic::AtomicUsize},
 };
 use sysinfo::System;
-use util::ResultExt;
+use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
 use worktree::Worktree;
 
 pub struct HeadlessProject {
@@ -405,7 +404,7 @@ impl HeadlessProject {
     ) -> Result<proto::AddWorktreeResponse> {
         use client::ErrorCodeExt;
         let fs = this.read_with(&cx, |this, _| this.fs.clone())?;
-        let path = PathBuf::from_proto(shellexpand::tilde(&message.payload.path).to_string());
+        let path = PathBuf::from(shellexpand::tilde(&message.payload.path).to_string());
 
         let canonicalized = match fs.canonicalize(&path).await {
             Ok(path) => path,
@@ -443,7 +442,7 @@ impl HeadlessProject {
             let worktree = worktree.read(cx);
             proto::AddWorktreeResponse {
                 worktree_id: worktree.id().to_proto(),
-                canonicalized_path: canonicalized.to_proto(),
+                canonicalized_path: canonicalized.to_string_lossy().to_string(),
             }
         })?;
 
@@ -492,16 +491,11 @@ impl HeadlessProject {
         mut cx: AsyncApp,
     ) -> Result<proto::OpenBufferResponse> {
         let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
+        let path = RelPath::from_proto(&message.payload.path)?;
         let (buffer_store, buffer) = this.update(&mut cx, |this, cx| {
             let buffer_store = this.buffer_store.clone();
             let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
-                buffer_store.open_buffer(
-                    ProjectPath {
-                        worktree_id,
-                        path: Arc::<Path>::from_proto(message.payload.path),
-                    },
-                    cx,
-                )
+                buffer_store.open_buffer(ProjectPath { worktree_id, path }, cx)
             });
             anyhow::Ok((buffer_store, buffer))
         })??;
@@ -590,7 +584,7 @@ impl HeadlessProject {
                 buffer_store.open_buffer(
                     ProjectPath {
                         worktree_id: worktree.read(cx).id(),
-                        path: path.into(),
+                        path: path,
                     },
                     cx,
                 )
@@ -630,7 +624,10 @@ impl HeadlessProject {
         mut cx: AsyncApp,
     ) -> Result<proto::FindSearchCandidatesResponse> {
         let message = envelope.payload;
-        let query = SearchQuery::from_proto(message.query.context("missing query field")?)?;
+        let query = SearchQuery::from_proto(
+            message.query.context("missing query field")?,
+            PathStyle::local(),
+        )?;
         let results = this.update(&mut cx, |this, cx| {
             this.buffer_store.update(cx, |buffer_store, cx| {
                 buffer_store.find_search_candidates(&query, message.limit as _, this.fs.clone(), cx)
@@ -662,7 +659,7 @@ impl HeadlessProject {
         cx: AsyncApp,
     ) -> Result<proto::ListRemoteDirectoryResponse> {
         let fs = cx.read_entity(&this, |this, _| this.fs.clone())?;
-        let expanded = PathBuf::from_proto(shellexpand::tilde(&envelope.payload.path).to_string());
+        let expanded = PathBuf::from(shellexpand::tilde(&envelope.payload.path).to_string());
         let check_info = envelope
             .payload
             .config
@@ -694,7 +691,7 @@ impl HeadlessProject {
         cx: AsyncApp,
     ) -> Result<proto::GetPathMetadataResponse> {
         let fs = cx.read_entity(&this, |this, _| this.fs.clone())?;
-        let expanded = PathBuf::from_proto(shellexpand::tilde(&envelope.payload.path).to_string());
+        let expanded = PathBuf::from(shellexpand::tilde(&envelope.payload.path).to_string());
 
         let metadata = fs.metadata(&expanded).await?;
         let is_dir = metadata.map(|metadata| metadata.is_dir).unwrap_or(false);
@@ -702,7 +699,7 @@ impl HeadlessProject {
         Ok(proto::GetPathMetadataResponse {
             exists: metadata.is_some(),
             is_dir,
-            path: expanded.to_proto(),
+            path: expanded.to_string_lossy().to_string(),
         })
     }
 

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -20,7 +20,7 @@ use language::{
 use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind, LanguageServerName};
 use node_runtime::NodeRuntime;
 use project::{
-    Project, ProjectPath,
+    Project,
     agent_server_store::AgentServerCommand,
     search::{SearchQuery, SearchResult},
 };
@@ -34,7 +34,7 @@ use std::{
 };
 #[cfg(not(windows))]
 use unindent::Unindent as _;
-use util::path;
+use util::{path, rel_path::rel_path};
 
 #[gpui::test]
 async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
@@ -57,7 +57,7 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test
     .await;
     fs.set_index_for_repo(
         Path::new(path!("/code/project1/.git")),
-        &[("src/lib.rs".into(), "fn one() -> usize { 0 }".into())],
+        &[("src/lib.rs", "fn one() -> usize { 0 }".into())],
     );
 
     let (project, _headless) = init_test(&fs, cx, server_cx).await;
@@ -73,11 +73,11 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test
     let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
     worktree.update(cx, |worktree, _cx| {
         assert_eq!(
-            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
+            worktree.paths().collect::<Vec<_>>(),
             vec![
-                Path::new("README.md"),
-                Path::new("src"),
-                Path::new("src/lib.rs"),
+                rel_path("README.md"),
+                rel_path("src"),
+                rel_path("src/lib.rs"),
             ]
         );
     });
@@ -86,7 +86,7 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test
     // contents are loaded from the remote filesystem.
     let buffer = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
+            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
         })
         .await
         .unwrap();
@@ -130,12 +130,12 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test
     cx.executor().run_until_parked();
     worktree.update(cx, |worktree, _cx| {
         assert_eq!(
-            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
+            worktree.paths().collect::<Vec<_>>(),
             vec![
-                Path::new("README.md"),
-                Path::new("src"),
-                Path::new("src/lib.rs"),
-                Path::new("src/main.rs"),
+                rel_path("README.md"),
+                rel_path("src"),
+                rel_path("src/lib.rs"),
+                rel_path("src/main.rs"),
             ]
         );
     });
@@ -150,12 +150,12 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test
     .unwrap();
     cx.executor().run_until_parked();
     buffer.update(cx, |buffer, _| {
-        assert_eq!(&**buffer.file().unwrap().path(), Path::new("src/lib2.rs"));
+        assert_eq!(&**buffer.file().unwrap().path(), rel_path("src/lib2.rs"));
     });
 
     fs.set_index_for_repo(
         Path::new(path!("/code/project1/.git")),
-        &[("src/lib2.rs".into(), "fn one() -> usize { 100 }".into())],
+        &[("src/lib2.rs", "fn one() -> usize { 100 }".into())],
     );
     cx.executor().run_until_parked();
     diff.update(cx, |diff, _| {
@@ -336,7 +336,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
 
     let buffer = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
+            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
         })
         .await
         .unwrap();
@@ -356,7 +356,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
             AllLanguageSettings::get(
                 Some(SettingsLocation {
                     worktree_id,
-                    path: Path::new("src/lib.rs")
+                    path: rel_path("src/lib.rs")
                 }),
                 cx
             )
@@ -463,7 +463,7 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
 
     let (buffer, _handle) = project
         .update(cx, |project, cx| {
-            project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
+            project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
         })
         .await
         .unwrap();
@@ -643,7 +643,7 @@ async fn test_remote_cancel_language_server_work(
 
     let (buffer, _handle) = project
         .update(cx, |project, cx| {
-            project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
+            project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
         })
         .await
         .unwrap();
@@ -757,7 +757,7 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont
 
     let buffer = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
+            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
         })
         .await
         .unwrap();
@@ -851,7 +851,7 @@ async fn test_remote_resolve_path_in_buffer(
 
     let buffer2 = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree2_id, Path::new("src/lib.rs")), cx)
+            project.open_buffer((worktree2_id, rel_path("src/lib.rs")), cx)
         })
         .await
         .unwrap();
@@ -863,10 +863,7 @@ async fn test_remote_resolve_path_in_buffer(
         .await
         .unwrap();
     assert!(path.is_file());
-    assert_eq!(
-        path.abs_path().unwrap().to_string_lossy(),
-        path!("/code/project2/README.md")
-    );
+    assert_eq!(path.abs_path().unwrap(), path!("/code/project2/README.md"));
 
     let path = project
         .update(cx, |project, cx| {
@@ -877,7 +874,7 @@ async fn test_remote_resolve_path_in_buffer(
     assert!(path.is_file());
     assert_eq!(
         path.project_path().unwrap().clone(),
-        ProjectPath::from((worktree2_id, "README.md"))
+        (worktree2_id, rel_path("README.md")).into()
     );
 
     let path = project
@@ -888,7 +885,7 @@ async fn test_remote_resolve_path_in_buffer(
         .unwrap();
     assert_eq!(
         path.project_path().unwrap().clone(),
-        ProjectPath::from((worktree2_id, "src"))
+        (worktree2_id, rel_path("src")).into()
     );
     assert!(path.is_dir());
 }
@@ -920,10 +917,7 @@ async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut T
         .unwrap();
 
     assert!(path.is_file());
-    assert_eq!(
-        path.abs_path().unwrap().to_string_lossy(),
-        path!("/code/project1/README.md")
-    );
+    assert_eq!(path.abs_path().unwrap(), path!("/code/project1/README.md"));
 
     let path = project
         .update(cx, |project, cx| {
@@ -933,10 +927,7 @@ async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut T
         .unwrap();
 
     assert!(path.is_dir());
-    assert_eq!(
-        path.abs_path().unwrap().to_string_lossy(),
-        path!("/code/project1/src")
-    );
+    assert_eq!(path.abs_path().unwrap(), path!("/code/project1/src"));
 
     let path = project
         .update(cx, |project, cx| {
@@ -973,14 +964,18 @@ async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut
     let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 
     // Open a buffer on the client but cancel after a random amount of time.
-    let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
+    let buffer = project.update(cx, |p, cx| {
+        p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
+    });
     cx.executor().simulate_random_delay().await;
     drop(buffer);
 
     // Try opening the same buffer again as the client, and ensure we can
     // still do it despite the cancellation above.
     let buffer = project
-        .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
+        .update(cx, |p, cx| {
+            p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
+        })
         .await
         .unwrap();
 
@@ -1042,10 +1037,7 @@ async fn test_adding_then_removing_then_adding_worktrees(
         assert!(worktree.is_visible());
         let entries = worktree.entries(true, 0).collect::<Vec<_>>();
         assert_eq!(entries.len(), 2);
-        assert_eq!(
-            entries[1].path.to_string_lossy().to_string(),
-            "README.md".to_string()
-        )
+        assert_eq!(entries[1].path.as_str(), "README.md")
     })
 }
 
@@ -1111,7 +1103,7 @@ async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext)
     let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
     let buffer = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
+            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
         })
         .await
         .unwrap();
@@ -1203,10 +1195,11 @@ async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestA
 
     cx.run_until_parked();
 
-    let entry = worktree
-        .update(cx, |worktree, cx| {
-            let entry = worktree.entry_for_path("README.md").unwrap();
-            worktree.rename_entry(entry.id, Path::new("README.rst"), cx)
+    let entry = project
+        .update(cx, |project, cx| {
+            let worktree = worktree.read(cx);
+            let entry = worktree.entry_for_path(rel_path("README.md")).unwrap();
+            project.rename_entry(entry.id, (worktree.id(), rel_path("README.rst")).into(), cx)
         })
         .await
         .unwrap()
@@ -1216,7 +1209,10 @@ async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestA
     cx.run_until_parked();
 
     worktree.update(cx, |worktree, _| {
-        assert_eq!(worktree.entry_for_path("README.rst").unwrap().id, entry.id)
+        assert_eq!(
+            worktree.entry_for_path(rel_path("README.rst")).unwrap().id,
+            entry.id
+        )
     });
 }
 
@@ -1277,7 +1273,7 @@ async fn test_copy_file_into_remote_project(
     worktree
         .update(cx, |worktree, cx| {
             worktree.copy_external_entries(
-                Path::new("src").into(),
+                rel_path("src").into(),
                 vec![
                     Path::new(path!("/local-code/dir1/file1")).into(),
                     Path::new(path!("/local-code/dir1/dir2")).into(),
@@ -1363,11 +1359,11 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC
     .await;
     fs.set_index_for_repo(
         Path::new("/code/project1/.git"),
-        &[("src/lib.rs".into(), text_1.clone())],
+        &[("src/lib.rs", text_1.clone())],
     );
     fs.set_head_for_repo(
         Path::new("/code/project1/.git"),
-        &[("src/lib.rs".into(), text_1.clone())],
+        &[("src/lib.rs", text_1.clone())],
         "deadbeef",
     );
 
@@ -1383,7 +1379,7 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC
 
     let buffer = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
+            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
         })
         .await
         .unwrap();
@@ -1409,7 +1405,7 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC
     // stage the current buffer's contents
     fs.set_index_for_repo(
         Path::new("/code/project1/.git"),
-        &[("src/lib.rs".into(), text_2.clone())],
+        &[("src/lib.rs", text_2.clone())],
     );
 
     cx.executor().run_until_parked();
@@ -1428,7 +1424,7 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC
     // commit the current buffer's contents
     fs.set_head_for_repo(
         Path::new("/code/project1/.git"),
-        &[("src/lib.rs".into(), text_2.clone())],
+        &[("src/lib.rs", text_2.clone())],
         "deadbeef",
     );
 
@@ -1492,7 +1488,7 @@ async fn test_remote_git_diffs_when_recv_update_repository_delay(
     let worktree_id = cx.update(|cx| worktree.read(cx).id());
     let buffer = project
         .update(cx, |project, cx| {
-            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
+            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
         })
         .await
         .unwrap();
@@ -1519,11 +1515,11 @@ async fn test_remote_git_diffs_when_recv_update_repository_delay(
 
     fs.set_index_for_repo(
         Path::new("/code/project1/.git"),
-        &[("src/lib.rs".into(), text_1.clone())],
+        &[("src/lib.rs", text_1.clone())],
     );
     fs.set_head_for_repo(
         Path::new("/code/project1/.git"),
-        &[("src/lib.rs".into(), text_1.clone())],
+        &[("src/lib.rs", text_1.clone())],
         "sha",
     );
 
@@ -1551,7 +1547,7 @@ async fn test_remote_git_diffs_when_recv_update_repository_delay(
     // stage the current buffer's contents
     fs.set_index_for_repo(
         Path::new("/code/project1/.git"),
-        &[("src/lib.rs".into(), text_2.clone())],
+        &[("src/lib.rs", text_2.clone())],
     );
 
     cx.executor().run_until_parked();
@@ -1570,7 +1566,7 @@ async fn test_remote_git_diffs_when_recv_update_repository_delay(
     // commit the current buffer's contents
     fs.set_head_for_repo(
         Path::new("/code/project1/.git"),
-        &[("src/lib.rs".into(), text_2.clone())],
+        &[("src/lib.rs", text_2.clone())],
         "sha",
     );
 

crates/repl/src/kernels/mod.rs 🔗

@@ -1,5 +1,5 @@
 mod native_kernel;
-use std::{fmt::Debug, future::Future, path::PathBuf, sync::Arc};
+use std::{fmt::Debug, future::Future, path::PathBuf};
 
 use futures::{
     channel::mpsc::{self, Receiver},
@@ -18,6 +18,7 @@ use anyhow::Result;
 use jupyter_protocol::JupyterKernelspec;
 use runtimelib::{ExecutionState, JupyterMessage, KernelInfoReply};
 use ui::{Icon, IconName, SharedString};
+use util::rel_path::RelPath;
 
 pub type JupyterMessageChannel = stream::SelectAll<Receiver<JupyterMessage>>;
 
@@ -84,7 +85,7 @@ pub fn python_env_kernel_specifications(
     let toolchains = project.read(cx).available_toolchains(
         ProjectPath {
             worktree_id,
-            path: Arc::from("".as_ref()),
+            path: RelPath::empty().into(),
         },
         python_language,
         cx,

crates/search/src/buffer_search.rs 🔗

@@ -34,7 +34,7 @@ use ui::{
     BASE_REM_SIZE_IN_PX, IconButton, IconButtonShape, IconName, Tooltip, h_flex, prelude::*,
     utils::SearchInputWidth,
 };
-use util::ResultExt;
+use util::{ResultExt, paths::PathMatcher};
 use workspace::{
     ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
     item::ItemHandle,
@@ -1214,6 +1214,8 @@ impl BufferSearchBar {
                 {
                     search
                 } else {
+                    // Value doesn't matter, we only construct empty matchers with it
+
                     if self.search_options.contains(SearchOptions::REGEX) {
                         match SearchQuery::regex(
                             query,
@@ -1222,8 +1224,8 @@ impl BufferSearchBar {
                             false,
                             self.search_options
                                 .contains(SearchOptions::ONE_MATCH_PER_LINE),
-                            Default::default(),
-                            Default::default(),
+                            PathMatcher::default(),
+                            PathMatcher::default(),
                             false,
                             None,
                         ) {
@@ -1241,8 +1243,8 @@ impl BufferSearchBar {
                             self.search_options.contains(SearchOptions::WHOLE_WORD),
                             self.search_options.contains(SearchOptions::CASE_SENSITIVE),
                             false,
-                            Default::default(),
-                            Default::default(),
+                            PathMatcher::default(),
+                            PathMatcher::default(),
                             false,
                             None,
                         ) {

crates/search/src/project_search.rs 🔗

@@ -32,12 +32,11 @@ use std::{
     any::{Any, TypeId},
     mem,
     ops::{Not, Range},
-    path::Path,
     pin::pin,
     sync::Arc,
 };
 use ui::{IconButtonShape, KeyBinding, Toggleable, Tooltip, prelude::*, utils::SearchInputWidth};
-use util::{ResultExt as _, paths::PathMatcher};
+use util::{ResultExt as _, paths::PathMatcher, rel_path::RelPath};
 use workspace::{
     DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation,
     ToolbarItemView, Workspace, WorkspaceId,
@@ -908,13 +907,11 @@ impl ProjectSearchView {
 
     pub fn new_search_in_directory(
         workspace: &mut Workspace,
-        dir_path: &Path,
+        dir_path: &RelPath,
         window: &mut Window,
         cx: &mut Context<Workspace>,
     ) {
-        let Some(filter_str) = dir_path.to_str() else {
-            return;
-        };
+        let filter_str = dir_path.display(workspace.path_style(cx));
 
         let weak_workspace = cx.entity().downgrade();
 
@@ -1145,6 +1142,7 @@ impl ProjectSearchView {
 
     fn build_search_query(&mut self, cx: &mut Context<Self>) -> Option<SearchQuery> {
         // Do not bail early in this function, as we want to fill out `self.panels_with_errors`.
+
         let text = self.search_query_text(cx);
         let open_buffers = if self.included_opened_only {
             Some(self.open_buffers(cx))
@@ -1154,7 +1152,7 @@ impl ProjectSearchView {
         let included_files = self
             .filters_enabled
             .then(|| {
-                match Self::parse_path_matches(&self.included_files_editor.read(cx).text(cx)) {
+                match self.parse_path_matches(self.included_files_editor.read(cx).text(cx), cx) {
                     Ok(included_files) => {
                         let should_unmark_error =
                             self.panels_with_errors.remove(&InputPanel::Include);
@@ -1174,11 +1172,11 @@ impl ProjectSearchView {
                     }
                 }
             })
-            .unwrap_or_default();
+            .unwrap_or(PathMatcher::default());
         let excluded_files = self
             .filters_enabled
             .then(|| {
-                match Self::parse_path_matches(&self.excluded_files_editor.read(cx).text(cx)) {
+                match self.parse_path_matches(self.excluded_files_editor.read(cx).text(cx), cx) {
                     Ok(excluded_files) => {
                         let should_unmark_error =
                             self.panels_with_errors.remove(&InputPanel::Exclude);
@@ -1199,7 +1197,7 @@ impl ProjectSearchView {
                     }
                 }
             })
-            .unwrap_or_default();
+            .unwrap_or(PathMatcher::default());
 
         // If the project contains multiple visible worktrees, we match the
         // include/exclude patterns against full paths to allow them to be
@@ -1300,14 +1298,15 @@ impl ProjectSearchView {
         buffers
     }
 
-    fn parse_path_matches(text: &str) -> anyhow::Result<PathMatcher> {
+    fn parse_path_matches(&self, text: String, cx: &App) -> anyhow::Result<PathMatcher> {
+        let path_style = self.entity.read(cx).project.read(cx).path_style(cx);
         let queries = text
             .split(',')
             .map(str::trim)
             .filter(|maybe_glob_str| !maybe_glob_str.is_empty())
             .map(str::to_owned)
             .collect::<Vec<_>>();
-        Ok(PathMatcher::new(&queries)?)
+        Ok(PathMatcher::new(&queries, path_style)?)
     }
 
     fn select_match(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
@@ -2354,7 +2353,7 @@ pub mod tests {
     use project::FakeFs;
     use serde_json::json;
     use settings::SettingsStore;
-    use util::path;
+    use util::{path, paths::PathStyle, rel_path::rel_path};
     use workspace::DeploySearch;
 
     #[gpui::test]
@@ -3204,7 +3203,7 @@ pub mod tests {
                 .read(cx)
                 .project()
                 .read(cx)
-                .entry_for_path(&(worktree_id, "a").into(), cx)
+                .entry_for_path(&(worktree_id, rel_path("a")).into(), cx)
                 .expect("no entry for /a/ directory")
                 .clone()
         });
@@ -3242,7 +3241,7 @@ pub mod tests {
                     search_view.included_files_editor.update(cx, |editor, cx| {
                         assert_eq!(
                             editor.display_text(cx),
-                            a_dir_entry.path.to_str().unwrap(),
+                            a_dir_entry.path.display(PathStyle::local()),
                             "New search in directory should have included dir entry path"
                         );
                     });
@@ -3638,7 +3637,7 @@ pub mod tests {
         window
             .update(cx, |workspace, window, cx| {
                 workspace.open_path(
-                    (worktree_id, "one.rs"),
+                    (worktree_id, rel_path("one.rs")),
                     Some(first_pane.downgrade()),
                     true,
                     window,
@@ -3855,7 +3854,7 @@ pub mod tests {
         window
             .update(cx, |workspace, window, cx| {
                 workspace.open_path(
-                    (worktree_id, "one.rs"),
+                    (worktree_id, rel_path("one.rs")),
                     Some(first_pane.downgrade()),
                     true,
                     window,
@@ -4089,7 +4088,7 @@ pub mod tests {
 
         let editor = workspace
             .update_in(&mut cx, |workspace, window, cx| {
-                workspace.open_path((worktree_id, "one.rs"), None, true, window, cx)
+                workspace.open_path((worktree_id, rel_path("one.rs")), None, true, window, cx)
             })
             .await
             .unwrap()

crates/settings/src/settings_store.rs 🔗

@@ -17,13 +17,14 @@ use std::{
     any::{Any, TypeId, type_name},
     fmt::Debug,
     ops::Range,
-    path::{Path, PathBuf},
+    path::PathBuf,
     rc::Rc,
     str::{self, FromStr},
     sync::Arc,
 };
 use util::{
     ResultExt as _,
+    rel_path::RelPath,
     schemars::{DefaultDenyUnknownFields, replace_subschema},
 };
 
@@ -134,7 +135,7 @@ pub trait Settings: 'static + Send + Sync + Sized {
 #[derive(Clone, Copy, Debug)]
 pub struct SettingsLocation<'a> {
     pub worktree_id: WorktreeId,
-    pub path: &'a Path,
+    pub path: &'a RelPath,
 }
 
 pub struct SettingsStore {
@@ -148,8 +149,8 @@ pub struct SettingsStore {
 
     merged_settings: Rc<SettingsContent>,
 
-    local_settings: BTreeMap<(WorktreeId, Arc<Path>), SettingsContent>,
-    raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc<Path>), (String, Option<Editorconfig>)>,
+    local_settings: BTreeMap<(WorktreeId, Arc<RelPath>), SettingsContent>,
+    raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc<RelPath>), (String, Option<Editorconfig>)>,
 
     _setting_file_updates: Task<()>,
     setting_file_updates_tx:
@@ -199,7 +200,7 @@ impl Global for SettingsStore {}
 #[derive(Debug)]
 struct SettingValue<T> {
     global_value: Option<T>,
-    local_values: Vec<(WorktreeId, Arc<Path>, T)>,
+    local_values: Vec<(WorktreeId, Arc<RelPath>, T)>,
 }
 
 trait AnySettingValue: 'static + Send + Sync {
@@ -208,9 +209,9 @@ trait AnySettingValue: 'static + Send + Sync {
     fn from_settings(&self, s: &SettingsContent, cx: &mut App) -> Box<dyn Any>;
 
     fn value_for_path(&self, path: Option<SettingsLocation>) -> &dyn Any;
-    fn all_local_values(&self) -> Vec<(WorktreeId, Arc<Path>, &dyn Any)>;
+    fn all_local_values(&self) -> Vec<(WorktreeId, Arc<RelPath>, &dyn Any)>;
     fn set_global_value(&mut self, value: Box<dyn Any>);
-    fn set_local_value(&mut self, root_id: WorktreeId, path: Arc<Path>, value: Box<dyn Any>);
+    fn set_local_value(&mut self, root_id: WorktreeId, path: Arc<RelPath>, value: Box<dyn Any>);
     fn import_from_vscode(
         &self,
         vscode_settings: &VsCodeSettings,
@@ -299,7 +300,7 @@ impl SettingsStore {
     }
 
     /// Get all values from project specific settings
-    pub fn get_all_locals<T: Settings>(&self) -> Vec<(WorktreeId, Arc<Path>, &T)> {
+    pub fn get_all_locals<T: Settings>(&self) -> Vec<(WorktreeId, Arc<RelPath>, &T)> {
         self.setting_values
             .get(&TypeId::of::<T>())
             .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
@@ -644,7 +645,7 @@ impl SettingsStore {
     pub fn set_local_settings(
         &mut self,
         root_id: WorktreeId,
-        directory_path: Arc<Path>,
+        directory_path: Arc<RelPath>,
         kind: LocalSettingsKind,
         settings_content: Option<&str>,
         cx: &mut App,
@@ -659,14 +660,20 @@ impl SettingsStore {
             (LocalSettingsKind::Tasks, _) => {
                 return Err(InvalidSettingsError::Tasks {
                     message: "Attempted to submit tasks into the settings store".to_string(),
-                    path: directory_path.join(task_file_name()),
+                    path: directory_path
+                        .join(RelPath::new(task_file_name()).unwrap())
+                        .as_std_path()
+                        .to_path_buf(),
                 });
             }
             (LocalSettingsKind::Debug, _) => {
                 return Err(InvalidSettingsError::Debug {
                     message: "Attempted to submit debugger config into the settings store"
                         .to_string(),
-                    path: directory_path.join(task_file_name()),
+                    path: directory_path
+                        .join(RelPath::new(task_file_name()).unwrap())
+                        .as_std_path()
+                        .to_path_buf(),
                 });
             }
             (LocalSettingsKind::Settings, None) => {
@@ -719,7 +726,7 @@ impl SettingsStore {
                             v.insert((editorconfig_contents.to_owned(), None));
                             return Err(InvalidSettingsError::Editorconfig {
                                 message: e.to_string(),
-                                path: directory_path.join(EDITORCONFIG_NAME),
+                                path: directory_path.join(RelPath::new(EDITORCONFIG_NAME).unwrap()),
                             });
                         }
                     },
@@ -736,7 +743,8 @@ impl SettingsStore {
                                     o.insert((editorconfig_contents.to_owned(), None));
                                     return Err(InvalidSettingsError::Editorconfig {
                                         message: e.to_string(),
-                                        path: directory_path.join(EDITORCONFIG_NAME),
+                                        path: directory_path
+                                            .join(RelPath::new(EDITORCONFIG_NAME).unwrap()),
                                     });
                                 }
                             }
@@ -772,20 +780,20 @@ impl SettingsStore {
     pub fn clear_local_settings(&mut self, root_id: WorktreeId, cx: &mut App) -> Result<()> {
         self.local_settings
             .retain(|(worktree_id, _), _| worktree_id != &root_id);
-        self.recompute_values(Some((root_id, "".as_ref())), cx)?;
+        self.recompute_values(Some((root_id, RelPath::empty())), cx)?;
         Ok(())
     }
 
     pub fn local_settings(
         &self,
         root_id: WorktreeId,
-    ) -> impl '_ + Iterator<Item = (Arc<Path>, &ProjectSettingsContent)> {
+    ) -> impl '_ + Iterator<Item = (Arc<RelPath>, &ProjectSettingsContent)> {
         self.local_settings
             .range(
-                (root_id, Path::new("").into())
+                (root_id, RelPath::empty().into())
                     ..(
                         WorktreeId::from_usize(root_id.to_usize() + 1),
-                        Path::new("").into(),
+                        RelPath::empty().into(),
                     ),
             )
             .map(|((_, path), content)| (path.clone(), &content.project))
@@ -794,13 +802,13 @@ impl SettingsStore {
     pub fn local_editorconfig_settings(
         &self,
         root_id: WorktreeId,
-    ) -> impl '_ + Iterator<Item = (Arc<Path>, String, Option<Editorconfig>)> {
+    ) -> impl '_ + Iterator<Item = (Arc<RelPath>, String, Option<Editorconfig>)> {
         self.raw_editorconfig_settings
             .range(
-                (root_id, Path::new("").into())
+                (root_id, RelPath::empty().into())
                     ..(
                         WorktreeId::from_usize(root_id.to_usize() + 1),
-                        Path::new("").into(),
+                        RelPath::empty().into(),
                     ),
             )
             .map(|((_, path), (content, parsed_content))| {
@@ -862,12 +870,12 @@ impl SettingsStore {
 
     fn recompute_values(
         &mut self,
-        changed_local_path: Option<(WorktreeId, &Path)>,
+        changed_local_path: Option<(WorktreeId, &RelPath)>,
         cx: &mut App,
     ) -> std::result::Result<(), InvalidSettingsError> {
         // Reload the global and local values for every setting.
         let mut project_settings_stack = Vec::<SettingsContent>::new();
-        let mut paths_stack = Vec::<Option<(WorktreeId, &Path)>>::new();
+        let mut paths_stack = Vec::<Option<(WorktreeId, &RelPath)>>::new();
 
         if changed_local_path.is_none() {
             let mut merged = self.default_settings.as_ref().clone();
@@ -931,7 +939,7 @@ impl SettingsStore {
     pub fn editorconfig_properties(
         &self,
         for_worktree: WorktreeId,
-        for_path: &Path,
+        for_path: &RelPath,
     ) -> Option<EditorconfigProperties> {
         let mut properties = EditorconfigProperties::new();
 
@@ -947,7 +955,9 @@ impl SettingsStore {
                 properties = EditorconfigProperties::new();
             }
             for section in parsed_editorconfig.sections {
-                section.apply_to(&mut properties, for_path).log_err()?;
+                section
+                    .apply_to(&mut properties, for_path.as_std_path())
+                    .log_err()?;
             }
         }
 
@@ -958,11 +968,11 @@ impl SettingsStore {
 
 #[derive(Debug, Clone, PartialEq)]
 pub enum InvalidSettingsError {
-    LocalSettings { path: PathBuf, message: String },
+    LocalSettings { path: Arc<RelPath>, message: String },
     UserSettings { message: String },
     ServerSettings { message: String },
     DefaultSettings { message: String },
-    Editorconfig { path: PathBuf, message: String },
+    Editorconfig { path: Arc<RelPath>, message: String },
     Tasks { path: PathBuf, message: String },
     Debug { path: PathBuf, message: String },
 }
@@ -1011,7 +1021,7 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
         type_name::<T>()
     }
 
-    fn all_local_values(&self) -> Vec<(WorktreeId, Arc<Path>, &dyn Any)> {
+    fn all_local_values(&self) -> Vec<(WorktreeId, Arc<RelPath>, &dyn Any)> {
         self.local_values
             .iter()
             .map(|(id, path, value)| (*id, path.clone(), value as _))
@@ -1036,7 +1046,7 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
         self.global_value = Some(*value.downcast().unwrap());
     }
 
-    fn set_local_value(&mut self, root_id: WorktreeId, path: Arc<Path>, value: Box<dyn Any>) {
+    fn set_local_value(&mut self, root_id: WorktreeId, path: Arc<RelPath>, value: Box<dyn Any>) {
         let value = *value.downcast().unwrap();
         match self
             .local_values
@@ -1067,6 +1077,7 @@ mod tests {
 
     use super::*;
     use unindent::Unindent;
+    use util::rel_path::rel_path;
 
     #[derive(Debug, PartialEq)]
     struct AutoUpdateSetting {
@@ -1178,7 +1189,7 @@ mod tests {
         store
             .set_local_settings(
                 WorktreeId::from_usize(1),
-                Path::new("/root1").into(),
+                rel_path("root1").into(),
                 LocalSettingsKind::Settings,
                 Some(r#"{ "tab_size": 5 }"#),
                 cx,
@@ -1187,7 +1198,7 @@ mod tests {
         store
             .set_local_settings(
                 WorktreeId::from_usize(1),
-                Path::new("/root1/subdir").into(),
+                rel_path("root1/subdir").into(),
                 LocalSettingsKind::Settings,
                 Some(r#"{ "preferred_line_length": 50 }"#),
                 cx,
@@ -1197,7 +1208,7 @@ mod tests {
         store
             .set_local_settings(
                 WorktreeId::from_usize(1),
-                Path::new("/root2").into(),
+                rel_path("root2").into(),
                 LocalSettingsKind::Settings,
                 Some(r#"{ "tab_size": 9, "auto_update": true}"#),
                 cx,
@@ -1207,7 +1218,7 @@ mod tests {
         assert_eq!(
             store.get::<DefaultLanguageSettings>(Some(SettingsLocation {
                 worktree_id: WorktreeId::from_usize(1),
-                path: Path::new("/root1/something"),
+                path: rel_path("root1/something"),
             })),
             &DefaultLanguageSettings {
                 preferred_line_length: 80,
@@ -1217,7 +1228,7 @@ mod tests {
         assert_eq!(
             store.get::<DefaultLanguageSettings>(Some(SettingsLocation {
                 worktree_id: WorktreeId::from_usize(1),
-                path: Path::new("/root1/subdir/something")
+                path: rel_path("root1/subdir/something"),
             })),
             &DefaultLanguageSettings {
                 preferred_line_length: 50,
@@ -1227,7 +1238,7 @@ mod tests {
         assert_eq!(
             store.get::<DefaultLanguageSettings>(Some(SettingsLocation {
                 worktree_id: WorktreeId::from_usize(1),
-                path: Path::new("/root2/something")
+                path: rel_path("root2/something"),
             })),
             &DefaultLanguageSettings {
                 preferred_line_length: 80,
@@ -1237,7 +1248,7 @@ mod tests {
         assert_eq!(
             store.get::<AutoUpdateSetting>(Some(SettingsLocation {
                 worktree_id: WorktreeId::from_usize(1),
-                path: Path::new("/root2/something")
+                path: rel_path("root2/something")
             })),
             &AutoUpdateSetting { auto_update: false }
         );

crates/tab_switcher/src/tab_switcher_tests.rs 🔗

@@ -4,8 +4,7 @@ use gpui::{TestAppContext, VisualTestContext};
 use menu::SelectPrevious;
 use project::{Project, ProjectPath};
 use serde_json::json;
-use std::path::Path;
-use util::path;
+use util::{path, rel_path::rel_path};
 use workspace::{AppState, Workspace};
 
 #[ctor::ctor]
@@ -331,7 +330,7 @@ async fn open_buffer(
     });
     let project_path = ProjectPath {
         worktree_id,
-        path: Arc::from(Path::new(file_path)),
+        path: rel_path(file_path).into(),
     };
     workspace
         .update_in(cx, move |workspace, window, cx| {

crates/tasks_ui/src/modal.rs 🔗

@@ -183,7 +183,7 @@ impl TasksModal {
                 id: _,
                 directory_in_worktree: dir,
                 id_base: _,
-            } => dir.ends_with(".zed"),
+            } => dir.file_name().is_some_and(|name| name == ".zed"),
             _ => false,
         });
         // todo(debugger): We're always adding lsp tasks here even if prefer_lsp is false
@@ -194,7 +194,7 @@ impl TasksModal {
                 TaskSourceKind::Worktree {
                     directory_in_worktree: dir,
                     ..
-                } => !(hide_vscode && dir.ends_with(".vscode")),
+                } => !(hide_vscode && dir.file_name().is_some_and(|name| name == ".vscode")),
                 TaskSourceKind::Language { .. } => add_current_language_tasks,
                 _ => true,
             }

crates/tasks_ui/src/tasks_ui.rs 🔗

@@ -399,7 +399,7 @@ mod tests {
     use serde_json::json;
     use task::{TaskContext, TaskVariables, VariableName};
     use ui::VisualContext;
-    use util::path;
+    use util::{path, rel_path::rel_path};
     use workspace::{AppState, Workspace};
 
     use crate::task_contexts;
@@ -479,8 +479,9 @@ mod tests {
 
         let buffer1 = workspace
             .update(cx, |this, cx| {
-                this.project()
-                    .update(cx, |this, cx| this.open_buffer((worktree_id, "a.ts"), cx))
+                this.project().update(cx, |this, cx| {
+                    this.open_buffer((worktree_id, rel_path("a.ts")), cx)
+                })
             })
             .await
             .unwrap();
@@ -493,7 +494,7 @@ mod tests {
         let buffer2 = workspace
             .update(cx, |this, cx| {
                 this.project().update(cx, |this, cx| {
-                    this.open_buffer((worktree_id, "rust/b.rs"), cx)
+                    this.open_buffer((worktree_id, rel_path("rust/b.rs")), cx)
                 })
             })
             .await

crates/terminal_view/src/terminal_path_like_target.rs 🔗

@@ -4,9 +4,13 @@ use editor::Editor;
 use gpui::{App, AppContext, Context, Task, WeakEntity, Window};
 use itertools::Itertools;
 use project::{Entry, Metadata};
-use std::path::{Path, PathBuf};
+use std::path::PathBuf;
 use terminal::PathLikeTarget;
-use util::{ResultExt, debug_panic, paths::PathWithPosition};
+use util::{
+    ResultExt, debug_panic,
+    paths::{PathStyle, PathWithPosition},
+    rel_path::RelPath,
+};
 use workspace::{OpenOptions, OpenVisible, Workspace};
 
 /// The way we found the open target. This is important to have for test assertions.
@@ -179,8 +183,9 @@ fn possible_open_target(
         let mut paths_to_check = Vec::with_capacity(potential_paths.len());
         let relative_cwd = cwd
             .and_then(|cwd| cwd.strip_prefix(&worktree_root).ok())
+            .and_then(|cwd| RelPath::from_std_path(cwd, PathStyle::local()).ok())
             .and_then(|cwd_stripped| {
-                (cwd_stripped != Path::new("")).then(|| {
+                (cwd_stripped.as_ref() != RelPath::empty()).then(|| {
                     is_cwd_in_worktree = true;
                     cwd_stripped
                 })
@@ -217,19 +222,21 @@ fn possible_open_target(
                 }
             };
 
-            if path_to_check.path.is_relative()
+            if let Ok(relative_path_to_check) =
+                RelPath::from_std_path(&path_to_check.path, PathStyle::local())
                 && !worktree.read(cx).is_single_file()
                 && let Some(entry) = relative_cwd
+                    .clone()
                     .and_then(|relative_cwd| {
                         worktree
                             .read(cx)
-                            .entry_for_path(&relative_cwd.join(&path_to_check.path))
+                            .entry_for_path(&relative_cwd.join(&relative_path_to_check))
                     })
-                    .or_else(|| worktree.read(cx).entry_for_path(&path_to_check.path))
+                    .or_else(|| worktree.read(cx).entry_for_path(&relative_path_to_check))
             {
                 open_target = Some(OpenTarget::Worktree(
                     PathWithPosition {
-                        path: worktree_root.join(&entry.path),
+                        path: worktree.read(cx).absolutize(&entry.path),
                         row: path_to_check.row,
                         column: path_to_check.column,
                     },
@@ -357,16 +364,18 @@ fn possible_open_target(
             for (worktree, worktree_paths_to_check) in worktree_paths_to_check {
                 let found_entry = worktree
                     .update(cx, |worktree, _| -> Option<OpenTarget> {
-                        let worktree_root = worktree.abs_path();
-                        let traversal = worktree.traverse_from_path(true, true, false, "".as_ref());
+                        let traversal =
+                            worktree.traverse_from_path(true, true, false, RelPath::empty());
                         for entry in traversal {
-                            if let Some(path_in_worktree) = worktree_paths_to_check
-                                .iter()
-                                .find(|path_to_check| entry.path.ends_with(&path_to_check.path))
+                            if let Some(path_in_worktree) =
+                                worktree_paths_to_check.iter().find(|path_to_check| {
+                                    RelPath::from_std_path(&path_to_check.path, PathStyle::local())
+                                        .is_ok_and(|path| entry.path.ends_with(&path))
+                                })
                             {
                                 return Some(OpenTarget::Worktree(
                                     PathWithPosition {
-                                        path: worktree_root.join(&entry.path),
+                                        path: worktree.absolutize(&entry.path),
                                         row: path_in_worktree.row,
                                         column: path_in_worktree.column,
                                     },
@@ -536,7 +545,7 @@ mod tests {
             fs.insert_tree(path, tree).await;
         }
 
-        let project = Project::test(
+        let project: gpui::Entity<Project> = Project::test(
             fs.clone(),
             worktree_roots.into_iter().map(Path::new),
             app_cx,
@@ -1005,30 +1014,32 @@ mod tests {
                     test_local!(
                         "foo/./bar.txt",
                         "/tmp/issue28339/foo/bar.txt",
-                        "/tmp/issue28339"
+                        "/tmp/issue28339",
+                        WorktreeExact
                     );
                     test_local!(
                         "foo/../foo/bar.txt",
                         "/tmp/issue28339/foo/bar.txt",
                         "/tmp/issue28339",
-                        FileSystemBackground
+                        WorktreeExact
                     );
                     test_local!(
                         "foo/..///foo/bar.txt",
                         "/tmp/issue28339/foo/bar.txt",
                         "/tmp/issue28339",
-                        FileSystemBackground
+                        WorktreeExact
                     );
                     test_local!(
                         "issue28339/../issue28339/foo/../foo/bar.txt",
                         "/tmp/issue28339/foo/bar.txt",
                         "/tmp/issue28339",
-                        FileSystemBackground
+                        WorktreeExact
                     );
                     test_local!(
                         "./bar.txt",
                         "/tmp/issue28339/foo/bar.txt",
-                        "/tmp/issue28339/foo"
+                        "/tmp/issue28339/foo",
+                        WorktreeExact
                     );
                     test_local!(
                         "../foo/bar.txt",

crates/terminal_view/src/terminal_view.rs 🔗

@@ -1574,6 +1574,7 @@ mod tests {
     use gpui::TestAppContext;
     use project::{Entry, Project, ProjectPath, Worktree};
     use std::path::Path;
+    use util::rel_path::RelPath;
     use workspace::AppState;
 
     // Working directory calculation tests
@@ -1735,7 +1736,7 @@ mod tests {
         let entry = cx
             .update(|cx| {
                 wt.update(cx, |wt, cx| {
-                    wt.create_entry(Path::new(""), is_dir, None, cx)
+                    wt.create_entry(RelPath::empty().into(), is_dir, None, cx)
                 })
             })
             .await

crates/title_bar/src/title_bar.rs 🔗

@@ -33,14 +33,14 @@ use onboarding_banner::OnboardingBanner;
 use project::{Project, WorktreeSettings};
 use remote::RemoteConnectionOptions;
 use settings::{Settings, SettingsLocation};
-use std::{path::Path, sync::Arc};
+use std::sync::Arc;
 use theme::ActiveTheme;
 use title_bar_settings::TitleBarSettings;
 use ui::{
     Avatar, Button, ButtonLike, ButtonStyle, Chip, ContextMenu, Icon, IconName, IconSize,
     IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, h_flex, prelude::*,
 };
-use util::ResultExt;
+use util::{ResultExt, rel_path::RelPath};
 use workspace::{Workspace, notifications::NotifyResultExt};
 use zed_actions::{OpenRecent, OpenRemote};
 
@@ -439,13 +439,13 @@ impl TitleBar {
                 let worktree = worktree.read(cx);
                 let settings_location = SettingsLocation {
                     worktree_id: worktree.id(),
-                    path: Path::new(""),
+                    path: RelPath::empty(),
                 };
 
                 let settings = WorktreeSettings::get(Some(settings_location), cx);
                 match &settings.project_name {
                     Some(name) => name.as_str(),
-                    None => worktree.root_name(),
+                    None => worktree.root_name_str(),
                 }
             })
             .next();

crates/toolchain_selector/src/active_toolchain.rs 🔗

@@ -1,4 +1,4 @@
-use std::{path::Path, sync::Arc};
+use std::sync::Arc;
 
 use editor::Editor;
 use gpui::{
@@ -8,7 +8,7 @@ use gpui::{
 use language::{Buffer, BufferEvent, LanguageName, Toolchain, ToolchainScope};
 use project::{Project, ProjectPath, Toolchains, WorktreeId, toolchain_store::ToolchainStoreEvent};
 use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, SharedString, Tooltip};
-use util::maybe;
+use util::{maybe, rel_path::RelPath};
 use workspace::{StatusItemView, Workspace, item::ItemHandle};
 
 use crate::ToolchainSelector;
@@ -83,10 +83,7 @@ impl ActiveToolchain {
                 let (worktree_id, path) = active_file
                     .update(cx, |this, cx| {
                         this.file().and_then(|file| {
-                            Some((
-                                file.worktree_id(cx),
-                                Arc::<Path>::from(file.path().parent()?),
-                            ))
+                            Some((file.worktree_id(cx), file.path().parent()?.into()))
                         })
                     })
                     .ok()
@@ -142,7 +139,7 @@ impl ActiveToolchain {
     fn active_toolchain(
         workspace: WeakEntity<Workspace>,
         worktree_id: WorktreeId,
-        relative_path: Arc<Path>,
+        relative_path: Arc<RelPath>,
         language_name: LanguageName,
         cx: &mut AsyncWindowContext,
     ) -> Task<Option<Toolchain>> {
@@ -205,7 +202,7 @@ impl ActiveToolchain {
                         .set_toolchain(
                             workspace_id,
                             worktree_id,
-                            relative_path.to_string_lossy().into_owned(),
+                            relative_path.clone(),
                             toolchain.clone(),
                         )
                         .await

crates/toolchain_selector/src/toolchain_selector.rs 🔗

@@ -24,7 +24,7 @@ use ui::{
     Divider, HighlightedLabel, KeyBinding, List, ListItem, ListItemSpacing, Navigable,
     NavigableEntry, prelude::*,
 };
-use util::{ResultExt, maybe, paths::PathStyle};
+use util::{ResultExt, maybe, paths::PathStyle, rel_path::RelPath};
 use workspace::{ModalView, Workspace};
 
 actions!(
@@ -48,7 +48,7 @@ pub struct ToolchainSelector {
     project: Entity<Project>,
     language_name: LanguageName,
     worktree_id: WorktreeId,
-    relative_path: Arc<Path>,
+    relative_path: Arc<RelPath>,
 }
 
 #[derive(Clone)]
@@ -132,7 +132,7 @@ impl AddToolchainState {
             tx,
             DirectoryLister::Project(project),
             false,
-            PathStyle::current(),
+            PathStyle::local(),
         )
         .show_hidden()
         .with_footer(Arc::new(move |_, cx| {
@@ -241,9 +241,7 @@ impl AddToolchainState {
 
                 // Suggest a default scope based on the applicability.
                 let scope = if let Some(project_path) = resolved_toolchain_path {
-                    if root_path.path.as_ref() != Path::new("")
-                        && project_path.starts_with(&root_path)
-                    {
+                    if !root_path.path.as_ref().is_empty() && project_path.starts_with(&root_path) {
                         ToolchainScope::Subproject(root_path.worktree_id, root_path.path)
                     } else {
                         ToolchainScope::Project
@@ -584,7 +582,7 @@ impl ToolchainSelector {
 
         let language_name = buffer.read(cx).language()?.name();
         let worktree_id = buffer.read(cx).file()?.worktree_id(cx);
-        let relative_path: Arc<Path> = Arc::from(buffer.read(cx).file()?.path().parent()?);
+        let relative_path: Arc<RelPath> = buffer.read(cx).file()?.path().parent()?.into();
         let worktree_root_path = project
             .read(cx)
             .worktree_for_id(worktree_id, cx)?
@@ -593,9 +591,13 @@ impl ToolchainSelector {
         let workspace_id = workspace.database_id()?;
         let weak = workspace.weak_handle();
         cx.spawn_in(window, async move |workspace, cx| {
-            let as_str = relative_path.to_string_lossy().into_owned();
             let active_toolchain = workspace::WORKSPACE_DB
-                .toolchain(workspace_id, worktree_id, as_str, language_name.clone())
+                .toolchain(
+                    workspace_id,
+                    worktree_id,
+                    relative_path.clone(),
+                    language_name.clone(),
+                )
                 .await
                 .ok()
                 .flatten();
@@ -628,7 +630,7 @@ impl ToolchainSelector {
         active_toolchain: Option<Toolchain>,
         worktree_id: WorktreeId,
         worktree_root: Arc<Path>,
-        relative_path: Arc<Path>,
+        relative_path: Arc<RelPath>,
         language_name: LanguageName,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -741,7 +743,7 @@ pub struct ToolchainSelectorDelegate {
     workspace: WeakEntity<Workspace>,
     worktree_id: WorktreeId,
     worktree_abs_path_root: Arc<Path>,
-    relative_path: Arc<Path>,
+    relative_path: Arc<RelPath>,
     placeholder_text: Arc<str>,
     add_toolchain_text: Arc<str>,
     project: Entity<Project>,
@@ -757,12 +759,13 @@ impl ToolchainSelectorDelegate {
         worktree_id: WorktreeId,
         worktree_abs_path_root: Arc<Path>,
         project: Entity<Project>,
-        relative_path: Arc<Path>,
+        relative_path: Arc<RelPath>,
         language_name: LanguageName,
         window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Self {
         let _project = project.clone();
+        let path_style = project.read(cx).path_style(cx);
 
         let _fetch_candidates_task = cx.spawn_in(window, {
             async move |this, cx| {
@@ -802,11 +805,10 @@ impl ToolchainSelectorDelegate {
                     .ok()?
                     .await?;
                 let pretty_path = {
-                    let path = relative_path.to_string_lossy();
-                    if path.is_empty() {
+                    if relative_path.is_empty() {
                         Cow::Borrowed("worktree root")
                     } else {
-                        Cow::Owned(format!("`{}`", path))
+                        Cow::Owned(format!("`{}`", relative_path.display(path_style)))
                     }
                 };
                 let placeholder_text =
@@ -898,7 +900,7 @@ impl PickerDelegate for ToolchainSelectorDelegate {
                 let workspace = self.workspace.clone();
                 let worktree_id = self.worktree_id;
                 let path = self.relative_path.clone();
-                let relative_path = self.relative_path.to_string_lossy().into_owned();
+                let relative_path = self.relative_path.clone();
                 cx.spawn_in(window, async move |_, cx| {
                     workspace::WORKSPACE_DB
                         .set_toolchain(workspace_id, worktree_id, relative_path, toolchain.clone())

crates/ui/src/components/label/highlighted_label.rs 🔗

@@ -21,6 +21,14 @@ impl HighlightedLabel {
             highlight_indices,
         }
     }
+
+    pub fn text(&self) -> &str {
+        self.label.as_str()
+    }
+
+    pub fn highlight_indices(&self) -> &[usize] {
+        &self.highlight_indices
+    }
 }
 
 impl LabelCommon for HighlightedLabel {

crates/util/Cargo.toml 🔗

@@ -21,6 +21,7 @@ async-fs.workspace = true
 async_zip.workspace = true
 collections.workspace = true
 dirs.workspace = true
+dunce = "1.0"
 futures-lite.workspace = true
 futures.workspace = true
 git2 = { workspace = true, optional = true }
@@ -50,7 +51,6 @@ nix = { workspace = true, features = ["user"] }
 
 [target.'cfg(windows)'.dependencies]
 tendril = "0.4.3"
-dunce = "1.0"
 
 [dev-dependencies]
 git2.workspace = true

crates/util/src/paths.rs 🔗

@@ -31,17 +31,9 @@ pub fn home_dir() -> &'static PathBuf {
     })
 }
 
-#[cfg(any(test, feature = "test-support"))]
-pub fn set_home_dir(path: PathBuf) {
-    HOME_DIR
-        .set(path)
-        .expect("set_home_dir called after home_dir was already accessed");
-}
-
 pub trait PathExt {
     fn compact(&self) -> PathBuf;
     fn extension_or_hidden_file_name(&self) -> Option<&str>;
-    fn to_sanitized_string(&self) -> String;
     fn try_from_bytes<'a>(bytes: &'a [u8]) -> anyhow::Result<Self>
     where
         Self: From<&'a Path>,
@@ -106,20 +98,6 @@ impl<T: AsRef<Path>> PathExt for T {
             .or_else(|| path.file_stem()?.to_str())
     }
 
-    /// Returns a sanitized string representation of the path.
-    /// Note, on Windows, this assumes that the path is a valid UTF-8 string and
-    /// is not a UNC path.
-    fn to_sanitized_string(&self) -> String {
-        #[cfg(target_os = "windows")]
-        {
-            self.as_ref().to_string_lossy().replace("/", "\\")
-        }
-        #[cfg(not(target_os = "windows"))]
-        {
-            self.as_ref().to_string_lossy().to_string()
-        }
-    }
-
     /// Converts a local path to one that can be used inside of WSL.
     /// Returns `None` if the path cannot be converted into a WSL one (network share).
     fn local_to_wsl(&self) -> Option<PathBuf> {
@@ -220,17 +198,6 @@ impl SanitizedPath {
     pub fn to_path_buf(&self) -> PathBuf {
         self.0.to_path_buf()
     }
-
-    pub fn to_glob_string(&self) -> String {
-        #[cfg(target_os = "windows")]
-        {
-            self.0.to_string_lossy().replace("/", "\\")
-        }
-        #[cfg(not(target_os = "windows"))]
-        {
-            self.0.to_string_lossy().to_string()
-        }
-    }
 }
 
 impl std::fmt::Debug for SanitizedPath {
@@ -265,7 +232,7 @@ impl AsRef<Path> for SanitizedPath {
     }
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 pub enum PathStyle {
     Posix,
     Windows,
@@ -273,83 +240,69 @@ pub enum PathStyle {
 
 impl PathStyle {
     #[cfg(target_os = "windows")]
-    pub const fn current() -> Self {
+    pub const fn local() -> Self {
         PathStyle::Windows
     }
 
     #[cfg(not(target_os = "windows"))]
-    pub const fn current() -> Self {
+    pub const fn local() -> Self {
         PathStyle::Posix
     }
 
     #[inline]
-    pub fn separator(&self) -> &str {
+    pub fn separator(&self) -> &'static str {
         match self {
             PathStyle::Posix => "/",
             PathStyle::Windows => "\\",
         }
     }
+
+    pub fn is_windows(&self) -> bool {
+        *self == PathStyle::Windows
+    }
+
+    pub fn join(self, left: impl AsRef<Path>, right: impl AsRef<Path>) -> Option<String> {
+        let right = right.as_ref().to_str()?;
+        if is_absolute(right, self) {
+            return None;
+        }
+        let left = left.as_ref().to_str()?;
+        if left.is_empty() {
+            Some(right.into())
+        } else {
+            Some(format!(
+                "{left}{}{right}",
+                if left.ends_with(self.separator()) {
+                    ""
+                } else {
+                    self.separator()
+                }
+            ))
+        }
+    }
 }
 
 #[derive(Debug, Clone)]
 pub struct RemotePathBuf {
-    inner: PathBuf,
     style: PathStyle,
-    string: String, // Cached string representation
+    string: String,
 }
 
 impl RemotePathBuf {
-    pub fn new(path: PathBuf, style: PathStyle) -> Self {
-        #[cfg(target_os = "windows")]
-        let string = match style {
-            PathStyle::Posix => path.to_string_lossy().replace('\\', "/"),
-            PathStyle::Windows => path.to_string_lossy().into(),
-        };
-        #[cfg(not(target_os = "windows"))]
-        let string = match style {
-            PathStyle::Posix => path.to_string_lossy().to_string(),
-            PathStyle::Windows => path.to_string_lossy().replace('/', "\\"),
-        };
-        Self {
-            inner: path,
-            style,
-            string,
-        }
+    pub fn new(string: String, style: PathStyle) -> Self {
+        Self { style, string }
     }
 
     pub fn from_str(path: &str, style: PathStyle) -> Self {
-        let path_buf = PathBuf::from(path);
-        Self::new(path_buf, style)
-    }
-
-    #[cfg(target_os = "windows")]
-    pub fn to_proto(&self) -> String {
-        match self.path_style() {
-            PathStyle::Posix => self.to_string(),
-            PathStyle::Windows => self.inner.to_string_lossy().replace('\\', "/"),
-        }
-    }
-
-    #[cfg(not(target_os = "windows"))]
-    pub fn to_proto(&self) -> String {
-        match self.path_style() {
-            PathStyle::Posix => self.inner.to_string_lossy().to_string(),
-            PathStyle::Windows => self.to_string(),
-        }
-    }
-
-    pub fn as_path(&self) -> &Path {
-        &self.inner
+        Self::new(path.to_string(), style)
     }
 
     pub fn path_style(&self) -> PathStyle {
         self.style
     }
 
-    pub fn parent(&self) -> Option<RemotePathBuf> {
-        self.inner
-            .parent()
-            .map(|p| RemotePathBuf::new(p.to_path_buf(), self.style))
+    pub fn to_proto(self) -> String {
+        self.string
     }
 }
 
@@ -359,6 +312,19 @@ impl Display for RemotePathBuf {
     }
 }
 
+pub 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('\\')))
+}
+
 /// A delimiter to use in `path_query:row_number:column_number` strings parsing.
 pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
 
@@ -589,10 +555,11 @@ impl PathWithPosition {
     }
 }
 
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Debug)]
 pub struct PathMatcher {
     sources: Vec<String>,
     glob: GlobSet,
+    path_style: PathStyle,
 }
 
 // impl std::fmt::Display for PathMatcher {
@@ -610,7 +577,10 @@ impl PartialEq for PathMatcher {
 impl Eq for PathMatcher {}
 
 impl PathMatcher {
-    pub fn new(globs: impl IntoIterator<Item = impl AsRef<str>>) -> Result<Self, globset::Error> {
+    pub fn new(
+        globs: impl IntoIterator<Item = impl AsRef<str>>,
+        path_style: PathStyle,
+    ) -> Result<Self, globset::Error> {
         let globs = globs
             .into_iter()
             .map(|as_str| Glob::new(as_str.as_ref()))
@@ -621,7 +591,11 @@ impl PathMatcher {
             glob_builder.add(single_glob);
         }
         let glob = glob_builder.build()?;
-        Ok(PathMatcher { glob, sources })
+        Ok(PathMatcher {
+            glob,
+            sources,
+            path_style,
+        })
     }
 
     pub fn sources(&self) -> &[String] {
@@ -639,7 +613,7 @@ impl PathMatcher {
 
     fn check_with_end_separator(&self, path: &Path) -> bool {
         let path_str = path.to_string_lossy();
-        let separator = std::path::MAIN_SEPARATOR_STR;
+        let separator = self.path_style.separator();
         if path_str.ends_with(separator) {
             false
         } else {
@@ -648,6 +622,16 @@ impl PathMatcher {
     }
 }
 
+impl Default for PathMatcher {
+    fn default() -> Self {
+        Self {
+            path_style: PathStyle::local(),
+            glob: GlobSet::empty(),
+            sources: vec![],
+        }
+    }
+}
+
 /// Custom character comparison that prioritizes lowercase for same letters
 fn compare_chars(a: char, b: char) -> Ordering {
     // First compare case-insensitive
@@ -1275,7 +1259,8 @@ mod tests {
     #[test]
     fn edge_of_glob() {
         let path = Path::new("/work/node_modules");
-        let path_matcher = PathMatcher::new(&["**/node_modules/**".to_owned()]).unwrap();
+        let path_matcher =
+            PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap();
         assert!(
             path_matcher.is_match(path),
             "Path matcher should match {path:?}"
@@ -1285,7 +1270,8 @@ mod tests {
     #[test]
     fn project_search() {
         let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules");
-        let path_matcher = PathMatcher::new(&["**/node_modules/**".to_owned()]).unwrap();
+        let path_matcher =
+            PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap();
         assert!(
             path_matcher.is_match(path),
             "Path matcher should match {path:?}"

crates/util/src/rel_path.rs 🔗

@@ -0,0 +1,515 @@
+use crate::paths::{PathStyle, is_absolute};
+use anyhow::{Context as _, Result, anyhow, bail};
+use serde::{Deserialize, Serialize};
+use std::{
+    borrow::Cow,
+    ffi::OsStr,
+    fmt,
+    ops::Deref,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+
+#[repr(transparent)]
+#[derive(PartialEq, Eq, Hash, Serialize)]
+pub struct RelPath(str);
+
+#[derive(Clone, Serialize, Deserialize)]
+pub struct RelPathBuf(String);
+
+impl RelPath {
+    pub fn empty() -> &'static Self {
+        unsafe { Self::new_unchecked("") }
+    }
+
+    #[track_caller]
+    pub fn new<S: AsRef<str> + ?Sized>(s: &S) -> anyhow::Result<&Self> {
+        let this = unsafe { Self::new_unchecked(s) };
+        if this.0.starts_with("/")
+            || this.0.ends_with("/")
+            || this
+                .components()
+                .any(|component| component == ".." || component == "." || component.is_empty())
+        {
+            bail!("invalid relative path: {:?}", &this.0);
+        }
+        Ok(this)
+    }
+
+    #[track_caller]
+    pub fn from_std_path(path: &Path, path_style: PathStyle) -> Result<Arc<Self>> {
+        let path = path.to_str().context("non utf-8 path")?;
+        let mut string = Cow::Borrowed(path);
+
+        if is_absolute(&string, path_style) {
+            return Err(anyhow!("absolute path not allowed: {path:?}"));
+        }
+
+        if path_style == PathStyle::Windows {
+            string = Cow::Owned(string.as_ref().replace('\\', "/"))
+        }
+
+        let mut this = RelPathBuf::new();
+        for component in unsafe { Self::new_unchecked(string.as_ref()) }.components() {
+            match component {
+                "" => {}
+                "." => {}
+                ".." => {
+                    if !this.pop() {
+                        return Err(anyhow!("path is not relative: {string:?}"));
+                    }
+                }
+                other => this.push(RelPath::new(other)?),
+            }
+        }
+
+        Ok(this.into())
+    }
+
+    pub unsafe fn new_unchecked<S: AsRef<str> + ?Sized>(s: &S) -> &Self {
+        unsafe { &*(s.as_ref() as *const str as *const Self) }
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+
+    pub fn components(&self) -> RelPathComponents<'_> {
+        RelPathComponents(&self.0)
+    }
+
+    pub fn ancestors(&self) -> RelPathAncestors<'_> {
+        RelPathAncestors(Some(&self.0))
+    }
+
+    pub fn file_name(&self) -> Option<&str> {
+        self.components().next_back()
+    }
+
+    pub fn file_stem(&self) -> Option<&str> {
+        Some(self.as_std_path().file_stem()?.to_str().unwrap())
+    }
+
+    pub fn extension(&self) -> Option<&str> {
+        Some(self.as_std_path().extension()?.to_str().unwrap())
+    }
+
+    pub fn parent(&self) -> Option<&Self> {
+        let mut components = self.components();
+        components.next_back()?;
+        Some(components.rest())
+    }
+
+    pub fn starts_with(&self, other: &Self) -> bool {
+        self.strip_prefix(other).is_ok()
+    }
+
+    pub fn ends_with(&self, other: &Self) -> bool {
+        if let Some(suffix) = self.0.strip_suffix(&other.0) {
+            if suffix.ends_with('/') {
+                return true;
+            } else if suffix.is_empty() {
+                return true;
+            }
+        }
+        false
+    }
+
+    pub fn strip_prefix(&self, other: &Self) -> Result<&Self> {
+        if other.is_empty() {
+            return Ok(self);
+        }
+        if let Some(suffix) = self.0.strip_prefix(&other.0) {
+            if let Some(suffix) = suffix.strip_prefix('/') {
+                return Ok(unsafe { Self::new_unchecked(suffix) });
+            } else if suffix.is_empty() {
+                return Ok(Self::empty());
+            }
+        }
+        Err(anyhow!("failed to strip prefix: {other:?} from {self:?}"))
+    }
+
+    pub fn len(&self) -> usize {
+        self.0.matches('/').count() + 1
+    }
+
+    pub fn last_n_components(&self, count: usize) -> Option<&Self> {
+        let len = self.len();
+        if len >= count {
+            let mut components = self.components();
+            for _ in 0..(len - count) {
+                components.next()?;
+            }
+            Some(components.rest())
+        } else {
+            None
+        }
+    }
+
+    pub fn push(&self, component: &str) -> Result<Arc<Self>> {
+        if component.is_empty() {
+            bail!("pushed component is empty");
+        } else if component.contains('/') {
+            bail!("pushed component contains a separator: {component:?}");
+        }
+        let path = format!(
+            "{}{}{}",
+            &self.0,
+            if self.is_empty() { "" } else { "/" },
+            component
+        );
+        Ok(Arc::from(unsafe { Self::new_unchecked(&path) }))
+    }
+
+    pub fn join(&self, other: &Self) -> Arc<Self> {
+        let result = if self.0.is_empty() {
+            Cow::Borrowed(&other.0)
+        } else if other.0.is_empty() {
+            Cow::Borrowed(&self.0)
+        } else {
+            Cow::Owned(format!("{}/{}", &self.0, &other.0))
+        };
+        Arc::from(unsafe { Self::new_unchecked(result.as_ref()) })
+    }
+
+    pub fn to_proto(&self) -> String {
+        self.0.to_owned()
+    }
+
+    pub fn to_rel_path_buf(&self) -> RelPathBuf {
+        RelPathBuf(self.0.to_string())
+    }
+
+    pub fn from_proto(path: &str) -> Result<Arc<Self>> {
+        Ok(Arc::from(Self::new(path)?))
+    }
+
+    pub fn as_str(&self) -> &str {
+        &self.0
+    }
+
+    pub fn display(&self, style: PathStyle) -> Cow<'_, str> {
+        match style {
+            PathStyle::Posix => Cow::Borrowed(&self.0),
+            PathStyle::Windows => Cow::Owned(self.0.replace('/', "\\")),
+        }
+    }
+
+    pub fn as_bytes(&self) -> &[u8] {
+        &self.0.as_bytes()
+    }
+
+    pub fn as_os_str(&self) -> &OsStr {
+        self.0.as_ref()
+    }
+
+    pub fn as_std_path(&self) -> &Path {
+        Path::new(&self.0)
+    }
+}
+
+impl PartialOrd for RelPath {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for RelPath {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.components().cmp(other.components())
+    }
+}
+
+impl fmt::Debug for RelPath {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::Debug::fmt(&self.0, f)
+    }
+}
+
+impl fmt::Debug for RelPathBuf {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::Debug::fmt(&self.0, f)
+    }
+}
+
+impl RelPathBuf {
+    pub fn new() -> Self {
+        Self(String::new())
+    }
+
+    pub fn pop(&mut self) -> bool {
+        if let Some(ix) = self.0.rfind('/') {
+            self.0.truncate(ix);
+            true
+        } else if !self.is_empty() {
+            self.0.clear();
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn push(&mut self, path: &RelPath) {
+        if !self.is_empty() {
+            self.0.push('/');
+        }
+        self.0.push_str(&path.0);
+    }
+
+    pub fn as_rel_path(&self) -> &RelPath {
+        unsafe { RelPath::new_unchecked(self.0.as_str()) }
+    }
+
+    pub fn set_extension(&mut self, extension: &str) -> bool {
+        if let Some(filename) = self.file_name() {
+            let mut filename = PathBuf::from(filename);
+            filename.set_extension(extension);
+            self.pop();
+            self.0.push_str(filename.to_str().unwrap());
+            true
+        } else {
+            false
+        }
+    }
+}
+
+impl Into<Arc<RelPath>> for RelPathBuf {
+    fn into(self) -> Arc<RelPath> {
+        Arc::from(self.as_rel_path())
+    }
+}
+
+impl AsRef<RelPath> for RelPathBuf {
+    fn as_ref(&self) -> &RelPath {
+        self.as_rel_path()
+    }
+}
+
+impl Deref for RelPathBuf {
+    type Target = RelPath;
+
+    fn deref(&self) -> &Self::Target {
+        self.as_ref()
+    }
+}
+
+impl AsRef<Path> for RelPath {
+    fn as_ref(&self) -> &Path {
+        Path::new(&self.0)
+    }
+}
+
+impl From<&RelPath> for Arc<RelPath> {
+    fn from(rel_path: &RelPath) -> Self {
+        let bytes: Arc<str> = Arc::from(&rel_path.0);
+        unsafe { Arc::from_raw(Arc::into_raw(bytes) as *const RelPath) }
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+#[track_caller]
+pub fn rel_path(path: &str) -> &RelPath {
+    RelPath::new(path).unwrap()
+}
+
+impl PartialEq<str> for RelPath {
+    fn eq(&self, other: &str) -> bool {
+        self.0 == *other
+    }
+}
+
+pub struct RelPathComponents<'a>(&'a str);
+
+pub struct RelPathAncestors<'a>(Option<&'a str>);
+
+const SEPARATOR: char = '/';
+
+impl<'a> RelPathComponents<'a> {
+    pub fn rest(&self) -> &'a RelPath {
+        unsafe { RelPath::new_unchecked(self.0) }
+    }
+}
+
+impl<'a> Iterator for RelPathComponents<'a> {
+    type Item = &'a str;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if let Some(sep_ix) = self.0.find(SEPARATOR) {
+            let (head, tail) = self.0.split_at(sep_ix);
+            self.0 = &tail[1..];
+            Some(head)
+        } else if self.0.is_empty() {
+            None
+        } else {
+            let result = self.0;
+            self.0 = "";
+            Some(result)
+        }
+    }
+}
+
+impl<'a> Iterator for RelPathAncestors<'a> {
+    type Item = &'a RelPath;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let result = self.0?;
+        if let Some(sep_ix) = result.rfind(SEPARATOR) {
+            self.0 = Some(&result[..sep_ix]);
+        } else if !result.is_empty() {
+            self.0 = Some("");
+        } else {
+            self.0 = None;
+        }
+        Some(unsafe { RelPath::new_unchecked(result) })
+    }
+}
+
+impl<'a> DoubleEndedIterator for RelPathComponents<'a> {
+    fn next_back(&mut self) -> Option<Self::Item> {
+        if let Some(sep_ix) = self.0.rfind(SEPARATOR) {
+            let (head, tail) = self.0.split_at(sep_ix);
+            self.0 = head;
+            Some(&tail[1..])
+        } else if self.0.is_empty() {
+            None
+        } else {
+            let result = self.0;
+            self.0 = "";
+            Some(result)
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use itertools::Itertools;
+    use std::path::PathBuf;
+
+    #[test]
+    fn test_path_construction() {
+        assert!(RelPath::new("/").is_err());
+        assert!(RelPath::new("/foo").is_err());
+        assert!(RelPath::new("foo/").is_err());
+        assert!(RelPath::new("foo//bar").is_err());
+        assert!(RelPath::new("foo/../bar").is_err());
+        assert!(RelPath::new("./foo/bar").is_err());
+        assert!(RelPath::new("..").is_err());
+
+        assert!(RelPath::from_std_path(Path::new("/"), PathStyle::local()).is_err());
+        assert!(RelPath::from_std_path(Path::new("//"), PathStyle::local()).is_err());
+        assert!(RelPath::from_std_path(Path::new("/foo/"), PathStyle::local()).is_err());
+        assert_eq!(
+            RelPath::from_std_path(&PathBuf::from_iter(["foo", ""]), PathStyle::local()).unwrap(),
+            Arc::from(rel_path("foo"))
+        );
+    }
+
+    #[test]
+    fn test_rel_path_from_std_path() {
+        assert_eq!(
+            RelPath::from_std_path(Path::new("foo/bar/../baz/./quux/"), PathStyle::local())
+                .unwrap()
+                .as_ref(),
+            rel_path("foo/baz/quux")
+        );
+    }
+
+    #[test]
+    fn test_rel_path_components() {
+        let path = rel_path("foo/bar/baz");
+        assert_eq!(
+            path.components().collect::<Vec<_>>(),
+            vec!["foo", "bar", "baz"]
+        );
+        assert_eq!(
+            path.components().rev().collect::<Vec<_>>(),
+            vec!["baz", "bar", "foo"]
+        );
+
+        let path = rel_path("");
+        let mut components = path.components();
+        assert_eq!(components.next(), None);
+    }
+
+    #[test]
+    fn test_rel_path_ancestors() {
+        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(), Some(rel_path("foo/bar")));
+        assert_eq!(ancestors.next(), Some(rel_path("foo")));
+        assert_eq!(ancestors.next(), Some(rel_path("")));
+        assert_eq!(ancestors.next(), None);
+
+        let path = rel_path("foo");
+        let mut ancestors = path.ancestors();
+        assert_eq!(ancestors.next(), Some(rel_path("foo")));
+        assert_eq!(ancestors.next(), Some(RelPath::empty()));
+        assert_eq!(ancestors.next(), None);
+
+        let path = RelPath::empty();
+        let mut ancestors = path.ancestors();
+        assert_eq!(ancestors.next(), Some(RelPath::empty()));
+        assert_eq!(ancestors.next(), None);
+    }
+
+    #[test]
+    fn test_rel_path_parent() {
+        assert_eq!(
+            rel_path("foo/bar/baz").parent(),
+            Some(RelPath::new("foo/bar").unwrap())
+        );
+        assert_eq!(rel_path("foo").parent(), Some(RelPath::empty()));
+        assert_eq!(rel_path("").parent(), None);
+    }
+
+    #[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::new(lhs).unwrap().cmp(RelPath::new(rhs).unwrap())
+            );
+        }
+    }
+
+    #[test]
+    fn test_strip_prefix() {
+        let parent = rel_path("");
+        let child = rel_path(".foo");
+
+        assert!(child.starts_with(parent));
+        assert_eq!(child.strip_prefix(parent).unwrap(), child);
+    }
+
+    #[test]
+    fn test_rel_path_constructors_absolute_path() {
+        assert!(RelPath::from_std_path(Path::new("/a/b"), PathStyle::Windows).is_err());
+        assert!(RelPath::from_std_path(Path::new("\\a\\b"), PathStyle::Windows).is_err());
+        assert!(RelPath::from_std_path(Path::new("/a/b"), PathStyle::Posix).is_err());
+        assert!(RelPath::from_std_path(Path::new("C:/a/b"), PathStyle::Windows).is_err());
+        assert!(RelPath::from_std_path(Path::new("C:\\a\\b"), PathStyle::Windows).is_err());
+        assert!(RelPath::from_std_path(Path::new("C:/a/b"), PathStyle::Posix).is_ok());
+    }
+
+    #[test]
+    fn test_push() {
+        assert_eq!(rel_path("a/b").push("c").unwrap().as_str(), "a/b/c");
+        assert_eq!(rel_path("").push("c").unwrap().as_str(), "c");
+        assert!(rel_path("a/b").push("").is_err());
+        assert!(rel_path("a/b").push("c/d").is_err());
+    }
+
+    #[test]
+    fn test_pop() {
+        let mut path = rel_path("a/b").to_rel_path_buf();
+        path.pop();
+        assert_eq!(path.as_rel_path().as_str(), "a");
+        path.pop();
+        assert_eq!(path.as_rel_path().as_str(), "");
+        path.pop();
+        assert_eq!(path.as_rel_path().as_str(), "");
+    }
+}

crates/util/src/util.rs 🔗

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

crates/vim/src/command.rs 🔗

@@ -1,4 +1,4 @@
-use anyhow::Result;
+use anyhow::{Result, anyhow};
 use collections::{HashMap, HashSet};
 use command_palette_hooks::CommandInterceptResult;
 use editor::{
@@ -6,7 +6,7 @@ use editor::{
     actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
     display_map::ToDisplayPoint,
 };
-use gpui::{Action, App, AppContext as _, Context, Global, Keystroke, Window, actions};
+use gpui::{Action, App, AppContext as _, Context, Global, Keystroke, Task, Window, actions};
 use itertools::Itertools;
 use language::Point;
 use multi_buffer::MultiBufferRow;
@@ -22,12 +22,12 @@ use std::{
     path::Path,
     process::Stdio,
     str::Chars,
-    sync::{Arc, OnceLock},
+    sync::OnceLock,
     time::Instant,
 };
 use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};
 use ui::ActiveTheme;
-use util::ResultExt;
+use util::{ResultExt, rel_path::RelPath};
 use workspace::{Item, SaveIntent, notifications::NotifyResultExt};
 use workspace::{SplitDirection, notifications::DetachAndPromptErr};
 use zed_actions::{OpenDocs, RevealTarget};
@@ -305,31 +305,54 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
             let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
                 return;
             };
-            let project_path = ProjectPath {
-                worktree_id: worktree.read(cx).id(),
-                path: Arc::from(Path::new(&action.filename)),
+            let path_style = worktree.read(cx).path_style();
+            let Ok(project_path) = RelPath::from_std_path(Path::new(&action.filename), path_style)
+                .map(|path| ProjectPath {
+                    worktree_id: worktree.read(cx).id(),
+                    path,
+                })
+            else {
+                // TODO implement save_as with absolute path
+                Task::ready(Err::<(), _>(anyhow!(
+                    "Cannot save buffer with absolute path"
+                )))
+                .detach_and_prompt_err(
+                    "Failed to save",
+                    window,
+                    cx,
+                    |_, _, _| None,
+                );
+                return;
             };
 
-            if project.read(cx).entry_for_path(&project_path, cx).is_some() && action.save_intent != Some(SaveIntent::Overwrite) {
+            if project.read(cx).entry_for_path(&project_path, cx).is_some()
+                && action.save_intent != Some(SaveIntent::Overwrite)
+            {
                 let answer = window.prompt(
                     gpui::PromptLevel::Critical,
-                    &format!("{} already exists. Do you want to replace it?", project_path.path.to_string_lossy()),
+                    &format!(
+                        "{} already exists. Do you want to replace it?",
+                        project_path.path.display(path_style)
+                    ),
                     Some(
-                        "A file or folder with the same name already exists. Replacing it will overwrite its current contents.",
+                        "A file or folder with the same name already exists. \
+                        Replacing it will overwrite its current contents.",
                     ),
                     &["Replace", "Cancel"],
-                cx);
+                    cx,
+                );
                 cx.spawn_in(window, async move |editor, cx| {
                     if answer.await.ok() != Some(0) {
                         return;
                     }
 
-                    let _ = editor.update_in(cx, |editor, window, cx|{
+                    let _ = editor.update_in(cx, |editor, window, cx| {
                         editor
                             .save_as(project, project_path, window, cx)
                             .detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None);
                     });
-                }).detach();
+                })
+                .detach();
             } else {
                 editor
                     .save_as(project, project_path, window, cx)
@@ -348,9 +371,15 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
             let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
                 return;
             };
+            let path_style = worktree.read(cx).path_style();
+            let Some(path) =
+                RelPath::from_std_path(Path::new(&action.filename), path_style).log_err()
+            else {
+                return;
+            };
             let project_path = ProjectPath {
                 worktree_id: worktree.read(cx).id(),
-                path: Arc::from(Path::new(&action.filename)),
+                path,
             };
 
             let direction = if action.vertical {
@@ -442,9 +471,15 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
             let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
                 return;
             };
+            let path_style = worktree.read(cx).path_style();
+            let Some(path) =
+                RelPath::from_std_path(Path::new(&action.filename), path_style).log_err()
+            else {
+                return;
+            };
             let project_path = ProjectPath {
                 worktree_id: worktree.read(cx).id(),
-                path: Arc::from(Path::new(&action.filename)),
+                path,
             };
 
             let _ = workspace.update(cx, |workspace, cx| {
@@ -1710,9 +1745,8 @@ impl Vim {
                         if let Some((_, buffer, _)) = editor.active_excerpt(cx)
                             && let Some(file) = buffer.read(cx).file()
                             && let Some(local) = file.as_local()
-                            && let Some(str) = local.path().to_str()
                         {
-                            ret.push_str(str)
+                            ret.push_str(&local.path().display(local.path_style(cx)));
                         }
                     });
                 }

crates/vim/src/normal.rs 🔗

@@ -863,7 +863,7 @@ impl Vim {
                         file.full_path(cx).to_string_lossy().to_string()
                     }
                 } else {
-                    file.path().to_string_lossy().to_string()
+                    file.path().display(file.path_style(cx)).into_owned()
                 }
             } else {
                 "[No Name]".into()

crates/vim/src/state.rs 🔗

@@ -34,6 +34,7 @@ use ui::{
     StyledTypography, Window, h_flex, rems,
 };
 use util::ResultExt;
+use util::rel_path::RelPath;
 use workspace::searchable::Direction;
 use workspace::{Workspace, WorkspaceDb, WorkspaceId};
 
@@ -343,9 +344,11 @@ impl MarksState {
                 .worktrees(cx)
                 .filter_map(|worktree| {
                     let relative = path.strip_prefix(worktree.read(cx).abs_path()).ok()?;
+                    let path = RelPath::from_std_path(relative, worktree.read(cx).path_style())
+                        .log_err()?;
                     Some(ProjectPath {
                         worktree_id: worktree.read(cx).id(),
-                        path: relative.into(),
+                        path,
                     })
                 })
                 .next();
@@ -872,7 +875,7 @@ impl VimGlobals {
                     buffer
                         .read(cx)
                         .file()
-                        .map(|file| file.path().to_string_lossy().to_string().into())
+                        .map(|file| file.path().display(file.path_style(cx)).into_owned().into())
                 } else {
                     None
                 }

crates/workspace/src/item.rs 🔗

@@ -1298,7 +1298,8 @@ pub mod test {
         InteractiveElement, IntoElement, Render, SharedString, Task, WeakEntity, Window,
     };
     use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
-    use std::{any::Any, cell::Cell, path::Path};
+    use std::{any::Any, cell::Cell};
+    use util::rel_path::rel_path;
 
     pub struct TestProjectItem {
         pub entry_id: Option<ProjectEntryId>,
@@ -1355,7 +1356,7 @@ pub mod test {
             let entry_id = Some(ProjectEntryId::from_proto(id));
             let project_path = Some(ProjectPath {
                 worktree_id: WorktreeId::from_usize(0),
-                path: Path::new(path).into(),
+                path: rel_path(path).into(),
             });
             cx.new(|_| Self {
                 entry_id,
@@ -1376,7 +1377,7 @@ pub mod test {
             let entry_id = Some(ProjectEntryId::from_proto(id));
             let project_path = Some(ProjectPath {
                 worktree_id: WorktreeId::from_usize(0),
-                path: Path::new(path).into(),
+                path: rel_path(path).into(),
             });
             cx.new(|_| Self {
                 entry_id,

crates/workspace/src/pane.rs 🔗

@@ -50,7 +50,7 @@ use ui::{
     PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip, prelude::*,
     right_click_menu,
 };
-use util::{ResultExt, debug_panic, maybe, truncate_and_remove_front};
+use util::{ResultExt, debug_panic, maybe, paths::PathStyle, truncate_and_remove_front};
 
 /// A selected entry in e.g. project panel.
 #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -1652,11 +1652,9 @@ impl Pane {
                 if !project_item.is_dirty() {
                     return;
                 }
-                let filename = project_item.project_path(cx).and_then(|path| {
-                    path.path
-                        .file_name()
-                        .and_then(|name| name.to_str().map(ToOwned::to_owned))
-                });
+                let filename = project_item
+                    .project_path(cx)
+                    .and_then(|path| path.path.file_name().map(ToOwned::to_owned));
                 file_names.insert(filename.unwrap_or("untitled".to_string()));
             });
         }
@@ -1965,9 +1963,10 @@ impl Pane {
 
         const DELETED_MESSAGE: &str = "This file has been deleted on disk since you started editing it. Do you want to recreate it?";
 
+        let path_style = project.read_with(cx, |project, cx| project.path_style(cx))?;
         if save_intent == SaveIntent::Skip {
             return Ok(true);
-        }
+        };
         let Some(item_ix) = pane
             .read_with(cx, |pane, _| pane.index_for_item(item))
             .ok()
@@ -2090,7 +2089,7 @@ impl Pane {
                     let answer_task = pane.update_in(cx, |pane, window, cx| {
                         if pane.save_modals_spawned.insert(item_id) {
                             pane.activate_item(item_ix, true, true, window, cx);
-                            let prompt = dirty_message_for(item.project_path(cx));
+                            let prompt = dirty_message_for(item.project_path(cx), path_style);
                             Some(window.prompt(
                                 PromptLevel::Warning,
                                 &prompt,
@@ -2184,7 +2183,7 @@ impl Pane {
                     let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
                     let new_path = ProjectPath {
                         worktree_id,
-                        path: path.into(),
+                        path: path,
                     };
 
                     pane.update_in(cx, |pane, window, cx| {
@@ -2321,10 +2320,10 @@ impl Pane {
             .worktree_for_entry(entry, cx)?
             .read(cx);
         let entry = worktree.entry_for_id(entry)?;
-        match &entry.canonical_path {
-            Some(canonical_path) => Some(canonical_path.to_path_buf()),
-            None => worktree.absolutize(&entry.path).ok(),
-        }
+        Some(match &entry.canonical_path {
+            Some(canonical_path) => canonical_path.to_path_buf(),
+            None => worktree.absolutize(&entry.path),
+        })
     }
 
     pub fn icon_color(selected: bool) -> Color {
@@ -2875,9 +2874,14 @@ impl Pane {
                                     menu.entry(
                                         "Copy Relative Path",
                                         Some(Box::new(zed_actions::workspace::CopyRelativePath)),
-                                        window.handler_for(&pane, move |_, _, cx| {
+                                        window.handler_for(&pane, move |this, _, cx| {
+                                            let Some(project) = this.project.upgrade() else {
+                                                return;
+                                            };
+                                            let path_style = project
+                                                .update(cx, |project, cx| project.path_style(cx));
                                             cx.write_to_clipboard(ClipboardItem::new_string(
-                                                relative_path.to_string_lossy().to_string(),
+                                                relative_path.display(path_style).to_string(),
                                             ));
                                         }),
                                     )
@@ -4007,16 +4011,15 @@ impl NavHistoryState {
     }
 }
 
-fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
+fn dirty_message_for(buffer_path: Option<ProjectPath>, path_style: PathStyle) -> String {
     let path = buffer_path
         .as_ref()
         .and_then(|p| {
-            p.path
-                .to_str()
-                .and_then(|s| if s.is_empty() { None } else { Some(s) })
+            let path = p.path.display(path_style);
+            if path.is_empty() { None } else { Some(path) }
         })
-        .unwrap_or("This buffer");
-    let path = truncate_and_remove_front(path, 80);
+        .unwrap_or("This buffer".into());
+    let path = truncate_and_remove_front(&path, 80);
     format!("{path} contains unsaved edits. Do you want to save it?")
 }
 

crates/workspace/src/persistence.rs 🔗

@@ -28,7 +28,7 @@ use sqlez::{
 };
 
 use ui::{App, SharedString, px};
-use util::{ResultExt, maybe};
+use util::{ResultExt, maybe, rel_path::RelPath};
 use uuid::Uuid;
 
 use crate::{
@@ -915,10 +915,13 @@ impl WorkspaceDb {
                     relative_worktree_path == String::default()
                 );
 
+                let Some(relative_path) = RelPath::new(&relative_worktree_path).log_err() else {
+                    continue;
+                };
                 if worktree_id != u64::MAX && relative_worktree_path != String::default() {
                     ToolchainScope::Subproject(
                         WorktreeId::from_usize(worktree_id as usize),
-                        Arc::from(relative_worktree_path.as_ref()),
+                        relative_path.into(),
                     )
                 } else {
                     ToolchainScope::Project
@@ -998,7 +1001,7 @@ impl WorkspaceDb {
                     for toolchain in toolchains {
                         let query = sql!(INSERT OR REPLACE INTO user_toolchains(remote_connection_id, workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8));
                         let (workspace_id, worktree_id, relative_worktree_path) = match scope {
-                            ToolchainScope::Subproject(worktree_id, ref path) => (Some(workspace.id), Some(worktree_id), Some(path.to_string_lossy().into_owned())),
+                            ToolchainScope::Subproject(worktree_id, ref path) => (Some(workspace.id), Some(worktree_id), Some(path.as_str().to_owned())),
                             ToolchainScope::Project => (Some(workspace.id), None, None),
                             ToolchainScope::Global => (None, None, None),
                         };
@@ -1637,25 +1640,41 @@ impl WorkspaceDb {
         &self,
         workspace_id: WorkspaceId,
         worktree_id: WorktreeId,
-        relative_worktree_path: String,
+        relative_worktree_path: Arc<RelPath>,
         language_name: LanguageName,
     ) -> Result<Option<Toolchain>> {
         self.write(move |this| {
             let mut select = this
                 .select_bound(sql!(
-                    SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? AND relative_worktree_path = ?
+                    SELECT
+                        name, path, raw_json
+                    FROM toolchains
+                    WHERE
+                        workspace_id = ? AND
+                        language_name = ? AND
+                        worktree_id = ? AND
+                        relative_worktree_path = ?
                 ))
                 .context("select toolchain")?;
 
-            let toolchain: Vec<(String, String, String)> =
-                select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize(), relative_worktree_path))?;
+            let toolchain: Vec<(String, String, String)> = select((
+                workspace_id,
+                language_name.as_ref().to_string(),
+                worktree_id.to_usize(),
+                relative_worktree_path.as_str().to_string(),
+            ))?;
 
-            Ok(toolchain.into_iter().next().and_then(|(name, path, raw_json)| Some(Toolchain {
-                name: name.into(),
-                path: path.into(),
-                language_name,
-                as_json: serde_json::Value::from_str(&raw_json).ok()?,
-            })))
+            Ok(toolchain
+                .into_iter()
+                .next()
+                .and_then(|(name, path, raw_json)| {
+                    Some(Toolchain {
+                        name: name.into(),
+                        path: path.into(),
+                        language_name,
+                        as_json: serde_json::Value::from_str(&raw_json).ok()?,
+                    })
+                }))
         })
         .await
     }
@@ -1663,31 +1682,46 @@ impl WorkspaceDb {
     pub(crate) async fn toolchains(
         &self,
         workspace_id: WorkspaceId,
-    ) -> Result<Vec<(Toolchain, WorktreeId, Arc<Path>)>> {
+    ) -> Result<Vec<(Toolchain, WorktreeId, Arc<RelPath>)>> {
         self.write(move |this| {
             let mut select = this
                 .select_bound(sql!(
-                    SELECT name, path, worktree_id, relative_worktree_path, language_name, raw_json FROM toolchains WHERE workspace_id = ?
+                    SELECT
+                        name, path, worktree_id, relative_worktree_path, language_name, raw_json
+                    FROM toolchains
+                    WHERE workspace_id = ?
                 ))
                 .context("select toolchains")?;
 
             let toolchain: Vec<(String, String, u64, String, String, String)> =
                 select(workspace_id)?;
 
-            Ok(toolchain.into_iter().filter_map(|(name, path, worktree_id, relative_worktree_path, language_name, raw_json)| Some((Toolchain {
-                name: name.into(),
-                path: path.into(),
-                language_name: LanguageName::new(&language_name),
-                as_json: serde_json::Value::from_str(&raw_json).ok()?,
-            }, WorktreeId::from_proto(worktree_id), Arc::from(relative_worktree_path.as_ref())))).collect())
+            Ok(toolchain
+                .into_iter()
+                .filter_map(
+                    |(name, path, worktree_id, relative_worktree_path, language, json)| {
+                        Some((
+                            Toolchain {
+                                name: name.into(),
+                                path: path.into(),
+                                language_name: LanguageName::new(&language),
+                                as_json: serde_json::Value::from_str(&json).ok()?,
+                            },
+                            WorktreeId::from_proto(worktree_id),
+                            RelPath::from_proto(&relative_worktree_path).log_err()?,
+                        ))
+                    },
+                )
+                .collect())
         })
         .await
     }
+
     pub async fn set_toolchain(
         &self,
         workspace_id: WorkspaceId,
         worktree_id: WorktreeId,
-        relative_worktree_path: String,
+        relative_worktree_path: Arc<RelPath>,
         toolchain: Toolchain,
     ) -> Result<()> {
         log::debug!(
@@ -1709,7 +1743,7 @@ impl WorkspaceDb {
             insert((
                 workspace_id,
                 worktree_id.to_usize(),
-                relative_worktree_path,
+                relative_worktree_path.as_str(),
                 toolchain.language_name.as_ref(),
                 toolchain.name.as_ref(),
                 toolchain.path.as_ref(),

crates/workspace/src/workspace.rs 🔗

@@ -104,7 +104,12 @@ use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
 pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 pub use ui;
 use ui::{Window, prelude::*};
-use util::{ResultExt, TryFutureExt, paths::SanitizedPath, serde::default_true};
+use util::{
+    ResultExt, TryFutureExt,
+    paths::{PathStyle, SanitizedPath},
+    rel_path::RelPath,
+    serde::default_true,
+};
 use uuid::Uuid;
 pub use workspace_settings::{
     AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
@@ -625,7 +630,7 @@ impl ProjectItemRegistry {
                     match project_item.await.with_context(|| {
                         format!(
                             "opening project path {:?}",
-                            entry_abs_path.as_deref().unwrap_or(&project_path.path)
+                            entry_abs_path.as_deref().unwrap_or(&project_path.path.as_std_path())
                         )
                     }) {
                         Ok(project_item) => {
@@ -1754,6 +1759,10 @@ impl Workspace {
         &self.project
     }
 
+    pub fn path_style(&self, cx: &App) -> PathStyle {
+        self.project.read(cx).path_style(cx)
+    }
+
     pub fn recently_activated_items(&self, cx: &App) -> HashMap<EntityId, usize> {
         let mut history: HashMap<EntityId, usize> = HashMap::default();
 
@@ -2622,7 +2631,12 @@ impl Workspace {
                                     .strip_prefix(worktree_abs_path.as_ref())
                                     .ok()
                                     .and_then(|relative_path| {
-                                        worktree.entry_for_path(relative_path)
+                                        let relative_path = RelPath::from_std_path(
+                                            relative_path,
+                                            PathStyle::local(),
+                                        )
+                                        .log_err()?;
+                                        worktree.entry_for_path(&relative_path)
                                     })
                             }
                             .map(|entry| entry.id);
@@ -2668,7 +2682,7 @@ impl Workspace {
                 self.open_path(project_path, None, true, window, cx)
             }
             ResolvedPath::AbsPath { path, .. } => self.open_abs_path(
-                path,
+                PathBuf::from(path),
                 OpenOptions {
                     visible: Some(OpenVisible::None),
                     ..Default::default()
@@ -2757,7 +2771,7 @@ impl Workspace {
                 worktree,
                 ProjectPath {
                     worktree_id,
-                    path: path.into(),
+                    path: path,
                 },
             ))
         })
@@ -4399,17 +4413,17 @@ impl Workspace {
         let project = self.project().read(cx);
         let mut title = String::new();
 
-        for (i, worktree) in project.worktrees(cx).enumerate() {
+        for (i, worktree) in project.visible_worktrees(cx).enumerate() {
             let name = {
                 let settings_location = SettingsLocation {
                     worktree_id: worktree.read(cx).id(),
-                    path: Path::new(""),
+                    path: RelPath::empty(),
                 };
 
                 let settings = WorktreeSettings::get(Some(settings_location), cx);
                 match &settings.project_name {
                     Some(name) => name.as_str(),
-                    None => worktree.read(cx).root_name(),
+                    None => worktree.read(cx).root_name_str(),
                 }
             };
             if i > 0 {
@@ -4423,18 +4437,14 @@ impl Workspace {
         }
 
         if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
-            let filename = path
-                .path
-                .file_name()
-                .map(|s| s.to_string_lossy())
-                .or_else(|| {
-                    Some(Cow::Borrowed(
-                        project
-                            .worktree_for_id(path.worktree_id, cx)?
-                            .read(cx)
-                            .root_name(),
-                    ))
-                });
+            let filename = path.path.file_name().or_else(|| {
+                Some(
+                    project
+                        .worktree_for_id(path.worktree_id, cx)?
+                        .read(cx)
+                        .root_name_str(),
+                )
+            });
 
             if let Some(filename) = filename {
                 title.push_str(" — ");
@@ -8174,6 +8184,7 @@ mod tests {
     use project::{Project, ProjectEntryId};
     use serde_json::json;
     use settings::SettingsStore;
+    use util::rel_path::rel_path;
 
     #[gpui::test]
     async fn test_tab_disambiguation(cx: &mut TestAppContext) {
@@ -8268,7 +8279,7 @@ mod tests {
             assert_eq!(
                 project.active_entry(),
                 project
-                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
+                    .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
                     .map(|e| e.id)
             );
         });
@@ -8283,7 +8294,7 @@ mod tests {
             assert_eq!(
                 project.active_entry(),
                 project
-                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
+                    .entry_for_path(&(worktree_id, rel_path("two.txt")).into(), cx)
                     .map(|e| e.id)
             );
         });
@@ -8299,7 +8310,7 @@ mod tests {
             assert_eq!(
                 project.active_entry(),
                 project
-                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
+                    .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
                     .map(|e| e.id)
             );
         });
@@ -10665,7 +10676,7 @@ mod tests {
 
             let handle = workspace
                 .update_in(cx, |workspace, window, cx| {
-                    let project_path = (worktree_id, "one.png");
+                    let project_path = (worktree_id, rel_path("one.png"));
                     workspace.open_path(project_path, None, true, window, cx)
                 })
                 .await
@@ -10679,7 +10690,7 @@ mod tests {
 
             let handle = workspace
                 .update_in(cx, |workspace, window, cx| {
-                    let project_path = (worktree_id, "two.ipynb");
+                    let project_path = (worktree_id, rel_path("two.ipynb"));
                     workspace.open_path(project_path, None, true, window, cx)
                 })
                 .await
@@ -10692,7 +10703,7 @@ mod tests {
 
             let handle = workspace
                 .update_in(cx, |workspace, window, cx| {
-                    let project_path = (worktree_id, "three.txt");
+                    let project_path = (worktree_id, rel_path("three.txt"));
                     workspace.open_path(project_path, None, true, window, cx)
                 })
                 .await;
@@ -10727,7 +10738,7 @@ mod tests {
 
             let handle = workspace
                 .update_in(cx, |workspace, window, cx| {
-                    let project_path = (worktree_id, "one.png");
+                    let project_path = (worktree_id, rel_path("one.png"));
                     workspace.open_path(project_path, None, true, window, cx)
                 })
                 .await
@@ -10741,7 +10752,7 @@ mod tests {
 
             let handle = workspace
                 .update_in(cx, |workspace, window, cx| {
-                    let project_path = (worktree_id, "three.txt");
+                    let project_path = (worktree_id, rel_path("three.txt"));
                     workspace.open_path(project_path, None, true, window, cx)
                 })
                 .await;
@@ -10755,7 +10766,7 @@ mod tests {
             .flat_map(|item| {
                 item.project_paths(cx)
                     .into_iter()
-                    .map(|path| path.path.to_string_lossy().to_string())
+                    .map(|path| path.path.as_str().to_string())
             })
             .collect()
     }

crates/worktree/src/worktree.rs 🔗

@@ -19,8 +19,7 @@ use futures::{
 };
 use fuzzy::CharBag;
 use git::{
-    COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK, LFS_DIR,
-    repository::RepoPath, status::GitSummary,
+    COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK, LFS_DIR, status::GitSummary,
 };
 use gpui::{
     App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, Task,
@@ -29,7 +28,7 @@ use ignore::IgnoreStack;
 use language::DiskState;
 
 use parking_lot::Mutex;
-use paths::{local_settings_folder_relative_path, local_vscode_folder_relative_path};
+use paths::{local_settings_folder_name, local_vscode_folder_name};
 use postage::{
     barrier,
     prelude::{Sink as _, Stream as _},
@@ -37,7 +36,7 @@ use postage::{
 };
 use rpc::{
     AnyProtoClient,
-    proto::{self, FromProto, ToProto, split_worktree_update},
+    proto::{self, split_worktree_update},
 };
 pub use settings::WorktreeId;
 use settings::{Settings, SettingsLocation, SettingsStore};
@@ -54,7 +53,7 @@ use std::{
     future::Future,
     mem::{self},
     ops::{Deref, DerefMut},
-    path::{Component, Path, PathBuf},
+    path::{Path, PathBuf},
     pin::Pin,
     sync::{
         Arc,
@@ -66,7 +65,8 @@ use sum_tree::{Bias, Dimensions, Edit, KeyedItem, SeekTarget, SumTree, Summary,
 use text::{LineEnding, Rope};
 use util::{
     ResultExt, debug_panic,
-    paths::{PathMatcher, SanitizedPath, home_dir},
+    paths::{PathMatcher, PathStyle, SanitizedPath, home_dir},
+    rel_path::RelPath,
 };
 pub use worktree_settings::WorktreeSettings;
 
@@ -132,12 +132,12 @@ pub struct LocalWorktree {
 }
 
 pub struct PathPrefixScanRequest {
-    path: Arc<Path>,
+    path: Arc<RelPath>,
     done: SmallVec<[barrier::Sender; 1]>,
 }
 
 struct ScanRequest {
-    relative_paths: Vec<Arc<Path>>,
+    relative_paths: Vec<Arc<RelPath>>,
     done: SmallVec<[barrier::Sender; 1]>,
 }
 
@@ -159,11 +159,12 @@ pub struct RemoteWorktree {
 pub struct Snapshot {
     id: WorktreeId,
     abs_path: Arc<SanitizedPath>,
-    root_name: String,
+    path_style: PathStyle,
+    root_name: Arc<RelPath>,
     root_char_bag: CharBag,
     entries_by_path: SumTree<Entry>,
     entries_by_id: SumTree<PathEntry>,
-    always_included_entries: Vec<Arc<Path>>,
+    always_included_entries: Vec<Arc<RelPath>>,
 
     /// A number that increases every time the worktree begins scanning
     /// a set of paths from the filesystem. This scanning could be caused
@@ -186,7 +187,7 @@ pub struct Snapshot {
 #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
 pub enum WorkDirectory {
     InProject {
-        relative_path: Arc<Path>,
+        relative_path: Arc<RelPath>,
     },
     AboveProject {
         absolute_path: Arc<Path>,
@@ -195,34 +196,10 @@ pub enum WorkDirectory {
 }
 
 impl WorkDirectory {
-    #[cfg(test)]
-    fn in_project(path: &str) -> Self {
-        let path = Path::new(path);
-        Self::InProject {
-            relative_path: path.into(),
-        }
-    }
-
-    //#[cfg(test)]
-    //fn canonicalize(&self) -> Self {
-    //    match self {
-    //        WorkDirectory::InProject { relative_path } => WorkDirectory::InProject {
-    //            relative_path: relative_path.clone(),
-    //        },
-    //        WorkDirectory::AboveProject {
-    //            absolute_path,
-    //            location_in_repo,
-    //        } => WorkDirectory::AboveProject {
-    //            absolute_path: absolute_path.canonicalize().unwrap().into(),
-    //            location_in_repo: location_in_repo.clone(),
-    //        },
-    //    }
-    //}
-
     fn path_key(&self) -> PathKey {
         match self {
             WorkDirectory::InProject { relative_path } => PathKey(relative_path.clone()),
-            WorkDirectory::AboveProject { .. } => PathKey(Path::new("").into()),
+            WorkDirectory::AboveProject { .. } => PathKey(RelPath::empty().into()),
         }
     }
 
@@ -232,106 +209,18 @@ impl WorkDirectory {
     /// is a repository in a directory between these two paths
     /// external .git folder in a parent folder of the project root.
     #[track_caller]
-    pub fn directory_contains(&self, path: impl AsRef<Path>) -> bool {
-        let path = path.as_ref();
-        debug_assert!(path.is_relative());
+    pub fn directory_contains(&self, path: &RelPath) -> bool {
         match self {
             WorkDirectory::InProject { relative_path } => path.starts_with(relative_path),
             WorkDirectory::AboveProject { .. } => true,
         }
     }
-
-    /// relativize returns the given project path relative to the root folder of the
-    /// repository.
-    /// If the root of the repository (and its .git folder) are located in a parent folder
-    /// of the project root folder, then the returned RepoPath is relative to the root
-    /// of the repository and not a valid path inside the project.
-    pub fn relativize(&self, path: &Path) -> Result<RepoPath> {
-        // path is assumed to be relative to worktree root.
-        debug_assert!(path.is_relative());
-        match self {
-            WorkDirectory::InProject { relative_path } => Ok(path
-                .strip_prefix(relative_path)
-                .map_err(|_| anyhow!("could not relativize {path:?} against {relative_path:?}"))?
-                .into()),
-            WorkDirectory::AboveProject {
-                location_in_repo, ..
-            } => {
-                // Avoid joining a `/` to location_in_repo in the case of a single-file worktree.
-                if path == Path::new("") {
-                    Ok(RepoPath(location_in_repo.clone()))
-                } else {
-                    Ok(location_in_repo.join(path).into())
-                }
-            }
-        }
-    }
-
-    /// This is the opposite operation to `relativize` above
-    pub fn try_unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> {
-        match self {
-            WorkDirectory::InProject { relative_path } => Some(relative_path.join(path).into()),
-            WorkDirectory::AboveProject {
-                location_in_repo, ..
-            } => {
-                // If we fail to strip the prefix, that means this status entry is
-                // external to this worktree, and we definitely won't have an entry_id
-                path.strip_prefix(location_in_repo).ok().map(Into::into)
-            }
-        }
-    }
-
-    pub fn unrelativize(&self, path: &RepoPath) -> Arc<Path> {
-        match self {
-            WorkDirectory::InProject { relative_path } => relative_path.join(path).into(),
-            WorkDirectory::AboveProject {
-                location_in_repo, ..
-            } => {
-                if &path.0 == location_in_repo {
-                    // Single-file worktree
-                    return location_in_repo
-                        .file_name()
-                        .map(Path::new)
-                        .unwrap_or(Path::new(""))
-                        .into();
-                }
-                let mut location_in_repo = &**location_in_repo;
-                let mut parents = PathBuf::new();
-                loop {
-                    if let Ok(segment) = path.strip_prefix(location_in_repo) {
-                        return parents.join(segment).into();
-                    }
-                    location_in_repo = location_in_repo.parent().unwrap_or(Path::new(""));
-                    parents.push(Component::ParentDir);
-                }
-            }
-        }
-    }
-
-    pub fn display_name(&self) -> String {
-        match self {
-            WorkDirectory::InProject { relative_path } => relative_path.display().to_string(),
-            WorkDirectory::AboveProject {
-                absolute_path,
-                location_in_repo,
-            } => {
-                let num_of_dots = location_in_repo.components().count();
-
-                "../".repeat(num_of_dots)
-                    + &absolute_path
-                        .file_name()
-                        .map(|s| s.to_string_lossy())
-                        .unwrap_or_default()
-                    + "/"
-            }
-        }
-    }
 }
 
 impl Default for WorkDirectory {
     fn default() -> Self {
         Self::InProject {
-            relative_path: Arc::from(Path::new("")),
+            relative_path: Arc::from(RelPath::empty()),
         }
     }
 }
@@ -340,7 +229,7 @@ impl Default for WorkDirectory {
 pub struct LocalSnapshot {
     snapshot: Snapshot,
     global_gitignore: Option<Arc<Gitignore>>,
-    /// All of the gitignore files in the worktree, indexed by their relative path.
+    /// All of the gitignore files in the worktree, indexed by their absolute path.
     /// The boolean indicates whether the gitignore needs to be updated.
     ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, bool)>,
     /// All of the git repositories in the worktree, indexed by the project entry
@@ -354,14 +243,14 @@ pub struct LocalSnapshot {
 struct BackgroundScannerState {
     snapshot: LocalSnapshot,
     scanned_dirs: HashSet<ProjectEntryId>,
-    path_prefixes_to_scan: HashSet<Arc<Path>>,
-    paths_to_scan: HashSet<Arc<Path>>,
+    path_prefixes_to_scan: HashSet<Arc<RelPath>>,
+    paths_to_scan: HashSet<Arc<RelPath>>,
     /// The ids of all of the entries that were removed from the snapshot
     /// as part of the current update. These entry ids may be re-used
     /// if the same inode is discovered at a new path, or if the given
     /// path is re-created after being deleted.
     removed_entries: HashMap<u64, Entry>,
-    changed_paths: Vec<Arc<Path>>,
+    changed_paths: Vec<Arc<RelPath>>,
     prev_snapshot: Snapshot,
 }
 
@@ -458,8 +347,6 @@ pub enum Event {
     DeletedEntry(ProjectEntryId),
 }
 
-const EMPTY_PATH: &str = "";
-
 impl EventEmitter<Event> for Worktree {}
 
 impl Worktree {
@@ -498,8 +385,10 @@ impl Worktree {
                     cx.entity_id().as_u64(),
                     abs_path
                         .file_name()
-                        .map_or(String::new(), |f| f.to_string_lossy().to_string()),
+                        .and_then(|f| f.to_str())
+                        .map_or(RelPath::empty().into(), |f| RelPath::new(f).unwrap().into()),
                     abs_path.clone(),
+                    PathStyle::local(),
                 ),
                 root_file_handle,
             };
@@ -507,7 +396,7 @@ impl Worktree {
             let worktree_id = snapshot.id();
             let settings_location = Some(SettingsLocation {
                 worktree_id,
-                path: Path::new(EMPTY_PATH),
+                path: RelPath::empty(),
             });
 
             let settings = WorktreeSettings::get(settings_location, cx).clone();
@@ -525,15 +414,19 @@ impl Worktree {
             let share_private_files = false;
             if let Some(metadata) = metadata {
                 let mut entry = Entry::new(
-                    Arc::from(Path::new("")),
+                    RelPath::empty().into(),
                     &metadata,
                     &next_entry_id,
                     snapshot.root_char_bag,
                     None,
                 );
                 if !metadata.is_dir {
-                    entry.is_private = !share_private_files
-                        && settings.is_path_private(abs_path.file_name().unwrap().as_ref());
+                    if let Some(file_name) = abs_path.file_name()
+                        && let Some(file_name) = file_name.to_str()
+                        && let Ok(path) = RelPath::new(file_name)
+                    {
+                        entry.is_private = !share_private_files && settings.is_path_private(path);
+                    }
                 }
                 snapshot.insert_entry(entry, fs.as_ref());
             }
@@ -564,13 +457,16 @@ impl Worktree {
         replica_id: ReplicaId,
         worktree: proto::WorktreeMetadata,
         client: AnyProtoClient,
+        path_style: PathStyle,
         cx: &mut App,
     ) -> Entity<Self> {
         cx.new(|cx: &mut Context<Self>| {
             let snapshot = Snapshot::new(
                 worktree.id,
-                worktree.root_name,
-                Arc::<Path>::from_proto(worktree.abs_path),
+                RelPath::from_proto(&worktree.root_name)
+                    .unwrap_or_else(|_| RelPath::empty().into()),
+                Path::new(&worktree.abs_path).into(),
+                path_style,
             );
 
             let background_snapshot = Arc::new(Mutex::new((
@@ -584,7 +480,7 @@ impl Worktree {
             let worktree_id = snapshot.id();
             let settings_location = Some(SettingsLocation {
                 worktree_id,
-                path: Path::new(EMPTY_PATH),
+                path: RelPath::empty(),
             });
 
             let settings = WorktreeSettings::get(settings_location, cx).clone();
@@ -701,7 +597,7 @@ impl Worktree {
     pub fn settings_location(&self, _: &Context<Self>) -> SettingsLocation<'static> {
         SettingsLocation {
             worktree_id: self.id(),
-            path: Path::new(EMPTY_PATH),
+            path: RelPath::empty(),
         }
     }
 
@@ -722,9 +618,9 @@ impl Worktree {
     pub fn metadata_proto(&self) -> proto::WorktreeMetadata {
         proto::WorktreeMetadata {
             id: self.id().to_proto(),
-            root_name: self.root_name().to_string(),
+            root_name: self.root_name().to_proto(),
             visible: self.is_visible(),
-            abs_path: self.abs_path().to_proto(),
+            abs_path: self.abs_path().to_string_lossy().to_string(),
         }
     }
 
@@ -791,7 +687,7 @@ impl Worktree {
         }
     }
 
-    pub fn load_file(&self, path: &Path, cx: &Context<Worktree>) -> Task<Result<LoadedFile>> {
+    pub fn load_file(&self, path: &RelPath, cx: &Context<Worktree>) -> Task<Result<LoadedFile>> {
         match self {
             Worktree::Local(this) => this.load_file(path, cx),
             Worktree::Remote(_) => {
@@ -802,7 +698,7 @@ impl Worktree {
 
     pub fn load_binary_file(
         &self,
-        path: &Path,
+        path: &RelPath,
         cx: &Context<Worktree>,
     ) -> Task<Result<LoadedBinaryFile>> {
         match self {
@@ -815,7 +711,7 @@ impl Worktree {
 
     pub fn write_file(
         &self,
-        path: &Path,
+        path: Arc<RelPath>,
         text: Rope,
         line_ending: LineEnding,
         cx: &Context<Worktree>,
@@ -830,12 +726,11 @@ impl Worktree {
 
     pub fn create_entry(
         &mut self,
-        path: impl Into<Arc<Path>>,
+        path: Arc<RelPath>,
         is_directory: bool,
         content: Option<Vec<u8>>,
         cx: &Context<Worktree>,
     ) -> Task<Result<CreatedEntry>> {
-        let path: Arc<Path> = path.into();
         let worktree_id = self.id();
         match self {
             Worktree::Local(this) => this.create_entry(path, is_directory, content, cx),
@@ -862,11 +757,8 @@ impl Worktree {
                             .await
                             .map(CreatedEntry::Included),
                         None => {
-                            let abs_path = this.read_with(cx, |worktree, _| {
-                                worktree
-                                    .absolutize(&path)
-                                    .with_context(|| format!("absolutizing {path:?}"))
-                            })??;
+                            let abs_path =
+                                this.read_with(cx, |worktree, _| worktree.absolutize(&path))?;
                             Ok(CreatedEntry::Excluded { abs_path })
                         }
                     }
@@ -902,7 +794,7 @@ impl Worktree {
         Some(task)
     }
 
-    fn get_children_ids_recursive(&self, path: &Path, ids: &mut Vec<ProjectEntryId>) {
+    fn get_children_ids_recursive(&self, path: &RelPath, ids: &mut Vec<ProjectEntryId>) {
         let children_iter = self.child_entries(path);
         for child in children_iter {
             ids.push(child.id);
@@ -910,63 +802,21 @@ impl Worktree {
         }
     }
 
-    pub fn rename_entry(
-        &mut self,
-        entry_id: ProjectEntryId,
-        new_path: impl Into<Arc<Path>>,
-        cx: &Context<Self>,
-    ) -> Task<Result<CreatedEntry>> {
-        let new_path = new_path.into();
-        match self {
-            Worktree::Local(this) => this.rename_entry(entry_id, new_path, cx),
-            Worktree::Remote(this) => this.rename_entry(entry_id, new_path, cx),
-        }
-    }
-
-    pub fn copy_entry(
-        &mut self,
-        entry_id: ProjectEntryId,
-        relative_worktree_source_path: Option<PathBuf>,
-        new_path: impl Into<Arc<Path>>,
-        cx: &Context<Self>,
-    ) -> Task<Result<Option<Entry>>> {
-        let new_path: Arc<Path> = new_path.into();
-        match self {
-            Worktree::Local(this) => {
-                this.copy_entry(entry_id, relative_worktree_source_path, new_path, cx)
-            }
-            Worktree::Remote(this) => {
-                let relative_worktree_source_path = relative_worktree_source_path
-                    .map(|relative_worktree_source_path| relative_worktree_source_path.to_proto());
-                let response = this.client.request(proto::CopyProjectEntry {
-                    project_id: this.project_id,
-                    entry_id: entry_id.to_proto(),
-                    relative_worktree_source_path,
-                    new_path: new_path.to_proto(),
-                });
-                cx.spawn(async move |this, cx| {
-                    let response = response.await?;
-                    match response.entry {
-                        Some(entry) => this
-                            .update(cx, |worktree, cx| {
-                                worktree.as_remote_mut().unwrap().insert_entry(
-                                    entry,
-                                    response.worktree_scan_id as usize,
-                                    cx,
-                                )
-                            })?
-                            .await
-                            .map(Some),
-                        None => Ok(None),
-                    }
-                })
-            }
-        }
-    }
+    // pub fn rename_entry(
+    //     &mut self,
+    //     entry_id: ProjectEntryId,
+    //     new_path: Arc<RelPath>,
+    //     cx: &Context<Self>,
+    // ) -> Task<Result<CreatedEntry>> {
+    //     match self {
+    //         Worktree::Local(this) => this.rename_entry(entry_id, new_path, cx),
+    //         Worktree::Remote(this) => this.rename_entry(entry_id, new_path, cx),
+    //     }
+    // }
 
     pub fn copy_external_entries(
         &mut self,
-        target_directory: Arc<Path>,
+        target_directory: Arc<RelPath>,
         paths: Vec<Arc<Path>>,
         fs: Arc<dyn Fs>,
         cx: &Context<Worktree>,
@@ -1035,16 +885,18 @@ impl Worktree {
         mut cx: AsyncApp,
     ) -> Result<proto::ProjectEntryResponse> {
         let (scan_id, entry) = this.update(&mut cx, |this, cx| {
-            (
+            anyhow::Ok((
                 this.scan_id(),
                 this.create_entry(
-                    Arc::<Path>::from_proto(request.path),
+                    RelPath::from_proto(&request.path).with_context(|| {
+                        format!("received invalid relative path {:?}", request.path)
+                    })?,
                     request.is_directory,
                     request.content,
                     cx,
                 ),
-            )
-        })?;
+            ))
+        })??;
         Ok(proto::ProjectEntryResponse {
             entry: match &entry.await? {
                 CreatedEntry::Included(entry) => Some(entry.into()),
@@ -1106,91 +958,38 @@ impl Worktree {
         })
     }
 
-    pub async fn handle_rename_entry(
-        this: Entity<Self>,
-        request: proto::RenameProjectEntry,
-        mut cx: AsyncApp,
-    ) -> Result<proto::ProjectEntryResponse> {
-        let (scan_id, task) = this.update(&mut cx, |this, cx| {
-            (
-                this.scan_id(),
-                this.rename_entry(
-                    ProjectEntryId::from_proto(request.entry_id),
-                    Arc::<Path>::from_proto(request.new_path),
-                    cx,
-                ),
-            )
-        })?;
-        Ok(proto::ProjectEntryResponse {
-            entry: match &task.await? {
-                CreatedEntry::Included(entry) => Some(entry.into()),
-                CreatedEntry::Excluded { .. } => None,
-            },
-            worktree_scan_id: scan_id as u64,
-        })
-    }
-
-    pub async fn handle_copy_entry(
-        this: Entity<Self>,
-        request: proto::CopyProjectEntry,
-        mut cx: AsyncApp,
-    ) -> Result<proto::ProjectEntryResponse> {
-        let (scan_id, task) = this.update(&mut cx, |this, cx| {
-            let relative_worktree_source_path = request
-                .relative_worktree_source_path
-                .map(PathBuf::from_proto);
-            (
-                this.scan_id(),
-                this.copy_entry(
-                    ProjectEntryId::from_proto(request.entry_id),
-                    relative_worktree_source_path,
-                    PathBuf::from_proto(request.new_path),
-                    cx,
-                ),
-            )
-        })?;
-        Ok(proto::ProjectEntryResponse {
-            entry: task.await?.as_ref().map(|e| e.into()),
-            worktree_scan_id: scan_id as u64,
-        })
-    }
-
-    pub fn dot_git_abs_path(&self, work_directory: &WorkDirectory) -> PathBuf {
-        let mut path = match work_directory {
-            WorkDirectory::InProject { relative_path } => self.abs_path().join(relative_path),
-            WorkDirectory::AboveProject { absolute_path, .. } => absolute_path.as_ref().to_owned(),
-        };
-        path.push(".git");
-        path
-    }
-
     pub fn is_single_file(&self) -> bool {
         self.root_dir().is_none()
     }
 
     /// For visible worktrees, returns the path with the worktree name as the first component.
     /// Otherwise, returns an absolute path.
-    pub fn full_path(&self, worktree_relative_path: &Path) -> PathBuf {
-        let mut full_path = PathBuf::new();
-
+    pub fn full_path(&self, worktree_relative_path: &RelPath) -> PathBuf {
         if self.is_visible() {
-            full_path.push(self.root_name());
+            self.root_name()
+                .join(worktree_relative_path)
+                .display(self.path_style)
+                .to_string()
+                .into()
         } else {
-            let path = self.abs_path();
-
-            if self.is_local() && path.starts_with(home_dir().as_path()) {
-                full_path.push("~");
-                full_path.push(path.strip_prefix(home_dir().as_path()).unwrap());
+            let full_path = self.abs_path();
+            let mut full_path_string = if self.is_local()
+                && let Ok(stripped) = full_path.strip_prefix(home_dir())
+            {
+                self.path_style
+                    .join("~", &*stripped.to_string_lossy())
+                    .unwrap()
             } else {
-                full_path.push(path)
+                full_path.to_string_lossy().to_string()
+            };
+
+            if worktree_relative_path.components().next().is_some() {
+                full_path_string.push_str(self.path_style.separator());
+                full_path_string.push_str(&worktree_relative_path.display(self.path_style));
             }
-        }
 
-        if worktree_relative_path.components().next().is_some() {
-            full_path.push(&worktree_relative_path);
+            full_path_string.into()
         }
-
-        full_path
     }
 }
 
@@ -1199,10 +998,14 @@ impl LocalWorktree {
         &self.fs
     }
 
-    pub fn is_path_private(&self, path: &Path) -> bool {
+    pub fn is_path_private(&self, path: &RelPath) -> bool {
         !self.share_private_files && self.settings.is_path_private(path)
     }
 
+    pub fn fs_is_case_sensitive(&self) -> bool {
+        self.fs_case_sensitive
+    }
+
     fn restart_background_scanners(&mut self, cx: &Context<Worktree>) {
         let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
         let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
@@ -1449,18 +1252,17 @@ impl LocalWorktree {
 
     fn load_binary_file(
         &self,
-        path: &Path,
+        path: &RelPath,
         cx: &Context<Worktree>,
     ) -> Task<Result<LoadedBinaryFile>> {
         let path = Arc::from(path);
         let abs_path = self.absolutize(&path);
         let fs = self.fs.clone();
         let entry = self.refresh_entry(path.clone(), None, cx);
-        let is_private = self.is_path_private(path.as_ref());
+        let is_private = self.is_path_private(&path);
 
         let worktree = cx.weak_entity();
         cx.background_spawn(async move {
-            let abs_path = abs_path?;
             let content = fs.load_bytes(&abs_path).await?;
 
             let worktree = worktree.upgrade().context("worktree was dropped")?;
@@ -1493,7 +1295,7 @@ impl LocalWorktree {
         })
     }
 
-    fn load_file(&self, path: &Path, cx: &Context<Worktree>) -> Task<Result<LoadedFile>> {
+    fn load_file(&self, path: &RelPath, cx: &Context<Worktree>) -> Task<Result<LoadedFile>> {
         let path = Arc::from(path);
         let abs_path = self.absolutize(&path);
         let fs = self.fs.clone();
@@ -1501,7 +1303,6 @@ impl LocalWorktree {
         let is_private = self.is_path_private(path.as_ref());
 
         cx.spawn(async move |this, _cx| {
-            let abs_path = abs_path?;
             // WARN: Temporary workaround for #27283.
             //       We are not efficient with our memory usage per file, and use in excess of 64GB for a 10GB file
             //       Therefore, as a temporary workaround to prevent system freezes, we just bail before opening a file
@@ -1549,31 +1350,27 @@ impl LocalWorktree {
     }
 
     /// Find the lowest path in the worktree's datastructures that is an ancestor
-    fn lowest_ancestor(&self, path: &Path) -> PathBuf {
+    fn lowest_ancestor(&self, path: &RelPath) -> Arc<RelPath> {
         let mut lowest_ancestor = None;
         for path in path.ancestors() {
             if self.entry_for_path(path).is_some() {
-                lowest_ancestor = Some(path.to_path_buf());
+                lowest_ancestor = Some(path.into());
                 break;
             }
         }
 
-        lowest_ancestor.unwrap_or_else(|| PathBuf::from(""))
+        lowest_ancestor.unwrap_or_else(|| RelPath::empty().into())
     }
 
     fn create_entry(
         &self,
-        path: impl Into<Arc<Path>>,
+        path: Arc<RelPath>,
         is_dir: bool,
         content: Option<Vec<u8>>,
         cx: &Context<Worktree>,
     ) -> Task<Result<CreatedEntry>> {
-        let path = path.into();
-        let abs_path = match self.absolutize(&path) {
-            Ok(path) => path,
-            Err(e) => return Task::ready(Err(e.context(format!("absolutizing path {path:?}")))),
-        };
-        let path_excluded = self.settings.is_path_excluded(&abs_path);
+        let abs_path = self.absolutize(&path);
+        let path_excluded = self.settings.is_path_excluded(&path);
         let fs = self.fs.clone();
         let task_abs_path = abs_path.clone();
         let write = cx.background_spawn(async move {
@@ -1599,13 +1396,13 @@ impl LocalWorktree {
                 let mut refreshes = Vec::new();
                 let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
                 for refresh_path in refresh_paths.ancestors() {
-                    if refresh_path == Path::new("") {
+                    if refresh_path == RelPath::empty() {
                         continue;
                     }
                     let refresh_full_path = lowest_ancestor.join(refresh_path);
 
                     refreshes.push(this.as_local_mut().unwrap().refresh_entry(
-                        refresh_full_path.into(),
+                        refresh_full_path,
                         None,
                         cx,
                     ));
@@ -1628,17 +1425,14 @@ impl LocalWorktree {
 
     fn write_file(
         &self,
-        path: impl Into<Arc<Path>>,
+        path: Arc<RelPath>,
         text: Rope,
         line_ending: LineEnding,
         cx: &Context<Worktree>,
     ) -> Task<Result<Arc<File>>> {
-        let path = path.into();
         let fs = self.fs.clone();
         let is_private = self.is_path_private(&path);
-        let Ok(abs_path) = self.absolutize(&path) else {
-            return Task::ready(Err(anyhow!("invalid path {path:?}")));
-        };
+        let abs_path = self.absolutize(&path);
 
         let write = cx.background_spawn({
             let fs = fs.clone();
@@ -1695,13 +1489,13 @@ impl LocalWorktree {
         let delete = cx.background_spawn(async move {
             if entry.is_file() {
                 if trash {
-                    fs.trash_file(&abs_path?, Default::default()).await?;
+                    fs.trash_file(&abs_path, Default::default()).await?;
                 } else {
-                    fs.remove_file(&abs_path?, Default::default()).await?;
+                    fs.remove_file(&abs_path, Default::default()).await?;
                 }
             } else if trash {
                 fs.trash_dir(
-                    &abs_path?,
+                    &abs_path,
                     RemoveOptions {
                         recursive: true,
                         ignore_if_not_exists: false,
@@ -1710,7 +1504,7 @@ impl LocalWorktree {
                 .await?;
             } else {
                 fs.remove_dir(
-                    &abs_path?,
+                    &abs_path,
                     RemoveOptions {
                         recursive: true,
                         ignore_if_not_exists: false,
@@ -1734,160 +1528,13 @@ impl LocalWorktree {
         }))
     }
 
-    /// Rename an entry.
-    ///
-    /// `new_path` is the new relative path to the worktree root.
-    /// If the root entry is renamed then `new_path` is the new root name instead.
-    fn rename_entry(
-        &self,
-        entry_id: ProjectEntryId,
-        new_path: impl Into<Arc<Path>>,
-        cx: &Context<Worktree>,
-    ) -> Task<Result<CreatedEntry>> {
-        let old_path = match self.entry_for_id(entry_id) {
-            Some(entry) => entry.path.clone(),
-            None => return Task::ready(Err(anyhow!("no entry to rename for id {entry_id:?}"))),
-        };
-        let new_path = new_path.into();
-        let abs_old_path = self.absolutize(&old_path);
-
-        let is_root_entry = self.root_entry().is_some_and(|e| e.id == entry_id);
-        let abs_new_path = if is_root_entry {
-            let Some(root_parent_path) = self.abs_path().parent() else {
-                return Task::ready(Err(anyhow!("no parent for path {:?}", self.abs_path)));
-            };
-            root_parent_path.join(&new_path)
-        } else {
-            let Ok(absolutize_path) = self.absolutize(&new_path) else {
-                return Task::ready(Err(anyhow!("absolutizing path {new_path:?}")));
-            };
-            absolutize_path
-        };
-
-        let fs = self.fs.clone();
-        let abs_path = abs_new_path.clone();
-        let case_sensitive = self.fs_case_sensitive;
-
-        let do_rename = async move |fs: &dyn Fs, old_path: &Path, new_path: &Path, overwrite| {
-            fs.rename(
-                &old_path,
-                &new_path,
-                fs::RenameOptions {
-                    overwrite,
-                    ..fs::RenameOptions::default()
-                },
-            )
-            .await
-            .with_context(|| format!("renaming {old_path:?} into {new_path:?}"))
-        };
-
-        let rename_task = cx.background_spawn(async move {
-            let abs_old_path = abs_old_path?;
-
-            // If we're on a case-insensitive FS and we're doing a case-only rename (i.e. `foobar` to `FOOBAR`)
-            // we want to overwrite, because otherwise we run into a file-already-exists error.
-            let overwrite = !case_sensitive
-                && abs_old_path != abs_new_path
-                && abs_old_path.to_str().map(|p| p.to_lowercase())
-                    == abs_new_path.to_str().map(|p| p.to_lowercase());
-
-            // The directory we're renaming into might not exist yet
-            if let Err(e) = do_rename(fs.as_ref(), &abs_old_path, &abs_new_path, overwrite).await {
-                if let Some(err) = e.downcast_ref::<std::io::Error>()
-                    && err.kind() == std::io::ErrorKind::NotFound
-                {
-                    if let Some(parent) = abs_new_path.parent() {
-                        fs.create_dir(parent)
-                            .await
-                            .with_context(|| format!("creating parent directory {parent:?}"))?;
-                        return do_rename(fs.as_ref(), &abs_old_path, &abs_new_path, overwrite)
-                            .await;
-                    }
-                }
-                return Err(e);
-            }
-            Ok(())
-        });
-
-        cx.spawn(async move |this, cx| {
-            rename_task.await?;
-            Ok(this
-                .update(cx, |this, cx| {
-                    let local = this.as_local_mut().unwrap();
-                    if is_root_entry {
-                        // We eagerly update `abs_path` and refresh this worktree.
-                        // Otherwise, the FS watcher would do it on the `RootUpdated` event,
-                        // but with a noticeable delay, so we handle it proactively.
-                        local.update_abs_path_and_refresh(
-                            Some(SanitizedPath::new_arc(&abs_path)),
-                            cx,
-                        );
-                        Task::ready(Ok(this.root_entry().cloned()))
-                    } else {
-                        // First refresh the parent directory (in case it was newly created)
-                        if let Some(parent) = new_path.parent() {
-                            let _ = local.refresh_entries_for_paths(vec![parent.into()]);
-                        }
-                        // Then refresh the new path
-                        local.refresh_entry(new_path.clone(), Some(old_path), cx)
-                    }
-                })?
-                .await?
-                .map(CreatedEntry::Included)
-                .unwrap_or_else(|| CreatedEntry::Excluded { abs_path }))
-        })
-    }
-
-    fn copy_entry(
-        &self,
-        entry_id: ProjectEntryId,
-        relative_worktree_source_path: Option<PathBuf>,
-        new_path: impl Into<Arc<Path>>,
-        cx: &Context<Worktree>,
-    ) -> Task<Result<Option<Entry>>> {
-        let old_path = match self.entry_for_id(entry_id) {
-            Some(entry) => entry.path.clone(),
-            None => return Task::ready(Ok(None)),
-        };
-        let new_path = new_path.into();
-        let abs_old_path =
-            if let Some(relative_worktree_source_path) = relative_worktree_source_path {
-                Ok(self.abs_path().join(relative_worktree_source_path))
-            } else {
-                self.absolutize(&old_path)
-            };
-        let abs_new_path = self.absolutize(&new_path);
-        let fs = self.fs.clone();
-        let copy = cx.background_spawn(async move {
-            copy_recursive(
-                fs.as_ref(),
-                &abs_old_path?,
-                &abs_new_path?,
-                Default::default(),
-            )
-            .await
-        });
-
-        cx.spawn(async move |this, cx| {
-            copy.await?;
-            this.update(cx, |this, cx| {
-                this.as_local_mut()
-                    .unwrap()
-                    .refresh_entry(new_path.clone(), None, cx)
-            })?
-            .await
-        })
-    }
-
     pub fn copy_external_entries(
         &self,
-        target_directory: Arc<Path>,
+        target_directory: Arc<RelPath>,
         paths: Vec<Arc<Path>>,
         cx: &Context<Worktree>,
     ) -> Task<Result<Vec<ProjectEntryId>>> {
-        let Ok(target_directory) = self.absolutize(&target_directory) else {
-            return Task::ready(Err(anyhow!("invalid target path")));
-        };
+        let target_directory = self.absolutize(&target_directory);
         let worktree_path = self.abs_path().clone();
         let fs = self.fs.clone();
         let paths = paths
@@ -1908,7 +1555,13 @@ impl LocalWorktree {
 
         let paths_to_refresh = paths
             .iter()
-            .filter_map(|(_, target)| Some(target.strip_prefix(&worktree_path).ok()?.into()))
+            .filter_map(|(_, target)| {
+                RelPath::from_std_path(
+                    target.strip_prefix(&worktree_path).ok()?,
+                    PathStyle::local(),
+                )
+                .ok()
+            })
             .collect::<Vec<_>>();
 
         cx.spawn(async move |this, cx| {
@@ -1986,7 +1639,7 @@ impl LocalWorktree {
         }))
     }
 
-    fn refresh_entries_for_paths(&self, paths: Vec<Arc<Path>>) -> barrier::Receiver {
+    pub fn refresh_entries_for_paths(&self, paths: Vec<Arc<RelPath>>) -> barrier::Receiver {
         let (tx, rx) = barrier::channel();
         self.scan_requests_tx
             .try_send(ScanRequest {
@@ -1998,11 +1651,14 @@ impl LocalWorktree {
     }
 
     #[cfg(feature = "test-support")]
-    pub fn manually_refresh_entries_for_paths(&self, paths: Vec<Arc<Path>>) -> barrier::Receiver {
+    pub fn manually_refresh_entries_for_paths(
+        &self,
+        paths: Vec<Arc<RelPath>>,
+    ) -> barrier::Receiver {
         self.refresh_entries_for_paths(paths)
     }
 
-    pub fn add_path_prefix_to_scan(&self, path_prefix: Arc<Path>) -> barrier::Receiver {
+    pub fn add_path_prefix_to_scan(&self, path_prefix: Arc<RelPath>) -> barrier::Receiver {
         let (tx, rx) = barrier::channel();
         self.path_prefixes_to_scan_tx
             .try_send(PathPrefixScanRequest {
@@ -2013,10 +1669,10 @@ impl LocalWorktree {
         rx
     }
 
-    fn refresh_entry(
+    pub fn refresh_entry(
         &self,
-        path: Arc<Path>,
-        old_path: Option<Arc<Path>>,
+        path: Arc<RelPath>,
+        old_path: Option<Arc<RelPath>>,
         cx: &Context<Worktree>,
     ) -> Task<Result<Option<Entry>>> {
         if self.settings.is_path_excluded(&path) {

crates/worktree/src/worktree_settings.rs 🔗

@@ -3,7 +3,11 @@ use std::path::Path;
 use anyhow::Context as _;
 use gpui::App;
 use settings::{Settings, SettingsContent};
-use util::{ResultExt, paths::PathMatcher};
+use util::{
+    ResultExt,
+    paths::{PathMatcher, PathStyle},
+    rel_path::RelPath,
+};
 
 #[derive(Clone, PartialEq, Eq)]
 pub struct WorktreeSettings {
@@ -14,19 +18,19 @@ pub struct WorktreeSettings {
 }
 
 impl WorktreeSettings {
-    pub fn is_path_private(&self, path: &Path) -> bool {
+    pub fn is_path_private(&self, path: &RelPath) -> bool {
         path.ancestors()
-            .any(|ancestor| self.private_files.is_match(ancestor))
+            .any(|ancestor| self.private_files.is_match(ancestor.as_std_path()))
     }
 
-    pub fn is_path_excluded(&self, path: &Path) -> bool {
+    pub fn is_path_excluded(&self, path: &RelPath) -> bool {
         path.ancestors()
-            .any(|ancestor| self.file_scan_exclusions.is_match(&ancestor))
+            .any(|ancestor| self.file_scan_exclusions.is_match(ancestor.as_std_path()))
     }
 
-    pub fn is_path_always_included(&self, path: &Path) -> bool {
+    pub fn is_path_always_included(&self, path: &RelPath) -> bool {
         path.ancestors()
-            .any(|ancestor| self.file_scan_inclusions.is_match(&ancestor))
+            .any(|ancestor| self.file_scan_inclusions.is_match(ancestor.as_std_path()))
     }
 }
 
@@ -90,5 +94,6 @@ impl Settings for WorktreeSettings {
 
 fn path_matchers(mut values: Vec<String>, context: &'static str) -> anyhow::Result<PathMatcher> {
     values.sort();
-    PathMatcher::new(values).with_context(|| format!("Failed to parse globs from {}", context))
+    PathMatcher::new(values, PathStyle::local())
+        .with_context(|| format!("Failed to parse globs from {}", context))
 }

crates/worktree/src/worktree_tests.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    Entry, EntryKind, Event, PathChange, WorkDirectory, Worktree, WorktreeModelHandle,
+    Entry, EntryKind, Event, PathChange, Worktree, WorktreeModelHandle,
     worktree_settings::WorktreeSettings,
 };
 use anyhow::Result;
@@ -20,7 +20,11 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::{ResultExt, path, test::TempTree};
+use util::{
+    ResultExt, path,
+    rel_path::{RelPath, rel_path},
+    test::TempTree,
+};
 
 #[gpui::test]
 async fn test_traversal(cx: &mut TestAppContext) {
@@ -56,10 +60,10 @@ async fn test_traversal(cx: &mut TestAppContext) {
                 .map(|entry| entry.path.as_ref())
                 .collect::<Vec<_>>(),
             vec![
-                Path::new(""),
-                Path::new(".gitignore"),
-                Path::new("a"),
-                Path::new("a/c"),
+                rel_path(""),
+                rel_path(".gitignore"),
+                rel_path("a"),
+                rel_path("a/c"),
             ]
         );
         assert_eq!(
@@ -67,11 +71,11 @@ async fn test_traversal(cx: &mut TestAppContext) {
                 .map(|entry| entry.path.as_ref())
                 .collect::<Vec<_>>(),
             vec![
-                Path::new(""),
-                Path::new(".gitignore"),
-                Path::new("a"),
-                Path::new("a/b"),
-                Path::new("a/c"),
+                rel_path(""),
+                rel_path(".gitignore"),
+                rel_path("a"),
+                rel_path("a/b"),
+                rel_path("a/c"),
             ]
         );
     })
@@ -121,14 +125,14 @@ async fn test_circular_symlinks(cx: &mut TestAppContext) {
                 .map(|entry| entry.path.as_ref())
                 .collect::<Vec<_>>(),
             vec![
-                Path::new(""),
-                Path::new("lib"),
-                Path::new("lib/a"),
-                Path::new("lib/a/a.txt"),
-                Path::new("lib/a/lib"),
-                Path::new("lib/b"),
-                Path::new("lib/b/b.txt"),
-                Path::new("lib/b/lib"),
+                rel_path(""),
+                rel_path("lib"),
+                rel_path("lib/a"),
+                rel_path("lib/a/a.txt"),
+                rel_path("lib/a/lib"),
+                rel_path("lib/b"),
+                rel_path("lib/b/b.txt"),
+                rel_path("lib/b/lib"),
             ]
         );
     });
@@ -147,14 +151,14 @@ async fn test_circular_symlinks(cx: &mut TestAppContext) {
                 .map(|entry| entry.path.as_ref())
                 .collect::<Vec<_>>(),
             vec![
-                Path::new(""),
-                Path::new("lib"),
-                Path::new("lib/a"),
-                Path::new("lib/a/a.txt"),
-                Path::new("lib/a/lib-2"),
-                Path::new("lib/b"),
-                Path::new("lib/b/b.txt"),
-                Path::new("lib/b/lib"),
+                rel_path(""),
+                rel_path("lib"),
+                rel_path("lib/a"),
+                rel_path("lib/a/a.txt"),
+                rel_path("lib/a/lib-2"),
+                rel_path("lib/b"),
+                rel_path("lib/b/b.txt"),
+                rel_path("lib/b/lib"),
             ]
         );
     });
@@ -236,18 +240,18 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
                 .map(|entry| (entry.path.as_ref(), entry.is_external))
                 .collect::<Vec<_>>(),
             vec![
-                (Path::new(""), false),
-                (Path::new("deps"), false),
-                (Path::new("deps/dep-dir2"), true),
-                (Path::new("deps/dep-dir3"), true),
-                (Path::new("src"), false),
-                (Path::new("src/a.rs"), false),
-                (Path::new("src/b.rs"), false),
+                (rel_path(""), false),
+                (rel_path("deps"), false),
+                (rel_path("deps/dep-dir2"), true),
+                (rel_path("deps/dep-dir3"), true),
+                (rel_path("src"), false),
+                (rel_path("src/a.rs"), false),
+                (rel_path("src/b.rs"), false),
             ]
         );
 
         assert_eq!(
-            tree.entry_for_path("deps/dep-dir2").unwrap().kind,
+            tree.entry_for_path(rel_path("deps/dep-dir2")).unwrap().kind,
             EntryKind::UnloadedDir
         );
     });
@@ -256,7 +260,7 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
     tree.read_with(cx, |tree, _| {
         tree.as_local()
             .unwrap()
-            .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()])
+            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3").into()])
     })
     .recv()
     .await;
@@ -269,24 +273,24 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
                 .map(|entry| (entry.path.as_ref(), entry.is_external))
                 .collect::<Vec<_>>(),
             vec![
-                (Path::new(""), false),
-                (Path::new("deps"), false),
-                (Path::new("deps/dep-dir2"), true),
-                (Path::new("deps/dep-dir3"), true),
-                (Path::new("deps/dep-dir3/deps"), true),
-                (Path::new("deps/dep-dir3/src"), true),
-                (Path::new("src"), false),
-                (Path::new("src/a.rs"), false),
-                (Path::new("src/b.rs"), false),
+                (rel_path(""), false),
+                (rel_path("deps"), false),
+                (rel_path("deps/dep-dir2"), true),
+                (rel_path("deps/dep-dir3"), true),
+                (rel_path("deps/dep-dir3/deps"), true),
+                (rel_path("deps/dep-dir3/src"), true),
+                (rel_path("src"), false),
+                (rel_path("src/a.rs"), false),
+                (rel_path("src/b.rs"), false),
             ]
         );
     });
     assert_eq!(
         mem::take(&mut *tree_updates.lock()),
         &[
-            (Path::new("deps/dep-dir3").into(), PathChange::Loaded),
-            (Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded),
-            (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded)
+            (rel_path("deps/dep-dir3").into(), PathChange::Loaded),
+            (rel_path("deps/dep-dir3/deps").into(), PathChange::Loaded),
+            (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded)
         ]
     );
 
@@ -294,7 +298,7 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
     tree.read_with(cx, |tree, _| {
         tree.as_local()
             .unwrap()
-            .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()])
+            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3/src").into()])
     })
     .recv()
     .await;
@@ -306,17 +310,17 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
                 .map(|entry| (entry.path.as_ref(), entry.is_external))
                 .collect::<Vec<_>>(),
             vec![
-                (Path::new(""), false),
-                (Path::new("deps"), false),
-                (Path::new("deps/dep-dir2"), true),
-                (Path::new("deps/dep-dir3"), true),
-                (Path::new("deps/dep-dir3/deps"), true),
-                (Path::new("deps/dep-dir3/src"), true),
-                (Path::new("deps/dep-dir3/src/e.rs"), true),
-                (Path::new("deps/dep-dir3/src/f.rs"), true),
-                (Path::new("src"), false),
-                (Path::new("src/a.rs"), false),
-                (Path::new("src/b.rs"), false),
+                (rel_path(""), false),
+                (rel_path("deps"), false),
+                (rel_path("deps/dep-dir2"), true),
+                (rel_path("deps/dep-dir3"), true),
+                (rel_path("deps/dep-dir3/deps"), true),
+                (rel_path("deps/dep-dir3/src"), true),
+                (rel_path("deps/dep-dir3/src/e.rs"), true),
+                (rel_path("deps/dep-dir3/src/f.rs"), true),
+                (rel_path("src"), false),
+                (rel_path("src/a.rs"), false),
+                (rel_path("src/b.rs"), false),
             ]
         );
     });
@@ -324,13 +328,13 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
     assert_eq!(
         mem::take(&mut *tree_updates.lock()),
         &[
-            (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded),
+            (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded),
             (
-                Path::new("deps/dep-dir3/src/e.rs").into(),
+                rel_path("deps/dep-dir3/src/e.rs").into(),
                 PathChange::Loaded
             ),
             (
-                Path::new("deps/dep-dir3/src/f.rs").into(),
+                rel_path("deps/dep-dir3/src/f.rs").into(),
                 PathChange::Loaded
             )
         ]
@@ -368,7 +372,7 @@ async fn test_renaming_case_only(cx: &mut TestAppContext) {
             tree.entries(true, 0)
                 .map(|entry| entry.path.as_ref())
                 .collect::<Vec<_>>(),
-            vec![Path::new(""), Path::new(OLD_NAME)]
+            vec![rel_path(""), rel_path(OLD_NAME)]
         );
     });
 
@@ -390,7 +394,7 @@ async fn test_renaming_case_only(cx: &mut TestAppContext) {
             tree.entries(true, 0)
                 .map(|entry| entry.path.as_ref())
                 .collect::<Vec<_>>(),
-            vec![Path::new(""), Path::new(NEW_NAME)]
+            vec![rel_path(""), rel_path(NEW_NAME)]
         );
     });
 }
@@ -446,13 +450,13 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
                 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
                 .collect::<Vec<_>>(),
             vec![
-                (Path::new(""), false),
-                (Path::new(".gitignore"), false),
-                (Path::new("one"), false),
-                (Path::new("one/node_modules"), true),
-                (Path::new("two"), false),
-                (Path::new("two/x.js"), false),
-                (Path::new("two/y.js"), false),
+                (rel_path(""), false),
+                (rel_path(".gitignore"), false),
+                (rel_path("one"), false),
+                (rel_path("one/node_modules"), true),
+                (rel_path("two"), false),
+                (rel_path("two/x.js"), false),
+                (rel_path("two/y.js"), false),
             ]
         );
     });
@@ -462,7 +466,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
     let prev_read_dir_count = fs.read_dir_call_count();
     let loaded = tree
         .update(cx, |tree, cx| {
-            tree.load_file("one/node_modules/b/b1.js".as_ref(), cx)
+            tree.load_file(rel_path("one/node_modules/b/b1.js"), cx)
         })
         .await
         .unwrap();
@@ -473,24 +477,24 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
                 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
                 .collect::<Vec<_>>(),
             vec![
-                (Path::new(""), false),
-                (Path::new(".gitignore"), false),
-                (Path::new("one"), false),
-                (Path::new("one/node_modules"), true),
-                (Path::new("one/node_modules/a"), true),
-                (Path::new("one/node_modules/b"), true),
-                (Path::new("one/node_modules/b/b1.js"), true),
-                (Path::new("one/node_modules/b/b2.js"), true),
-                (Path::new("one/node_modules/c"), true),
-                (Path::new("two"), false),
-                (Path::new("two/x.js"), false),
-                (Path::new("two/y.js"), false),
+                (rel_path(""), false),
+                (rel_path(".gitignore"), false),
+                (rel_path("one"), false),
+                (rel_path("one/node_modules"), true),
+                (rel_path("one/node_modules/a"), true),
+                (rel_path("one/node_modules/b"), true),
+                (rel_path("one/node_modules/b/b1.js"), true),
+                (rel_path("one/node_modules/b/b2.js"), true),
+                (rel_path("one/node_modules/c"), true),
+                (rel_path("two"), false),
+                (rel_path("two/x.js"), false),
+                (rel_path("two/y.js"), false),
             ]
         );
 
         assert_eq!(
             loaded.file.path.as_ref(),
-            Path::new("one/node_modules/b/b1.js")
+            rel_path("one/node_modules/b/b1.js")
         );
 
         // Only the newly-expanded directories are scanned.
@@ -502,7 +506,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
     let prev_read_dir_count = fs.read_dir_call_count();
     let loaded = tree
         .update(cx, |tree, cx| {
-            tree.load_file("one/node_modules/a/a2.js".as_ref(), cx)
+            tree.load_file(rel_path("one/node_modules/a/a2.js"), cx)
         })
         .await
         .unwrap();
@@ -513,26 +517,26 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
                 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
                 .collect::<Vec<_>>(),
             vec![
-                (Path::new(""), false),
-                (Path::new(".gitignore"), false),
-                (Path::new("one"), false),
-                (Path::new("one/node_modules"), true),
-                (Path::new("one/node_modules/a"), true),
-                (Path::new("one/node_modules/a/a1.js"), true),
-                (Path::new("one/node_modules/a/a2.js"), true),
-                (Path::new("one/node_modules/b"), true),
-                (Path::new("one/node_modules/b/b1.js"), true),
-                (Path::new("one/node_modules/b/b2.js"), true),
-                (Path::new("one/node_modules/c"), true),
-                (Path::new("two"), false),
-                (Path::new("two/x.js"), false),
-                (Path::new("two/y.js"), false),
+                (rel_path(""), false),
+                (rel_path(".gitignore"), false),
+                (rel_path("one"), false),
+                (rel_path("one/node_modules"), true),
+                (rel_path("one/node_modules/a"), true),
+                (rel_path("one/node_modules/a/a1.js"), true),
+                (rel_path("one/node_modules/a/a2.js"), true),
+                (rel_path("one/node_modules/b"), true),
+                (rel_path("one/node_modules/b/b1.js"), true),
+                (rel_path("one/node_modules/b/b2.js"), true),
+                (rel_path("one/node_modules/c"), true),
+                (rel_path("two"), false),
+                (rel_path("two/x.js"), false),
+                (rel_path("two/y.js"), false),
             ]
         );
 
         assert_eq!(
             loaded.file.path.as_ref(),
-            Path::new("one/node_modules/a/a2.js")
+            rel_path("one/node_modules/a/a2.js")
         );
 
         // Only the newly-expanded directory is scanned.
@@ -610,7 +614,7 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
     tree.read_with(cx, |tree, _| {
         tree.as_local()
             .unwrap()
-            .refresh_entries_for_paths(vec![Path::new("node_modules/d/d.js").into()])
+            .refresh_entries_for_paths(vec![rel_path("node_modules/d/d.js").into()])
     })
     .recv()
     .await;
@@ -622,18 +626,18 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
                 .map(|e| (e.path.as_ref(), e.is_ignored))
                 .collect::<Vec<_>>(),
             &[
-                (Path::new(""), false),
-                (Path::new(".gitignore"), false),
-                (Path::new("a"), false),
-                (Path::new("a/a.js"), false),
-                (Path::new("b"), false),
-                (Path::new("b/b.js"), false),
-                (Path::new("node_modules"), true),
-                (Path::new("node_modules/c"), true),
-                (Path::new("node_modules/d"), true),
-                (Path::new("node_modules/d/d.js"), true),
-                (Path::new("node_modules/d/e"), true),
-                (Path::new("node_modules/d/f"), true),
+                (rel_path(""), false),
+                (rel_path(".gitignore"), false),
+                (rel_path("a"), false),
+                (rel_path("a/a.js"), false),
+                (rel_path("b"), false),
+                (rel_path("b/b.js"), false),
+                (rel_path("node_modules"), true),
+                (rel_path("node_modules/c"), true),
+                (rel_path("node_modules/d"), true),
+                (rel_path("node_modules/d/d.js"), true),
+                (rel_path("node_modules/d/e"), true),
+                (rel_path("node_modules/d/f"), true),
             ]
         );
     });
@@ -654,23 +658,23 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
                 .map(|e| (e.path.as_ref(), e.is_ignored))
                 .collect::<Vec<_>>(),
             &[
-                (Path::new(""), false),
-                (Path::new(".gitignore"), false),
-                (Path::new("a"), false),
-                (Path::new("a/a.js"), false),
-                (Path::new("b"), false),
-                (Path::new("b/b.js"), false),
+                (rel_path(""), false),
+                (rel_path(".gitignore"), false),
+                (rel_path("a"), false),
+                (rel_path("a/a.js"), false),
+                (rel_path("b"), false),
+                (rel_path("b/b.js"), false),
                 // This directory is no longer ignored
-                (Path::new("node_modules"), false),
-                (Path::new("node_modules/c"), false),
-                (Path::new("node_modules/c/c.js"), false),
-                (Path::new("node_modules/d"), false),
-                (Path::new("node_modules/d/d.js"), false),
+                (rel_path("node_modules"), false),
+                (rel_path("node_modules/c"), false),
+                (rel_path("node_modules/c/c.js"), false),
+                (rel_path("node_modules/d"), false),
+                (rel_path("node_modules/d/d.js"), false),
                 // This subdirectory is now ignored
-                (Path::new("node_modules/d/e"), true),
-                (Path::new("node_modules/d/f"), false),
-                (Path::new("node_modules/d/f/f1.js"), false),
-                (Path::new("node_modules/d/f/f2.js"), false),
+                (rel_path("node_modules/d/e"), true),
+                (rel_path("node_modules/d/f"), false),
+                (rel_path("node_modules/d/f/f1.js"), false),
+                (rel_path("node_modules/d/f/f2.js"), false),
             ]
         );
     });
@@ -711,7 +715,7 @@ async fn test_write_file(cx: &mut TestAppContext) {
     worktree
         .update(cx, |tree, cx| {
             tree.write_file(
-                Path::new("tracked-dir/file.txt"),
+                rel_path("tracked-dir/file.txt").into(),
                 "hello".into(),
                 Default::default(),
                 cx,
@@ -722,7 +726,7 @@ async fn test_write_file(cx: &mut TestAppContext) {
     worktree
         .update(cx, |tree, cx| {
             tree.write_file(
-                Path::new("ignored-dir/file.txt"),
+                rel_path("ignored-dir/file.txt").into(),
                 "world".into(),
                 Default::default(),
                 cx,
@@ -732,8 +736,12 @@ async fn test_write_file(cx: &mut TestAppContext) {
         .unwrap();
 
     worktree.read_with(cx, |tree, _| {
-        let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
-        let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
+        let tracked = tree
+            .entry_for_path(rel_path("tracked-dir/file.txt"))
+            .unwrap();
+        let ignored = tree
+            .entry_for_path(rel_path("ignored-dir/file.txt"))
+            .unwrap();
         assert!(!tracked.is_ignored);
         assert!(ignored.is_ignored);
     });
@@ -918,11 +926,11 @@ async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppC
 
     tree.read_with(cx, |tree, _| {
         assert!(
-            tree.entry_for_path("node_modules")
+            tree.entry_for_path(rel_path("node_modules"))
                 .is_some_and(|f| f.is_always_included)
         );
         assert!(
-            tree.entry_for_path("node_modules/prettier/package.json")
+            tree.entry_for_path(rel_path("node_modules/prettier/package.json"))
                 .is_some_and(|f| f.is_always_included)
         );
     });
@@ -941,11 +949,11 @@ async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppC
 
     tree.read_with(cx, |tree, _| {
         assert!(
-            tree.entry_for_path("node_modules")
+            tree.entry_for_path(rel_path("node_modules"))
                 .is_some_and(|f| !f.is_always_included)
         );
         assert!(
-            tree.entry_for_path("node_modules/prettier/package.json")
+            tree.entry_for_path(rel_path("node_modules/prettier/package.json"))
                 .is_some_and(|f| !f.is_always_included)
         );
     });
@@ -1272,7 +1280,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
         .update(cx, |tree, cx| {
             tree.as_local_mut()
                 .unwrap()
-                .create_entry("a/e".as_ref(), true, None, cx)
+                .create_entry(rel_path("a/e").into(), true, None, cx)
         })
         .await
         .unwrap()
@@ -1282,7 +1290,10 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
 
     cx.executor().run_until_parked();
     tree.read_with(cx, |tree, _| {
-        assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
+        assert_eq!(
+            tree.entry_for_path(rel_path("a/e")).unwrap().kind,
+            EntryKind::Dir
+        );
     });
 
     let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
@@ -1319,9 +1330,12 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
 
     let entry = tree_fake
         .update(cx, |tree, cx| {
-            tree.as_local_mut()
-                .unwrap()
-                .create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
+            tree.as_local_mut().unwrap().create_entry(
+                rel_path("a/b/c/d.txt").into(),
+                false,
+                None,
+                cx,
+            )
         })
         .await
         .unwrap()
@@ -1331,9 +1345,13 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
 
     cx.executor().run_until_parked();
     tree_fake.read_with(cx, |tree, _| {
-        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
-        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
-        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
+        assert!(
+            tree.entry_for_path(rel_path("a/b/c/d.txt"))
+                .unwrap()
+                .is_file()
+        );
+        assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir());
+        assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir());
     });
 
     let fs_real = Arc::new(RealFs::new(None, cx.executor()));
@@ -1353,9 +1371,12 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
 
     let entry = tree_real
         .update(cx, |tree, cx| {
-            tree.as_local_mut()
-                .unwrap()
-                .create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
+            tree.as_local_mut().unwrap().create_entry(
+                rel_path("a/b/c/d.txt").into(),
+                false,
+                None,
+                cx,
+            )
         })
         .await
         .unwrap()
@@ -1365,17 +1386,24 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
 
     cx.executor().run_until_parked();
     tree_real.read_with(cx, |tree, _| {
-        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
-        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
-        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
+        assert!(
+            tree.entry_for_path(rel_path("a/b/c/d.txt"))
+                .unwrap()
+                .is_file()
+        );
+        assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir());
+        assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir());
     });
 
     // Test smallest change
     let entry = tree_real
         .update(cx, |tree, cx| {
-            tree.as_local_mut()
-                .unwrap()
-                .create_entry("a/b/c/e.txt".as_ref(), false, None, cx)
+            tree.as_local_mut().unwrap().create_entry(
+                rel_path("a/b/c/e.txt").into(),
+                false,
+                None,
+                cx,
+            )
         })
         .await
         .unwrap()
@@ -1385,15 +1413,22 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
 
     cx.executor().run_until_parked();
     tree_real.read_with(cx, |tree, _| {
-        assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
+        assert!(
+            tree.entry_for_path(rel_path("a/b/c/e.txt"))
+                .unwrap()
+                .is_file()
+        );
     });
 
     // Test largest change
     let entry = tree_real
         .update(cx, |tree, cx| {
-            tree.as_local_mut()
-                .unwrap()
-                .create_entry("d/e/f/g.txt".as_ref(), false, None, cx)
+            tree.as_local_mut().unwrap().create_entry(
+                rel_path("d/e/f/g.txt").into(),
+                false,
+                None,
+                cx,
+            )
         })
         .await
         .unwrap()
@@ -1403,10 +1438,14 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
 
     cx.executor().run_until_parked();
     tree_real.read_with(cx, |tree, _| {
-        assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
-        assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
-        assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
-        assert!(tree.entry_for_path("d/").unwrap().is_dir());
+        assert!(
+            tree.entry_for_path(rel_path("d/e/f/g.txt"))
+                .unwrap()
+                .is_file()
+        );
+        assert!(tree.entry_for_path(rel_path("d/e/f")).unwrap().is_dir());
+        assert!(tree.entry_for_path(rel_path("d/e")).unwrap().is_dir());
+        assert!(tree.entry_for_path(rel_path("d")).unwrap().is_dir());
     });
 }
 
@@ -1701,37 +1740,13 @@ fn randomly_mutate_worktree(
     let entry = snapshot.entries(false, 0).choose(rng).unwrap();
 
     match rng.random_range(0_u32..100) {
-        0..=33 if entry.path.as_ref() != Path::new("") => {
+        0..=33 if entry.path.as_ref() != RelPath::empty() => {
             log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
             worktree.delete_entry(entry.id, false, cx).unwrap()
         }
-        ..=66 if entry.path.as_ref() != Path::new("") => {
-            let other_entry = snapshot.entries(false, 0).choose(rng).unwrap();
-            let new_parent_path = if other_entry.is_dir() {
-                other_entry.path.clone()
-            } else {
-                other_entry.path.parent().unwrap().into()
-            };
-            let mut new_path = new_parent_path.join(random_filename(rng));
-            if new_path.starts_with(&entry.path) {
-                new_path = random_filename(rng).into();
-            }
-
-            log::info!(
-                "renaming entry {:?} ({}) to {:?}",
-                entry.path,
-                entry.id.0,
-                new_path
-            );
-            let task = worktree.rename_entry(entry.id, new_path, cx);
-            cx.background_spawn(async move {
-                task.await?.into_included().unwrap();
-                Ok(())
-            })
-        }
         _ => {
             if entry.is_dir() {
-                let child_path = entry.path.join(random_filename(rng));
+                let child_path = entry.path.join(rel_path(&random_filename(rng)));
                 let is_dir = rng.random_bool(0.3);
                 log::info!(
                     "creating {} at {:?}",
@@ -1744,7 +1759,7 @@ fn randomly_mutate_worktree(
                     Ok(())
                 })
             } else {
-                log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
+                log::info!("overwriting file {:?} ({})", &entry.path, entry.id.0);
                 let task =
                     worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
                 cx.background_spawn(async move {
@@ -1794,7 +1809,7 @@ async fn randomly_mutate_fs(
         }
     } else if rng.random_bool(0.05) {
         let ignore_dir_path = dirs.choose(rng).unwrap();
-        let ignore_path = ignore_dir_path.join(*GITIGNORE);
+        let ignore_path = ignore_dir_path.join(GITIGNORE);
 
         let subdirs = dirs
             .iter()
@@ -1923,101 +1938,6 @@ fn random_filename(rng: &mut impl Rng) -> String {
         .collect()
 }
 
-#[gpui::test]
-async fn test_rename_file_to_new_directory(cx: &mut TestAppContext) {
-    init_test(cx);
-    let fs = FakeFs::new(cx.background_executor.clone());
-    let expected_contents = "content";
-    fs.as_fake()
-        .insert_tree(
-            "/root",
-            json!({
-                "test.txt": expected_contents
-            }),
-        )
-        .await;
-    let worktree = Worktree::local(
-        Path::new("/root"),
-        true,
-        fs.clone(),
-        Arc::default(),
-        &mut cx.to_async(),
-    )
-    .await
-    .unwrap();
-    cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
-        .await;
-
-    let entry_id = worktree.read_with(cx, |worktree, _| {
-        worktree.entry_for_path("test.txt").unwrap().id
-    });
-    let _result = worktree
-        .update(cx, |worktree, cx| {
-            worktree.rename_entry(entry_id, Path::new("dir1/dir2/dir3/test.txt"), cx)
-        })
-        .await
-        .unwrap();
-    worktree.read_with(cx, |worktree, _| {
-        assert!(
-            worktree.entry_for_path("test.txt").is_none(),
-            "Old file should have been removed"
-        );
-        assert!(
-            worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_some(),
-            "Whole directory hierarchy and the new file should have been created"
-        );
-    });
-    assert_eq!(
-        worktree
-            .update(cx, |worktree, cx| {
-                worktree.load_file("dir1/dir2/dir3/test.txt".as_ref(), cx)
-            })
-            .await
-            .unwrap()
-            .text,
-        expected_contents,
-        "Moved file's contents should be preserved"
-    );
-
-    let entry_id = worktree.read_with(cx, |worktree, _| {
-        worktree
-            .entry_for_path("dir1/dir2/dir3/test.txt")
-            .unwrap()
-            .id
-    });
-    let _result = worktree
-        .update(cx, |worktree, cx| {
-            worktree.rename_entry(entry_id, Path::new("dir1/dir2/test.txt"), cx)
-        })
-        .await
-        .unwrap();
-    worktree.read_with(cx, |worktree, _| {
-        assert!(
-            worktree.entry_for_path("test.txt").is_none(),
-            "First file should not reappear"
-        );
-        assert!(
-            worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_none(),
-            "Old file should have been removed"
-        );
-        assert!(
-            worktree.entry_for_path("dir1/dir2/test.txt").is_some(),
-            "No error should have occurred after moving into existing directory"
-        );
-    });
-    assert_eq!(
-        worktree
-            .update(cx, |worktree, cx| {
-                worktree.load_file("dir1/dir2/test.txt".as_ref(), cx)
-            })
-            .await
-            .unwrap()
-            .text,
-        expected_contents,
-        "Moved file's contents should be preserved"
-    );
-}
-
 #[gpui::test]
 async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
     init_test(cx);
@@ -2036,48 +1956,11 @@ async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
     cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
         .await;
     tree.read_with(cx, |tree, _| {
-        let entry = tree.entry_for_path("").unwrap();
+        let entry = tree.entry_for_path(rel_path("")).unwrap();
         assert!(entry.is_private);
     });
 }
 
-#[gpui::test]
-fn test_unrelativize() {
-    let work_directory = WorkDirectory::in_project("");
-    pretty_assertions::assert_eq!(
-        work_directory.try_unrelativize(&"crates/gpui/gpui.rs".into()),
-        Some(Path::new("crates/gpui/gpui.rs").into())
-    );
-
-    let work_directory = WorkDirectory::in_project("vendor/some-submodule");
-    pretty_assertions::assert_eq!(
-        work_directory.try_unrelativize(&"src/thing.c".into()),
-        Some(Path::new("vendor/some-submodule/src/thing.c").into())
-    );
-
-    let work_directory = WorkDirectory::AboveProject {
-        absolute_path: Path::new("/projects/zed").into(),
-        location_in_repo: Path::new("crates/gpui").into(),
-    };
-
-    pretty_assertions::assert_eq!(
-        work_directory.try_unrelativize(&"crates/util/util.rs".into()),
-        None,
-    );
-
-    pretty_assertions::assert_eq!(
-        work_directory.unrelativize(&"crates/util/util.rs".into()),
-        Path::new("../util/util.rs").into()
-    );
-
-    pretty_assertions::assert_eq!(work_directory.try_unrelativize(&"README.md".into()), None,);
-
-    pretty_assertions::assert_eq!(
-        work_directory.unrelativize(&"README.md".into()),
-        Path::new("../../README.md").into()
-    );
-}
-
 #[gpui::test]
 async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) {
     init_test(cx);
@@ -2259,7 +2142,7 @@ fn check_worktree_entries(
     expected_included_paths: &[&str],
 ) {
     for path in expected_excluded_paths {
-        let entry = tree.entry_for_path(path);
+        let entry = tree.entry_for_path(rel_path(path));
         assert!(
             entry.is_none(),
             "expected path '{path}' to be excluded, but got entry: {entry:?}",
@@ -2267,7 +2150,7 @@ fn check_worktree_entries(
     }
     for path in expected_ignored_paths {
         let entry = tree
-            .entry_for_path(path)
+            .entry_for_path(rel_path(path))
             .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
         assert!(
             entry.is_ignored,
@@ -2276,7 +2159,7 @@ fn check_worktree_entries(
     }
     for path in expected_tracked_paths {
         let entry = tree
-            .entry_for_path(path)
+            .entry_for_path(rel_path(path))
             .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
         assert!(
             !entry.is_ignored || entry.is_always_included,
@@ -2285,7 +2168,7 @@ fn check_worktree_entries(
     }
     for path in expected_included_paths {
         let entry = tree
-            .entry_for_path(path)
+            .entry_for_path(rel_path(path))
             .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
         assert!(
             entry.is_always_included,

crates/zed/src/zed.rs 🔗

@@ -71,6 +71,7 @@ use terminal_view::terminal_panel::{self, TerminalPanel};
 use theme::{ActiveTheme, ThemeSettings};
 use ui::{PopoverMenuHandle, prelude::*};
 use util::markdown::MarkdownString;
+use util::rel_path::RelPath;
 use util::{ResultExt, asset_str};
 use uuid::Uuid;
 use vim_mode_setting::VimModeSetting;
@@ -1653,7 +1654,7 @@ fn open_project_debug_tasks_file(
 
 fn open_local_file(
     workspace: &mut Workspace,
-    settings_relative_path: &'static Path,
+    settings_relative_path: &'static RelPath,
     initial_contents: Cow<'static, str>,
     window: &mut Window,
     cx: &mut Context<Workspace>,
@@ -1969,7 +1970,7 @@ mod tests {
         time::Duration,
     };
     use theme::{ThemeRegistry, ThemeSettings};
-    use util::path;
+    use util::{path, rel_path::rel_path};
     use workspace::{
         NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection,
         WorkspaceHandle,
@@ -2749,7 +2750,7 @@ mod tests {
         fn assert_project_panel_selection(
             workspace: &Workspace,
             expected_worktree_path: &Path,
-            expected_entry_path: &Path,
+            expected_entry_path: &RelPath,
             cx: &App,
         ) {
             let project_panel = [
@@ -2797,7 +2798,7 @@ mod tests {
             assert_project_panel_selection(
                 workspace,
                 Path::new(path!("/dir1")),
-                Path::new("a.txt"),
+                rel_path("a.txt"),
                 cx,
             );
             assert_eq!(
@@ -2835,7 +2836,7 @@ mod tests {
             assert_project_panel_selection(
                 workspace,
                 Path::new(path!("/dir2/b.txt")),
-                Path::new(""),
+                rel_path(""),
                 cx,
             );
             let worktree_roots = workspace
@@ -2884,7 +2885,7 @@ mod tests {
             assert_project_panel_selection(
                 workspace,
                 Path::new(path!("/dir3")),
-                Path::new("c.txt"),
+                rel_path("c.txt"),
                 cx,
             );
             let worktree_roots = workspace
@@ -2930,12 +2931,7 @@ mod tests {
             .await;
         cx.read(|cx| {
             let workspace = workspace.read(cx);
-            assert_project_panel_selection(
-                workspace,
-                Path::new(path!("/d.txt")),
-                Path::new(""),
-                cx,
-            );
+            assert_project_panel_selection(workspace, Path::new(path!("/d.txt")), rel_path(""), cx);
             let worktree_roots = workspace
                 .worktrees(cx)
                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
@@ -3061,9 +3057,7 @@ mod tests {
                 .zip(paths_to_open.iter())
                 .map(|(i, path)| {
                     match i {
-                        Some(Ok(i)) => {
-                            Some(i.project_path(cx).map(|p| p.path.display().to_string()))
-                        }
+                        Some(Ok(i)) => Some(i.project_path(cx).map(|p| p.path)),
                         Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
                         None => None,
                     }
@@ -3076,8 +3070,8 @@ mod tests {
             opened_paths,
             vec![
                 None,
-                Some(path!(".git/HEAD").to_string()),
-                Some(path!("excluded_dir/file").to_string()),
+                Some(rel_path(".git/HEAD").into()),
+                Some(rel_path("excluded_dir/file").into()),
             ],
             "Excluded files should get opened, excluded dir should not get opened"
         );
@@ -3096,14 +3090,12 @@ mod tests {
                         i.project_path(cx)
                             .expect("all excluded files that got open should have a path")
                             .path
-                            .display()
-                            .to_string()
                     })
                     .collect::<Vec<_>>();
                 opened_buffer_paths.sort();
                 assert_eq!(
                     opened_buffer_paths,
-                    vec![path!(".git/HEAD").to_string(), path!("excluded_dir/file").to_string()],
+                    vec![rel_path(".git/HEAD").into(), rel_path("excluded_dir/file").into()],
                     "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
                 );
             });
@@ -3296,7 +3288,7 @@ mod tests {
                     cx,
                 );
                 workspace.open_path(
-                    (worktree.read(cx).id(), "the-new-name.rs"),
+                    (worktree.read(cx).id(), rel_path("the-new-name.rs")),
                     None,
                     true,
                     window,
@@ -4828,7 +4820,8 @@ mod tests {
         // 5. Critical: Verify .zed is actually excluded from worktree
         let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap());
 
-        let has_zed_entry = cx.update(|cx| worktree.read(cx).entry_for_path(".zed").is_some());
+        let has_zed_entry =
+            cx.update(|cx| worktree.read(cx).entry_for_path(rel_path(".zed")).is_some());
 
         eprintln!(
             "Is .zed directory visible in worktree after exclusion: {}",

crates/zeta/src/license_detection.rs 🔗

@@ -2,7 +2,7 @@ use std::{
     collections::BTreeSet,
     fmt::{Display, Formatter},
     ops::Range,
-    path::{Path, PathBuf},
+    path::PathBuf,
     sync::{Arc, LazyLock},
 };
 
@@ -14,7 +14,7 @@ use itertools::Itertools;
 use postage::watch;
 use project::Worktree;
 use strum::VariantArray;
-use util::{ResultExt as _, maybe};
+use util::{ResultExt as _, maybe, rel_path::RelPath};
 use worktree::ChildEntriesOptions;
 
 /// Matches the most common license locations, with US and UK English spelling.
@@ -283,14 +283,13 @@ impl LicenseDetectionWatcher {
             return Self::Remote;
         };
         let fs = local_worktree.fs().clone();
-        let worktree_abs_path = local_worktree.abs_path().clone();
 
         let options = ChildEntriesOptions {
             include_files: true,
             include_dirs: false,
             include_ignored: true,
         };
-        for top_file in local_worktree.child_entries_with_options(Path::new(""), options) {
+        for top_file in local_worktree.child_entries_with_options(RelPath::empty(), options) {
             let path_bytes = top_file.path.as_os_str().as_encoded_bytes();
             if top_file.is_created() && LICENSE_FILE_NAME_REGEX.is_match(path_bytes) {
                 let rel_path = top_file.path.clone();
@@ -312,12 +311,13 @@ impl LicenseDetectionWatcher {
                 worktree::Event::DeletedEntry(_) | worktree::Event::UpdatedGitRepositories(_) => {}
             });
 
+        let worktree_snapshot = worktree.read(cx).snapshot();
         let (mut is_open_source_tx, is_open_source_rx) = watch::channel_with::<bool>(false);
 
         let _is_open_source_task = cx.background_spawn(async move {
             let mut eligible_licenses = BTreeSet::new();
             while let Some(rel_path) = files_to_check_rx.next().await {
-                let abs_path = worktree_abs_path.join(&rel_path);
+                let abs_path = worktree_snapshot.absolutize(&rel_path);
                 let was_open_source = !eligible_licenses.is_empty();
                 if Self::is_path_eligible(&fs, abs_path).await.unwrap_or(false) {
                     eligible_licenses.insert(rel_path);
@@ -384,6 +384,8 @@ impl LicenseDetectionWatcher {
 
 #[cfg(test)]
 mod tests {
+    use std::path::Path;
+
     use fs::FakeFs;
     use gpui::TestAppContext;
     use rand::Rng as _;

crates/zeta/src/zeta.rs 🔗

@@ -51,6 +51,7 @@ use std::{
 use telemetry_events::EditPredictionRating;
 use thiserror::Error;
 use util::ResultExt;
+use util::rel_path::RelPath;
 use uuid::Uuid;
 use workspace::notifications::{ErrorMessagePrompt, NotificationId, show_app_notification};
 use worktree::Worktree;
@@ -1180,11 +1181,11 @@ impl Event {
                 let old_path = old_snapshot
                     .file()
                     .map(|f| f.path().as_ref())
-                    .unwrap_or(Path::new("untitled"));
+                    .unwrap_or(RelPath::new("untitled").unwrap());
                 let new_path = new_snapshot
                     .file()
                     .map(|f| f.path().as_ref())
-                    .unwrap_or(Path::new("untitled"));
+                    .unwrap_or(RelPath::new("untitled").unwrap());
                 if old_path != new_path {
                     writeln!(prompt, "User renamed {:?} to {:?}\n", old_path, new_path).unwrap();
                 }
@@ -1631,7 +1632,7 @@ mod tests {
     use parking_lot::Mutex;
     use serde_json::json;
     use settings::SettingsStore;
-    use util::path;
+    use util::{path, rel_path::rel_path};
 
     use super::*;
 
@@ -2026,7 +2027,7 @@ mod tests {
                     .worktree_for_root_name("closed_source_worktree", cx)
                     .unwrap();
                 worktree2.update(cx, |worktree2, cx| {
-                    worktree2.load_file(Path::new("main.rs"), cx)
+                    worktree2.load_file(rel_path("main.rs"), cx)
                 })
             })
             .await

crates/zeta2/src/zeta2.rs 🔗

@@ -28,6 +28,7 @@ use std::str::FromStr as _;
 use std::sync::Arc;
 use std::time::{Duration, Instant};
 use thiserror::Error;
+use util::rel_path::RelPathBuf;
 use util::some_or_debug_panic;
 use workspace::notifications::{ErrorMessagePrompt, NotificationId, show_app_notification};
 
@@ -342,20 +343,21 @@ impl Zeta {
                             new_snapshot,
                             ..
                         } => {
-                            let path = new_snapshot.file().map(|f| f.path().to_path_buf());
+                            let path = new_snapshot.file().map(|f| f.path().clone());
 
                             let old_path = old_snapshot.file().and_then(|f| {
-                                let old_path = f.path().as_ref();
-                                if Some(old_path) != path.as_deref() {
-                                    Some(old_path.to_path_buf())
+                                let old_path = f.path();
+                                if Some(old_path) != path.as_ref() {
+                                    Some(old_path.clone())
                                 } else {
                                     None
                                 }
                             });
 
                             predict_edits_v3::Event::BufferChange {
-                                old_path,
-                                path,
+                                old_path: old_path
+                                    .map(|old_path| old_path.as_std_path().to_path_buf()),
+                                path: path.map(|path| path.as_std_path().to_path_buf()),
                                 diff: language::unified_diff(
                                     &old_snapshot.text(),
                                     &new_snapshot.text(),
@@ -731,7 +733,7 @@ fn make_cloud_request(
         let project_entry_id = snippet.declaration.project_entry_id();
         let Some(path) = worktrees.iter().find_map(|worktree| {
             worktree.entry_for_id(project_entry_id).map(|entry| {
-                let mut full_path = PathBuf::new();
+                let mut full_path = RelPathBuf::new();
                 full_path.push(worktree.root_name());
                 full_path.push(&entry.path);
                 full_path
@@ -753,7 +755,7 @@ fn make_cloud_request(
 
         let (text, text_is_truncated) = snippet.declaration.item_text();
         referenced_declarations.push(predict_edits_v3::ReferencedDeclaration {
-            path,
+            path: path.as_std_path().to_path_buf(),
             text: text.into(),
             range: snippet.declaration.item_range(),
             text_is_truncated,

crates/zeta2_tools/src/zeta2_tools.rs 🔗

@@ -1,11 +1,4 @@
-use std::{
-    collections::hash_map::Entry,
-    ffi::OsStr,
-    path::{Path, PathBuf},
-    str::FromStr,
-    sync::Arc,
-    time::Duration,
-};
+use std::{collections::hash_map::Entry, path::PathBuf, str::FromStr, sync::Arc, time::Duration};
 
 use chrono::TimeDelta;
 use client::{Client, UserStore};
@@ -20,7 +13,7 @@ use language::{Buffer, DiskState};
 use project::{Project, WorktreeId};
 use ui::prelude::*;
 use ui_input::SingleLineInput;
-use util::ResultExt;
+use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
 use workspace::{Item, SplitDirection, Workspace};
 use zeta2::{Zeta, ZetaOptions};
 
@@ -271,9 +264,9 @@ impl Zeta2Inspector {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let Some(worktree_id) = self
-            .project
-            .read(cx)
+        let project = self.project.read(cx);
+        let path_style = project.path_style(cx);
+        let Some(worktree_id) = project
             .worktrees(cx)
             .next()
             .map(|worktree| worktree.read(cx).id())
@@ -311,7 +304,8 @@ impl Zeta2Inspector {
                         let multibuffer = cx.new(|cx| {
                             let mut multibuffer = MultiBuffer::new(language::Capability::ReadOnly);
                             let excerpt_file = Arc::new(ExcerptMetadataFile {
-                                title: PathBuf::from("Cursor Excerpt").into(),
+                                title: RelPath::new("Cursor Excerpt").unwrap().into(),
+                                path_style,
                                 worktree_id,
                             });
 
@@ -344,13 +338,15 @@ impl Zeta2Inspector {
                                     .path_for_entry(snippet.declaration.project_entry_id(), cx);
 
                                 let snippet_file = Arc::new(ExcerptMetadataFile {
-                                    title: PathBuf::from(format!(
+                                    title: RelPath::new(&format!(
                                         "{} (Score density: {})",
-                                        path.map(|p| p.path.to_string_lossy().to_string())
+                                        path.map(|p| p.path.display(path_style).to_string())
                                             .unwrap_or_else(|| "".to_string()),
                                         snippet.score_density(SnippetStyle::Declaration)
                                     ))
+                                    .unwrap()
                                     .into(),
+                                    path_style,
                                     worktree_id,
                                 });
 
@@ -639,8 +635,9 @@ impl Render for Zeta2Inspector {
 // Using same approach as commit view
 
 struct ExcerptMetadataFile {
-    title: Arc<Path>,
+    title: Arc<RelPath>,
     worktree_id: WorktreeId,
+    path_style: PathStyle,
 }
 
 impl language::File for ExcerptMetadataFile {
@@ -652,18 +649,22 @@ impl language::File for ExcerptMetadataFile {
         DiskState::New
     }
 
-    fn path(&self) -> &Arc<Path> {
+    fn path(&self) -> &Arc<RelPath> {
         &self.title
     }
 
     fn full_path(&self, _: &App) -> PathBuf {
-        self.title.as_ref().into()
+        self.title.as_std_path().to_path_buf()
     }
 
-    fn file_name<'a>(&'a self, _: &'a App) -> &'a OsStr {
+    fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
         self.title.file_name().unwrap()
     }
 
+    fn path_style(&self, _: &App) -> PathStyle {
+        self.path_style
+    }
+
     fn worktree_id(&self, _: &App) -> WorktreeId {
         self.worktree_id
     }

crates/zeta_cli/src/main.rs 🔗

@@ -19,6 +19,8 @@ use std::process::exit;
 use std::str::FromStr;
 use std::sync::Arc;
 use std::time::Duration;
+use util::paths::PathStyle;
+use util::rel_path::RelPath;
 use zeta::{PerformPredictEditsParams, Zeta};
 
 use crate::headless::ZetaCliAppState;
@@ -102,7 +104,7 @@ impl FromStr for FileOrStdin {
 
 #[derive(Debug, Clone)]
 struct CursorPosition {
-    path: PathBuf,
+    path: Arc<RelPath>,
     point: Point,
 }
 
@@ -118,7 +120,7 @@ impl FromStr for CursorPosition {
             ));
         }
 
-        let path = PathBuf::from(parts[0]);
+        let path = RelPath::from_std_path(Path::new(&parts[0]), PathStyle::local())?;
         let line: u32 = parts[1]
             .parse()
             .map_err(|_| anyhow!("Invalid line number: '{}'", parts[1]))?;
@@ -152,9 +154,6 @@ async fn get_context(
     } = args;
 
     let worktree_path = worktree_path.canonicalize()?;
-    if cursor.path.is_absolute() {
-        return Err(anyhow!("Absolute paths are not supported in --cursor"));
-    }
 
     let project = cx.update(|cx| {
         Project::local(
@@ -183,12 +182,9 @@ async fn get_context(
         (None, buffer)
     };
 
-    let worktree_name = worktree_path
-        .file_name()
-        .ok_or_else(|| anyhow!("--worktree path must end with a folder name"))?;
-    let full_path_str = PathBuf::from(worktree_name)
-        .join(&cursor.path)
-        .to_string_lossy()
+    let full_path_str = worktree
+        .read_with(cx, |worktree, _| worktree.root_name().join(&cursor.path))?
+        .display(PathStyle::local())
         .to_string();
 
     let snapshot = cx.update(|cx| buffer.read(cx).snapshot())?;
@@ -275,12 +271,12 @@ async fn get_context(
 pub async fn open_buffer(
     project: &Entity<Project>,
     worktree: &Entity<Worktree>,
-    path: &Path,
+    path: &RelPath,
     cx: &mut AsyncApp,
 ) -> Result<Entity<Buffer>> {
     let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
         worktree_id: worktree.id(),
-        path: path.to_path_buf().into(),
+        path: path.into(),
     })?;
 
     project
@@ -291,17 +287,20 @@ pub async fn open_buffer(
 pub async fn open_buffer_with_language_server(
     project: &Entity<Project>,
     worktree: &Entity<Worktree>,
-    path: &Path,
+    path: &RelPath,
     cx: &mut AsyncApp,
 ) -> Result<(Entity<Entity<Buffer>>, Entity<Buffer>)> {
     let buffer = open_buffer(project, worktree, path, cx).await?;
 
-    let lsp_open_handle = project.update(cx, |project, cx| {
-        project.register_buffer_with_language_servers(&buffer, cx)
+    let (lsp_open_handle, path_style) = project.update(cx, |project, cx| {
+        (
+            project.register_buffer_with_language_servers(&buffer, cx),
+            project.path_style(cx),
+        )
     })?;
 
-    let log_prefix = path.to_string_lossy().to_string();
-    wait_for_lang_server(&project, &buffer, log_prefix, cx).await?;
+    let log_prefix = path.display(path_style);
+    wait_for_lang_server(&project, &buffer, log_prefix.into_owned(), cx).await?;
 
     Ok((lsp_open_handle, buffer))
 }