Enable tests in project panel 2 (#3325)

Max Brunsfeld created

Change summary

crates/editor2/src/editor.rs               |    4 
crates/gpui2/src/app/test_context.rs       |   24 
crates/gpui2/src/platform/test/platform.rs |   78 
crates/gpui2/src/platform/test/window.rs   |   28 
crates/project_panel2/src/project_panel.rs | 2628 ++++++++++++-----------
5 files changed, 1,429 insertions(+), 1,333 deletions(-)

Detailed changes

crates/editor2/src/editor.rs 🔗

@@ -9168,6 +9168,10 @@ impl Editor {
         cx.focus(&self.focus_handle)
     }
 
+    pub fn is_focused(&self, cx: &WindowContext) -> bool {
+        self.focus_handle.is_focused(cx)
+    }
+
     fn handle_focus_in(&mut self, cx: &mut ViewContext<Self>) {
         if self.focus_handle.is_focused(cx) {
             // todo!()

crates/gpui2/src/app/test_context.rs 🔗

@@ -14,6 +14,7 @@ pub struct TestAppContext {
     pub background_executor: BackgroundExecutor,
     pub foreground_executor: ForegroundExecutor,
     pub dispatcher: TestDispatcher,
+    pub test_platform: Rc<TestPlatform>,
 }
 
 impl Context for TestAppContext {
@@ -77,17 +78,15 @@ impl TestAppContext {
         let arc_dispatcher = Arc::new(dispatcher.clone());
         let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
         let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
-        let platform = Rc::new(TestPlatform::new(
-            background_executor.clone(),
-            foreground_executor.clone(),
-        ));
+        let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
         let asset_source = Arc::new(());
         let http_client = util::http::FakeHttpClient::with_404_response();
         Self {
-            app: AppContext::new(platform, asset_source, http_client),
+            app: AppContext::new(platform.clone(), asset_source, http_client),
             background_executor,
             foreground_executor,
             dispatcher: dispatcher.clone(),
+            test_platform: platform,
         }
     }
 
@@ -152,6 +151,21 @@ impl TestAppContext {
         (view, VisualTestContext::from_window(*window.deref(), self))
     }
 
+    pub fn simulate_new_path_selection(
+        &self,
+        select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
+    ) {
+        self.test_platform.simulate_new_path_selection(select_path);
+    }
+
+    pub fn simulate_prompt_answer(&self, button_ix: usize) {
+        self.test_platform.simulate_prompt_answer(button_ix);
+    }
+
+    pub fn has_pending_prompt(&self) -> bool {
+        self.test_platform.has_pending_prompt()
+    }
+
     pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
     where
         Fut: Future<Output = R> + 'static,

crates/gpui2/src/platform/test/platform.rs 🔗

@@ -3,8 +3,15 @@ use crate::{
     PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
 };
 use anyhow::{anyhow, Result};
+use collections::VecDeque;
+use futures::channel::oneshot;
 use parking_lot::Mutex;
-use std::{rc::Rc, sync::Arc};
+use std::{
+    cell::RefCell,
+    path::PathBuf,
+    rc::{Rc, Weak},
+    sync::Arc,
+};
 
 pub struct TestPlatform {
     background_executor: BackgroundExecutor,
@@ -13,18 +20,60 @@ pub struct TestPlatform {
     active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
     active_display: Rc<dyn PlatformDisplay>,
     active_cursor: Mutex<CursorStyle>,
+    pub(crate) prompts: RefCell<TestPrompts>,
+    weak: Weak<Self>,
+}
+
+#[derive(Default)]
+pub(crate) struct TestPrompts {
+    multiple_choice: VecDeque<oneshot::Sender<usize>>,
+    new_path: VecDeque<(PathBuf, oneshot::Sender<Option<PathBuf>>)>,
 }
 
 impl TestPlatform {
-    pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Self {
-        TestPlatform {
+    pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc<Self> {
+        Rc::new_cyclic(|weak| TestPlatform {
             background_executor: executor,
             foreground_executor,
-
+            prompts: Default::default(),
             active_cursor: Default::default(),
             active_display: Rc::new(TestDisplay::new()),
             active_window: Default::default(),
-        }
+            weak: weak.clone(),
+        })
+    }
+
+    pub(crate) fn simulate_new_path_selection(
+        &self,
+        select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
+    ) {
+        let (path, tx) = self
+            .prompts
+            .borrow_mut()
+            .new_path
+            .pop_front()
+            .expect("no pending new path prompt");
+        tx.send(select_path(&path)).ok();
+    }
+
+    pub(crate) fn simulate_prompt_answer(&self, response_ix: usize) {
+        let tx = self
+            .prompts
+            .borrow_mut()
+            .multiple_choice
+            .pop_front()
+            .expect("no pending multiple choice prompt");
+        tx.send(response_ix).ok();
+    }
+
+    pub(crate) fn has_pending_prompt(&self) -> bool {
+        !self.prompts.borrow().multiple_choice.is_empty()
+    }
+
+    pub(crate) fn prompt(&self) -> oneshot::Receiver<usize> {
+        let (tx, rx) = oneshot::channel();
+        self.prompts.borrow_mut().multiple_choice.push_back(tx);
+        rx
     }
 }
 
@@ -86,7 +135,11 @@ impl Platform for TestPlatform {
         options: WindowOptions,
     ) -> Box<dyn crate::PlatformWindow> {
         *self.active_window.lock() = Some(handle);
-        Box::new(TestWindow::new(options, self.active_display.clone()))
+        Box::new(TestWindow::new(
+            options,
+            self.weak.clone(),
+            self.active_display.clone(),
+        ))
     }
 
     fn set_display_link_output_callback(
@@ -116,15 +169,20 @@ impl Platform for TestPlatform {
     fn prompt_for_paths(
         &self,
         _options: crate::PathPromptOptions,
-    ) -> futures::channel::oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
+    ) -> oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
         unimplemented!()
     }
 
     fn prompt_for_new_path(
         &self,
-        _directory: &std::path::Path,
-    ) -> futures::channel::oneshot::Receiver<Option<std::path::PathBuf>> {
-        unimplemented!()
+        directory: &std::path::Path,
+    ) -> oneshot::Receiver<Option<std::path::PathBuf>> {
+        let (tx, rx) = oneshot::channel();
+        self.prompts
+            .borrow_mut()
+            .new_path
+            .push_back((directory.to_path_buf(), tx));
+        rx
     }
 
     fn reveal_path(&self, _path: &std::path::Path) {

crates/gpui2/src/platform/test/window.rs 🔗

@@ -1,15 +1,13 @@
-use std::{
-    rc::Rc,
-    sync::{self, Arc},
+use crate::{
+    px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay,
+    PlatformInputHandler, PlatformWindow, Point, Scene, Size, TestPlatform, TileId,
+    WindowAppearance, WindowBounds, WindowOptions,
 };
-
 use collections::HashMap;
 use parking_lot::Mutex;
-
-use crate::{
-    px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay,
-    PlatformInputHandler, PlatformWindow, Point, Scene, Size, TileId, WindowAppearance,
-    WindowBounds, WindowOptions,
+use std::{
+    rc::{Rc, Weak},
+    sync::{self, Arc},
 };
 
 #[derive(Default)]
@@ -25,16 +23,22 @@ pub struct TestWindow {
     current_scene: Mutex<Option<Scene>>,
     display: Rc<dyn PlatformDisplay>,
     input_handler: Option<Box<dyn PlatformInputHandler>>,
-
     handlers: Mutex<Handlers>,
+    platform: Weak<TestPlatform>,
     sprite_atlas: Arc<dyn PlatformAtlas>,
 }
+
 impl TestWindow {
-    pub fn new(options: WindowOptions, display: Rc<dyn PlatformDisplay>) -> Self {
+    pub fn new(
+        options: WindowOptions,
+        platform: Weak<TestPlatform>,
+        display: Rc<dyn PlatformDisplay>,
+    ) -> Self {
         Self {
             bounds: options.bounds,
             current_scene: Default::default(),
             display,
+            platform,
             input_handler: None,
             sprite_atlas: Arc::new(TestAtlas::new()),
             handlers: Default::default(),
@@ -89,7 +93,7 @@ impl PlatformWindow for TestWindow {
         _msg: &str,
         _answers: &[&str],
     ) -> futures::channel::oneshot::Receiver<usize> {
-        todo!()
+        self.platform.upgrade().expect("platform dropped").prompt()
     }
 
     fn activate(&self) {

crates/project_panel2/src/project_panel.rs 🔗

@@ -197,23 +197,20 @@ impl ProjectPanel {
                 editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => {
                     this.autoscroll(cx);
                 }
+                editor::Event::Blurred => {
+                    if this
+                        .edit_state
+                        .as_ref()
+                        .map_or(false, |state| state.processing_filename.is_none())
+                    {
+                        this.edit_state = None;
+                        this.update_visible_entries(None, cx);
+                    }
+                }
                 _ => {}
             })
             .detach();
 
-            // cx.observe_focus(&filename_editor, |this, _, is_focused, cx| {
-            //     if !is_focused
-            //         && this
-            //             .edit_state
-            //             .as_ref()
-            //             .map_or(false, |state| state.processing_filename.is_none())
-            //     {
-            //         this.edit_state = None;
-            //         this.update_visible_entries(None, cx);
-            //     }
-            // })
-            // .detach();
-
             // cx.observe_global::<FileAssociations, _>(|_, cx| {
             //     cx.notify();
             // })
@@ -1570,1296 +1567,1315 @@ impl ClipboardEntry {
     }
 }
 
-// todo!()
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use gpui::{AnyWindowHandle, TestAppContext, View, WindowHandle};
-//     use pretty_assertions::assert_eq;
-//     use project::FakeFs;
-//     use serde_json::json;
-//     use settings::SettingsStore;
-//     use std::{
-//         collections::HashSet,
-//         path::{Path, PathBuf},
-//         sync::atomic::{self, AtomicUsize},
-//     };
-//     use workspace::{pane, AppState};
-
-//     #[gpui::test]
-//     async fn test_visible_list(cx: &mut gpui::TestAppContext) {
-//         init_test(cx);
-
-//         let fs = FakeFs::new(cx.executor().clone());
-//         fs.insert_tree(
-//             "/root1",
-//             json!({
-//                 ".dockerignore": "",
-//                 ".git": {
-//                     "HEAD": "",
-//                 },
-//                 "a": {
-//                     "0": { "q": "", "r": "", "s": "" },
-//                     "1": { "t": "", "u": "" },
-//                     "2": { "v": "", "w": "", "x": "", "y": "" },
-//                 },
-//                 "b": {
-//                     "3": { "Q": "" },
-//                     "4": { "R": "", "S": "", "T": "", "U": "" },
-//                 },
-//                 "C": {
-//                     "5": {},
-//                     "6": { "V": "", "W": "" },
-//                     "7": { "X": "" },
-//                     "8": { "Y": {}, "Z": "" }
-//                 }
-//             }),
-//         )
-//         .await;
-//         fs.insert_tree(
-//             "/root2",
-//             json!({
-//                 "d": {
-//                     "9": ""
-//                 },
-//                 "e": {}
-//             }),
-//         )
-//         .await;
-
-//         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-//         let workspace = cx
-//             .add_window(|cx| Workspace::test_new(project.clone(), cx))
-//             .root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..50, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    > b",
-//                 "    > C",
-//                 "      .dockerignore",
-//                 "v root2",
-//                 "    > d",
-//                 "    > e",
-//             ]
-//         );
-
-//         toggle_expand_dir(&panel, "root1/b", cx);
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..50, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    v b  <== selected",
-//                 "        > 3",
-//                 "        > 4",
-//                 "    > C",
-//                 "      .dockerignore",
-//                 "v root2",
-//                 "    > d",
-//                 "    > e",
-//             ]
-//         );
-
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 6..9, cx),
-//             &[
-//                 //
-//                 "    > C",
-//                 "      .dockerignore",
-//                 "v root2",
-//             ]
-//         );
-//     }
-
-//     #[gpui::test(iterations = 30)]
-//     async fn test_editing_files(cx: &mut gpui::TestAppContext) {
-//         init_test(cx);
-
-//         let fs = FakeFs::new(cx.background());
-//         fs.insert_tree(
-//             "/root1",
-//             json!({
-//                 ".dockerignore": "",
-//                 ".git": {
-//                     "HEAD": "",
-//                 },
-//                 "a": {
-//                     "0": { "q": "", "r": "", "s": "" },
-//                     "1": { "t": "", "u": "" },
-//                     "2": { "v": "", "w": "", "x": "", "y": "" },
-//                 },
-//                 "b": {
-//                     "3": { "Q": "" },
-//                     "4": { "R": "", "S": "", "T": "", "U": "" },
-//                 },
-//                 "C": {
-//                     "5": {},
-//                     "6": { "V": "", "W": "" },
-//                     "7": { "X": "" },
-//                     "8": { "Y": {}, "Z": "" }
-//                 }
-//             }),
-//         )
-//         .await;
-//         fs.insert_tree(
-//             "/root2",
-//             json!({
-//                 "d": {
-//                     "9": ""
-//                 },
-//                 "e": {}
-//             }),
-//         )
-//         .await;
-
-//         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-//         let workspace = window.root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-//         select_path(&panel, "root1", cx);
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1  <== selected",
-//                 "    > .git",
-//                 "    > a",
-//                 "    > b",
-//                 "    > C",
-//                 "      .dockerignore",
-//                 "v root2",
-//                 "    > d",
-//                 "    > e",
-//             ]
-//         );
-
-//         // Add a file with the root folder selected. The filename editor is placed
-//         // before the first file in the root folder.
-//         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-//         window.read_with(cx, |cx| {
-//             let panel = panel.read(cx);
-//             assert!(panel.filename_editor.is_focused(cx));
-//         });
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    > b",
-//                 "    > C",
-//                 "      [EDITOR: '']  <== selected",
-//                 "      .dockerignore",
-//                 "v root2",
-//                 "    > d",
-//                 "    > e",
-//             ]
-//         );
-
-//         let confirm = panel.update(cx, |panel, cx| {
-//             panel
-//                 .filename_editor
-//                 .update(cx, |editor, cx| editor.set_text("the-new-filename", cx));
-//             panel.confirm(&Confirm, cx).unwrap()
-//         });
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    > b",
-//                 "    > C",
-//                 "      [PROCESSING: 'the-new-filename']  <== selected",
-//                 "      .dockerignore",
-//                 "v root2",
-//                 "    > d",
-//                 "    > e",
-//             ]
-//         );
-
-//         confirm.await.unwrap();
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    > b",
-//                 "    > C",
-//                 "      .dockerignore",
-//                 "      the-new-filename  <== selected",
-//                 "v root2",
-//                 "    > d",
-//                 "    > e",
-//             ]
-//         );
-
-//         select_path(&panel, "root1/b", cx);
-//         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    v b",
-//                 "        > 3",
-//                 "        > 4",
-//                 "          [EDITOR: '']  <== selected",
-//                 "    > C",
-//                 "      .dockerignore",
-//                 "      the-new-filename",
-//             ]
-//         );
-
-//         panel
-//             .update(cx, |panel, cx| {
-//                 panel
-//                     .filename_editor
-//                     .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx));
-//                 panel.confirm(&Confirm, cx).unwrap()
-//             })
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    v b",
-//                 "        > 3",
-//                 "        > 4",
-//                 "          another-filename.txt  <== selected",
-//                 "    > C",
-//                 "      .dockerignore",
-//                 "      the-new-filename",
-//             ]
-//         );
-
-//         select_path(&panel, "root1/b/another-filename.txt", cx);
-//         panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    v b",
-//                 "        > 3",
-//                 "        > 4",
-//                 "          [EDITOR: 'another-filename.txt']  <== selected",
-//                 "    > C",
-//                 "      .dockerignore",
-//                 "      the-new-filename",
-//             ]
-//         );
-
-//         let confirm = panel.update(cx, |panel, cx| {
-//             panel.filename_editor.update(cx, |editor, cx| {
-//                 let file_name_selections = editor.selections.all::<usize>(cx);
-//                 assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
-//                 let file_name_selection = &file_name_selections[0];
-//                 assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
-//                 assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension");
-
-//                 editor.set_text("a-different-filename.tar.gz", cx)
-//             });
-//             panel.confirm(&Confirm, cx).unwrap()
-//         });
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    v b",
-//                 "        > 3",
-//                 "        > 4",
-//                 "          [PROCESSING: 'a-different-filename.tar.gz']  <== selected",
-//                 "    > C",
-//                 "      .dockerignore",
-//                 "      the-new-filename",
-//             ]
-//         );
-
-//         confirm.await.unwrap();
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    v b",
-//                 "        > 3",
-//                 "        > 4",
-//                 "          a-different-filename.tar.gz  <== selected",
-//                 "    > C",
-//                 "      .dockerignore",
-//                 "      the-new-filename",
-//             ]
-//         );
-
-//         panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    v b",
-//                 "        > 3",
-//                 "        > 4",
-//                 "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
-//                 "    > C",
-//                 "      .dockerignore",
-//                 "      the-new-filename",
-//             ]
-//         );
-
-//         panel.update(cx, |panel, cx| {
-//             panel.filename_editor.update(cx, |editor, cx| {
-//                 let file_name_selections = editor.selections.all::<usize>(cx);
-//                 assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
-//                 let file_name_selection = &file_name_selections[0];
-//                 assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
-//                 assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot");
-
-//             });
-//             panel.cancel(&Cancel, cx)
-//         });
-
-//         panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    v b",
-//                 "        > [EDITOR: '']  <== selected",
-//                 "        > 3",
-//                 "        > 4",
-//                 "          a-different-filename.tar.gz",
-//                 "    > C",
-//                 "      .dockerignore",
-//             ]
-//         );
-
-//         let confirm = panel.update(cx, |panel, cx| {
-//             panel
-//                 .filename_editor
-//                 .update(cx, |editor, cx| editor.set_text("new-dir", cx));
-//             panel.confirm(&Confirm, cx).unwrap()
-//         });
-//         panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx));
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    v b",
-//                 "        > [PROCESSING: 'new-dir']",
-//                 "        > 3  <== selected",
-//                 "        > 4",
-//                 "          a-different-filename.tar.gz",
-//                 "    > C",
-//                 "      .dockerignore",
-//             ]
-//         );
-
-//         confirm.await.unwrap();
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    v b",
-//                 "        > 3  <== selected",
-//                 "        > 4",
-//                 "        > new-dir",
-//                 "          a-different-filename.tar.gz",
-//                 "    > C",
-//                 "      .dockerignore",
-//             ]
-//         );
-
-//         panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx));
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    v b",
-//                 "        > [EDITOR: '3']  <== selected",
-//                 "        > 4",
-//                 "        > new-dir",
-//                 "          a-different-filename.tar.gz",
-//                 "    > C",
-//                 "      .dockerignore",
-//             ]
-//         );
-
-//         // Dismiss the rename editor when it loses focus.
-//         workspace.update(cx, |_, cx| cx.focus_self());
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    v b",
-//                 "        > 3  <== selected",
-//                 "        > 4",
-//                 "        > new-dir",
-//                 "          a-different-filename.tar.gz",
-//                 "    > C",
-//                 "      .dockerignore",
-//             ]
-//         );
-//     }
-
-//     #[gpui::test(iterations = 30)]
-//     async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
-//         init_test(cx);
-
-//         let fs = FakeFs::new(cx.background());
-//         fs.insert_tree(
-//             "/root1",
-//             json!({
-//                 ".dockerignore": "",
-//                 ".git": {
-//                     "HEAD": "",
-//                 },
-//                 "a": {
-//                     "0": { "q": "", "r": "", "s": "" },
-//                     "1": { "t": "", "u": "" },
-//                     "2": { "v": "", "w": "", "x": "", "y": "" },
-//                 },
-//                 "b": {
-//                     "3": { "Q": "" },
-//                     "4": { "R": "", "S": "", "T": "", "U": "" },
-//                 },
-//                 "C": {
-//                     "5": {},
-//                     "6": { "V": "", "W": "" },
-//                     "7": { "X": "" },
-//                     "8": { "Y": {}, "Z": "" }
-//                 }
-//             }),
-//         )
-//         .await;
-//         fs.insert_tree(
-//             "/root2",
-//             json!({
-//                 "d": {
-//                     "9": ""
-//                 },
-//                 "e": {}
-//             }),
-//         )
-//         .await;
-
-//         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-//         let workspace = window.root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-//         select_path(&panel, "root1", cx);
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1  <== selected",
-//                 "    > .git",
-//                 "    > a",
-//                 "    > b",
-//                 "    > C",
-//                 "      .dockerignore",
-//                 "v root2",
-//                 "    > d",
-//                 "    > e",
-//             ]
-//         );
-
-//         // Add a file with the root folder selected. The filename editor is placed
-//         // before the first file in the root folder.
-//         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-//         window.read_with(cx, |cx| {
-//             let panel = panel.read(cx);
-//             assert!(panel.filename_editor.is_focused(cx));
-//         });
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    > b",
-//                 "    > C",
-//                 "      [EDITOR: '']  <== selected",
-//                 "      .dockerignore",
-//                 "v root2",
-//                 "    > d",
-//                 "    > e",
-//             ]
-//         );
-
-//         let confirm = panel.update(cx, |panel, cx| {
-//             panel.filename_editor.update(cx, |editor, cx| {
-//                 editor.set_text("/bdir1/dir2/the-new-filename", cx)
-//             });
-//             panel.confirm(&Confirm, cx).unwrap()
-//         });
-
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    > b",
-//                 "    > C",
-//                 "      [PROCESSING: '/bdir1/dir2/the-new-filename']  <== selected",
-//                 "      .dockerignore",
-//                 "v root2",
-//                 "    > d",
-//                 "    > e",
-//             ]
-//         );
-
-//         confirm.await.unwrap();
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..13, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    > b",
-//                 "    v bdir1",
-//                 "        v dir2",
-//                 "              the-new-filename  <== selected",
-//                 "    > C",
-//                 "      .dockerignore",
-//                 "v root2",
-//                 "    > d",
-//                 "    > e",
-//             ]
-//         );
-//     }
-
-//     #[gpui::test]
-//     async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
-//         init_test(cx);
-
-//         let fs = FakeFs::new(cx.background());
-//         fs.insert_tree(
-//             "/root1",
-//             json!({
-//                 "one.two.txt": "",
-//                 "one.txt": ""
-//             }),
-//         )
-//         .await;
-
-//         let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
-//         let workspace = cx
-//             .add_window(|cx| Workspace::test_new(project.clone(), cx))
-//             .root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-//         panel.update(cx, |panel, cx| {
-//             panel.select_next(&Default::default(), cx);
-//             panel.select_next(&Default::default(), cx);
-//         });
-
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..50, cx),
-//             &[
-//                 //
-//                 "v root1",
-//                 "      one.two.txt  <== selected",
-//                 "      one.txt",
-//             ]
-//         );
-
-//         // Regression test - file name is created correctly when
-//         // the copied file's name contains multiple dots.
-//         panel.update(cx, |panel, cx| {
-//             panel.copy(&Default::default(), cx);
-//             panel.paste(&Default::default(), cx);
-//         });
-//         cx.foreground().run_until_parked();
-
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..50, cx),
-//             &[
-//                 //
-//                 "v root1",
-//                 "      one.two copy.txt",
-//                 "      one.two.txt  <== selected",
-//                 "      one.txt",
-//             ]
-//         );
-
-//         panel.update(cx, |panel, cx| {
-//             panel.paste(&Default::default(), cx);
-//         });
-//         cx.foreground().run_until_parked();
-
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..50, cx),
-//             &[
-//                 //
-//                 "v root1",
-//                 "      one.two copy 1.txt",
-//                 "      one.two copy.txt",
-//                 "      one.two.txt  <== selected",
-//                 "      one.txt",
-//             ]
-//         );
-//     }
-
-//     #[gpui::test]
-//     async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
-//         init_test_with_editor(cx);
-
-//         let fs = FakeFs::new(cx.background());
-//         fs.insert_tree(
-//             "/src",
-//             json!({
-//                 "test": {
-//                     "first.rs": "// First Rust file",
-//                     "second.rs": "// Second Rust file",
-//                     "third.rs": "// Third Rust file",
-//                 }
-//             }),
-//         )
-//         .await;
-
-//         let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-//         let workspace = window.root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-//         toggle_expand_dir(&panel, "src/test", cx);
-//         select_path(&panel, "src/test/first.rs", cx);
-//         panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
-//         cx.foreground().run_until_parked();
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v src",
-//                 "    v test",
-//                 "          first.rs  <== selected",
-//                 "          second.rs",
-//                 "          third.rs"
-//             ]
-//         );
-//         ensure_single_file_is_opened(window, "test/first.rs", cx);
-
-//         submit_deletion(window.into(), &panel, cx);
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v src",
-//                 "    v test",
-//                 "          second.rs",
-//                 "          third.rs"
-//             ],
-//             "Project panel should have no deleted file, no other file is selected in it"
-//         );
-//         ensure_no_open_items_and_panes(window.into(), &workspace, cx);
-
-//         select_path(&panel, "src/test/second.rs", cx);
-//         panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
-//         cx.foreground().run_until_parked();
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v src",
-//                 "    v test",
-//                 "          second.rs  <== selected",
-//                 "          third.rs"
-//             ]
-//         );
-//         ensure_single_file_is_opened(window, "test/second.rs", cx);
-
-//         window.update(cx, |cx| {
-//             let active_items = workspace
-//                 .read(cx)
-//                 .panes()
-//                 .iter()
-//                 .filter_map(|pane| pane.read(cx).active_item())
-//                 .collect::<Vec<_>>();
-//             assert_eq!(active_items.len(), 1);
-//             let open_editor = active_items
-//                 .into_iter()
-//                 .next()
-//                 .unwrap()
-//                 .downcast::<Editor>()
-//                 .expect("Open item should be an editor");
-//             open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
-//         });
-//         submit_deletion(window.into(), &panel, cx);
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &["v src", "    v test", "          third.rs"],
-//             "Project panel should have no deleted file, with one last file remaining"
-//         );
-//         ensure_no_open_items_and_panes(window.into(), &workspace, cx);
-//     }
-
-//     #[gpui::test]
-//     async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
-//         init_test_with_editor(cx);
-
-//         let fs = FakeFs::new(cx.background());
-//         fs.insert_tree(
-//             "/src",
-//             json!({
-//                 "test": {
-//                     "first.rs": "// First Rust file",
-//                     "second.rs": "// Second Rust file",
-//                     "third.rs": "// Third Rust file",
-//                 }
-//             }),
-//         )
-//         .await;
-
-//         let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-//         let workspace = window.root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-//         select_path(&panel, "src/", cx);
-//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
-//         cx.foreground().run_until_parked();
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &["v src  <== selected", "    > test"]
-//         );
-//         panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
-//         window.read_with(cx, |cx| {
-//             let panel = panel.read(cx);
-//             assert!(panel.filename_editor.is_focused(cx));
-//         });
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &["v src", "    > [EDITOR: '']  <== selected", "    > test"]
-//         );
-//         panel.update(cx, |panel, cx| {
-//             panel
-//                 .filename_editor
-//                 .update(cx, |editor, cx| editor.set_text("test", cx));
-//             assert!(
-//                 panel.confirm(&Confirm, cx).is_none(),
-//                 "Should not allow to confirm on conflicting new directory name"
-//             )
-//         });
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &["v src", "    > test"],
-//             "File list should be unchanged after failed folder create confirmation"
-//         );
-
-//         select_path(&panel, "src/test/", cx);
-//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
-//         cx.foreground().run_until_parked();
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &["v src", "    > test  <== selected"]
-//         );
-//         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-//         window.read_with(cx, |cx| {
-//             let panel = panel.read(cx);
-//             assert!(panel.filename_editor.is_focused(cx));
-//         });
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v src",
-//                 "    v test",
-//                 "          [EDITOR: '']  <== selected",
-//                 "          first.rs",
-//                 "          second.rs",
-//                 "          third.rs"
-//             ]
-//         );
-//         panel.update(cx, |panel, cx| {
-//             panel
-//                 .filename_editor
-//                 .update(cx, |editor, cx| editor.set_text("first.rs", cx));
-//             assert!(
-//                 panel.confirm(&Confirm, cx).is_none(),
-//                 "Should not allow to confirm on conflicting new file name"
-//             )
-//         });
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v src",
-//                 "    v test",
-//                 "          first.rs",
-//                 "          second.rs",
-//                 "          third.rs"
-//             ],
-//             "File list should be unchanged after failed file create confirmation"
-//         );
-
-//         select_path(&panel, "src/test/first.rs", cx);
-//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
-//         cx.foreground().run_until_parked();
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v src",
-//                 "    v test",
-//                 "          first.rs  <== selected",
-//                 "          second.rs",
-//                 "          third.rs"
-//             ],
-//         );
-//         panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
-//         window.read_with(cx, |cx| {
-//             let panel = panel.read(cx);
-//             assert!(panel.filename_editor.is_focused(cx));
-//         });
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v src",
-//                 "    v test",
-//                 "          [EDITOR: 'first.rs']  <== selected",
-//                 "          second.rs",
-//                 "          third.rs"
-//             ]
-//         );
-//         panel.update(cx, |panel, cx| {
-//             panel
-//                 .filename_editor
-//                 .update(cx, |editor, cx| editor.set_text("second.rs", cx));
-//             assert!(
-//                 panel.confirm(&Confirm, cx).is_none(),
-//                 "Should not allow to confirm on conflicting file rename"
-//             )
-//         });
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v src",
-//                 "    v test",
-//                 "          first.rs  <== selected",
-//                 "          second.rs",
-//                 "          third.rs"
-//             ],
-//             "File list should be unchanged after failed rename confirmation"
-//         );
-//     }
-
-//     #[gpui::test]
-//     async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) {
-//         init_test_with_editor(cx);
-
-//         let fs = FakeFs::new(cx.background());
-//         fs.insert_tree(
-//             "/src",
-//             json!({
-//                 "test": {
-//                     "first.rs": "// First Rust file",
-//                     "second.rs": "// Second Rust file",
-//                     "third.rs": "// Third Rust file",
-//                 }
-//             }),
-//         )
-//         .await;
-
-//         let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
-//         let workspace = cx
-//             .add_window(|cx| Workspace::test_new(project.clone(), cx))
-//             .root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-//         let new_search_events_count = Arc::new(AtomicUsize::new(0));
-//         let _subscription = panel.update(cx, |_, cx| {
-//             let subcription_count = Arc::clone(&new_search_events_count);
-//             cx.subscribe(&cx.handle(), move |_, _, event, _| {
-//                 if matches!(event, Event::NewSearchInDirectory { .. }) {
-//                     subcription_count.fetch_add(1, atomic::Ordering::SeqCst);
-//                 }
-//             })
-//         });
-
-//         toggle_expand_dir(&panel, "src/test", cx);
-//         select_path(&panel, "src/test/first.rs", cx);
-//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
-//         cx.foreground().run_until_parked();
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v src",
-//                 "    v test",
-//                 "          first.rs  <== selected",
-//                 "          second.rs",
-//                 "          third.rs"
-//             ]
-//         );
-//         panel.update(cx, |panel, cx| {
-//             panel.new_search_in_directory(&NewSearchInDirectory, cx)
-//         });
-//         assert_eq!(
-//             new_search_events_count.load(atomic::Ordering::SeqCst),
-//             0,
-//             "Should not trigger new search in directory when called on a file"
-//         );
-
-//         select_path(&panel, "src/test", cx);
-//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
-//         cx.foreground().run_until_parked();
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v src",
-//                 "    v test  <== selected",
-//                 "          first.rs",
-//                 "          second.rs",
-//                 "          third.rs"
-//             ]
-//         );
-//         panel.update(cx, |panel, cx| {
-//             panel.new_search_in_directory(&NewSearchInDirectory, cx)
-//         });
-//         assert_eq!(
-//             new_search_events_count.load(atomic::Ordering::SeqCst),
-//             1,
-//             "Should trigger new search in directory when called on a directory"
-//         );
-//     }
-
-//     #[gpui::test]
-//     async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
-//         init_test_with_editor(cx);
-
-//         let fs = FakeFs::new(cx.background());
-//         fs.insert_tree(
-//             "/project_root",
-//             json!({
-//                 "dir_1": {
-//                     "nested_dir": {
-//                         "file_a.py": "# File contents",
-//                         "file_b.py": "# File contents",
-//                         "file_c.py": "# File contents",
-//                     },
-//                     "file_1.py": "# File contents",
-//                     "file_2.py": "# File contents",
-//                     "file_3.py": "# File contents",
-//                 },
-//                 "dir_2": {
-//                     "file_1.py": "# File contents",
-//                     "file_2.py": "# File contents",
-//                     "file_3.py": "# File contents",
-//                 }
-//             }),
-//         )
-//         .await;
-
-//         let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
-//         let workspace = cx
-//             .add_window(|cx| Workspace::test_new(project.clone(), cx))
-//             .root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-//         panel.update(cx, |panel, cx| {
-//             panel.collapse_all_entries(&CollapseAllEntries, cx)
-//         });
-//         cx.foreground().run_until_parked();
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &["v project_root", "    > dir_1", "    > dir_2",]
-//         );
-
-//         // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
-//         toggle_expand_dir(&panel, "project_root/dir_1", cx);
-//         cx.foreground().run_until_parked();
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v project_root",
-//                 "    v dir_1  <== selected",
-//                 "        > nested_dir",
-//                 "          file_1.py",
-//                 "          file_2.py",
-//                 "          file_3.py",
-//                 "    > dir_2",
-//             ]
-//         );
-//     }
-
-//     #[gpui::test]
-//     async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
-//         init_test(cx);
-
-//         let fs = FakeFs::new(cx.background());
-//         fs.as_fake().insert_tree("/root", json!({})).await;
-//         let project = Project::test(fs, ["/root".as_ref()], cx).await;
-//         let workspace = cx
-//             .add_window(|cx| Workspace::test_new(project.clone(), cx))
-//             .root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-//         // Make a new buffer with no backing file
-//         workspace.update(cx, |workspace, cx| {
-//             Editor::new_file(workspace, &Default::default(), cx)
-//         });
-
-//         // "Save as"" the buffer, creating a new backing file for it
-//         let task = workspace.update(cx, |workspace, cx| {
-//             workspace.save_active_item(workspace::SaveIntent::Save, cx)
-//         });
-
-//         cx.foreground().run_until_parked();
-//         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new")));
-//         task.await.unwrap();
-
-//         // Rename the file
-//         select_path(&panel, "root/new", cx);
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &["v root", "      new  <== selected"]
-//         );
-//         panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
-//         panel.update(cx, |panel, cx| {
-//             panel
-//                 .filename_editor
-//                 .update(cx, |editor, cx| editor.set_text("newer", cx));
-//         });
-//         panel
-//             .update(cx, |panel, cx| panel.confirm(&Confirm, cx))
-//             .unwrap()
-//             .await
-//             .unwrap();
-
-//         cx.foreground().run_until_parked();
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &["v root", "      newer  <== selected"]
-//         );
-
-//         workspace
-//             .update(cx, |workspace, cx| {
-//                 workspace.save_active_item(workspace::SaveIntent::Save, cx)
-//             })
-//             .await
-//             .unwrap();
-
-//         cx.foreground().run_until_parked();
-//         // assert that saving the file doesn't restore "new"
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &["v root", "      newer  <== selected"]
-//         );
-//     }
-
-//     fn toggle_expand_dir(
-//         panel: &View<ProjectPanel>,
-//         path: impl AsRef<Path>,
-//         cx: &mut TestAppContext,
-//     ) {
-//         let path = path.as_ref();
-//         panel.update(cx, |panel, cx| {
-//             for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
-//                 let worktree = worktree.read(cx);
-//                 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
-//                     let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
-//                     panel.toggle_expanded(entry_id, cx);
-//                     return;
-//                 }
-//             }
-//             panic!("no worktree for path {:?}", path);
-//         });
-//     }
-
-//     fn select_path(panel: &View<ProjectPanel>, path: impl AsRef<Path>, cx: &mut TestAppContext) {
-//         let path = path.as_ref();
-//         panel.update(cx, |panel, cx| {
-//             for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
-//                 let worktree = worktree.read(cx);
-//                 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
-//                     let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
-//                     panel.selection = Some(Selection {
-//                         worktree_id: worktree.id(),
-//                         entry_id,
-//                     });
-//                     return;
-//                 }
-//             }
-//             panic!("no worktree for path {:?}", path);
-//         });
-//     }
-
-//     fn visible_entries_as_strings(
-//         panel: &View<ProjectPanel>,
-//         range: Range<usize>,
-//         cx: &mut TestAppContext,
-//     ) -> Vec<String> {
-//         let mut result = Vec::new();
-//         let mut project_entries = HashSet::new();
-//         let mut has_editor = false;
-
-//         panel.update(cx, |panel, cx| {
-//             panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
-//                 if details.is_editing {
-//                     assert!(!has_editor, "duplicate editor entry");
-//                     has_editor = true;
-//                 } else {
-//                     assert!(
-//                         project_entries.insert(project_entry),
-//                         "duplicate project entry {:?} {:?}",
-//                         project_entry,
-//                         details
-//                     );
-//                 }
-
-//                 let indent = "    ".repeat(details.depth);
-//                 let icon = if details.kind.is_dir() {
-//                     if details.is_expanded {
-//                         "v "
-//                     } else {
-//                         "> "
-//                     }
-//                 } else {
-//                     "  "
-//                 };
-//                 let name = if details.is_editing {
-//                     format!("[EDITOR: '{}']", details.filename)
-//                 } else if details.is_processing {
-//                     format!("[PROCESSING: '{}']", details.filename)
-//                 } else {
-//                     details.filename.clone()
-//                 };
-//                 let selected = if details.is_selected {
-//                     "  <== selected"
-//                 } else {
-//                     ""
-//                 };
-//                 result.push(format!("{indent}{icon}{name}{selected}"));
-//             });
-//         });
-
-//         result
-//     }
-
-//     fn init_test(cx: &mut TestAppContext) {
-//         cx.foreground().forbid_parking();
-//         cx.update(|cx| {
-//             cx.set_global(SettingsStore::test(cx));
-//             init_settings(cx);
-//             theme::init(cx);
-//             language::init(cx);
-//             editor::init_settings(cx);
-//             crate::init((), cx);
-//             workspace::init_settings(cx);
-//             client::init_settings(cx);
-//             Project::init_settings(cx);
-//         });
-//     }
-
-//     fn init_test_with_editor(cx: &mut TestAppContext) {
-//         cx.foreground().forbid_parking();
-//         cx.update(|cx| {
-//             let app_state = AppState::test(cx);
-//             theme::init(cx);
-//             init_settings(cx);
-//             language::init(cx);
-//             editor::init(cx);
-//             pane::init(cx);
-//             crate::init((), cx);
-//             workspace::init(app_state.clone(), cx);
-//             Project::init_settings(cx);
-//         });
-//     }
-
-//     fn ensure_single_file_is_opened(
-//         window: WindowHandle<Workspace>,
-//         expected_path: &str,
-//         cx: &mut TestAppContext,
-//     ) {
-//         window.update_root(cx, |workspace, cx| {
-//             let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
-//             assert_eq!(worktrees.len(), 1);
-//             let worktree_id = WorktreeId::from_usize(worktrees[0].id());
-
-//             let open_project_paths = workspace
-//                 .panes()
-//                 .iter()
-//                 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
-//                 .collect::<Vec<_>>();
-//             assert_eq!(
-//                 open_project_paths,
-//                 vec![ProjectPath {
-//                     worktree_id,
-//                     path: Arc::from(Path::new(expected_path))
-//                 }],
-//                 "Should have opened file, selected in project panel"
-//             );
-//         });
-//     }
-
-//     fn submit_deletion(
-//         window: AnyWindowHandle,
-//         panel: &View<ProjectPanel>,
-//         cx: &mut TestAppContext,
-//     ) {
-//         assert!(
-//             !window.has_pending_prompt(cx),
-//             "Should have no prompts before the deletion"
-//         );
-//         panel.update(cx, |panel, cx| {
-//             panel
-//                 .delete(&Delete, cx)
-//                 .expect("Deletion start")
-//                 .detach_and_log_err(cx);
-//         });
-//         assert!(
-//             window.has_pending_prompt(cx),
-//             "Should have a prompt after the deletion"
-//         );
-//         window.simulate_prompt_answer(0, cx);
-//         assert!(
-//             !window.has_pending_prompt(cx),
-//             "Should have no prompts after prompt was replied to"
-//         );
-//         cx.foreground().run_until_parked();
-//     }
-
-//     fn ensure_no_open_items_and_panes(
-//         window: AnyWindowHandle,
-//         workspace: &View<Workspace>,
-//         cx: &mut TestAppContext,
-//     ) {
-//         assert!(
-//             !window.has_pending_prompt(cx),
-//             "Should have no prompts after deletion operation closes the file"
-//         );
-//         window.read_with(cx, |cx| {
-//             let open_project_paths = workspace
-//                 .read(cx)
-//                 .panes()
-//                 .iter()
-//                 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
-//                 .collect::<Vec<_>>();
-//             assert!(
-//                 open_project_paths.is_empty(),
-//                 "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
-//             );
-//         });
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::{TestAppContext, View, VisualTestContext, WindowHandle};
+    use pretty_assertions::assert_eq;
+    use project::FakeFs;
+    use serde_json::json;
+    use settings::SettingsStore;
+    use std::{
+        collections::HashSet,
+        path::{Path, PathBuf},
+        sync::atomic::{self, AtomicUsize},
+    };
+    use workspace::{pane, AppState};
+
+    #[gpui::test]
+    async fn test_visible_list(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/root1",
+            json!({
+                ".dockerignore": "",
+                ".git": {
+                    "HEAD": "",
+                },
+                "a": {
+                    "0": { "q": "", "r": "", "s": "" },
+                    "1": { "t": "", "u": "" },
+                    "2": { "v": "", "w": "", "x": "", "y": "" },
+                },
+                "b": {
+                    "3": { "Q": "" },
+                    "4": { "R": "", "S": "", "T": "", "U": "" },
+                },
+                "C": {
+                    "5": {},
+                    "6": { "V": "", "W": "" },
+                    "7": { "X": "" },
+                    "8": { "Y": {}, "Z": "" }
+                }
+            }),
+        )
+        .await;
+        fs.insert_tree(
+            "/root2",
+            json!({
+                "d": {
+                    "9": ""
+                },
+                "e": {}
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
+        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..50, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        toggle_expand_dir(&panel, "root1/b", cx);
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..50, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b  <== selected",
+                "        > 3",
+                "        > 4",
+                "    > C",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 6..9, cx),
+            &[
+                //
+                "    > C",
+                "      .dockerignore",
+                "v root2",
+            ]
+        );
+    }
+
+    #[gpui::test(iterations = 30)]
+    async fn test_editing_files(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/root1",
+            json!({
+                ".dockerignore": "",
+                ".git": {
+                    "HEAD": "",
+                },
+                "a": {
+                    "0": { "q": "", "r": "", "s": "" },
+                    "1": { "t": "", "u": "" },
+                    "2": { "v": "", "w": "", "x": "", "y": "" },
+                },
+                "b": {
+                    "3": { "Q": "" },
+                    "4": { "R": "", "S": "", "T": "", "U": "" },
+                },
+                "C": {
+                    "5": {},
+                    "6": { "V": "", "W": "" },
+                    "7": { "X": "" },
+                    "8": { "Y": {}, "Z": "" }
+                }
+            }),
+        )
+        .await;
+        fs.insert_tree(
+            "/root2",
+            json!({
+                "d": {
+                    "9": ""
+                },
+                "e": {}
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
+        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+
+        select_path(&panel, "root1", cx);
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1  <== selected",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        // Add a file with the root folder selected. The filename editor is placed
+        // before the first file in the root folder.
+        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
+        panel.update(cx, |panel, cx| {
+            assert!(panel.filename_editor.read(cx).is_focused(cx));
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      [EDITOR: '']  <== selected",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        let confirm = panel.update(cx, |panel, cx| {
+            panel
+                .filename_editor
+                .update(cx, |editor, cx| editor.set_text("the-new-filename", cx));
+            panel.confirm_edit(cx).unwrap()
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      [PROCESSING: 'the-new-filename']  <== selected",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        confirm.await.unwrap();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      .dockerignore",
+                "      the-new-filename  <== selected",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        select_path(&panel, "root1/b", cx);
+        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > 3",
+                "        > 4",
+                "          [EDITOR: '']  <== selected",
+                "    > C",
+                "      .dockerignore",
+                "      the-new-filename",
+            ]
+        );
+
+        panel
+            .update(cx, |panel, cx| {
+                panel
+                    .filename_editor
+                    .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx));
+                panel.confirm_edit(cx).unwrap()
+            })
+            .await
+            .unwrap();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > 3",
+                "        > 4",
+                "          another-filename.txt  <== selected",
+                "    > C",
+                "      .dockerignore",
+                "      the-new-filename",
+            ]
+        );
+
+        select_path(&panel, "root1/b/another-filename.txt", cx);
+        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > 3",
+                "        > 4",
+                "          [EDITOR: 'another-filename.txt']  <== selected",
+                "    > C",
+                "      .dockerignore",
+                "      the-new-filename",
+            ]
+        );
+
+        let confirm = panel.update(cx, |panel, cx| {
+            panel.filename_editor.update(cx, |editor, cx| {
+                let file_name_selections = editor.selections.all::<usize>(cx);
+                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
+                let file_name_selection = &file_name_selections[0];
+                assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
+                assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension");
+
+                editor.set_text("a-different-filename.tar.gz", cx)
+            });
+            panel.confirm_edit(cx).unwrap()
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > 3",
+                "        > 4",
+                "          [PROCESSING: 'a-different-filename.tar.gz']  <== selected",
+                "    > C",
+                "      .dockerignore",
+                "      the-new-filename",
+            ]
+        );
+
+        confirm.await.unwrap();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > 3",
+                "        > 4",
+                "          a-different-filename.tar.gz  <== selected",
+                "    > C",
+                "      .dockerignore",
+                "      the-new-filename",
+            ]
+        );
+
+        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > 3",
+                "        > 4",
+                "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
+                "    > C",
+                "      .dockerignore",
+                "      the-new-filename",
+            ]
+        );
+
+        panel.update(cx, |panel, cx| {
+            panel.filename_editor.update(cx, |editor, cx| {
+                let file_name_selections = editor.selections.all::<usize>(cx);
+                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
+                let file_name_selection = &file_name_selections[0];
+                assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
+                assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot..");
+
+            });
+            panel.cancel(&Cancel, cx)
+        });
+
+        panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > [EDITOR: '']  <== selected",
+                "        > 3",
+                "        > 4",
+                "          a-different-filename.tar.gz",
+                "    > C",
+                "      .dockerignore",
+            ]
+        );
+
+        let confirm = panel.update(cx, |panel, cx| {
+            panel
+                .filename_editor
+                .update(cx, |editor, cx| editor.set_text("new-dir", cx));
+            panel.confirm_edit(cx).unwrap()
+        });
+        panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx));
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > [PROCESSING: 'new-dir']",
+                "        > 3  <== selected",
+                "        > 4",
+                "          a-different-filename.tar.gz",
+                "    > C",
+                "      .dockerignore",
+            ]
+        );
+
+        confirm.await.unwrap();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > 3  <== selected",
+                "        > 4",
+                "        > new-dir",
+                "          a-different-filename.tar.gz",
+                "    > C",
+                "      .dockerignore",
+            ]
+        );
+
+        panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx));
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > [EDITOR: '3']  <== selected",
+                "        > 4",
+                "        > new-dir",
+                "          a-different-filename.tar.gz",
+                "    > C",
+                "      .dockerignore",
+            ]
+        );
+
+        // Dismiss the rename editor when it loses focus.
+        workspace.update(cx, |_, cx| cx.blur()).unwrap();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > 3  <== selected",
+                "        > 4",
+                "        > new-dir",
+                "          a-different-filename.tar.gz",
+                "    > C",
+                "      .dockerignore",
+            ]
+        );
+    }
+
+    #[gpui::test(iterations = 10)]
+    async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/root1",
+            json!({
+                ".dockerignore": "",
+                ".git": {
+                    "HEAD": "",
+                },
+                "a": {
+                    "0": { "q": "", "r": "", "s": "" },
+                    "1": { "t": "", "u": "" },
+                    "2": { "v": "", "w": "", "x": "", "y": "" },
+                },
+                "b": {
+                    "3": { "Q": "" },
+                    "4": { "R": "", "S": "", "T": "", "U": "" },
+                },
+                "C": {
+                    "5": {},
+                    "6": { "V": "", "W": "" },
+                    "7": { "X": "" },
+                    "8": { "Y": {}, "Z": "" }
+                }
+            }),
+        )
+        .await;
+        fs.insert_tree(
+            "/root2",
+            json!({
+                "d": {
+                    "9": ""
+                },
+                "e": {}
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
+        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+
+        select_path(&panel, "root1", cx);
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1  <== selected",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        // Add a file with the root folder selected. The filename editor is placed
+        // before the first file in the root folder.
+        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
+        panel.update(cx, |panel, cx| {
+            assert!(panel.filename_editor.read(cx).is_focused(cx));
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      [EDITOR: '']  <== selected",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        let confirm = panel.update(cx, |panel, cx| {
+            panel.filename_editor.update(cx, |editor, cx| {
+                editor.set_text("/bdir1/dir2/the-new-filename", cx)
+            });
+            panel.confirm_edit(cx).unwrap()
+        });
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      [PROCESSING: '/bdir1/dir2/the-new-filename']  <== selected",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        confirm.await.unwrap();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..13, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    v bdir1",
+                "        v dir2",
+                "              the-new-filename  <== selected",
+                "    > C",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/root1",
+            json!({
+                "one.two.txt": "",
+                "one.txt": ""
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
+        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+
+        panel.update(cx, |panel, cx| {
+            panel.select_next(&Default::default(), cx);
+            panel.select_next(&Default::default(), cx);
+        });
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..50, cx),
+            &[
+                //
+                "v root1",
+                "      one.two.txt  <== selected",
+                "      one.txt",
+            ]
+        );
+
+        // Regression test - file name is created correctly when
+        // the copied file's name contains multiple dots.
+        panel.update(cx, |panel, cx| {
+            panel.copy(&Default::default(), cx);
+            panel.paste(&Default::default(), cx);
+        });
+        cx.executor().run_until_parked();
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..50, cx),
+            &[
+                //
+                "v root1",
+                "      one.two copy.txt",
+                "      one.two.txt  <== selected",
+                "      one.txt",
+            ]
+        );
+
+        panel.update(cx, |panel, cx| {
+            panel.paste(&Default::default(), cx);
+        });
+        cx.executor().run_until_parked();
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..50, cx),
+            &[
+                //
+                "v root1",
+                "      one.two copy 1.txt",
+                "      one.two copy.txt",
+                "      one.two.txt  <== selected",
+                "      one.txt",
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
+        init_test_with_editor(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/src",
+            json!({
+                "test": {
+                    "first.rs": "// First Rust file",
+                    "second.rs": "// Second Rust file",
+                    "third.rs": "// Third Rust file",
+                }
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
+        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+
+        toggle_expand_dir(&panel, "src/test", cx);
+        select_path(&panel, "src/test/first.rs", cx);
+        panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          first.rs  <== selected",
+                "          second.rs",
+                "          third.rs"
+            ]
+        );
+        ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
+
+        submit_deletion(&panel, cx);
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          second.rs",
+                "          third.rs"
+            ],
+            "Project panel should have no deleted file, no other file is selected in it"
+        );
+        ensure_no_open_items_and_panes(&workspace, cx);
+
+        select_path(&panel, "src/test/second.rs", cx);
+        panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          second.rs  <== selected",
+                "          third.rs"
+            ]
+        );
+        ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
+
+        workspace
+            .update(cx, |workspace, cx| {
+                let active_items = workspace
+                    .panes()
+                    .iter()
+                    .filter_map(|pane| pane.read(cx).active_item())
+                    .collect::<Vec<_>>();
+                assert_eq!(active_items.len(), 1);
+                let open_editor = active_items
+                    .into_iter()
+                    .next()
+                    .unwrap()
+                    .downcast::<Editor>()
+                    .expect("Open item should be an editor");
+                open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
+            })
+            .unwrap();
+        submit_deletion(&panel, cx);
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &["v src", "    v test", "          third.rs"],
+            "Project panel should have no deleted file, with one last file remaining"
+        );
+        ensure_no_open_items_and_panes(&workspace, cx);
+    }
+
+    #[gpui::test]
+    async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
+        init_test_with_editor(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/src",
+            json!({
+                "test": {
+                    "first.rs": "// First Rust file",
+                    "second.rs": "// Second Rust file",
+                    "third.rs": "// Third Rust file",
+                }
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
+        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+
+        select_path(&panel, "src/", cx);
+        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                //
+                "v src  <== selected",
+                "    > test"
+            ]
+        );
+        panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
+        panel.update(cx, |panel, cx| {
+            assert!(panel.filename_editor.read(cx).is_focused(cx));
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                //
+                "v src",
+                "    > [EDITOR: '']  <== selected",
+                "    > test"
+            ]
+        );
+        panel.update(cx, |panel, cx| {
+            panel
+                .filename_editor
+                .update(cx, |editor, cx| editor.set_text("test", cx));
+            assert!(
+                panel.confirm_edit(cx).is_none(),
+                "Should not allow to confirm on conflicting new directory name"
+            )
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                //
+                "v src",
+                "    > test"
+            ],
+            "File list should be unchanged after failed folder create confirmation"
+        );
+
+        select_path(&panel, "src/test/", cx);
+        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                //
+                "v src",
+                "    > test  <== selected"
+            ]
+        );
+        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
+        panel.update(cx, |panel, cx| {
+            assert!(panel.filename_editor.read(cx).is_focused(cx));
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          [EDITOR: '']  <== selected",
+                "          first.rs",
+                "          second.rs",
+                "          third.rs"
+            ]
+        );
+        panel.update(cx, |panel, cx| {
+            panel
+                .filename_editor
+                .update(cx, |editor, cx| editor.set_text("first.rs", cx));
+            assert!(
+                panel.confirm_edit(cx).is_none(),
+                "Should not allow to confirm on conflicting new file name"
+            )
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          first.rs",
+                "          second.rs",
+                "          third.rs"
+            ],
+            "File list should be unchanged after failed file create confirmation"
+        );
+
+        select_path(&panel, "src/test/first.rs", cx);
+        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          first.rs  <== selected",
+                "          second.rs",
+                "          third.rs"
+            ],
+        );
+        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
+        panel.update(cx, |panel, cx| {
+            assert!(panel.filename_editor.read(cx).is_focused(cx));
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          [EDITOR: 'first.rs']  <== selected",
+                "          second.rs",
+                "          third.rs"
+            ]
+        );
+        panel.update(cx, |panel, cx| {
+            panel
+                .filename_editor
+                .update(cx, |editor, cx| editor.set_text("second.rs", cx));
+            assert!(
+                panel.confirm_edit(cx).is_none(),
+                "Should not allow to confirm on conflicting file rename"
+            )
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          first.rs  <== selected",
+                "          second.rs",
+                "          third.rs"
+            ],
+            "File list should be unchanged after failed rename confirmation"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) {
+        init_test_with_editor(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/src",
+            json!({
+                "test": {
+                    "first.rs": "// First Rust file",
+                    "second.rs": "// Second Rust file",
+                    "third.rs": "// Third Rust file",
+                }
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
+        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+
+        let new_search_events_count = Arc::new(AtomicUsize::new(0));
+        let _subscription = panel.update(cx, |_, cx| {
+            let subcription_count = Arc::clone(&new_search_events_count);
+            let view = cx.view().clone();
+            cx.subscribe(&view, move |_, _, event, _| {
+                if matches!(event, Event::NewSearchInDirectory { .. }) {
+                    subcription_count.fetch_add(1, atomic::Ordering::SeqCst);
+                }
+            })
+        });
+
+        toggle_expand_dir(&panel, "src/test", cx);
+        select_path(&panel, "src/test/first.rs", cx);
+        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          first.rs  <== selected",
+                "          second.rs",
+                "          third.rs"
+            ]
+        );
+        panel.update(cx, |panel, cx| {
+            panel.new_search_in_directory(&NewSearchInDirectory, cx)
+        });
+        assert_eq!(
+            new_search_events_count.load(atomic::Ordering::SeqCst),
+            0,
+            "Should not trigger new search in directory when called on a file"
+        );
+
+        select_path(&panel, "src/test", cx);
+        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test  <== selected",
+                "          first.rs",
+                "          second.rs",
+                "          third.rs"
+            ]
+        );
+        panel.update(cx, |panel, cx| {
+            panel.new_search_in_directory(&NewSearchInDirectory, cx)
+        });
+        assert_eq!(
+            new_search_events_count.load(atomic::Ordering::SeqCst),
+            1,
+            "Should trigger new search in directory when called on a directory"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
+        init_test_with_editor(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/project_root",
+            json!({
+                "dir_1": {
+                    "nested_dir": {
+                        "file_a.py": "# File contents",
+                        "file_b.py": "# File contents",
+                        "file_c.py": "# File contents",
+                    },
+                    "file_1.py": "# File contents",
+                    "file_2.py": "# File contents",
+                    "file_3.py": "# File contents",
+                },
+                "dir_2": {
+                    "file_1.py": "# File contents",
+                    "file_2.py": "# File contents",
+                    "file_3.py": "# File contents",
+                }
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
+        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+
+        panel.update(cx, |panel, cx| {
+            panel.collapse_all_entries(&CollapseAllEntries, cx)
+        });
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &["v project_root", "    > dir_1", "    > dir_2",]
+        );
+
+        // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
+        toggle_expand_dir(&panel, "project_root/dir_1", cx);
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v project_root",
+                "    v dir_1  <== selected",
+                "        > nested_dir",
+                "          file_1.py",
+                "          file_2.py",
+                "          file_3.py",
+                "    > dir_2",
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.as_fake().insert_tree("/root", json!({})).await;
+        let project = Project::test(fs, ["/root".as_ref()], cx).await;
+        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+
+        // Make a new buffer with no backing file
+        workspace
+            .update(cx, |workspace, cx| {
+                Editor::new_file(workspace, &Default::default(), cx)
+            })
+            .unwrap();
+
+        // "Save as"" the buffer, creating a new backing file for it
+        let save_task = workspace
+            .update(cx, |workspace, cx| {
+                workspace.save_active_item(workspace::SaveIntent::Save, cx)
+            })
+            .unwrap();
+
+        cx.executor().run_until_parked();
+        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new")));
+        save_task.await.unwrap();
+
+        // Rename the file
+        select_path(&panel, "root/new", cx);
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &["v root", "      new  <== selected"]
+        );
+        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
+        panel.update(cx, |panel, cx| {
+            panel
+                .filename_editor
+                .update(cx, |editor, cx| editor.set_text("newer", cx));
+        });
+        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &["v root", "      newer  <== selected"]
+        );
+
+        workspace
+            .update(cx, |workspace, cx| {
+                workspace.save_active_item(workspace::SaveIntent::Save, cx)
+            })
+            .unwrap()
+            .await
+            .unwrap();
+
+        cx.executor().run_until_parked();
+        // assert that saving the file doesn't restore "new"
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &["v root", "      newer  <== selected"]
+        );
+    }
+
+    fn toggle_expand_dir(
+        panel: &View<ProjectPanel>,
+        path: impl AsRef<Path>,
+        cx: &mut VisualTestContext,
+    ) {
+        let path = path.as_ref();
+        panel.update(cx, |panel, cx| {
+            for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
+                let worktree = worktree.read(cx);
+                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
+                    let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
+                    panel.toggle_expanded(entry_id, cx);
+                    return;
+                }
+            }
+            panic!("no worktree for path {:?}", path);
+        });
+    }
+
+    fn select_path(panel: &View<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
+        let path = path.as_ref();
+        panel.update(cx, |panel, cx| {
+            for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
+                let worktree = worktree.read(cx);
+                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
+                    let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
+                    panel.selection = Some(Selection {
+                        worktree_id: worktree.id(),
+                        entry_id,
+                    });
+                    return;
+                }
+            }
+            panic!("no worktree for path {:?}", path);
+        });
+    }
+
+    fn visible_entries_as_strings(
+        panel: &View<ProjectPanel>,
+        range: Range<usize>,
+        cx: &mut VisualTestContext,
+    ) -> Vec<String> {
+        let mut result = Vec::new();
+        let mut project_entries = HashSet::new();
+        let mut has_editor = false;
+
+        panel.update(cx, |panel, cx| {
+            panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
+                if details.is_editing {
+                    assert!(!has_editor, "duplicate editor entry");
+                    has_editor = true;
+                } else {
+                    assert!(
+                        project_entries.insert(project_entry),
+                        "duplicate project entry {:?} {:?}",
+                        project_entry,
+                        details
+                    );
+                }
+
+                let indent = "    ".repeat(details.depth);
+                let icon = if details.kind.is_dir() {
+                    if details.is_expanded {
+                        "v "
+                    } else {
+                        "> "
+                    }
+                } else {
+                    "  "
+                };
+                let name = if details.is_editing {
+                    format!("[EDITOR: '{}']", details.filename)
+                } else if details.is_processing {
+                    format!("[PROCESSING: '{}']", details.filename)
+                } else {
+                    details.filename.clone()
+                };
+                let selected = if details.is_selected {
+                    "  <== selected"
+                } else {
+                    ""
+                };
+                result.push(format!("{indent}{icon}{name}{selected}"));
+            });
+        });
+
+        result
+    }
+
+    fn init_test(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            let settings_store = SettingsStore::test(cx);
+            cx.set_global(settings_store);
+            init_settings(cx);
+            theme::init(cx);
+            language::init(cx);
+            editor::init_settings(cx);
+            crate::init((), cx);
+            workspace::init_settings(cx);
+            client::init_settings(cx);
+            Project::init_settings(cx);
+        });
+    }
+
+    fn init_test_with_editor(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            let app_state = AppState::test(cx);
+            theme::init(cx);
+            init_settings(cx);
+            language::init(cx);
+            editor::init(cx);
+            pane::init(cx);
+            crate::init((), cx);
+            workspace::init(app_state.clone(), cx);
+            Project::init_settings(cx);
+        });
+    }
+
+    fn ensure_single_file_is_opened(
+        window: &WindowHandle<Workspace>,
+        expected_path: &str,
+        cx: &mut TestAppContext,
+    ) {
+        window
+            .update(cx, |workspace, cx| {
+                let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
+                assert_eq!(worktrees.len(), 1);
+                let worktree_id = worktrees[0].read(cx).id();
+
+                let open_project_paths = workspace
+                    .panes()
+                    .iter()
+                    .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
+                    .collect::<Vec<_>>();
+                assert_eq!(
+                    open_project_paths,
+                    vec![ProjectPath {
+                        worktree_id,
+                        path: Arc::from(Path::new(expected_path))
+                    }],
+                    "Should have opened file, selected in project panel"
+                );
+            })
+            .unwrap();
+    }
+
+    fn submit_deletion(panel: &View<ProjectPanel>, cx: &mut VisualTestContext) {
+        assert!(
+            !cx.has_pending_prompt(),
+            "Should have no prompts before the deletion"
+        );
+        panel.update(cx, |panel, cx| panel.delete(&Delete, cx));
+        assert!(
+            cx.has_pending_prompt(),
+            "Should have a prompt after the deletion"
+        );
+        cx.simulate_prompt_answer(0);
+        assert!(
+            !cx.has_pending_prompt(),
+            "Should have no prompts after prompt was replied to"
+        );
+        cx.executor().run_until_parked();
+    }
+
+    fn ensure_no_open_items_and_panes(
+        workspace: &WindowHandle<Workspace>,
+        cx: &mut VisualTestContext,
+    ) {
+        assert!(
+            !cx.has_pending_prompt(),
+            "Should have no prompts after deletion operation closes the file"
+        );
+        workspace
+            .read_with(cx, |workspace, cx| {
+                let open_project_paths = workspace
+                    .panes()
+                    .iter()
+                    .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
+                    .collect::<Vec<_>>();
+                assert!(
+                    open_project_paths.is_empty(),
+                    "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
+                );
+            })
+            .unwrap();
+    }
+}