Make `SanitizedPath` wrap `Path` instead of `Arc<Path>` to avoid allocation (#37106)

Michael Sloan created

Release Notes:

- N/A

Change summary

crates/assistant_slash_commands/src/file_command.rs |   2 
crates/fs/src/fs.rs                                 |   8 
crates/fs/src/fs_watcher.rs                         |   4 
crates/gpui/src/platform/windows/platform.rs        |   2 
crates/paths/src/paths.rs                           |   2 
crates/project/src/git_store.rs                     |   3 
crates/project/src/lsp_store.rs                     |   4 
crates/project/src/project.rs                       |   5 
crates/project/src/worktree_store.rs                |  27 +-
crates/remote/src/transport/ssh.rs                  |   2 
crates/util/src/paths.rs                            | 122 +++++++++++---
crates/workspace/src/path_list.rs                   |   2 
crates/workspace/src/workspace.rs                   |   2 
crates/worktree/src/worktree.rs                     |  56 +++---
14 files changed, 156 insertions(+), 85 deletions(-)

Detailed changes

crates/assistant_slash_commands/src/file_command.rs 🔗

@@ -492,7 +492,7 @@ mod custom_path_matcher {
         pub fn new(globs: &[String]) -> Result<Self, globset::Error> {
             let globs = globs
                 .iter()
-                .map(|glob| Glob::new(&SanitizedPath::from(glob).to_glob_string()))
+                .map(|glob| Glob::new(&SanitizedPath::new(glob).to_glob_string()))
                 .collect::<Result<Vec<_>, _>>()?;
             let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
             let sources_with_trailing_slash = globs

crates/fs/src/fs.rs 🔗

@@ -495,7 +495,8 @@ impl Fs for RealFs {
         };
         // todo(windows)
         // When new version of `windows-rs` release, make this operation `async`
-        let path = SanitizedPath::from(path.canonicalize()?);
+        let path = path.canonicalize()?;
+        let path = SanitizedPath::new(&path);
         let path_string = path.to_string();
         let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(path_string))?.get()?;
         file.DeleteAsync(StorageDeleteOption::Default)?.get()?;
@@ -522,7 +523,8 @@ impl Fs for RealFs {
 
         // todo(windows)
         // When new version of `windows-rs` release, make this operation `async`
-        let path = SanitizedPath::from(path.canonicalize()?);
+        let path = path.canonicalize()?;
+        let path = SanitizedPath::new(&path);
         let path_string = path.to_string();
         let folder = StorageFolder::GetFolderFromPathAsync(&HSTRING::from(path_string))?.get()?;
         folder.DeleteAsync(StorageDeleteOption::Default)?.get()?;
@@ -783,7 +785,7 @@ impl Fs for RealFs {
             {
                 target = parent.join(target);
                 if let Ok(canonical) = self.canonicalize(&target).await {
-                    target = SanitizedPath::from(canonical).as_path().to_path_buf();
+                    target = SanitizedPath::new(&canonical).as_path().to_path_buf();
                 }
             }
             watcher.add(&target).ok();

crates/fs/src/fs_watcher.rs 🔗

@@ -42,7 +42,7 @@ impl Drop for FsWatcher {
 
 impl Watcher for FsWatcher {
     fn add(&self, path: &std::path::Path) -> anyhow::Result<()> {
-        let root_path = SanitizedPath::from(path);
+        let root_path = SanitizedPath::new_arc(path);
 
         let tx = self.tx.clone();
         let pending_paths = self.pending_path_events.clone();
@@ -70,7 +70,7 @@ impl Watcher for FsWatcher {
                             .paths
                             .iter()
                             .filter_map(|event_path| {
-                                let event_path = SanitizedPath::from(event_path);
+                                let event_path = SanitizedPath::new(event_path);
                                 event_path.starts_with(&root_path).then(|| PathEvent {
                                     path: event_path.as_path().to_path_buf(),
                                     kind,

crates/gpui/src/platform/windows/platform.rs 🔗

@@ -851,7 +851,7 @@ fn file_save_dialog(
     if !directory.to_string_lossy().is_empty()
         && let Some(full_path) = directory.canonicalize().log_err()
     {
-        let full_path = SanitizedPath::from(full_path);
+        let full_path = SanitizedPath::new(&full_path);
         let full_path_string = full_path.to_string();
         let path_item: IShellItem =
             unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_string), None)? };

crates/paths/src/paths.rs 🔗

@@ -63,7 +63,7 @@ pub fn set_custom_data_dir(dir: &str) -> &'static PathBuf {
             let abs_path = path
                 .canonicalize()
                 .expect("failed to canonicalize custom data directory's path to an absolute path");
-            path = PathBuf::from(util::paths::SanitizedPath::from(abs_path))
+            path = util::paths::SanitizedPath::new(&abs_path).into()
         }
         std::fs::create_dir_all(&path).expect("failed to create custom data directory");
         path

crates/project/src/git_store.rs 🔗

@@ -62,7 +62,7 @@ use std::{
 };
 use sum_tree::{Edit, SumTree, TreeSet};
 use text::{Bias, BufferId};
-use util::{ResultExt, debug_panic, post_inc};
+use util::{ResultExt, debug_panic, paths::SanitizedPath, post_inc};
 use worktree::{
     File, PathChange, PathKey, PathProgress, PathSummary, PathTarget, ProjectEntryId,
     UpdatedGitRepositoriesSet, UpdatedGitRepository, Worktree,
@@ -3234,6 +3234,7 @@ impl Repository {
         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 = SanitizedPath::new(&abs_path);
         let (worktree, relative_path) = worktree_store.find_worktree(abs_path, cx)?;
         Some(ProjectPath {
             worktree_id: worktree.read(cx).id(),

crates/project/src/lsp_store.rs 🔗

@@ -3186,7 +3186,7 @@ impl LocalLspStore {
             } else {
                 let (path, pattern) = match &watcher.glob_pattern {
                     lsp::GlobPattern::String(s) => {
-                        let watcher_path = SanitizedPath::from(s);
+                        let watcher_path = SanitizedPath::new(s);
                         let path = glob_literal_prefix(watcher_path.as_path());
                         let pattern = watcher_path
                             .as_path()
@@ -3278,7 +3278,7 @@ impl LocalLspStore {
             let worktree_root_path = tree.abs_path();
             match &watcher.glob_pattern {
                 lsp::GlobPattern::String(s) => {
-                    let watcher_path = SanitizedPath::from(s);
+                    let watcher_path = SanitizedPath::new(s);
                     let relative = watcher_path
                         .as_path()
                         .strip_prefix(&worktree_root_path)

crates/project/src/project.rs 🔗

@@ -2061,13 +2061,12 @@ impl Project {
         exclude_sub_dirs: bool,
         cx: &App,
     ) -> Option<bool> {
-        let sanitized_path = SanitizedPath::from(path);
-        let path = sanitized_path.as_path();
+        let path = SanitizedPath::new(path).as_path();
         self.worktrees(cx)
             .filter_map(|worktree| {
                 let worktree = worktree.read(cx);
                 let abs_path = worktree.as_local()?.abs_path();
-                let contains = path == abs_path
+                let contains = path == abs_path.as_ref()
                     || (path.starts_with(abs_path) && (!exclude_sub_dirs || !metadata.is_dir));
                 contains.then(|| worktree.is_visible())
             })

crates/project/src/worktree_store.rs 🔗

@@ -61,7 +61,7 @@ pub struct WorktreeStore {
     worktrees_reordered: bool,
     #[allow(clippy::type_complexity)]
     loading_worktrees:
-        HashMap<SanitizedPath, Shared<Task<Result<Entity<Worktree>, Arc<anyhow::Error>>>>>,
+        HashMap<Arc<SanitizedPath>, Shared<Task<Result<Entity<Worktree>, Arc<anyhow::Error>>>>>,
     state: WorktreeStoreState,
 }
 
@@ -153,10 +153,10 @@ impl WorktreeStore {
 
     pub fn find_worktree(
         &self,
-        abs_path: impl Into<SanitizedPath>,
+        abs_path: impl AsRef<Path>,
         cx: &App,
     ) -> Option<(Entity<Worktree>, PathBuf)> {
-        let abs_path: SanitizedPath = abs_path.into();
+        let abs_path = SanitizedPath::new(&abs_path);
         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()));
@@ -212,11 +212,11 @@ impl WorktreeStore {
 
     pub fn create_worktree(
         &mut self,
-        abs_path: impl Into<SanitizedPath>,
+        abs_path: impl AsRef<Path>,
         visible: bool,
         cx: &mut Context<Self>,
     ) -> Task<Result<Entity<Worktree>>> {
-        let abs_path: SanitizedPath = abs_path.into();
+        let abs_path: Arc<SanitizedPath> = SanitizedPath::new_arc(&abs_path);
         if !self.loading_worktrees.contains_key(&abs_path) {
             let task = match &self.state {
                 WorktreeStoreState::Remote {
@@ -227,8 +227,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.as_path().to_path_buf(), *path_style);
+                        let abs_path = RemotePathBuf::new(abs_path.to_path_buf(), *path_style);
                         self.create_ssh_worktree(upstream_client.clone(), abs_path, visible, cx)
                     }
                 }
@@ -321,15 +320,21 @@ impl WorktreeStore {
     fn create_local_worktree(
         &mut self,
         fs: Arc<dyn Fs>,
-        abs_path: impl Into<SanitizedPath>,
+        abs_path: Arc<SanitizedPath>,
         visible: bool,
         cx: &mut Context<Self>,
     ) -> Task<Result<Entity<Worktree>, Arc<anyhow::Error>>> {
         let next_entry_id = self.next_entry_id.clone();
-        let path: SanitizedPath = abs_path.into();
 
         cx.spawn(async move |this, cx| {
-            let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, cx).await;
+            let worktree = Worktree::local(
+                SanitizedPath::cast_arc(abs_path.clone()),
+                visible,
+                fs,
+                next_entry_id,
+                cx,
+            )
+            .await;
 
             let worktree = worktree?;
 
@@ -337,7 +342,7 @@ impl WorktreeStore {
 
             if visible {
                 cx.update(|cx| {
-                    cx.add_recent_document(path.as_path());
+                    cx.add_recent_document(abs_path.as_path());
                 })
                 .log_err();
             }

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

@@ -902,7 +902,7 @@ impl SshRemoteConnection {
             // On Windows, the binding needs to be set to the canonical path
             #[cfg(target_os = "windows")]
             let src =
-                SanitizedPath::from(smol::fs::canonicalize("./target").await?).to_glob_string();
+                SanitizedPath::new(&smol::fs::canonicalize("./target").await?).to_glob_string();
             #[cfg(not(target_os = "windows"))]
             let src = "./target";
             run_cmd(

crates/util/src/paths.rs 🔗

@@ -3,6 +3,7 @@ use regex::Regex;
 use serde::{Deserialize, Serialize};
 use std::cmp::Ordering;
 use std::fmt::{Display, Formatter};
+use std::mem;
 use std::path::StripPrefixError;
 use std::sync::{Arc, OnceLock};
 use std::{
@@ -99,21 +100,86 @@ 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(pub Arc<Path>);
+/// In memory, this is identical to `Path`. On non-Windows conversions to this type are no-ops. On
+/// windows, these conversions sanitize UNC paths by removing the `\\\\?\\` prefix.
+#[derive(Eq, PartialEq, Hash, Ord, PartialOrd)]
+#[repr(transparent)]
+pub struct SanitizedPath(Path);
 
 impl SanitizedPath {
-    pub fn starts_with(&self, prefix: &SanitizedPath) -> bool {
+    pub fn new<T: AsRef<Path> + ?Sized>(path: &T) -> &Self {
+        #[cfg(not(target_os = "windows"))]
+        return Self::unchecked_new(path.as_ref());
+
+        #[cfg(target_os = "windows")]
+        return Self::unchecked_new(dunce::simplified(path.as_ref()));
+    }
+
+    pub fn unchecked_new<T: AsRef<Path> + ?Sized>(path: &T) -> &Self {
+        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl
+        unsafe { mem::transmute::<&Path, &Self>(path.as_ref()) }
+    }
+
+    pub fn from_arc(path: Arc<Path>) -> Arc<Self> {
+        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl
+        #[cfg(not(target_os = "windows"))]
+        return unsafe { mem::transmute::<Arc<Path>, Arc<Self>>(path) };
+
+        // TODO: could avoid allocating here if dunce::simplified results in the same path
+        #[cfg(target_os = "windows")]
+        return Self::new(&path).into();
+    }
+
+    pub fn new_arc<T: AsRef<Path> + ?Sized>(path: &T) -> Arc<Self> {
+        Self::new(path).into()
+    }
+
+    pub fn cast_arc(path: Arc<Self>) -> Arc<Path> {
+        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl
+        unsafe { mem::transmute::<Arc<Self>, Arc<Path>>(path) }
+    }
+
+    pub fn cast_arc_ref(path: &Arc<Self>) -> &Arc<Path> {
+        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl
+        unsafe { mem::transmute::<&Arc<Self>, &Arc<Path>>(path) }
+    }
+
+    pub fn starts_with(&self, prefix: &Self) -> bool {
         self.0.starts_with(&prefix.0)
     }
 
-    pub fn as_path(&self) -> &Arc<Path> {
+    pub fn as_path(&self) -> &Path {
         &self.0
     }
 
+    pub fn file_name(&self) -> Option<&std::ffi::OsStr> {
+        self.0.file_name()
+    }
+
+    pub fn extension(&self) -> Option<&std::ffi::OsStr> {
+        self.0.extension()
+    }
+
+    pub fn join<P: AsRef<Path>>(&self, path: P) -> PathBuf {
+        self.0.join(path)
+    }
+
+    pub fn parent(&self) -> Option<&Self> {
+        self.0.parent().map(Self::unchecked_new)
+    }
+
+    pub fn strip_prefix(&self, base: &Self) -> Result<&Path, StripPrefixError> {
+        self.0.strip_prefix(base.as_path())
+    }
+
+    pub fn to_str(&self) -> Option<&str> {
+        self.0.to_str()
+    }
+
+    pub fn to_path_buf(&self) -> PathBuf {
+        self.0.to_path_buf()
+    }
+
     pub fn to_glob_string(&self) -> String {
         #[cfg(target_os = "windows")]
         {
@@ -124,13 +190,11 @@ impl SanitizedPath {
             self.0.to_string_lossy().to_string()
         }
     }
+}
 
-    pub fn join(&self, path: &Self) -> Self {
-        self.0.join(&path.0).into()
-    }
-
-    pub fn strip_prefix(&self, base: &Self) -> Result<&Path, StripPrefixError> {
-        self.0.strip_prefix(base.as_path())
+impl std::fmt::Debug for SanitizedPath {
+    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
+        std::fmt::Debug::fmt(&self.0, formatter)
     }
 }
 
@@ -140,29 +204,23 @@ impl Display for SanitizedPath {
     }
 }
 
-impl From<SanitizedPath> for Arc<Path> {
-    fn from(sanitized_path: SanitizedPath) -> Self {
-        sanitized_path.0
+impl From<&SanitizedPath> for Arc<SanitizedPath> {
+    fn from(sanitized_path: &SanitizedPath) -> Self {
+        let path: Arc<Path> = sanitized_path.0.into();
+        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl
+        unsafe { mem::transmute(path) }
     }
 }
 
-impl From<SanitizedPath> for PathBuf {
-    fn from(sanitized_path: SanitizedPath) -> Self {
-        sanitized_path.0.as_ref().into()
+impl From<&SanitizedPath> for PathBuf {
+    fn from(sanitized_path: &SanitizedPath) -> Self {
+        sanitized_path.as_path().into()
     }
 }
 
-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())
+impl AsRef<Path> for SanitizedPath {
+    fn as_ref(&self) -> &Path {
+        &self.0
     }
 }
 
@@ -1195,14 +1253,14 @@ mod tests {
     #[cfg(target_os = "windows")]
     fn test_sanitized_path() {
         let path = Path::new("C:\\Users\\someone\\test_file.rs");
-        let sanitized_path = SanitizedPath::from(path);
+        let sanitized_path = SanitizedPath::new(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);
+        let sanitized_path = SanitizedPath::new(path);
         assert_eq!(
             sanitized_path.to_string(),
             "C:\\Users\\someone\\test_file.rs"

crates/workspace/src/path_list.rs 🔗

@@ -26,7 +26,7 @@ impl PathList {
         let mut indexed_paths: Vec<(usize, PathBuf)> = paths
             .iter()
             .enumerate()
-            .map(|(ix, path)| (ix, SanitizedPath::from(path).into()))
+            .map(|(ix, path)| (ix, SanitizedPath::new(path).into()))
             .collect();
         indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b));
         let order = indexed_paths.iter().map(|e| e.0).collect::<Vec<_>>().into();

crates/workspace/src/workspace.rs 🔗

@@ -2576,7 +2576,7 @@ impl Workspace {
                 };
 
                 let this = this.clone();
-                let abs_path: Arc<Path> = SanitizedPath::from(abs_path.clone()).into();
+                let abs_path: Arc<Path> = SanitizedPath::new(&abs_path).as_path().into();
                 let fs = fs.clone();
                 let pane = pane.clone();
                 let task = cx.spawn(async move |cx| {

crates/worktree/src/worktree.rs 🔗

@@ -158,7 +158,7 @@ pub struct RemoteWorktree {
 #[derive(Clone)]
 pub struct Snapshot {
     id: WorktreeId,
-    abs_path: SanitizedPath,
+    abs_path: Arc<SanitizedPath>,
     root_name: String,
     root_char_bag: CharBag,
     entries_by_path: SumTree<Entry>,
@@ -457,7 +457,7 @@ enum ScanState {
         scanning: bool,
     },
     RootUpdated {
-        new_path: Option<SanitizedPath>,
+        new_path: Option<Arc<SanitizedPath>>,
     },
 }
 
@@ -763,8 +763,8 @@ impl Worktree {
 
     pub fn abs_path(&self) -> Arc<Path> {
         match self {
-            Worktree::Local(worktree) => worktree.abs_path.clone().into(),
-            Worktree::Remote(worktree) => worktree.abs_path.clone().into(),
+            Worktree::Local(worktree) => SanitizedPath::cast_arc(worktree.abs_path.clone()),
+            Worktree::Remote(worktree) => SanitizedPath::cast_arc(worktree.abs_path.clone()),
         }
     }
 
@@ -1813,7 +1813,7 @@ impl LocalWorktree {
                         // 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::from(abs_path.clone())),
+                            Some(SanitizedPath::new_arc(&abs_path)),
                             cx,
                         );
                         Task::ready(Ok(this.root_entry().cloned()))
@@ -2090,7 +2090,7 @@ impl LocalWorktree {
 
     fn update_abs_path_and_refresh(
         &mut self,
-        new_path: Option<SanitizedPath>,
+        new_path: Option<Arc<SanitizedPath>>,
         cx: &Context<Worktree>,
     ) {
         if let Some(new_path) = new_path {
@@ -2340,7 +2340,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.into(),
+            abs_path: SanitizedPath::from_arc(abs_path),
             root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
             root_name,
             always_included_entries: Default::default(),
@@ -2368,7 +2368,7 @@ impl Snapshot {
     //
     // 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.as_path()
+        SanitizedPath::cast_arc_ref(&self.abs_path)
     }
 
     fn build_initial_update(&self, project_id: u64, worktree_id: u64) -> proto::UpdateWorktree {
@@ -2464,7 +2464,7 @@ impl Snapshot {
         Some(removed_entry.path)
     }
 
-    fn update_abs_path(&mut self, abs_path: SanitizedPath, root_name: String) {
+    fn update_abs_path(&mut self, abs_path: Arc<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();
@@ -2483,7 +2483,7 @@ impl Snapshot {
             update.removed_entries.len()
         );
         self.update_abs_path(
-            SanitizedPath::from(PathBuf::from_proto(update.abs_path)),
+            SanitizedPath::new_arc(&PathBuf::from_proto(update.abs_path)),
             update.root_name,
         );
 
@@ -3849,7 +3849,11 @@ impl BackgroundScanner {
                     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.into(), &root_entry, &scan_job_tx);
+                state.enqueue_scan_dir(
+                    SanitizedPath::cast_arc(root_abs_path),
+                    &root_entry,
+                    &scan_job_tx,
+                );
             }
         };
 
@@ -3930,8 +3934,9 @@ 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.as_path()).await {
-            Ok(path) => SanitizedPath::from(path),
+        let root_canonical_path = self.fs.canonicalize(root_path.as_path()).await;
+        let root_canonical_path = match &root_canonical_path {
+            Ok(path) => SanitizedPath::new(path),
             Err(err) => {
                 log::error!("failed to canonicalize root path {root_path:?}: {err}");
                 return true;
@@ -3959,8 +3964,8 @@ impl BackgroundScanner {
         }
 
         self.reload_entries_for_paths(
-            root_path,
-            root_canonical_path,
+            &root_path,
+            &root_canonical_path,
             &request.relative_paths,
             abs_paths,
             None,
@@ -3972,8 +3977,9 @@ 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.as_path()).await {
-            Ok(path) => SanitizedPath::from(path),
+        let root_canonical_path = self.fs.canonicalize(root_path.as_path()).await;
+        let root_canonical_path = match &root_canonical_path {
+            Ok(path) => SanitizedPath::new(path),
             Err(err) => {
                 let new_path = self
                     .state
@@ -3982,7 +3988,7 @@ impl BackgroundScanner {
                     .root_file_handle
                     .clone()
                     .and_then(|handle| handle.current_path(&self.fs).log_err())
-                    .map(SanitizedPath::from)
+                    .map(|path| SanitizedPath::new_arc(&path))
                     .filter(|new_path| *new_path != root_path);
 
                 if let Some(new_path) = new_path.as_ref() {
@@ -4011,7 +4017,7 @@ impl BackgroundScanner {
         abs_paths.sort_unstable();
         abs_paths.dedup_by(|a, b| a.starts_with(b));
         abs_paths.retain(|abs_path| {
-            let abs_path = SanitizedPath::from(abs_path);
+            let abs_path = &SanitizedPath::new(abs_path);
 
             let snapshot = &self.state.lock().snapshot;
             {
@@ -4054,7 +4060,7 @@ impl BackgroundScanner {
                         return false;
                     };
 
-                if abs_path.0.file_name() == Some(*GITIGNORE) {
+                if abs_path.file_name() == Some(*GITIGNORE) {
                     for (_, repo) in snapshot.git_repositories.iter().filter(|(_, repo)| repo.directory_contains(&relative_path)) {
                         if !dot_git_abs_paths.iter().any(|dot_git_abs_path| dot_git_abs_path == repo.common_dir_abs_path.as_ref()) {
                             dot_git_abs_paths.push(repo.common_dir_abs_path.to_path_buf());
@@ -4093,8 +4099,8 @@ 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_canonical_path,
+            &root_path,
+            &root_canonical_path,
             &relative_paths,
             abs_paths,
             Some(scan_job_tx.clone()),
@@ -4441,8 +4447,8 @@ impl BackgroundScanner {
     /// All list arguments should be sorted before calling this function
     async fn reload_entries_for_paths(
         &self,
-        root_abs_path: SanitizedPath,
-        root_canonical_path: SanitizedPath,
+        root_abs_path: &SanitizedPath,
+        root_canonical_path: &SanitizedPath,
         relative_paths: &[Arc<Path>],
         abs_paths: Vec<PathBuf>,
         scan_queue_tx: Option<Sender<ScanJob>>,
@@ -4470,7 +4476,7 @@ impl BackgroundScanner {
                             }
                         }
 
-                        anyhow::Ok(Some((metadata, SanitizedPath::from(canonical_path))))
+                        anyhow::Ok(Some((metadata, SanitizedPath::new_arc(&canonical_path))))
                     } else {
                         Ok(None)
                     }