From e83c5a04bd334d1576d5d927b76ff0beb55eb15b Mon Sep 17 00:00:00 2001 From: dino Date: Tue, 25 Nov 2025 17:47:46 -0500 Subject: [PATCH 1/3] chore(fs): add create_parents to rename options - Introduce `create_parents` field to `fs::RenameOptions` - Update `fs::RealFs.rename` to ensure that the `create_parents` option is respected An update to the `fs::FakeFs.rename` function will be done in a later commit. --- crates/assistant_text_thread/src/text_thread.rs | 1 + crates/fs/src/fs.rs | 8 ++++++++ crates/project/src/agent_server_store.rs | 1 + crates/project/src/lsp_store.rs | 1 + crates/worktree/src/worktree_tests.rs | 2 ++ 5 files changed, 13 insertions(+) diff --git a/crates/assistant_text_thread/src/text_thread.rs b/crates/assistant_text_thread/src/text_thread.rs index 613c9b862e8a0b..7f24c8f665f8d3 100644 --- a/crates/assistant_text_thread/src/text_thread.rs +++ b/crates/assistant_text_thread/src/text_thread.rs @@ -2933,6 +2933,7 @@ impl TextThread { RenameOptions { overwrite: true, ignore_if_exists: true, + create_parents: false, }, ) .await?; diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 93192ecd2bd244..e3e82ac819a2c8 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -192,6 +192,8 @@ pub struct CopyOptions { pub struct RenameOptions { pub overwrite: bool, pub ignore_if_exists: bool, + /// Whether to create parent directories if they do not exist. + pub create_parents: bool, } #[derive(Copy, Clone, Default)] @@ -577,6 +579,12 @@ impl Fs for RealFs { } } + if options.create_parents { + if let Some(parent) = target.parent() { + self.create_dir(parent).await?; + } + } + smol::fs::rename(source, target).await?; Ok(()) } diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs index d6bd83531eda51..ef12e222009a59 100644 --- a/crates/project/src/agent_server_store.rs +++ b/crates/project/src/agent_server_store.rs @@ -1089,6 +1089,7 @@ async fn download_latest_version( RenameOptions { ignore_if_exists: true, overwrite: true, + create_parents: false, }, ) .await?; diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 4f7022a264db18..0e0ae0b9c4aa6d 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3028,6 +3028,7 @@ impl LocalLspStore { .map(|options| fs::RenameOptions { overwrite: options.overwrite.unwrap_or(false), ignore_if_exists: options.ignore_if_exists.unwrap_or(false), + create_parents: true, }) .unwrap_or_default(), ) diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index e8d98b3508bd14..50e2c6acae0013 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -379,6 +379,7 @@ async fn test_renaming_case_only(cx: &mut TestAppContext) { fs::RenameOptions { overwrite: true, ignore_if_exists: true, + create_parents: false, }, ) .await @@ -1986,6 +1987,7 @@ async fn randomly_mutate_fs( fs::RenameOptions { overwrite: true, ignore_if_exists: true, + create_parents: false, }, ) .await From c78f45243f78a6f9656b18d9ca1820c70767c3d2 Mon Sep 17 00:00:00 2001 From: dino Date: Tue, 25 Nov 2025 17:53:30 -0500 Subject: [PATCH 2/3] fix(lsp): ensure parent directory creation on rename Update the `fs::RenameOptions` used by `project::lsp_store::LocalLspStore.deserialize_workspace_edit` in order to always set `create_parents` to `true`. Doing this ensures that we'll always create the folders for the new file path provided by the language server instead of failing to handle the request in case the parent directory doesn't exist. --- crates/project/src/lsp_store.rs | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 0e0ae0b9c4aa6d..a69d2553692277 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3021,18 +3021,23 @@ impl LocalLspStore { .new_uri .to_file_path() .map_err(|()| anyhow!("can't convert URI to path"))?; - fs.rename( - &source_abs_path, - &target_abs_path, - op.options - .map(|options| fs::RenameOptions { - overwrite: options.overwrite.unwrap_or(false), - ignore_if_exists: options.ignore_if_exists.unwrap_or(false), - create_parents: true, - }) - .unwrap_or_default(), - ) - .await?; + + let options = fs::RenameOptions { + overwrite: op + .options + .as_ref() + .and_then(|options| options.overwrite) + .unwrap_or(false), + ignore_if_exists: op + .options + .as_ref() + .and_then(|options| options.ignore_if_exists) + .unwrap_or(false), + create_parents: true, + }; + + fs.rename(&source_abs_path, &target_abs_path, options) + .await?; } lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Delete(op)) => { From e7c8c361a031cd1d9a64c6bf40314a5b606dfa68 Mon Sep 17 00:00:00 2001 From: dino Date: Tue, 25 Nov 2025 18:27:29 -0500 Subject: [PATCH 3/3] test(fs): add test for create_parents option in rename --- crates/fs/src/fs.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index e3e82ac819a2c8..f25c25f21da09b 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -2356,6 +2356,12 @@ impl Fs for FakeFs { let old_path = normalize_path(old_path); let new_path = normalize_path(new_path); + if options.create_parents { + if let Some(parent) = new_path.parent() { + self.create_dir(parent).await?; + } + } + let mut state = self.state.lock(); let moved_entry = state.write_path(&old_path, |e| { if let btree_map::Entry::Occupied(e) = e { @@ -3393,4 +3399,63 @@ mod tests { let content = std::fs::read_to_string(&file_to_be_replaced).unwrap(); assert_eq!(content, "Hello"); } + + #[gpui::test] + async fn test_rename(executor: BackgroundExecutor) { + let fs = FakeFs::new(executor.clone()); + fs.insert_tree( + path!("/root"), + json!({ + "src": { + "file_a.txt": "content a", + "file_b.txt": "content b" + } + }), + ) + .await; + + fs.rename( + Path::new(path!("/root/src/file_a.txt")), + Path::new(path!("/root/src/new/renamed_a.txt")), + RenameOptions { + create_parents: true, + ..Default::default() + }, + ) + .await + .unwrap(); + + // Assert that the `file_a.txt` file was being renamed and moved to a + // different directory that did not exist before. + assert_eq!( + fs.files(), + vec![ + PathBuf::from(path!("/root/src/file_b.txt")), + PathBuf::from(path!("/root/src/new/renamed_a.txt")), + ] + ); + + let result = fs + .rename( + Path::new(path!("/root/src/file_b.txt")), + Path::new(path!("/root/src/old/renamed_b.txt")), + RenameOptions { + create_parents: false, + ..Default::default() + }, + ) + .await; + + // Assert that the `file_b.txt` file was not renamed nor moved, as + // `create_parents` was set to `false`. + // different directory that did not exist before. + assert!(result.is_err()); + assert_eq!( + fs.files(), + vec![ + PathBuf::from(path!("/root/src/file_b.txt")), + PathBuf::from(path!("/root/src/new/renamed_a.txt")), + ] + ); + } }