diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index ebe78d95b3..02365c3c23 100644 --- a/crates/editor2/src/editor.rs +++ b/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) { if self.focus_handle.is_focused(cx) { // todo!() diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 51c564fdd8..9a1e4ecd54 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/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, } 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, + ) { + 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(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task where Fut: Future + 'static, diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 79a80a3d51..df1ed9b2a6 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/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>>, active_display: Rc, active_cursor: Mutex, + pub(crate) prompts: RefCell, + weak: Weak, +} + +#[derive(Default)] +pub(crate) struct TestPrompts { + multiple_choice: VecDeque>, + new_path: VecDeque<(PathBuf, oneshot::Sender>)>, } impl TestPlatform { - pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Self { - TestPlatform { + pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc { + 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, + ) { + 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 { + 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 { *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>> { + ) -> oneshot::Receiver>> { unimplemented!() } fn prompt_for_new_path( &self, - _directory: &std::path::Path, - ) -> futures::channel::oneshot::Receiver> { - unimplemented!() + directory: &std::path::Path, + ) -> oneshot::Receiver> { + 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) { diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index cf9143162e..adb15c4266 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -1,15 +1,13 @@ -use std::{ - rc::Rc, - sync::{self, Arc}, -}; - -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, + PlatformInputHandler, PlatformWindow, Point, Scene, Size, TestPlatform, TileId, + WindowAppearance, WindowBounds, WindowOptions, +}; +use collections::HashMap; +use parking_lot::Mutex; +use std::{ + rc::{Rc, Weak}, + sync::{self, Arc}, }; #[derive(Default)] @@ -25,16 +23,22 @@ pub struct TestWindow { current_scene: Mutex>, display: Rc, input_handler: Option>, - handlers: Mutex, + platform: Weak, sprite_atlas: Arc, } + impl TestWindow { - pub fn new(options: WindowOptions, display: Rc) -> Self { + pub fn new( + options: WindowOptions, + platform: Weak, + display: Rc, + ) -> 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 { - todo!() + self.platform.upgrade().expect("platform dropped").prompt() } fn activate(&self) { diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 1f415e899e..a43c67deaf 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/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::(|_, 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::(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::(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::>(); -// assert_eq!(active_items.len(), 1); -// let open_editor = active_items -// .into_iter() -// .next() -// .unwrap() -// .downcast::() -// .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, -// path: impl AsRef, -// cx: &mut TestAppContext, -// ) { -// let path = path.as_ref(); -// panel.update(cx, |panel, cx| { -// for worktree in panel.project.read(cx).worktrees().collect::>() { -// 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, path: impl AsRef, cx: &mut TestAppContext) { -// let path = path.as_ref(); -// panel.update(cx, |panel, cx| { -// for worktree in panel.project.read(cx).worktrees().collect::>() { -// 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, -// range: Range, -// cx: &mut TestAppContext, -// ) -> Vec { -// 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, -// expected_path: &str, -// cx: &mut TestAppContext, -// ) { -// window.update_root(cx, |workspace, cx| { -// let worktrees = workspace.worktrees(cx).collect::>(); -// 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::>(); -// 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, -// 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, -// 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::>(); -// 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::(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::(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::>(); + assert_eq!(active_items.len(), 1); + let open_editor = active_items + .into_iter() + .next() + .unwrap() + .downcast::() + .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, + path: impl AsRef, + cx: &mut VisualTestContext, + ) { + let path = path.as_ref(); + panel.update(cx, |panel, cx| { + for worktree in panel.project.read(cx).worktrees().collect::>() { + 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, path: impl AsRef, cx: &mut VisualTestContext) { + let path = path.as_ref(); + panel.update(cx, |panel, cx| { + for worktree in panel.project.read(cx).worktrees().collect::>() { + 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, + range: Range, + cx: &mut VisualTestContext, + ) -> Vec { + 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, + expected_path: &str, + cx: &mut TestAppContext, + ) { + window + .update(cx, |workspace, cx| { + let worktrees = workspace.worktrees(cx).collect::>(); + 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::>(); + 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, 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, + 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::>(); + assert!( + open_project_paths.is_empty(), + "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" + ); + }) + .unwrap(); + } +}