Better absolute path handling (#19727)

张小白 created

Closes #19866

This PR supersedes #19228, as #19228 encountered too many merge
conflicts.

After some exploration, I found that for paths with the `\\?\` prefix,
we can safely remove it and consistently use the clean paths in all
cases. Previously, in #19228, I thought we would still need the `\\?\`
prefix for IO operations to handle long paths better. However, this
turns out to be unnecessary because Rust automatically manages this for
us when calling IO-related APIs. For details, refer to Rust's internal
function
[`get_long_path`](https://github.com/rust-lang/rust/blob/017ae1b21f7be6dcdcfc95631e54bde806653a8a/library/std/src/sys/path/windows.rs#L225-L233).

Therefore, we can always store and use paths without the `\\?\` prefix.

This PR introduces a `SanitizedPath` structure, which represents a path
stripped of the `\\?\` prefix. To prevent untrimmed paths from being
mistakenly passed into `Worktree`, the type of `Worktree`’s `abs_path`
member variable has been changed to `SanitizedPath`.

Additionally, this PR reverts the changes of #15856 and #18726. After
testing, it appears that the issues those PRs addressed can be resolved
by this PR.

### Existing Issue
To keep the scope of modifications manageable, `Worktree::abs_path` has
retained its current signature as `fn abs_path(&self) -> Arc<Path>`,
rather than returning a `SanitizedPath`. Updating the method to return
`SanitizedPath`β€”which may better resolve path inconsistenciesβ€”would
likely introduce extensive changes similar to those in #19228.

Currently, the limitation is as follows:

```rust
let abs_path: &Arc<Path> = snapshot.abs_path();
let some_non_trimmed_path = Path::new("\\\\?\\C:\\Users\\user\\Desktop\\project"); 
// The caller performs some actions here:
some_non_trimmed_path.strip_prefix(abs_path);  // This fails
some_non_trimmed_path.starts_with(abs_path);   // This fails too
```

The final two lines will fail because `snapshot.abs_path()` returns a
clean path without the `\\?\` prefix. I have identified two relevant
instances that may face this issue:
-
[lsp_store.rs#L3578](https://github.com/zed-industries/zed/blob/0173479d18e2526c1f9c8b25ac94ec66b992a2b2/crates/project/src/lsp_store.rs#L3578)
-
[worktree.rs#L4338](https://github.com/zed-industries/zed/blob/0173479d18e2526c1f9c8b25ac94ec66b992a2b2/crates/worktree/src/worktree.rs#L4338)

Switching `Worktree::abs_path` to return `SanitizedPath` would resolve
these issues but would also lead to many code changes.

Any suggestions or feedback on this approach are very welcome.

cc @SomeoneToIgnore 

Release Notes:

- N/A

Change summary

Cargo.lock                                   |   7 +
Cargo.toml                                   |  12 +
crates/fs/src/fs.rs                          |  20 +--
crates/gpui/src/platform/windows/platform.rs |  14 +-
crates/project/src/lsp_store.rs              |   3 
crates/project/src/worktree_store.rs         |  42 ++++---
crates/terminal_view/src/terminal_view.rs    |  15 --
crates/util/Cargo.toml                       |   1 
crates/util/src/paths.rs                     |  60 +++++++++++
crates/workspace/src/workspace.rs            |   6 
crates/worktree/src/worktree.rs              | 117 +++++++++++++--------
crates/zed/src/main.rs                       |   5 
12 files changed, 189 insertions(+), 113 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -3752,6 +3752,12 @@ dependencies = [
  "phf",
 ]
 
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
 [[package]]
 name = "dwrote"
 version = "0.11.2"
@@ -13689,6 +13695,7 @@ dependencies = [
  "async-fs 1.6.0",
  "collections",
  "dirs 4.0.0",
+ "dunce",
  "futures 0.3.31",
  "futures-lite 1.13.0",
  "git2",

Cargo.toml πŸ”—

@@ -228,7 +228,9 @@ git = { path = "crates/git" }
 git_hosting_providers = { path = "crates/git_hosting_providers" }
 go_to_line = { path = "crates/go_to_line" }
 google_ai = { path = "crates/google_ai" }
-gpui = { path = "crates/gpui", default-features = false, features = ["http_client"]}
+gpui = { path = "crates/gpui", default-features = false, features = [
+    "http_client",
+] }
 gpui_macros = { path = "crates/gpui_macros" }
 html_to_markdown = { path = "crates/html_to_markdown" }
 http_client = { path = "crates/http_client" }
@@ -403,10 +405,10 @@ parking_lot = "0.12.1"
 pathdiff = "0.2"
 pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
 pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
-pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c"  }
-pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c"  }
-pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c"  }
-pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c"  }
+pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
+pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
+pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
+pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
 postage = { version = "0.5", features = ["futures-traits"] }
 pretty_assertions = { version = "1.3.0", features = ["unstable"] }
 profiling = "1"

crates/fs/src/fs.rs πŸ”—

@@ -452,18 +452,16 @@ impl Fs for RealFs {
 
     #[cfg(target_os = "windows")]
     async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
+        use util::paths::SanitizedPath;
         use windows::{
             core::HSTRING,
             Storage::{StorageDeleteOption, StorageFile},
         };
         // todo(windows)
         // When new version of `windows-rs` release, make this operation `async`
-        let path = path.canonicalize()?.to_string_lossy().to_string();
-        let path_str = path.trim_start_matches("\\\\?\\");
-        if path_str.is_empty() {
-            anyhow::bail!("File path is empty!");
-        }
-        let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(path_str))?.get()?;
+        let path = SanitizedPath::from(path.canonicalize()?);
+        let path_string = path.to_string();
+        let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(path_string))?.get()?;
         file.DeleteAsync(StorageDeleteOption::Default)?.get()?;
         Ok(())
     }
@@ -480,19 +478,17 @@ impl Fs for RealFs {
 
     #[cfg(target_os = "windows")]
     async fn trash_dir(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
+        use util::paths::SanitizedPath;
         use windows::{
             core::HSTRING,
             Storage::{StorageDeleteOption, StorageFolder},
         };
 
-        let path = path.canonicalize()?.to_string_lossy().to_string();
-        let path_str = path.trim_start_matches("\\\\?\\");
-        if path_str.is_empty() {
-            anyhow::bail!("Folder path is empty!");
-        }
         // todo(windows)
         // When new version of `windows-rs` release, make this operation `async`
-        let folder = StorageFolder::GetFolderFromPathAsync(&HSTRING::from(path_str))?.get()?;
+        let path = SanitizedPath::from(path.canonicalize()?);
+        let path_string = path.to_string();
+        let folder = StorageFolder::GetFolderFromPathAsync(&HSTRING::from(path_string))?.get()?;
         folder.DeleteAsync(StorageDeleteOption::Default)?.get()?;
         Ok(())
     }

crates/gpui/src/platform/windows/platform.rs πŸ”—

@@ -6,7 +6,7 @@ use std::{
     sync::Arc,
 };
 
-use ::util::ResultExt;
+use ::util::{paths::SanitizedPath, ResultExt};
 use anyhow::{anyhow, Context, Result};
 use async_task::Runnable;
 use futures::channel::oneshot::{self, Receiver};
@@ -645,13 +645,11 @@ fn file_save_dialog(directory: PathBuf) -> Result<Option<PathBuf>> {
     let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? };
     if !directory.to_string_lossy().is_empty() {
         if let Some(full_path) = directory.canonicalize().log_err() {
-            let full_path = full_path.to_string_lossy();
-            let full_path_str = full_path.trim_start_matches("\\\\?\\");
-            if !full_path_str.is_empty() {
-                let path_item: IShellItem =
-                    unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_str), None)? };
-                unsafe { dialog.SetFolder(&path_item).log_err() };
-            }
+            let full_path = SanitizedPath::from(full_path);
+            let full_path_string = full_path.to_string();
+            let path_item: IShellItem =
+                unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_string), None)? };
+            unsafe { dialog.SetFolder(&path_item).log_err() };
         }
     }
     unsafe {

crates/project/src/lsp_store.rs πŸ”—

@@ -5577,7 +5577,7 @@ impl LspStore {
 
         let worktree = worktree_handle.read(cx);
         let worktree_id = worktree.id();
-        let worktree_path = worktree.abs_path();
+        let root_path = worktree.abs_path();
         let key = (worktree_id, adapter.name.clone());
 
         if self.language_server_ids.contains_key(&key) {
@@ -5599,7 +5599,6 @@ impl LspStore {
             as Arc<dyn LspAdapterDelegate>;
 
         let server_id = self.languages.next_language_server_id();
-        let root_path = worktree_path.clone();
         log::info!(
             "attempting to start language server {:?}, path: {root_path:?}, id: {server_id}",
             adapter.name.0

crates/project/src/worktree_store.rs πŸ”—

@@ -23,7 +23,7 @@ use smol::{
     stream::StreamExt,
 };
 use text::ReplicaId;
-use util::ResultExt;
+use util::{paths::SanitizedPath, ResultExt};
 use worktree::{Entry, ProjectEntryId, Worktree, WorktreeId, WorktreeSettings};
 
 use crate::{search::SearchQuery, ProjectPath};
@@ -52,7 +52,7 @@ pub struct WorktreeStore {
     worktrees_reordered: bool,
     #[allow(clippy::type_complexity)]
     loading_worktrees:
-        HashMap<Arc<Path>, Shared<Task<Result<Model<Worktree>, Arc<anyhow::Error>>>>>,
+        HashMap<SanitizedPath, Shared<Task<Result<Model<Worktree>, Arc<anyhow::Error>>>>>,
     state: WorktreeStoreState,
 }
 
@@ -147,11 +147,12 @@ impl WorktreeStore {
 
     pub fn find_worktree(
         &self,
-        abs_path: &Path,
+        abs_path: impl Into<SanitizedPath>,
         cx: &AppContext,
     ) -> Option<(Model<Worktree>, PathBuf)> {
+        let abs_path: SanitizedPath = abs_path.into();
         for tree in self.worktrees() {
-            if let Ok(relative_path) = abs_path.strip_prefix(tree.read(cx).abs_path()) {
+            if let Ok(relative_path) = abs_path.as_path().strip_prefix(tree.read(cx).abs_path()) {
                 return Some((tree.clone(), relative_path.into()));
             }
         }
@@ -192,12 +193,12 @@ impl WorktreeStore {
 
     pub fn create_worktree(
         &mut self,
-        abs_path: impl AsRef<Path>,
+        abs_path: impl Into<SanitizedPath>,
         visible: bool,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Model<Worktree>>> {
-        let path: Arc<Path> = abs_path.as_ref().into();
-        if !self.loading_worktrees.contains_key(&path) {
+        let abs_path: SanitizedPath = abs_path.into();
+        if !self.loading_worktrees.contains_key(&abs_path) {
             let task = match &self.state {
                 WorktreeStoreState::Remote {
                     upstream_client, ..
@@ -205,20 +206,26 @@ impl WorktreeStore {
                     if upstream_client.is_via_collab() {
                         Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab"))))
                     } else {
-                        self.create_ssh_worktree(upstream_client.clone(), abs_path, visible, cx)
+                        self.create_ssh_worktree(
+                            upstream_client.clone(),
+                            abs_path.clone(),
+                            visible,
+                            cx,
+                        )
                     }
                 }
                 WorktreeStoreState::Local { fs } => {
-                    self.create_local_worktree(fs.clone(), abs_path, visible, cx)
+                    self.create_local_worktree(fs.clone(), abs_path.clone(), visible, cx)
                 }
             };
 
-            self.loading_worktrees.insert(path.clone(), task.shared());
+            self.loading_worktrees
+                .insert(abs_path.clone(), task.shared());
         }
-        let task = self.loading_worktrees.get(&path).unwrap().clone();
+        let task = self.loading_worktrees.get(&abs_path).unwrap().clone();
         cx.spawn(|this, mut cx| async move {
             let result = task.await;
-            this.update(&mut cx, |this, _| this.loading_worktrees.remove(&path))
+            this.update(&mut cx, |this, _| this.loading_worktrees.remove(&abs_path))
                 .ok();
             match result {
                 Ok(worktree) => Ok(worktree),
@@ -230,12 +237,11 @@ impl WorktreeStore {
     fn create_ssh_worktree(
         &mut self,
         client: AnyProtoClient,
-        abs_path: impl AsRef<Path>,
+        abs_path: impl Into<SanitizedPath>,
         visible: bool,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
-        let path_key: Arc<Path> = abs_path.as_ref().into();
-        let mut abs_path = path_key.clone().to_string_lossy().to_string();
+        let mut abs_path = Into::<SanitizedPath>::into(abs_path).to_string();
         // If we start with `/~` that means the ssh path was something like `ssh://user@host/~/home-dir-folder/`
         // in which case want to strip the leading the `/`.
         // On the host-side, the `~` will get expanded.
@@ -293,12 +299,12 @@ impl WorktreeStore {
     fn create_local_worktree(
         &mut self,
         fs: Arc<dyn Fs>,
-        abs_path: impl AsRef<Path>,
+        abs_path: impl Into<SanitizedPath>,
         visible: bool,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
         let next_entry_id = self.next_entry_id.clone();
-        let path: Arc<Path> = abs_path.as_ref().into();
+        let path: SanitizedPath = abs_path.into();
 
         cx.spawn(move |this, mut cx| async move {
             let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx).await;
@@ -308,7 +314,7 @@ impl WorktreeStore {
 
             if visible {
                 cx.update(|cx| {
-                    cx.add_recent_document(&path);
+                    cx.add_recent_document(path.as_path());
                 })
                 .log_err();
             }

crates/terminal_view/src/terminal_view.rs πŸ”—

@@ -798,7 +798,6 @@ fn possible_open_paths_metadata(
     cx.background_executor().spawn(async move {
         let mut paths_with_metadata = Vec::with_capacity(potential_paths.len());
 
-        #[cfg(not(target_os = "windows"))]
         let mut fetch_metadata_tasks = potential_paths
             .into_iter()
             .map(|potential_path| async {
@@ -814,20 +813,6 @@ fn possible_open_paths_metadata(
             })
             .collect::<FuturesUnordered<_>>();
 
-        #[cfg(target_os = "windows")]
-        let mut fetch_metadata_tasks = potential_paths
-            .iter()
-            .map(|potential_path| async {
-                let metadata = fs.metadata(potential_path).await.ok().flatten();
-                let path = PathBuf::from(
-                    potential_path
-                        .to_string_lossy()
-                        .trim_start_matches("\\\\?\\"),
-                );
-                (PathWithPosition { path, row, column }, metadata)
-            })
-            .collect::<FuturesUnordered<_>>();
-
         while let Some((path, metadata)) = fetch_metadata_tasks.next().await {
             if let Some(metadata) = metadata {
                 paths_with_metadata.push((path, metadata));

crates/util/Cargo.toml πŸ”—

@@ -37,6 +37,7 @@ unicase.workspace = true
 
 [target.'cfg(windows)'.dependencies]
 tendril = "0.4.3"
+dunce = "1.0"
 
 [dev-dependencies]
 git2.workspace = true

crates/util/src/paths.rs πŸ”—

@@ -1,5 +1,5 @@
 use std::cmp;
-use std::sync::OnceLock;
+use std::sync::{Arc, OnceLock};
 use std::{
     ffi::OsStr,
     path::{Path, PathBuf},
@@ -95,6 +95,46 @@ impl<T: AsRef<Path>> PathExt for T {
     }
 }
 
+/// Due to the issue of UNC paths on Windows, which can cause bugs in various parts of Zed, introducing this `SanitizedPath`
+/// leverages Rust's type system to ensure that all paths entering Zed are always "sanitized" by removing the `\\\\?\\` prefix.
+/// On non-Windows operating systems, this struct is effectively a no-op.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct SanitizedPath(Arc<Path>);
+
+impl SanitizedPath {
+    pub fn starts_with(&self, prefix: &SanitizedPath) -> bool {
+        self.0.starts_with(&prefix.0)
+    }
+
+    pub fn as_path(&self) -> &Arc<Path> {
+        &self.0
+    }
+
+    pub fn to_string(&self) -> String {
+        self.0.to_string_lossy().to_string()
+    }
+}
+
+impl From<SanitizedPath> for Arc<Path> {
+    fn from(sanitized_path: SanitizedPath) -> Self {
+        sanitized_path.0
+    }
+}
+
+impl<T: AsRef<Path>> From<T> for SanitizedPath {
+    #[cfg(not(target_os = "windows"))]
+    fn from(path: T) -> Self {
+        let path = path.as_ref();
+        SanitizedPath(path.into())
+    }
+
+    #[cfg(target_os = "windows")]
+    fn from(path: T) -> Self {
+        let path = path.as_ref();
+        SanitizedPath(dunce::simplified(path).into())
+    }
+}
+
 /// A delimiter to use in `path_query:row_number:column_number` strings parsing.
 pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
 
@@ -805,4 +845,22 @@ mod tests {
             "Path matcher should match {path:?}"
         );
     }
+
+    #[test]
+    #[cfg(target_os = "windows")]
+    fn test_sanitized_path() {
+        let path = Path::new("C:\\Users\\someone\\test_file.rs");
+        let sanitized_path = SanitizedPath::from(path);
+        assert_eq!(
+            sanitized_path.to_string(),
+            "C:\\Users\\someone\\test_file.rs"
+        );
+
+        let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs");
+        let sanitized_path = SanitizedPath::from(path);
+        assert_eq!(
+            sanitized_path.to_string(),
+            "C:\\Users\\someone\\test_file.rs"
+        );
+    }
 }

crates/workspace/src/workspace.rs πŸ”—

@@ -97,7 +97,7 @@ use ui::{
     IntoElement, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext,
     VisualContext as _, WindowContext,
 };
-use util::{ResultExt, TryFutureExt};
+use util::{paths::SanitizedPath, ResultExt, TryFutureExt};
 use uuid::Uuid;
 pub use workspace_settings::{
     AutosaveSetting, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
@@ -2024,7 +2024,7 @@ impl Workspace {
                 };
 
                 let this = this.clone();
-                let abs_path = abs_path.clone();
+                let abs_path: Arc<Path> = SanitizedPath::from(abs_path.clone()).into();
                 let fs = fs.clone();
                 let pane = pane.clone();
                 let task = cx.spawn(move |mut cx| async move {
@@ -2033,7 +2033,7 @@ impl Workspace {
                         this.update(&mut cx, |workspace, cx| {
                             let worktree = worktree.read(cx);
                             let worktree_abs_path = worktree.abs_path();
-                            let entry_id = if abs_path == worktree_abs_path.as_ref() {
+                            let entry_id = if abs_path.as_ref() == worktree_abs_path.as_ref() {
                                 worktree.root_entry()
                             } else {
                                 abs_path

crates/worktree/src/worktree.rs πŸ”—

@@ -66,7 +66,7 @@ use std::{
 use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
 use text::{LineEnding, Rope};
 use util::{
-    paths::{home_dir, PathMatcher},
+    paths::{home_dir, PathMatcher, SanitizedPath},
     ResultExt,
 };
 pub use worktree_settings::WorktreeSettings;
@@ -149,7 +149,7 @@ pub struct RemoteWorktree {
 #[derive(Clone)]
 pub struct Snapshot {
     id: WorktreeId,
-    abs_path: Arc<Path>,
+    abs_path: SanitizedPath,
     root_name: String,
     root_char_bag: CharBag,
     entries_by_path: SumTree<Entry>,
@@ -356,7 +356,7 @@ enum ScanState {
         scanning: bool,
     },
     RootUpdated {
-        new_path: Option<Arc<Path>>,
+        new_path: Option<SanitizedPath>,
     },
 }
 
@@ -654,8 +654,8 @@ impl Worktree {
 
     pub fn abs_path(&self) -> Arc<Path> {
         match self {
-            Worktree::Local(worktree) => worktree.abs_path.clone(),
-            Worktree::Remote(worktree) => worktree.abs_path.clone(),
+            Worktree::Local(worktree) => worktree.abs_path.clone().into(),
+            Worktree::Remote(worktree) => worktree.abs_path.clone().into(),
         }
     }
 
@@ -1026,6 +1026,7 @@ impl LocalWorktree {
     }
 
     pub fn contains_abs_path(&self, path: &Path) -> bool {
+        let path = SanitizedPath::from(path);
         path.starts_with(&self.abs_path)
     }
 
@@ -1066,13 +1067,13 @@ impl LocalWorktree {
         let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
         let background_scanner = cx.background_executor().spawn({
             let abs_path = &snapshot.abs_path;
-            let abs_path = if cfg!(target_os = "windows") {
-                abs_path
-                    .canonicalize()
-                    .unwrap_or_else(|_| abs_path.to_path_buf())
-            } else {
-                abs_path.to_path_buf()
-            };
+            #[cfg(target_os = "windows")]
+            let abs_path = abs_path
+                .as_path()
+                .canonicalize()
+                .unwrap_or_else(|_| abs_path.as_path().to_path_buf());
+            #[cfg(not(target_os = "windows"))]
+            let abs_path = abs_path.as_path().to_path_buf();
             let background = cx.background_executor().clone();
             async move {
                 let (events, watcher) = fs.watch(&abs_path, FS_WATCH_LATENCY).await;
@@ -1135,6 +1136,7 @@ impl LocalWorktree {
                                 this.snapshot.git_repositories = Default::default();
                                 this.snapshot.ignores_by_parent_abs_path = Default::default();
                                 let root_name = new_path
+                                    .as_path()
                                     .file_name()
                                     .map_or(String::new(), |f| f.to_string_lossy().to_string());
                                 this.snapshot.update_abs_path(new_path, root_name);
@@ -2075,7 +2077,7 @@ impl Snapshot {
     pub fn new(id: u64, root_name: String, abs_path: Arc<Path>) -> Self {
         Snapshot {
             id: WorktreeId::from_usize(id as usize),
-            abs_path,
+            abs_path: abs_path.into(),
             root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
             root_name,
             always_included_entries: Default::default(),
@@ -2091,8 +2093,20 @@ impl Snapshot {
         self.id
     }
 
+    // TODO:
+    // Consider the following:
+    //
+    // ```rust
+    // let abs_path: Arc<Path> = snapshot.abs_path(); // e.g. "C:\Users\user\Desktop\project"
+    // let some_non_trimmed_path = Path::new("\\\\?\\C:\\Users\\user\\Desktop\\project\\main.rs");
+    // // The caller perform some actions here:
+    // some_non_trimmed_path.strip_prefix(abs_path);  // This fails
+    // some_non_trimmed_path.starts_with(abs_path);   // This fails too
+    // ```
+    //
+    // This is definitely a bug, but it's not clear if we should handle it here or not.
     pub fn abs_path(&self) -> &Arc<Path> {
-        &self.abs_path
+        self.abs_path.as_path()
     }
 
     fn build_initial_update(&self, project_id: u64, worktree_id: u64) -> proto::UpdateWorktree {
@@ -2132,9 +2146,9 @@ impl Snapshot {
             return Err(anyhow!("invalid path"));
         }
         if path.file_name().is_some() {
-            Ok(self.abs_path.join(path))
+            Ok(self.abs_path.as_path().join(path))
         } else {
-            Ok(self.abs_path.to_path_buf())
+            Ok(self.abs_path.as_path().to_path_buf())
         }
     }
 
@@ -2193,7 +2207,7 @@ impl Snapshot {
             .and_then(|entry| entry.git_status)
     }
 
-    fn update_abs_path(&mut self, abs_path: Arc<Path>, root_name: String) {
+    fn update_abs_path(&mut self, abs_path: SanitizedPath, root_name: String) {
         self.abs_path = abs_path;
         if root_name != self.root_name {
             self.root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect();
@@ -2212,7 +2226,7 @@ impl Snapshot {
             update.removed_entries.len()
         );
         self.update_abs_path(
-            Arc::from(PathBuf::from(update.abs_path).as_path()),
+            SanitizedPath::from(PathBuf::from(update.abs_path)),
             update.root_name,
         );
 
@@ -2632,7 +2646,7 @@ impl LocalSnapshot {
 
     fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
         if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) {
-            let abs_path = self.abs_path.join(&entry.path);
+            let abs_path = self.abs_path.as_path().join(&entry.path);
             match smol::block_on(build_gitignore(&abs_path, fs)) {
                 Ok(ignore) => {
                     self.ignores_by_parent_abs_path
@@ -2786,8 +2800,9 @@ impl LocalSnapshot {
 
         if git_state {
             for ignore_parent_abs_path in self.ignores_by_parent_abs_path.keys() {
-                let ignore_parent_path =
-                    ignore_parent_abs_path.strip_prefix(&self.abs_path).unwrap();
+                let ignore_parent_path = ignore_parent_abs_path
+                    .strip_prefix(self.abs_path.as_path())
+                    .unwrap();
                 assert!(self.entry_for_path(ignore_parent_path).is_some());
                 assert!(self
                     .entry_for_path(ignore_parent_path.join(*GITIGNORE))
@@ -2941,7 +2956,7 @@ impl BackgroundScannerState {
         }
 
         if let Some(ignore) = ignore {
-            let abs_parent_path = self.snapshot.abs_path.join(parent_path).into();
+            let abs_parent_path = self.snapshot.abs_path.as_path().join(parent_path).into();
             self.snapshot
                 .ignores_by_parent_abs_path
                 .insert(abs_parent_path, (ignore, false));
@@ -3004,7 +3019,11 @@ impl BackgroundScannerState {
             }
 
             if entry.path.file_name() == Some(&GITIGNORE) {
-                let abs_parent_path = self.snapshot.abs_path.join(entry.path.parent().unwrap());
+                let abs_parent_path = self
+                    .snapshot
+                    .abs_path
+                    .as_path()
+                    .join(entry.path.parent().unwrap());
                 if let Some((_, needs_update)) = self
                     .snapshot
                     .ignores_by_parent_abs_path
@@ -3085,7 +3104,7 @@ impl BackgroundScannerState {
             return None;
         }
 
-        let dot_git_abs_path = self.snapshot.abs_path.join(&dot_git_path);
+        let dot_git_abs_path = self.snapshot.abs_path.as_path().join(&dot_git_path);
 
         let t0 = Instant::now();
         let repository = fs.open_repo(&dot_git_abs_path)?;
@@ -3299,9 +3318,9 @@ impl language::LocalFile for File {
     fn abs_path(&self, cx: &AppContext) -> PathBuf {
         let worktree_path = &self.worktree.read(cx).as_local().unwrap().abs_path;
         if self.path.as_ref() == Path::new("") {
-            worktree_path.to_path_buf()
+            worktree_path.as_path().to_path_buf()
         } else {
-            worktree_path.join(&self.path)
+            worktree_path.as_path().join(&self.path)
         }
     }
 
@@ -3712,7 +3731,7 @@ impl BackgroundScanner {
         // the git repository in an ancestor directory. Find any gitignore files
         // in ancestor directories.
         let root_abs_path = self.state.lock().snapshot.abs_path.clone();
-        for (index, ancestor) in root_abs_path.ancestors().enumerate() {
+        for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() {
             if index != 0 {
                 if let Ok(ignore) =
                     build_gitignore(&ancestor.join(*GITIGNORE), self.fs.as_ref()).await
@@ -3744,7 +3763,13 @@ impl BackgroundScanner {
                         self.state.lock().insert_git_repository_for_path(
                             Path::new("").into(),
                             ancestor_dot_git.into(),
-                            Some(root_abs_path.strip_prefix(ancestor).unwrap().into()),
+                            Some(
+                                root_abs_path
+                                    .as_path()
+                                    .strip_prefix(ancestor)
+                                    .unwrap()
+                                    .into(),
+                            ),
                             self.fs.as_ref(),
                             self.watcher.as_ref(),
                         );
@@ -3763,12 +3788,12 @@ impl BackgroundScanner {
             if let Some(mut root_entry) = state.snapshot.root_entry().cloned() {
                 let ignore_stack = state
                     .snapshot
-                    .ignore_stack_for_abs_path(&root_abs_path, true);
-                if ignore_stack.is_abs_path_ignored(&root_abs_path, true) {
+                    .ignore_stack_for_abs_path(root_abs_path.as_path(), true);
+                if ignore_stack.is_abs_path_ignored(root_abs_path.as_path(), true) {
                     root_entry.is_ignored = true;
                     state.insert_entry(root_entry.clone(), self.fs.as_ref(), self.watcher.as_ref());
                 }
-                state.enqueue_scan_dir(root_abs_path, &root_entry, &scan_job_tx);
+                state.enqueue_scan_dir(root_abs_path.into(), &root_entry, &scan_job_tx);
             }
         };
 
@@ -3818,7 +3843,7 @@ impl BackgroundScanner {
                         {
                             let mut state = self.state.lock();
                             state.path_prefixes_to_scan.insert(path_prefix.clone());
-                            state.snapshot.abs_path.join(&path_prefix)
+                            state.snapshot.abs_path.as_path().join(&path_prefix)
                         };
 
                         if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() {
@@ -3845,7 +3870,7 @@ impl BackgroundScanner {
         self.forcibly_load_paths(&request.relative_paths).await;
 
         let root_path = self.state.lock().snapshot.abs_path.clone();
-        let root_canonical_path = match self.fs.canonicalize(&root_path).await {
+        let root_canonical_path = match self.fs.canonicalize(root_path.as_path()).await {
             Ok(path) => path,
             Err(err) => {
                 log::error!("failed to canonicalize root path: {}", err);
@@ -3874,7 +3899,7 @@ impl BackgroundScanner {
         }
 
         self.reload_entries_for_paths(
-            root_path,
+            root_path.into(),
             root_canonical_path,
             &request.relative_paths,
             abs_paths,
@@ -3887,7 +3912,7 @@ impl BackgroundScanner {
 
     async fn process_events(&self, mut abs_paths: Vec<PathBuf>) {
         let root_path = self.state.lock().snapshot.abs_path.clone();
-        let root_canonical_path = match self.fs.canonicalize(&root_path).await {
+        let root_canonical_path = match self.fs.canonicalize(root_path.as_path()).await {
             Ok(path) => path,
             Err(err) => {
                 let new_path = self
@@ -3897,21 +3922,20 @@ impl BackgroundScanner {
                     .root_file_handle
                     .clone()
                     .and_then(|handle| handle.current_path(&self.fs).log_err())
-                    .filter(|new_path| **new_path != *root_path);
+                    .map(SanitizedPath::from)
+                    .filter(|new_path| *new_path != root_path);
 
                 if let Some(new_path) = new_path.as_ref() {
                     log::info!(
                         "root renamed from {} to {}",
-                        root_path.display(),
-                        new_path.display()
+                        root_path.as_path().display(),
+                        new_path.as_path().display()
                     )
                 } else {
                     log::warn!("root path could not be canonicalized: {}", err);
                 }
                 self.status_updates_tx
-                    .unbounded_send(ScanState::RootUpdated {
-                        new_path: new_path.map(|p| p.into()),
-                    })
+                    .unbounded_send(ScanState::RootUpdated { new_path })
                     .ok();
                 return;
             }
@@ -4006,7 +4030,7 @@ impl BackgroundScanner {
         let (scan_job_tx, scan_job_rx) = channel::unbounded();
         log::debug!("received fs events {:?}", relative_paths);
         self.reload_entries_for_paths(
-            root_path,
+            root_path.into(),
             root_canonical_path,
             &relative_paths,
             abs_paths,
@@ -4044,7 +4068,7 @@ impl BackgroundScanner {
                 for ancestor in path.ancestors() {
                     if let Some(entry) = state.snapshot.entry_for_path(ancestor) {
                         if entry.kind == EntryKind::UnloadedDir {
-                            let abs_path = root_path.join(ancestor);
+                            let abs_path = root_path.as_path().join(ancestor);
                             state.enqueue_scan_dir(abs_path.into(), entry, &scan_job_tx);
                             state.paths_to_scan.insert(path.clone());
                             break;
@@ -4548,7 +4572,7 @@ impl BackgroundScanner {
             snapshot
                 .ignores_by_parent_abs_path
                 .retain(|parent_abs_path, (_, needs_update)| {
-                    if let Ok(parent_path) = parent_abs_path.strip_prefix(&abs_path) {
+                    if let Ok(parent_path) = parent_abs_path.strip_prefix(abs_path.as_path()) {
                         if *needs_update {
                             *needs_update = false;
                             if snapshot.snapshot.entry_for_path(parent_path).is_some() {
@@ -4627,7 +4651,10 @@ impl BackgroundScanner {
 
         let mut entries_by_id_edits = Vec::new();
         let mut entries_by_path_edits = Vec::new();
-        let path = job.abs_path.strip_prefix(&snapshot.abs_path).unwrap();
+        let path = job
+            .abs_path
+            .strip_prefix(snapshot.abs_path.as_path())
+            .unwrap();
         let repo = snapshot.repo_for_path(path);
         for mut entry in snapshot.child_entries(path).cloned() {
             let was_ignored = entry.is_ignored;

crates/zed/src/main.rs πŸ”—

@@ -1124,10 +1124,7 @@ impl ToString for IdType {
 
 fn parse_url_arg(arg: &str, cx: &AppContext) -> Result<String> {
     match std::fs::canonicalize(Path::new(&arg)) {
-        Ok(path) => Ok(format!(
-            "file://{}",
-            path.to_string_lossy().trim_start_matches(r#"\\?\"#)
-        )),
+        Ok(path) => Ok(format!("file://{}", path.display())),
         Err(error) => {
             if arg.starts_with("file://")
                 || arg.starts_with("zed-cli://")