Fix case-only renaming of files in project panel (#7835)

Thorsten Ball created

This is a follow-up to #7768 but now also fixes #5211.

Explanation is relatively simple: case-only renames previously failed
because while Zed would think that `foobar` and `FOOBAR` are different,
the filesystem would give us an file-already-exists error when renaming.

So what we're doing here is to check whether we're on a case-insensitive
filesystem and if so, we overwrite the old file.

Release Notes:

- Fixed case-only renaming of files in project panel.
([#5211](https://github.com/zed-industries/zed/issues/5211)).

Proof:



https://github.com/zed-industries/zed/assets/1185253/57d5063f-09d9-47b1-a2df-3d7edefca97d

Change summary

crates/project/src/worktree.rs | 33 +++++++++++++++++++++++++++++++--
1 file changed, 31 insertions(+), 2 deletions(-)

Detailed changes

crates/project/src/worktree.rs 🔗

@@ -93,6 +93,7 @@ pub struct LocalWorktree {
     diagnostic_summaries: HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>,
     client: Arc<Client>,
     fs: Arc<dyn Fs>,
+    fs_case_sensitive: bool,
     visible: bool,
 }
 
@@ -314,6 +315,13 @@ impl Worktree {
             .await
             .context("failed to stat worktree path")?;
 
+        let fs_case_sensitive = fs.is_case_sensitive().await.unwrap_or_else(|e| {
+            log::error!(
+                "Failed to determine whether filesystem is case sensitive (falling back to true) due to error: {e:#}"
+            );
+            true
+        });
+
         let closure_fs = Arc::clone(&fs);
         let closure_next_entry_id = Arc::clone(&next_entry_id);
         let closure_abs_path = abs_path.to_path_buf();
@@ -435,6 +443,7 @@ impl Worktree {
                 diagnostic_summaries: Default::default(),
                 client,
                 fs,
+                fs_case_sensitive,
                 visible,
             })
         })
@@ -1301,9 +1310,29 @@ impl LocalWorktree {
         let abs_old_path = self.absolutize(&old_path);
         let abs_new_path = self.absolutize(&new_path);
         let fs = self.fs.clone();
+        let case_sensitive = self.fs_case_sensitive;
         let rename = cx.background_executor().spawn(async move {
-            fs.rename(&abs_old_path?, &abs_new_path?, Default::default())
-                .await
+            let abs_old_path = abs_old_path?;
+            let abs_new_path = abs_new_path?;
+
+            let abs_old_path_lower = abs_old_path.to_str().map(|p| p.to_lowercase());
+            let abs_new_path_lower = abs_new_path.to_str().map(|p| p.to_lowercase());
+
+            // 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_lower == abs_new_path_lower;
+
+            fs.rename(
+                &abs_old_path,
+                &abs_new_path,
+                fs::RenameOptions {
+                    overwrite,
+                    ..Default::default()
+                },
+            )
+            .await
         });
 
         cx.spawn(|this, mut cx| async move {