Use `gpui::test` in async tests in zed

Antonio Scandurra and Max Brunsfeld created

Co-Authored-By: Max Brunsfeld <max@zed.dev>

Change summary

zed/src/file_finder.rs | 382 +++++++++++++--------------
zed/src/workspace.rs   | 589 +++++++++++++++++++++----------------------
zed/src/worktree.rs    | 410 ++++++++++++++---------------
3 files changed, 674 insertions(+), 707 deletions(-)

Detailed changes

zed/src/file_finder.rs 🔗

@@ -453,220 +453,208 @@ impl FileFinder {
 mod tests {
     use super::*;
     use crate::{editor, settings, test::temp_tree, workspace::Workspace};
-    use gpui::App;
     use serde_json::json;
     use std::fs;
     use tempdir::TempDir;
 
-    #[test]
-    fn test_matching_paths() {
-        App::test_async((), |mut app| async move {
-            let tmp_dir = TempDir::new("example").unwrap();
-            fs::create_dir(tmp_dir.path().join("a")).unwrap();
-            fs::write(tmp_dir.path().join("a/banana"), "banana").unwrap();
-            fs::write(tmp_dir.path().join("a/bandana"), "bandana").unwrap();
-            app.update(|ctx| {
-                super::init(ctx);
-                editor::init(ctx);
-            });
-
-            let settings = settings::channel(&app.font_cache()).unwrap().1;
-            let (window_id, workspace) = app.add_window(|ctx| {
-                let mut workspace = Workspace::new(0, settings, ctx);
-                workspace.add_worktree(tmp_dir.path(), ctx);
-                workspace
-            });
-            app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
-                .await;
-            app.dispatch_action(
-                window_id,
-                vec![workspace.id()],
-                "file_finder:toggle".into(),
-                (),
-            );
+    #[gpui::test]
+    async fn test_matching_paths(mut app: gpui::TestAppContext) {
+        let tmp_dir = TempDir::new("example").unwrap();
+        fs::create_dir(tmp_dir.path().join("a")).unwrap();
+        fs::write(tmp_dir.path().join("a/banana"), "banana").unwrap();
+        fs::write(tmp_dir.path().join("a/bandana"), "bandana").unwrap();
+        app.update(|ctx| {
+            super::init(ctx);
+            editor::init(ctx);
+        });
 
-            let finder = app.read(|ctx| {
-                workspace
-                    .read(ctx)
-                    .modal()
-                    .cloned()
-                    .unwrap()
-                    .downcast::<FileFinder>()
-                    .unwrap()
-            });
-            let query_buffer = app.read(|ctx| finder.read(ctx).query_buffer.clone());
-
-            let chain = vec![finder.id(), query_buffer.id()];
-            app.dispatch_action(window_id, chain.clone(), "buffer:insert", "b".to_string());
-            app.dispatch_action(window_id, chain.clone(), "buffer:insert", "n".to_string());
-            app.dispatch_action(window_id, chain.clone(), "buffer:insert", "a".to_string());
-            finder
-                .condition(&app, |finder, _| finder.matches.len() == 2)
-                .await;
-
-            let active_pane = app.read(|ctx| workspace.read(ctx).active_pane().clone());
-            app.dispatch_action(
-                window_id,
-                vec![workspace.id(), finder.id()],
-                "menu:select_next",
-                (),
-            );
-            app.dispatch_action(
-                window_id,
-                vec![workspace.id(), finder.id()],
-                "file_finder:confirm",
-                (),
-            );
-            active_pane
-                .condition(&app, |pane, _| pane.active_item().is_some())
-                .await;
-            app.read(|ctx| {
-                let active_item = active_pane.read(ctx).active_item().unwrap();
-                assert_eq!(active_item.title(ctx), "bandana");
-            });
+        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let (window_id, workspace) = app.add_window(|ctx| {
+            let mut workspace = Workspace::new(0, settings, ctx);
+            workspace.add_worktree(tmp_dir.path(), ctx);
+            workspace
+        });
+        app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
+            .await;
+        app.dispatch_action(
+            window_id,
+            vec![workspace.id()],
+            "file_finder:toggle".into(),
+            (),
+        );
+
+        let finder = app.read(|ctx| {
+            workspace
+                .read(ctx)
+                .modal()
+                .cloned()
+                .unwrap()
+                .downcast::<FileFinder>()
+                .unwrap()
+        });
+        let query_buffer = app.read(|ctx| finder.read(ctx).query_buffer.clone());
+
+        let chain = vec![finder.id(), query_buffer.id()];
+        app.dispatch_action(window_id, chain.clone(), "buffer:insert", "b".to_string());
+        app.dispatch_action(window_id, chain.clone(), "buffer:insert", "n".to_string());
+        app.dispatch_action(window_id, chain.clone(), "buffer:insert", "a".to_string());
+        finder
+            .condition(&app, |finder, _| finder.matches.len() == 2)
+            .await;
+
+        let active_pane = app.read(|ctx| workspace.read(ctx).active_pane().clone());
+        app.dispatch_action(
+            window_id,
+            vec![workspace.id(), finder.id()],
+            "menu:select_next",
+            (),
+        );
+        app.dispatch_action(
+            window_id,
+            vec![workspace.id(), finder.id()],
+            "file_finder:confirm",
+            (),
+        );
+        active_pane
+            .condition(&app, |pane, _| pane.active_item().is_some())
+            .await;
+        app.read(|ctx| {
+            let active_item = active_pane.read(ctx).active_item().unwrap();
+            assert_eq!(active_item.title(ctx), "bandana");
         });
     }
 
-    #[test]
-    fn test_matching_cancellation() {
-        App::test_async((), |mut app| async move {
-            let tmp_dir = temp_tree(json!({
-                "hello": "",
-                "goodbye": "",
-                "halogen-light": "",
-                "happiness": "",
-                "height": "",
-                "hi": "",
-                "hiccup": "",
-            }));
-            let settings = settings::channel(&app.font_cache()).unwrap().1;
-            let (_, workspace) = app.add_window(|ctx| {
-                let mut workspace = Workspace::new(0, settings.clone(), ctx);
-                workspace.add_worktree(tmp_dir.path(), ctx);
-                workspace
-            });
-            app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
-                .await;
-            let (_, finder) =
-                app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
-
-            let query = "hi".to_string();
-            finder.update(&mut app, |f, ctx| f.spawn_search(query.clone(), ctx));
-            finder.condition(&app, |f, _| f.matches.len() == 5).await;
-
-            finder.update(&mut app, |finder, ctx| {
-                let matches = finder.matches.clone();
-
-                // Simulate a search being cancelled after the time limit,
-                // returning only a subset of the matches that would have been found.
-                finder.spawn_search(query.clone(), ctx);
-                finder.update_matches(
-                    (
-                        finder.latest_search_id,
-                        true, // did-cancel
-                        query.clone(),
-                        vec![matches[1].clone(), matches[3].clone()],
-                    ),
-                    ctx,
-                );
-
-                // Simulate another cancellation.
-                finder.spawn_search(query.clone(), ctx);
-                finder.update_matches(
-                    (
-                        finder.latest_search_id,
-                        true, // did-cancel
-                        query.clone(),
-                        vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
-                    ),
-                    ctx,
-                );
+    #[gpui::test]
+    async fn test_matching_cancellation(mut app: gpui::TestAppContext) {
+        let tmp_dir = temp_tree(json!({
+            "hello": "",
+            "goodbye": "",
+            "halogen-light": "",
+            "happiness": "",
+            "height": "",
+            "hi": "",
+            "hiccup": "",
+        }));
+        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let (_, workspace) = app.add_window(|ctx| {
+            let mut workspace = Workspace::new(0, settings.clone(), ctx);
+            workspace.add_worktree(tmp_dir.path(), ctx);
+            workspace
+        });
+        app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
+            .await;
+        let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
+
+        let query = "hi".to_string();
+        finder.update(&mut app, |f, ctx| f.spawn_search(query.clone(), ctx));
+        finder.condition(&app, |f, _| f.matches.len() == 5).await;
+
+        finder.update(&mut app, |finder, ctx| {
+            let matches = finder.matches.clone();
+
+            // Simulate a search being cancelled after the time limit,
+            // returning only a subset of the matches that would have been found.
+            finder.spawn_search(query.clone(), ctx);
+            finder.update_matches(
+                (
+                    finder.latest_search_id,
+                    true, // did-cancel
+                    query.clone(),
+                    vec![matches[1].clone(), matches[3].clone()],
+                ),
+                ctx,
+            );
 
-                assert_eq!(finder.matches, matches[0..4])
-            });
+            // Simulate another cancellation.
+            finder.spawn_search(query.clone(), ctx);
+            finder.update_matches(
+                (
+                    finder.latest_search_id,
+                    true, // did-cancel
+                    query.clone(),
+                    vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
+                ),
+                ctx,
+            );
+
+            assert_eq!(finder.matches, matches[0..4])
         });
     }
 
-    #[test]
-    fn test_single_file_worktrees() {
-        App::test_async((), |mut app| async move {
-            let temp_dir = TempDir::new("test-single-file-worktrees").unwrap();
-            let dir_path = temp_dir.path().join("the-parent-dir");
-            let file_path = dir_path.join("the-file");
-            fs::create_dir(&dir_path).unwrap();
-            fs::write(&file_path, "").unwrap();
-
-            let settings = settings::channel(&app.font_cache()).unwrap().1;
-            let (_, workspace) = app.add_window(|ctx| {
-                let mut workspace = Workspace::new(0, settings.clone(), ctx);
-                workspace.add_worktree(&file_path, ctx);
-                workspace
-            });
-            app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
-                .await;
-            let (_, finder) =
-                app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
-
-            // Even though there is only one worktree, that worktree's filename
-            // is included in the matching, because the worktree is a single file.
-            finder.update(&mut app, |f, ctx| f.spawn_search("thf".into(), ctx));
-            finder.condition(&app, |f, _| f.matches.len() == 1).await;
-
-            app.read(|ctx| {
-                let finder = finder.read(ctx);
-                let (file_name, file_name_positions, full_path, full_path_positions) =
-                    finder.labels_for_match(&finder.matches[0], ctx).unwrap();
-
-                assert_eq!(file_name, "the-file");
-                assert_eq!(file_name_positions, &[0, 1, 4]);
-                assert_eq!(full_path, "the-file");
-                assert_eq!(full_path_positions, &[0, 1, 4]);
-            });
-
-            // Since the worktree root is a file, searching for its name followed by a slash does
-            // not match anything.
-            finder.update(&mut app, |f, ctx| f.spawn_search("thf/".into(), ctx));
-            finder.condition(&app, |f, _| f.matches.len() == 0).await;
+    #[gpui::test]
+    async fn test_single_file_worktrees(mut app: gpui::TestAppContext) {
+        let temp_dir = TempDir::new("test-single-file-worktrees").unwrap();
+        let dir_path = temp_dir.path().join("the-parent-dir");
+        let file_path = dir_path.join("the-file");
+        fs::create_dir(&dir_path).unwrap();
+        fs::write(&file_path, "").unwrap();
+
+        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let (_, workspace) = app.add_window(|ctx| {
+            let mut workspace = Workspace::new(0, settings.clone(), ctx);
+            workspace.add_worktree(&file_path, ctx);
+            workspace
         });
+        app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
+            .await;
+        let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
+
+        // Even though there is only one worktree, that worktree's filename
+        // is included in the matching, because the worktree is a single file.
+        finder.update(&mut app, |f, ctx| f.spawn_search("thf".into(), ctx));
+        finder.condition(&app, |f, _| f.matches.len() == 1).await;
+
+        app.read(|ctx| {
+            let finder = finder.read(ctx);
+            let (file_name, file_name_positions, full_path, full_path_positions) =
+                finder.labels_for_match(&finder.matches[0], ctx).unwrap();
+
+            assert_eq!(file_name, "the-file");
+            assert_eq!(file_name_positions, &[0, 1, 4]);
+            assert_eq!(full_path, "the-file");
+            assert_eq!(full_path_positions, &[0, 1, 4]);
+        });
+
+        // Since the worktree root is a file, searching for its name followed by a slash does
+        // not match anything.
+        finder.update(&mut app, |f, ctx| f.spawn_search("thf/".into(), ctx));
+        finder.condition(&app, |f, _| f.matches.len() == 0).await;
     }
 
-    #[test]
-    fn test_multiple_matches_with_same_relative_path() {
-        App::test_async((), |mut app| async move {
-            let tmp_dir = temp_tree(json!({
-                "dir1": { "a.txt": "" },
-                "dir2": { "a.txt": "" }
-            }));
-            let settings = settings::channel(&app.font_cache()).unwrap().1;
+    #[gpui::test]
+    async fn test_multiple_matches_with_same_relative_path(mut app: gpui::TestAppContext) {
+        let tmp_dir = temp_tree(json!({
+            "dir1": { "a.txt": "" },
+            "dir2": { "a.txt": "" }
+        }));
+        let settings = settings::channel(&app.font_cache()).unwrap().1;
 
-            let (_, workspace) = app.add_window(|ctx| Workspace::new(0, settings.clone(), ctx));
+        let (_, workspace) = app.add_window(|ctx| Workspace::new(0, settings.clone(), ctx));
 
-            workspace
-                .update(&mut app, |workspace, ctx| {
-                    workspace.open_paths(
-                        &[tmp_dir.path().join("dir1"), tmp_dir.path().join("dir2")],
-                        ctx,
-                    )
-                })
-                .await;
-            app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
-                .await;
-
-            let (_, finder) =
-                app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
-
-            // Run a search that matches two files with the same relative path.
-            finder.update(&mut app, |f, ctx| f.spawn_search("a.t".into(), ctx));
-            finder.condition(&app, |f, _| f.matches.len() == 2).await;
-
-            // Can switch between different matches with the same relative path.
-            finder.update(&mut app, |f, ctx| {
-                assert_eq!(f.selected_index(), 0);
-                f.select_next(&(), ctx);
-                assert_eq!(f.selected_index(), 1);
-                f.select_prev(&(), ctx);
-                assert_eq!(f.selected_index(), 0);
-            });
+        workspace
+            .update(&mut app, |workspace, ctx| {
+                workspace.open_paths(
+                    &[tmp_dir.path().join("dir1"), tmp_dir.path().join("dir2")],
+                    ctx,
+                )
+            })
+            .await;
+        app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
+            .await;
+
+        let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
+
+        // Run a search that matches two files with the same relative path.
+        finder.update(&mut app, |f, ctx| f.spawn_search("a.t".into(), ctx));
+        finder.condition(&app, |f, _| f.matches.len() == 2).await;
+
+        // Can switch between different matches with the same relative path.
+        finder.update(&mut app, |f, ctx| {
+            assert_eq!(f.selected_index(), 0);
+            f.select_next(&(), ctx);
+            assert_eq!(f.selected_index(), 1);
+            f.select_prev(&(), ctx);
+            assert_eq!(f.selected_index(), 0);
         });
     }
 }

zed/src/workspace.rs 🔗

@@ -729,7 +729,6 @@ impl WorkspaceHandle for ViewHandle<Workspace> {
 mod tests {
     use super::*;
     use crate::{editor::BufferView, settings, test::temp_tree};
-    use gpui::App;
     use serde_json::json;
     use std::collections::HashSet;
     use tempdir::TempDir;
@@ -793,332 +792,324 @@ mod tests {
         assert_eq!(app.window_ids().count(), 2);
     }
 
-    #[test]
-    fn test_open_entry() {
-        App::test_async((), |mut app| async move {
-            let dir = temp_tree(json!({
-                "a": {
-                    "file1": "contents 1",
-                    "file2": "contents 2",
-                    "file3": "contents 3",
-                },
-            }));
-
-            let settings = settings::channel(&app.font_cache()).unwrap().1;
-
-            let (_, workspace) = app.add_window(|ctx| {
-                let mut workspace = Workspace::new(0, settings, ctx);
-                workspace.add_worktree(dir.path(), ctx);
-                workspace
-            });
-
-            app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
-                .await;
-            let entries = app.read(|ctx| workspace.file_entries(ctx));
-            let file1 = entries[0].clone();
-            let file2 = entries[1].clone();
-            let file3 = entries[2].clone();
+    #[gpui::test]
+    async fn test_open_entry(mut app: gpui::TestAppContext) {
+        let dir = temp_tree(json!({
+            "a": {
+                "file1": "contents 1",
+                "file2": "contents 2",
+                "file3": "contents 3",
+            },
+        }));
 
-            // Open the first entry
-            workspace
-                .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx))
-                .unwrap()
-                .await;
-            app.read(|ctx| {
-                let pane = workspace.read(ctx).active_pane().read(ctx);
-                assert_eq!(
-                    pane.active_item().unwrap().entry_id(ctx),
-                    Some(file1.clone())
-                );
-                assert_eq!(pane.items().len(), 1);
-            });
+        let settings = settings::channel(&app.font_cache()).unwrap().1;
 
-            // Open the second entry
+        let (_, workspace) = app.add_window(|ctx| {
+            let mut workspace = Workspace::new(0, settings, ctx);
+            workspace.add_worktree(dir.path(), ctx);
             workspace
-                .update(&mut app, |w, ctx| w.open_entry(file2.clone(), ctx))
-                .unwrap()
-                .await;
-            app.read(|ctx| {
-                let pane = workspace.read(ctx).active_pane().read(ctx);
-                assert_eq!(
-                    pane.active_item().unwrap().entry_id(ctx),
-                    Some(file2.clone())
-                );
-                assert_eq!(pane.items().len(), 2);
-            });
-
-            // Open the first entry again. The existing pane item is activated.
-            workspace.update(&mut app, |w, ctx| {
-                assert!(w.open_entry(file1.clone(), ctx).is_none())
-            });
-            app.read(|ctx| {
-                let pane = workspace.read(ctx).active_pane().read(ctx);
-                assert_eq!(
-                    pane.active_item().unwrap().entry_id(ctx),
-                    Some(file1.clone())
-                );
-                assert_eq!(pane.items().len(), 2);
-            });
-
-            // Split the pane with the first entry, then open the second entry again.
-            workspace.update(&mut app, |w, ctx| {
-                w.split_pane(w.active_pane().clone(), SplitDirection::Right, ctx);
-                assert!(w.open_entry(file2.clone(), ctx).is_none());
-                assert_eq!(
-                    w.active_pane()
-                        .read(ctx)
-                        .active_item()
-                        .unwrap()
-                        .entry_id(ctx.as_ref()),
-                    Some(file2.clone())
-                );
-            });
-
-            // Open the third entry twice concurrently. Two pane items
-            // are added.
-            let (t1, t2) = workspace.update(&mut app, |w, ctx| {
-                (
-                    w.open_entry(file3.clone(), ctx).unwrap(),
-                    w.open_entry(file3.clone(), ctx).unwrap(),
-                )
-            });
-            t1.await;
-            t2.await;
-            app.read(|ctx| {
-                let pane = workspace.read(ctx).active_pane().read(ctx);
-                assert_eq!(
-                    pane.active_item().unwrap().entry_id(ctx),
-                    Some(file3.clone())
-                );
-                let pane_entries = pane
-                    .items()
-                    .iter()
-                    .map(|i| i.entry_id(ctx).unwrap())
-                    .collect::<Vec<_>>();
-                assert_eq!(pane_entries, &[file1, file2, file3.clone(), file3]);
-            });
         });
-    }
 
-    #[test]
-    fn test_open_paths() {
-        App::test_async((), |mut app| async move {
-            let dir1 = temp_tree(json!({
-                "a.txt": "",
-            }));
-            let dir2 = temp_tree(json!({
-                "b.txt": "",
-            }));
-
-            let settings = settings::channel(&app.font_cache()).unwrap().1;
-            let (_, workspace) = app.add_window(|ctx| {
-                let mut workspace = Workspace::new(0, settings, ctx);
-                workspace.add_worktree(dir1.path(), ctx);
-                workspace
-            });
-            app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
-                .await;
-
-            // Open a file within an existing worktree.
-            app.update(|ctx| {
-                workspace.update(ctx, |view, ctx| {
-                    view.open_paths(&[dir1.path().join("a.txt")], ctx)
-                })
-            })
+        app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
             .await;
-            app.read(|ctx| {
-                assert_eq!(
-                    workspace
-                        .read(ctx)
-                        .active_pane()
-                        .read(ctx)
-                        .active_item()
-                        .unwrap()
-                        .title(ctx),
-                    "a.txt"
-                );
-            });
+        let entries = app.read(|ctx| workspace.file_entries(ctx));
+        let file1 = entries[0].clone();
+        let file2 = entries[1].clone();
+        let file3 = entries[2].clone();
+
+        // Open the first entry
+        workspace
+            .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx))
+            .unwrap()
+            .await;
+        app.read(|ctx| {
+            let pane = workspace.read(ctx).active_pane().read(ctx);
+            assert_eq!(
+                pane.active_item().unwrap().entry_id(ctx),
+                Some(file1.clone())
+            );
+            assert_eq!(pane.items().len(), 1);
+        });
 
-            // Open a file outside of any existing worktree.
-            app.update(|ctx| {
-                workspace.update(ctx, |view, ctx| {
-                    view.open_paths(&[dir2.path().join("b.txt")], ctx)
-                })
-            })
+        // Open the second entry
+        workspace
+            .update(&mut app, |w, ctx| w.open_entry(file2.clone(), ctx))
+            .unwrap()
             .await;
-            app.read(|ctx| {
-                let worktree_roots = workspace
+        app.read(|ctx| {
+            let pane = workspace.read(ctx).active_pane().read(ctx);
+            assert_eq!(
+                pane.active_item().unwrap().entry_id(ctx),
+                Some(file2.clone())
+            );
+            assert_eq!(pane.items().len(), 2);
+        });
+
+        // Open the first entry again. The existing pane item is activated.
+        workspace.update(&mut app, |w, ctx| {
+            assert!(w.open_entry(file1.clone(), ctx).is_none())
+        });
+        app.read(|ctx| {
+            let pane = workspace.read(ctx).active_pane().read(ctx);
+            assert_eq!(
+                pane.active_item().unwrap().entry_id(ctx),
+                Some(file1.clone())
+            );
+            assert_eq!(pane.items().len(), 2);
+        });
+
+        // Split the pane with the first entry, then open the second entry again.
+        workspace.update(&mut app, |w, ctx| {
+            w.split_pane(w.active_pane().clone(), SplitDirection::Right, ctx);
+            assert!(w.open_entry(file2.clone(), ctx).is_none());
+            assert_eq!(
+                w.active_pane()
                     .read(ctx)
-                    .worktrees()
-                    .iter()
-                    .map(|w| w.read(ctx).abs_path())
-                    .collect::<HashSet<_>>();
-                assert_eq!(
-                    worktree_roots,
-                    vec![dir1.path(), &dir2.path().join("b.txt")]
-                        .into_iter()
-                        .collect(),
-                );
-                assert_eq!(
-                    workspace
-                        .read(ctx)
-                        .active_pane()
-                        .read(ctx)
-                        .active_item()
-                        .unwrap()
-                        .title(ctx),
-                    "b.txt"
-                );
-            });
+                    .active_item()
+                    .unwrap()
+                    .entry_id(ctx.as_ref()),
+                Some(file2.clone())
+            );
+        });
+
+        // Open the third entry twice concurrently. Two pane items
+        // are added.
+        let (t1, t2) = workspace.update(&mut app, |w, ctx| {
+            (
+                w.open_entry(file3.clone(), ctx).unwrap(),
+                w.open_entry(file3.clone(), ctx).unwrap(),
+            )
+        });
+        t1.await;
+        t2.await;
+        app.read(|ctx| {
+            let pane = workspace.read(ctx).active_pane().read(ctx);
+            assert_eq!(
+                pane.active_item().unwrap().entry_id(ctx),
+                Some(file3.clone())
+            );
+            let pane_entries = pane
+                .items()
+                .iter()
+                .map(|i| i.entry_id(ctx).unwrap())
+                .collect::<Vec<_>>();
+            assert_eq!(pane_entries, &[file1, file2, file3.clone(), file3]);
         });
     }
 
-    #[test]
-    fn test_open_and_save_new_file() {
-        App::test_async((), |mut app| async move {
-            let dir = TempDir::new("test-new-file").unwrap();
-            let settings = settings::channel(&app.font_cache()).unwrap().1;
-            let (_, workspace) = app.add_window(|ctx| {
-                let mut workspace = Workspace::new(0, settings, ctx);
-                workspace.add_worktree(dir.path(), ctx);
-                workspace
-            });
-            let tree = app.read(|ctx| {
+    #[gpui::test]
+    async fn test_open_paths(mut app: gpui::TestAppContext) {
+        let dir1 = temp_tree(json!({
+            "a.txt": "",
+        }));
+        let dir2 = temp_tree(json!({
+            "b.txt": "",
+        }));
+
+        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let (_, workspace) = app.add_window(|ctx| {
+            let mut workspace = Workspace::new(0, settings, ctx);
+            workspace.add_worktree(dir1.path(), ctx);
+            workspace
+        });
+        app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
+            .await;
+
+        // Open a file within an existing worktree.
+        app.update(|ctx| {
+            workspace.update(ctx, |view, ctx| {
+                view.open_paths(&[dir1.path().join("a.txt")], ctx)
+            })
+        })
+        .await;
+        app.read(|ctx| {
+            assert_eq!(
                 workspace
                     .read(ctx)
-                    .worktrees()
-                    .iter()
-                    .next()
+                    .active_pane()
+                    .read(ctx)
+                    .active_item()
                     .unwrap()
-                    .clone()
-            });
-            tree.flush_fs_events(&app).await;
+                    .title(ctx),
+                "a.txt"
+            );
+        });
 
-            // Create a new untitled buffer
-            let editor = workspace.update(&mut app, |workspace, ctx| {
-                workspace.open_new_file(&(), ctx);
-                workspace
-                    .active_item(ctx)
-                    .unwrap()
-                    .to_any()
-                    .downcast::<BufferView>()
-                    .unwrap()
-            });
-            editor.update(&mut app, |editor, ctx| {
-                assert!(!editor.is_dirty(ctx.as_ref()));
-                assert_eq!(editor.title(ctx.as_ref()), "untitled");
-                editor.insert(&"hi".to_string(), ctx);
-                assert!(editor.is_dirty(ctx.as_ref()));
-            });
-
-            // Save the buffer. This prompts for a filename.
-            workspace.update(&mut app, |workspace, ctx| {
-                workspace.save_active_item(&(), ctx)
-            });
-            app.simulate_new_path_selection(|parent_dir| {
-                assert_eq!(parent_dir, dir.path());
-                Some(parent_dir.join("the-new-name"))
-            });
-            app.read(|ctx| {
-                assert!(editor.is_dirty(ctx));
-                assert_eq!(editor.title(ctx), "untitled");
-            });
-
-            // When the save completes, the buffer's title is updated.
-            tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
-                .await;
-            app.read(|ctx| {
-                assert!(!editor.is_dirty(ctx));
-                assert_eq!(editor.title(ctx), "the-new-name");
-            });
-
-            // Edit the file and save it again. This time, there is no filename prompt.
-            editor.update(&mut app, |editor, ctx| {
-                editor.insert(&" there".to_string(), ctx);
-                assert_eq!(editor.is_dirty(ctx.as_ref()), true);
-            });
-            workspace.update(&mut app, |workspace, ctx| {
-                workspace.save_active_item(&(), ctx)
-            });
-            assert!(!app.did_prompt_for_new_path());
-            editor
-                .condition(&app, |editor, ctx| !editor.is_dirty(ctx))
-                .await;
-            app.read(|ctx| assert_eq!(editor.title(ctx), "the-new-name"));
-
-            // Open the same newly-created file in another pane item. The new editor should reuse
-            // the same buffer.
-            workspace.update(&mut app, |workspace, ctx| {
-                workspace.open_new_file(&(), ctx);
-                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, ctx);
-                assert!(workspace
-                    .open_entry((tree.id(), Path::new("the-new-name").into()), ctx)
-                    .is_none());
-            });
-            let editor2 = workspace.update(&mut app, |workspace, ctx| {
+        // Open a file outside of any existing worktree.
+        app.update(|ctx| {
+            workspace.update(ctx, |view, ctx| {
+                view.open_paths(&[dir2.path().join("b.txt")], ctx)
+            })
+        })
+        .await;
+        app.read(|ctx| {
+            let worktree_roots = workspace
+                .read(ctx)
+                .worktrees()
+                .iter()
+                .map(|w| w.read(ctx).abs_path())
+                .collect::<HashSet<_>>();
+            assert_eq!(
+                worktree_roots,
+                vec![dir1.path(), &dir2.path().join("b.txt")]
+                    .into_iter()
+                    .collect(),
+            );
+            assert_eq!(
                 workspace
-                    .active_item(ctx)
-                    .unwrap()
-                    .to_any()
-                    .downcast::<BufferView>()
+                    .read(ctx)
+                    .active_pane()
+                    .read(ctx)
+                    .active_item()
                     .unwrap()
-            });
-            app.read(|ctx| {
-                assert_eq!(editor2.read(ctx).buffer(), editor.read(ctx).buffer());
-            })
+                    .title(ctx),
+                "b.txt"
+            );
         });
     }
 
-    #[test]
-    fn test_pane_actions() {
-        App::test_async((), |mut app| async move {
-            app.update(|ctx| pane::init(ctx));
-
-            let dir = temp_tree(json!({
-                "a": {
-                    "file1": "contents 1",
-                    "file2": "contents 2",
-                    "file3": "contents 3",
-                },
-            }));
-
-            let settings = settings::channel(&app.font_cache()).unwrap().1;
-            let (window_id, workspace) = app.add_window(|ctx| {
-                let mut workspace = Workspace::new(0, settings, ctx);
-                workspace.add_worktree(dir.path(), ctx);
-                workspace
-            });
-            app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
-                .await;
-            let entries = app.read(|ctx| workspace.file_entries(ctx));
-            let file1 = entries[0].clone();
+    #[gpui::test]
+    async fn test_open_and_save_new_file(mut app: gpui::TestAppContext) {
+        let dir = TempDir::new("test-new-file").unwrap();
+        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let (_, workspace) = app.add_window(|ctx| {
+            let mut workspace = Workspace::new(0, settings, ctx);
+            workspace.add_worktree(dir.path(), ctx);
+            workspace
+        });
+        let tree = app.read(|ctx| {
+            workspace
+                .read(ctx)
+                .worktrees()
+                .iter()
+                .next()
+                .unwrap()
+                .clone()
+        });
+        tree.flush_fs_events(&app).await;
+
+        // Create a new untitled buffer
+        let editor = workspace.update(&mut app, |workspace, ctx| {
+            workspace.open_new_file(&(), ctx);
+            workspace
+                .active_item(ctx)
+                .unwrap()
+                .to_any()
+                .downcast::<BufferView>()
+                .unwrap()
+        });
+        editor.update(&mut app, |editor, ctx| {
+            assert!(!editor.is_dirty(ctx.as_ref()));
+            assert_eq!(editor.title(ctx.as_ref()), "untitled");
+            editor.insert(&"hi".to_string(), ctx);
+            assert!(editor.is_dirty(ctx.as_ref()));
+        });
+
+        // Save the buffer. This prompts for a filename.
+        workspace.update(&mut app, |workspace, ctx| {
+            workspace.save_active_item(&(), ctx)
+        });
+        app.simulate_new_path_selection(|parent_dir| {
+            assert_eq!(parent_dir, dir.path());
+            Some(parent_dir.join("the-new-name"))
+        });
+        app.read(|ctx| {
+            assert!(editor.is_dirty(ctx));
+            assert_eq!(editor.title(ctx), "untitled");
+        });
 
-            let pane_1 = app.read(|ctx| workspace.read(ctx).active_pane().clone());
+        // When the save completes, the buffer's title is updated.
+        tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
+            .await;
+        app.read(|ctx| {
+            assert!(!editor.is_dirty(ctx));
+            assert_eq!(editor.title(ctx), "the-new-name");
+        });
 
+        // Edit the file and save it again. This time, there is no filename prompt.
+        editor.update(&mut app, |editor, ctx| {
+            editor.insert(&" there".to_string(), ctx);
+            assert_eq!(editor.is_dirty(ctx.as_ref()), true);
+        });
+        workspace.update(&mut app, |workspace, ctx| {
+            workspace.save_active_item(&(), ctx)
+        });
+        assert!(!app.did_prompt_for_new_path());
+        editor
+            .condition(&app, |editor, ctx| !editor.is_dirty(ctx))
+            .await;
+        app.read(|ctx| assert_eq!(editor.title(ctx), "the-new-name"));
+
+        // Open the same newly-created file in another pane item. The new editor should reuse
+        // the same buffer.
+        workspace.update(&mut app, |workspace, ctx| {
+            workspace.open_new_file(&(), ctx);
+            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, ctx);
+            assert!(workspace
+                .open_entry((tree.id(), Path::new("the-new-name").into()), ctx)
+                .is_none());
+        });
+        let editor2 = workspace.update(&mut app, |workspace, ctx| {
             workspace
-                .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx))
+                .active_item(ctx)
                 .unwrap()
-                .await;
-            app.read(|ctx| {
-                assert_eq!(
-                    pane_1.read(ctx).active_item().unwrap().entry_id(ctx),
-                    Some(file1.clone())
-                );
-            });
+                .to_any()
+                .downcast::<BufferView>()
+                .unwrap()
+        });
+        app.read(|ctx| {
+            assert_eq!(editor2.read(ctx).buffer(), editor.read(ctx).buffer());
+        })
+    }
+
+    #[gpui::test]
+    async fn test_pane_actions(mut app: gpui::TestAppContext) {
+        app.update(|ctx| pane::init(ctx));
+
+        let dir = temp_tree(json!({
+            "a": {
+                "file1": "contents 1",
+                "file2": "contents 2",
+                "file3": "contents 3",
+            },
+        }));
+
+        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let (window_id, workspace) = app.add_window(|ctx| {
+            let mut workspace = Workspace::new(0, settings, ctx);
+            workspace.add_worktree(dir.path(), ctx);
+            workspace
+        });
+        app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
+            .await;
+        let entries = app.read(|ctx| workspace.file_entries(ctx));
+        let file1 = entries[0].clone();
+
+        let pane_1 = app.read(|ctx| workspace.read(ctx).active_pane().clone());
+
+        workspace
+            .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx))
+            .unwrap()
+            .await;
+        app.read(|ctx| {
+            assert_eq!(
+                pane_1.read(ctx).active_item().unwrap().entry_id(ctx),
+                Some(file1.clone())
+            );
+        });
 
-            app.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ());
-            app.update(|ctx| {
-                let pane_2 = workspace.read(ctx).active_pane().clone();
-                assert_ne!(pane_1, pane_2);
+        app.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ());
+        app.update(|ctx| {
+            let pane_2 = workspace.read(ctx).active_pane().clone();
+            assert_ne!(pane_1, pane_2);
 
-                let pane2_item = pane_2.read(ctx).active_item().unwrap();
-                assert_eq!(pane2_item.entry_id(ctx.as_ref()), Some(file1.clone()));
+            let pane2_item = pane_2.read(ctx).active_item().unwrap();
+            assert_eq!(pane2_item.entry_id(ctx.as_ref()), Some(file1.clone()));
 
-                ctx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ());
-                let workspace_view = workspace.read(ctx);
-                assert_eq!(workspace_view.panes.len(), 1);
-                assert_eq!(workspace_view.active_pane(), &pane_1);
-            });
+            ctx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ());
+            let workspace_view = workspace.read(ctx);
+            assert_eq!(workspace_view.panes.len(), 1);
+            assert_eq!(workspace_view.active_pane(), &pane_1);
         });
     }
 }

zed/src/worktree.rs 🔗

@@ -1353,7 +1353,6 @@ mod tests {
     use crate::editor::Buffer;
     use crate::test::*;
     use anyhow::Result;
-    use gpui::App;
     use rand::prelude::*;
     use serde_json::json;
     use std::env;
@@ -1361,248 +1360,237 @@ mod tests {
     use std::os::unix;
     use std::time::{SystemTime, UNIX_EPOCH};
 
-    #[test]
-    fn test_populate_and_search() {
-        App::test_async((), |mut app| async move {
-            let dir = temp_tree(json!({
-                "root": {
-                    "apple": "",
-                    "banana": {
-                        "carrot": {
-                            "date": "",
-                            "endive": "",
-                        }
-                    },
-                    "fennel": {
-                        "grape": "",
+    #[gpui::test]
+    async fn test_populate_and_search(mut app: gpui::TestAppContext) {
+        let dir = temp_tree(json!({
+            "root": {
+                "apple": "",
+                "banana": {
+                    "carrot": {
+                        "date": "",
+                        "endive": "",
                     }
+                },
+                "fennel": {
+                    "grape": "",
                 }
-            }));
+            }
+        }));
 
-            let root_link_path = dir.path().join("root_link");
-            unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
-            unix::fs::symlink(
-                &dir.path().join("root/fennel"),
-                &dir.path().join("root/finnochio"),
-            )
-            .unwrap();
+        let root_link_path = dir.path().join("root_link");
+        unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
+        unix::fs::symlink(
+            &dir.path().join("root/fennel"),
+            &dir.path().join("root/finnochio"),
+        )
+        .unwrap();
 
-            let tree = app.add_model(|ctx| Worktree::new(root_link_path, ctx));
+        let tree = app.add_model(|ctx| Worktree::new(root_link_path, ctx));
 
-            app.read(|ctx| tree.read(ctx).scan_complete()).await;
-            app.read(|ctx| {
-                let tree = tree.read(ctx);
-                assert_eq!(tree.file_count(), 5);
+        app.read(|ctx| tree.read(ctx).scan_complete()).await;
+        app.read(|ctx| {
+            let tree = tree.read(ctx);
+            assert_eq!(tree.file_count(), 5);
 
-                assert_eq!(
-                    tree.inode_for_path("fennel/grape"),
-                    tree.inode_for_path("finnochio/grape")
-                );
+            assert_eq!(
+                tree.inode_for_path("fennel/grape"),
+                tree.inode_for_path("finnochio/grape")
+            );
 
-                let results = match_paths(
-                    Some(tree.snapshot()).iter(),
-                    "bna",
-                    false,
-                    false,
-                    false,
-                    10,
-                    Default::default(),
-                    ctx.thread_pool().clone(),
-                )
-                .into_iter()
-                .map(|result| result.path)
-                .collect::<Vec<Arc<Path>>>();
-                assert_eq!(
-                    results,
-                    vec![
-                        PathBuf::from("banana/carrot/date").into(),
-                        PathBuf::from("banana/carrot/endive").into(),
-                    ]
-                );
-            })
-        });
+            let results = match_paths(
+                Some(tree.snapshot()).iter(),
+                "bna",
+                false,
+                false,
+                false,
+                10,
+                Default::default(),
+                ctx.thread_pool().clone(),
+            )
+            .into_iter()
+            .map(|result| result.path)
+            .collect::<Vec<Arc<Path>>>();
+            assert_eq!(
+                results,
+                vec![
+                    PathBuf::from("banana/carrot/date").into(),
+                    PathBuf::from("banana/carrot/endive").into(),
+                ]
+            );
+        })
     }
 
-    #[test]
-    fn test_save_file() {
-        App::test_async((), |mut app| async move {
-            let dir = temp_tree(json!({
-                "file1": "the old contents",
-            }));
+    #[gpui::test]
+    async fn test_save_file(mut app: gpui::TestAppContext) {
+        let dir = temp_tree(json!({
+            "file1": "the old contents",
+        }));
 
-            let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx));
-            app.read(|ctx| tree.read(ctx).scan_complete()).await;
-            app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1));
+        let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx));
+        app.read(|ctx| tree.read(ctx).scan_complete()).await;
+        app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1));
 
-            let buffer =
-                app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx));
+        let buffer =
+            app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx));
 
-            let path = tree.update(&mut app, |tree, ctx| {
-                let path = tree.files(0).next().unwrap().path().clone();
-                assert_eq!(path.file_name().unwrap(), "file1");
-                smol::block_on(tree.save(&path, buffer.read(ctx).snapshot(), ctx.as_ref()))
-                    .unwrap();
-                path
-            });
+        let path = tree.update(&mut app, |tree, ctx| {
+            let path = tree.files(0).next().unwrap().path().clone();
+            assert_eq!(path.file_name().unwrap(), "file1");
+            smol::block_on(tree.save(&path, buffer.read(ctx).snapshot(), ctx.as_ref())).unwrap();
+            path
+        });
 
-            let history = app
-                .read(|ctx| tree.read(ctx).load_history(&path, ctx))
-                .await
-                .unwrap();
-            app.read(|ctx| {
-                assert_eq!(history.base_text.as_ref(), buffer.read(ctx).text());
-            });
+        let history = app
+            .read(|ctx| tree.read(ctx).load_history(&path, ctx))
+            .await
+            .unwrap();
+        app.read(|ctx| {
+            assert_eq!(history.base_text.as_ref(), buffer.read(ctx).text());
         });
     }
 
-    #[test]
-    fn test_save_in_single_file_worktree() {
-        App::test_async((), |mut app| async move {
-            let dir = temp_tree(json!({
-                "file1": "the old contents",
-            }));
-
-            let tree = app.add_model(|ctx| Worktree::new(dir.path().join("file1"), ctx));
-            app.read(|ctx| tree.read(ctx).scan_complete()).await;
-            app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1));
+    #[gpui::test]
+    async fn test_save_in_single_file_worktree(mut app: gpui::TestAppContext) {
+        let dir = temp_tree(json!({
+            "file1": "the old contents",
+        }));
 
-            let buffer =
-                app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx));
+        let tree = app.add_model(|ctx| Worktree::new(dir.path().join("file1"), ctx));
+        app.read(|ctx| tree.read(ctx).scan_complete()).await;
+        app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1));
 
-            let file = app.read(|ctx| tree.file("", ctx));
-            app.update(|ctx| {
-                assert_eq!(file.path().file_name(), None);
-                smol::block_on(file.save(buffer.read(ctx).snapshot(), ctx.as_ref())).unwrap();
-            });
+        let buffer =
+            app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx));
 
-            let history = app.read(|ctx| file.load_history(ctx)).await.unwrap();
-            app.read(|ctx| assert_eq!(history.base_text.as_ref(), buffer.read(ctx).text()));
+        let file = app.read(|ctx| tree.file("", ctx));
+        app.update(|ctx| {
+            assert_eq!(file.path().file_name(), None);
+            smol::block_on(file.save(buffer.read(ctx).snapshot(), ctx.as_ref())).unwrap();
         });
+
+        let history = app.read(|ctx| file.load_history(ctx)).await.unwrap();
+        app.read(|ctx| assert_eq!(history.base_text.as_ref(), buffer.read(ctx).text()));
     }
 
-    #[test]
-    fn test_rescan_simple() {
-        App::test_async((), |mut app| async move {
-            let dir = temp_tree(json!({
-                "a": {
-                    "file1": "",
-                    "file2": "",
-                    "file3": "",
-                },
-                "b": {
-                    "c": {
-                        "file4": "",
-                        "file5": "",
-                    }
+    #[gpui::test]
+    async fn test_rescan_simple(mut app: gpui::TestAppContext) {
+        let dir = temp_tree(json!({
+            "a": {
+                "file1": "",
+                "file2": "",
+                "file3": "",
+            },
+            "b": {
+                "c": {
+                    "file4": "",
+                    "file5": "",
                 }
-            }));
-
-            let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx));
-            let (file2, file3, file4, file5, non_existent_file) = app.read(|ctx| {
-                (
-                    tree.file("a/file2", ctx),
-                    tree.file("a/file3", ctx),
-                    tree.file("b/c/file4", ctx),
-                    tree.file("b/c/file5", ctx),
-                    tree.file("a/filex", ctx),
-                )
-            });
+            }
+        }));
+
+        let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx));
+        let (file2, file3, file4, file5, non_existent_file) = app.read(|ctx| {
+            (
+                tree.file("a/file2", ctx),
+                tree.file("a/file3", ctx),
+                tree.file("b/c/file4", ctx),
+                tree.file("b/c/file5", ctx),
+                tree.file("a/filex", ctx),
+            )
+        });
 
-            // The worktree hasn't scanned the directories containing these paths,
-            // so it can't determine that the paths are deleted.
-            assert!(!file2.is_deleted());
-            assert!(!file3.is_deleted());
-            assert!(!file4.is_deleted());
-            assert!(!file5.is_deleted());
-            assert!(!non_existent_file.is_deleted());
+        // The worktree hasn't scanned the directories containing these paths,
+        // so it can't determine that the paths are deleted.
+        assert!(!file2.is_deleted());
+        assert!(!file3.is_deleted());
+        assert!(!file4.is_deleted());
+        assert!(!file5.is_deleted());
+        assert!(!non_existent_file.is_deleted());
+
+        // After scanning, the worktree knows which files exist and which don't.
+        app.read(|ctx| tree.read(ctx).scan_complete()).await;
+        assert!(!file2.is_deleted());
+        assert!(!file3.is_deleted());
+        assert!(!file4.is_deleted());
+        assert!(!file5.is_deleted());
+        assert!(non_existent_file.is_deleted());
+
+        tree.flush_fs_events(&app).await;
+        std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
+        std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
+        std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
+        std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
+        tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
+            .await;
 
-            // After scanning, the worktree knows which files exist and which don't.
-            app.read(|ctx| tree.read(ctx).scan_complete()).await;
+        app.read(|ctx| {
+            assert_eq!(
+                tree.read(ctx)
+                    .paths()
+                    .map(|p| p.to_str().unwrap())
+                    .collect::<Vec<_>>(),
+                vec![
+                    "a",
+                    "a/file1",
+                    "a/file2.new",
+                    "b",
+                    "d",
+                    "d/file3",
+                    "d/file4"
+                ]
+            );
+
+            assert_eq!(file2.path().to_str().unwrap(), "a/file2.new");
+            assert_eq!(file4.path().as_ref(), Path::new("d/file4"));
+            assert_eq!(file5.path().as_ref(), Path::new("d/file5"));
             assert!(!file2.is_deleted());
-            assert!(!file3.is_deleted());
             assert!(!file4.is_deleted());
-            assert!(!file5.is_deleted());
-            assert!(non_existent_file.is_deleted());
-
-            tree.flush_fs_events(&app).await;
-            std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
-            std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
-            std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
-            std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
-            tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
-                .await;
-
-            app.read(|ctx| {
-                assert_eq!(
-                    tree.read(ctx)
-                        .paths()
-                        .map(|p| p.to_str().unwrap())
-                        .collect::<Vec<_>>(),
-                    vec![
-                        "a",
-                        "a/file1",
-                        "a/file2.new",
-                        "b",
-                        "d",
-                        "d/file3",
-                        "d/file4"
-                    ]
-                );
+            assert!(file5.is_deleted());
 
-                assert_eq!(file2.path().to_str().unwrap(), "a/file2.new");
-                assert_eq!(file4.path().as_ref(), Path::new("d/file4"));
-                assert_eq!(file5.path().as_ref(), Path::new("d/file5"));
-                assert!(!file2.is_deleted());
-                assert!(!file4.is_deleted());
-                assert!(file5.is_deleted());
-
-                // Right now, this rename isn't detected because the target path
-                // no longer exists on the file system by the time we process the
-                // rename event.
-                assert_eq!(file3.path().as_ref(), Path::new("a/file3"));
-                assert!(file3.is_deleted());
-            });
+            // Right now, this rename isn't detected because the target path
+            // no longer exists on the file system by the time we process the
+            // rename event.
+            assert_eq!(file3.path().as_ref(), Path::new("a/file3"));
+            assert!(file3.is_deleted());
         });
     }
 
-    #[test]
-    fn test_rescan_with_gitignore() {
-        App::test_async((), |mut app| async move {
-            let dir = temp_tree(json!({
-                ".git": {},
-                ".gitignore": "ignored-dir\n",
-                "tracked-dir": {
-                    "tracked-file1": "tracked contents",
-                },
-                "ignored-dir": {
-                    "ignored-file1": "ignored contents",
-                }
-            }));
-
-            let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx));
-            app.read(|ctx| tree.read(ctx).scan_complete()).await;
-            tree.flush_fs_events(&app).await;
-            app.read(|ctx| {
-                let tree = tree.read(ctx);
-                let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap();
-                let ignored = tree.entry_for_path("ignored-dir/ignored-file1").unwrap();
-                assert_eq!(tracked.is_ignored(), false);
-                assert_eq!(ignored.is_ignored(), true);
-            });
+    #[gpui::test]
+    async fn test_rescan_with_gitignore(mut app: gpui::TestAppContext) {
+        let dir = temp_tree(json!({
+            ".git": {},
+            ".gitignore": "ignored-dir\n",
+            "tracked-dir": {
+                "tracked-file1": "tracked contents",
+            },
+            "ignored-dir": {
+                "ignored-file1": "ignored contents",
+            }
+        }));
+
+        let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx));
+        app.read(|ctx| tree.read(ctx).scan_complete()).await;
+        tree.flush_fs_events(&app).await;
+        app.read(|ctx| {
+            let tree = tree.read(ctx);
+            let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap();
+            let ignored = tree.entry_for_path("ignored-dir/ignored-file1").unwrap();
+            assert_eq!(tracked.is_ignored(), false);
+            assert_eq!(ignored.is_ignored(), true);
+        });
 
-            fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap();
-            fs::write(dir.path().join("ignored-dir/ignored-file2"), "").unwrap();
-            tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
-                .await;
-            app.read(|ctx| {
-                let tree = tree.read(ctx);
-                let dot_git = tree.entry_for_path(".git").unwrap();
-                let tracked = tree.entry_for_path("tracked-dir/tracked-file2").unwrap();
-                let ignored = tree.entry_for_path("ignored-dir/ignored-file2").unwrap();
-                assert_eq!(tracked.is_ignored(), false);
-                assert_eq!(ignored.is_ignored(), true);
-                assert_eq!(dot_git.is_ignored(), true);
-            });
+        fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap();
+        fs::write(dir.path().join("ignored-dir/ignored-file2"), "").unwrap();
+        tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
+            .await;
+        app.read(|ctx| {
+            let tree = tree.read(ctx);
+            let dot_git = tree.entry_for_path(".git").unwrap();
+            let tracked = tree.entry_for_path("tracked-dir/tracked-file2").unwrap();
+            let ignored = tree.entry_for_path("ignored-dir/ignored-file2").unwrap();
+            assert_eq!(tracked.is_ignored(), false);
+            assert_eq!(ignored.is_ignored(), true);
+            assert_eq!(dot_git.is_ignored(), true);
         });
     }