fs: Bring back copy paste again (#25543)

张小白 created

Closes #25317

cc @0xtimsb 

Release Notes:

- N/A

Change summary

crates/fs/src/fs.rs                   | 182 ++++++++++++++++++++++++++++
crates/project/src/project_tests.rs   |  16 +-
crates/worktree/src/worktree_tests.rs |   6 
3 files changed, 189 insertions(+), 15 deletions(-)

Detailed changes

crates/fs/src/fs.rs 🔗

@@ -1337,7 +1337,10 @@ impl FakeFs {
     pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
         let mut result = Vec::new();
         let mut queue = collections::VecDeque::new();
-        queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
+        queue.push_back((
+            PathBuf::from(util::path!("/")),
+            self.state.lock().root.clone(),
+        ));
         while let Some((path, entry)) = queue.pop_front() {
             if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
                 for (name, entry) in entries {
@@ -1358,7 +1361,10 @@ impl FakeFs {
     pub fn directories(&self, include_dot_git: bool) -> Vec<PathBuf> {
         let mut result = Vec::new();
         let mut queue = collections::VecDeque::new();
-        queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
+        queue.push_back((
+            PathBuf::from(util::path!("/")),
+            self.state.lock().root.clone(),
+        ));
         while let Some((path, entry)) = queue.pop_front() {
             if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
                 for (name, entry) in entries {
@@ -2020,7 +2026,11 @@ pub async fn copy_recursive<'a>(
         let Ok(item_relative_path) = item.strip_prefix(source) else {
             continue;
         };
-        let target_item = target.join(item_relative_path);
+        let target_item = if item_relative_path == Path::new("") {
+            target.to_path_buf()
+        } else {
+            target.join(item_relative_path)
+        };
         if is_dir {
             if !options.overwrite && fs.metadata(&target_item).await.is_ok_and(|m| m.is_some()) {
                 if options.ignore_if_exists {
@@ -2174,6 +2184,142 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_copy_recursive_with_single_file(executor: BackgroundExecutor) {
+        let fs = FakeFs::new(executor.clone());
+        fs.insert_tree(
+            path!("/outer"),
+            json!({
+                "a": "A",
+                "b": "B",
+                "inner": {}
+            }),
+        )
+        .await;
+
+        assert_eq!(
+            fs.files(),
+            vec![
+                PathBuf::from(path!("/outer/a")),
+                PathBuf::from(path!("/outer/b")),
+            ]
+        );
+
+        let source = Path::new(path!("/outer/a"));
+        let target = Path::new(path!("/outer/a copy"));
+        copy_recursive(fs.as_ref(), source, target, Default::default())
+            .await
+            .unwrap();
+
+        assert_eq!(
+            fs.files(),
+            vec![
+                PathBuf::from(path!("/outer/a")),
+                PathBuf::from(path!("/outer/a copy")),
+                PathBuf::from(path!("/outer/b")),
+            ]
+        );
+
+        let source = Path::new(path!("/outer/a"));
+        let target = Path::new(path!("/outer/inner/a copy"));
+        copy_recursive(fs.as_ref(), source, target, Default::default())
+            .await
+            .unwrap();
+
+        assert_eq!(
+            fs.files(),
+            vec![
+                PathBuf::from(path!("/outer/a")),
+                PathBuf::from(path!("/outer/a copy")),
+                PathBuf::from(path!("/outer/b")),
+                PathBuf::from(path!("/outer/inner/a copy")),
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_copy_recursive_with_single_dir(executor: BackgroundExecutor) {
+        let fs = FakeFs::new(executor.clone());
+        fs.insert_tree(
+            path!("/outer"),
+            json!({
+                "a": "A",
+                "empty": {},
+                "non-empty": {
+                    "b": "B",
+                }
+            }),
+        )
+        .await;
+
+        assert_eq!(
+            fs.files(),
+            vec![
+                PathBuf::from(path!("/outer/a")),
+                PathBuf::from(path!("/outer/non-empty/b")),
+            ]
+        );
+        assert_eq!(
+            fs.directories(false),
+            vec![
+                PathBuf::from(path!("/")),
+                PathBuf::from(path!("/outer")),
+                PathBuf::from(path!("/outer/empty")),
+                PathBuf::from(path!("/outer/non-empty")),
+            ]
+        );
+
+        let source = Path::new(path!("/outer/empty"));
+        let target = Path::new(path!("/outer/empty copy"));
+        copy_recursive(fs.as_ref(), source, target, Default::default())
+            .await
+            .unwrap();
+
+        assert_eq!(
+            fs.files(),
+            vec![
+                PathBuf::from(path!("/outer/a")),
+                PathBuf::from(path!("/outer/non-empty/b")),
+            ]
+        );
+        assert_eq!(
+            fs.directories(false),
+            vec![
+                PathBuf::from(path!("/")),
+                PathBuf::from(path!("/outer")),
+                PathBuf::from(path!("/outer/empty")),
+                PathBuf::from(path!("/outer/empty copy")),
+                PathBuf::from(path!("/outer/non-empty")),
+            ]
+        );
+
+        let source = Path::new(path!("/outer/non-empty"));
+        let target = Path::new(path!("/outer/non-empty copy"));
+        copy_recursive(fs.as_ref(), source, target, Default::default())
+            .await
+            .unwrap();
+
+        assert_eq!(
+            fs.files(),
+            vec![
+                PathBuf::from(path!("/outer/a")),
+                PathBuf::from(path!("/outer/non-empty/b")),
+                PathBuf::from(path!("/outer/non-empty copy/b")),
+            ]
+        );
+        assert_eq!(
+            fs.directories(false),
+            vec![
+                PathBuf::from(path!("/")),
+                PathBuf::from(path!("/outer")),
+                PathBuf::from(path!("/outer/empty")),
+                PathBuf::from(path!("/outer/empty copy")),
+                PathBuf::from(path!("/outer/non-empty")),
+                PathBuf::from(path!("/outer/non-empty copy")),
+            ]
+        );
+    }
+
     #[gpui::test]
     async fn test_copy_recursive(executor: BackgroundExecutor) {
         let fs = FakeFs::new(executor.clone());
@@ -2185,7 +2331,8 @@ mod tests {
                     "b": "B",
                     "inner3": {
                         "d": "D",
-                    }
+                    },
+                    "inner4": {}
                 },
                 "inner2": {
                     "c": "C",
@@ -2203,6 +2350,17 @@ mod tests {
                 PathBuf::from(path!("/outer/inner1/inner3/d")),
             ]
         );
+        assert_eq!(
+            fs.directories(false),
+            vec![
+                PathBuf::from(path!("/")),
+                PathBuf::from(path!("/outer")),
+                PathBuf::from(path!("/outer/inner1")),
+                PathBuf::from(path!("/outer/inner2")),
+                PathBuf::from(path!("/outer/inner1/inner3")),
+                PathBuf::from(path!("/outer/inner1/inner4")),
+            ]
+        );
 
         let source = Path::new(path!("/outer"));
         let target = Path::new(path!("/outer/inner1/outer"));
@@ -2223,6 +2381,22 @@ mod tests {
                 PathBuf::from(path!("/outer/inner1/outer/inner1/inner3/d")),
             ]
         );
+        assert_eq!(
+            fs.directories(false),
+            vec![
+                PathBuf::from(path!("/")),
+                PathBuf::from(path!("/outer")),
+                PathBuf::from(path!("/outer/inner1")),
+                PathBuf::from(path!("/outer/inner2")),
+                PathBuf::from(path!("/outer/inner1/inner3")),
+                PathBuf::from(path!("/outer/inner1/inner4")),
+                PathBuf::from(path!("/outer/inner1/outer")),
+                PathBuf::from(path!("/outer/inner1/outer/inner1")),
+                PathBuf::from(path!("/outer/inner1/outer/inner2")),
+                PathBuf::from(path!("/outer/inner1/outer/inner1/inner3")),
+                PathBuf::from(path!("/outer/inner1/outer/inner1/inner4")),
+            ]
+        );
     }
 
     #[gpui::test]

crates/project/src/project_tests.rs 🔗

@@ -4958,14 +4958,14 @@ async fn test_create_entry(cx: &mut gpui::TestAppContext) {
     assert_eq!(
         fs.paths(true),
         vec![
-            PathBuf::from("/"),
-            PathBuf::from("/one"),
-            PathBuf::from("/one/two"),
-            PathBuf::from("/one/two/c.rs"),
-            PathBuf::from("/one/two/three"),
-            PathBuf::from("/one/two/three/a.txt"),
-            PathBuf::from("/one/two/three/b.."),
-            PathBuf::from("/one/two/three/four"),
+            PathBuf::from(path!("/")),
+            PathBuf::from(path!("/one")),
+            PathBuf::from(path!("/one/two")),
+            PathBuf::from(path!("/one/two/c.rs")),
+            PathBuf::from(path!("/one/two/three")),
+            PathBuf::from(path!("/one/two/three/a.txt")),
+            PathBuf::from(path!("/one/two/three/b..")),
+            PathBuf::from(path!("/one/two/three/four")),
         ]
     );
 

crates/worktree/src/worktree_tests.rs 🔗

@@ -26,7 +26,7 @@ use std::{
     sync::Arc,
     time::Duration,
 };
-use util::{test::TempTree, ResultExt};
+use util::{path, test::TempTree, ResultExt};
 
 #[gpui::test]
 async fn test_traversal(cx: &mut TestAppContext) {
@@ -1650,7 +1650,7 @@ async fn test_random_worktree_operations_during_initial_scan(
         .map(|o| o.parse().unwrap())
         .unwrap_or(20);
 
-    let root_dir = Path::new("/test");
+    let root_dir = Path::new(path!("/test"));
     let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
     fs.as_fake().insert_tree(root_dir, json!({})).await;
     for _ in 0..initial_entries {
@@ -1741,7 +1741,7 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng)
         .map(|o| o.parse().unwrap())
         .unwrap_or(20);
 
-    let root_dir = Path::new("/test");
+    let root_dir = Path::new(path!("/test"));
     let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
     fs.as_fake().insert_tree(root_dir, json!({})).await;
     for _ in 0..initial_entries {