@@ -1775,36 +1775,54 @@ impl LocalWorktree {
};
absolutize_path
};
- let abs_path = abs_new_path.clone();
+
let fs = self.fs.clone();
+ let abs_path = abs_new_path.clone();
let case_sensitive = self.fs_case_sensitive;
- let rename = cx.background_spawn(async move {
- 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;
+ let do_rename = async move |fs: &dyn Fs, old_path: &Path, new_path: &Path, overwrite| {
fs.rename(
- &abs_old_path,
- &abs_new_path,
+ &old_path,
+ &new_path,
fs::RenameOptions {
overwrite,
- ..Default::default()
+ ..fs::RenameOptions::default()
},
)
.await
- .with_context(|| format!("Renaming {abs_old_path:?} into {abs_new_path:?}"))
+ .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.await?;
+ rename_task.await?;
Ok(this
.update(cx, |this, cx| {
let local = this.as_local_mut().unwrap();
@@ -1818,6 +1836,11 @@ impl LocalWorktree {
);
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)
}
})?
@@ -1924,6 +1924,101 @@ 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);