diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 7fb443160b..c321f8704e 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -1362,7 +1362,8 @@ impl Fs for RealFs { let trashed_entry = self .trash .lock() - .remove(trash_id) + .get(trash_id) + .cloned() .ok_or(TrashRestoreError::AlreadyRestored)?; let restored_item_path = trashed_entry.original_parent.join(&trashed_entry.name); @@ -1375,7 +1376,9 @@ impl Fs for RealFs { tx.send(res) }) .expect("The OS can spawn a threads"); + rx.await.expect("Restore all never panics")?; + self.trash.lock().remove(trash_id); Ok(restored_item_path) } } @@ -3346,7 +3349,7 @@ impl Fs for FakeFs { async fn restore(&self, trash_id: TrashId) -> Result { let mut state = self.state.lock(); - let Some((trashed_entry, fake_entry)) = state.trash.lock().remove(trash_id) else { + let Some((trashed_entry, fake_entry)) = state.trash.lock().get(trash_id).cloned() else { return Err(TrashRestoreError::AlreadyRestored); }; @@ -3366,6 +3369,7 @@ impl Fs for FakeFs { match result { Ok(_) => { + state.trash.lock().remove(trash_id); state.emit_event([(path.clone(), Some(PathEventKind::Created))]); Ok(path) } diff --git a/crates/fs/tests/integration/fs_tests.rs b/crates/fs/tests/integration/fs_tests.rs index a32c3bda84..06e9facb98 100644 --- a/crates/fs/tests/integration/fs_tests.rs +++ b/crates/fs/tests/integration/fs_tests.rs @@ -1126,3 +1126,30 @@ async fn test_realfs_watch_stress_reports_missed_paths( missed_paths.len() ); } + +#[gpui::test] +async fn restore_can_be_retried_after_collision(cx: &mut TestAppContext) { + let fs = FakeFs::new(cx.background_executor.clone()); + let path = path!("/root/a.txt"); + let remove_options = RemoveOptions::default(); + fs.insert_tree(path!("/root"), json!({ "a.txt": "original"})) + .await; + + // We'll first trash the `a.txt` file so we can hold onto its `TrashId`, + // allowing us to later attempt restoring it again, ensuring that it didn't + // get removed from the trash state, even if restoring failed. + let trash_id = fs.trash(path.as_ref(), remove_options).await.unwrap(); + + fs.insert_file(path, "conflicting".into()).await; + let err = fs.restore(trash_id).await.unwrap_err(); + assert!(matches!(err, TrashRestoreError::Collision { .. })); + + fs.remove_file(path.as_ref(), remove_options).await.unwrap(); + let restored_path = fs.restore(trash_id).await.unwrap(); + assert_eq!(fs.load(restored_path.as_path()).await.unwrap(), "original"); + + assert!(matches!( + fs.restore(trash_id).await.unwrap_err(), + TrashRestoreError::AlreadyRestored + )); +} diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 670bdd4098..1dcb28933b 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -4831,18 +4831,20 @@ impl ProjectPanel { } // update selection if let Some(entry_id) = last_succeed { - project_panel.update_in(cx, |project_panel, window, cx| { - project_panel.selection = Some(SelectedEntry { - worktree_id, - entry_id, - }); - // if only one entry was dragged and it was disambiguated, open the rename editor - if item_count == 1 && disambiguation_range.is_some() { - project_panel.rename_impl(disambiguation_range, window, cx); - } + project_panel + .update_in(cx, |project_panel, window, cx| { + project_panel.selection = Some(SelectedEntry { + worktree_id, + entry_id, + }); + // if only one entry was dragged and it was disambiguated, open the rename editor + if item_count == 1 && disambiguation_range.is_some() { + project_panel.rename_impl(disambiguation_range, window, cx); + } - project_panel.undo_manager.record(changes) - })??; + project_panel.undo_manager.record(changes) + })? + .log_err(); } std::result::Result::Ok::<(), anyhow::Error>(())