#![cfg(test)] use client::proto;use collections::HashSet;use fs::{FakeFs, Fs};use gpui::{App, BorrowAppContext, Entity, VisualTestContext};use project::Project;use serde_json::{Value, json};use settings::SettingsStore;use std::path::Path;use std::sync::Arc;use workspace::{MultiWorkspace, register_project_item}; use crate::project_panel_tests::{self, TestProjectItemView, find_project_entry, select_path};use crate::{NewDirectory, NewFile, ProjectPanel, Redo, Rename, Trash, Undo}; struct TestContext { panel: Entity<ProjectPanel>, fs: Arc<FakeFs>, cx: VisualTestContext,} // Using the `util::path` macro requires a string literal, which would mean that// callers of, for example, `rename`, would now need to know about `/` and// use `path!` in tests.//// As such, we define it as a function here to make the helper methods more// ergonomic for our use case.fn path(path: impl AsRef<str>) -> String { let path = path.as_ref(); #[cfg(target_os = "windows")] { let mut path = path.replace("/", "\\"); if path.starts_with("\\") { path = format!("C:{}", &path); } path } #[cfg(not(target_os = "windows"))] { path.to_string() }} impl TestContext { async fn undo(&mut self) { self.panel.update_in(&mut self.cx, |panel, window, cx| { panel.undo(&Undo, window, cx); }); self.cx.run_until_parked(); } async fn redo(&mut self) { self.panel.update_in(&mut self.cx, |panel, window, cx| { panel.redo(&Redo, window, cx); }); self.cx.run_until_parked(); } /// Note this only works when every file has an extension fn assert_fs_state_is(&mut self, state: &[&str]) { let state: HashSet<_> = state .into_iter() .map(|s| path(format!("/workspace/{s}"))) .chain([path("/workspace"), path("/")]) .map(|s| Path::new(&s).to_path_buf()) .collect(); let dirs: HashSet<_> = state .iter() .map(|p| match p.extension() { Some(_) => p.parent().unwrap_or(Path::new(&path("/"))).to_owned(), None => p.clone(), }) .collect(); assert_eq!( self.fs .directories(true) .into_iter() .collect::<HashSet<_>>(), dirs ); assert_eq!( self.fs.paths(true).into_iter().collect::<HashSet<_>>(), state ); } fn assert_exists(&mut self, file: &str) { assert!( find_project_entry(&self.panel, &format!("workspace/{file}"), &mut self.cx).is_some(), "{file} should exist" ); } fn assert_not_exists(&mut self, file: &str) { assert_eq!( find_project_entry(&self.panel, &format!("workspace/{file}"), &mut self.cx), None, "{file} should not exist" ); } async fn rename(&mut self, from: &str, to: &str) { let from = format!("workspace/{from}"); let Self { panel, cx, .. } = self; select_path(&panel, &from, cx); panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); cx.run_until_parked(); let confirm = panel.update_in(cx, |panel, window, cx| { panel .filename_editor .update(cx, |editor, cx| editor.set_text(to, window, cx)); panel.confirm_edit(true, window, cx).unwrap() }); confirm.await.unwrap(); cx.run_until_parked(); } async fn create_file(&mut self, path: &str) { let Self { panel, cx, .. } = self; select_path(&panel, "workspace", cx); panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); cx.run_until_parked(); let confirm = panel.update_in(cx, |panel, window, cx| { panel .filename_editor .update(cx, |editor, cx| editor.set_text(path, window, cx)); panel.confirm_edit(true, window, cx).unwrap() }); confirm.await.unwrap(); cx.run_until_parked(); } async fn create_directory(&mut self, path: &str) { let Self { panel, cx, .. } = self; select_path(&panel, "workspace", cx); panel.update_in(cx, |panel, window, cx| { panel.new_directory(&NewDirectory, window, cx) }); cx.run_until_parked(); let confirm = panel.update_in(cx, |panel, window, cx| { panel .filename_editor .update(cx, |editor, cx| editor.set_text(path, window, cx)); panel.confirm_edit(true, window, cx).unwrap() }); confirm.await.unwrap(); cx.run_until_parked(); } /// Drags the `files` to the provided `directory`. fn drag(&mut self, files: &[&str], directory: &str) { self.panel .update(&mut self.cx, |panel, _| panel.marked_entries.clear()); files.into_iter().for_each(|file| { project_panel_tests::select_path_with_mark( &self.panel, &format!("workspace/{file}"), &mut self.cx, ) }); project_panel_tests::drag_selection_to( &self.panel, &format!("workspace/{directory}"), false, &mut self.cx, ); } /// Only supports files in root (otherwise would need toggle_expand_dir). /// For undo redo the paths themselves do not matter so this is fine async fn cut(&mut self, file: &str) { project_panel_tests::select_path_with_mark( &self.panel, &format!("workspace/{file}"), &mut self.cx, ); self.panel.update_in(&mut self.cx, |panel, window, cx| { panel.cut(&Default::default(), window, cx); }); } /// Only supports files in root (otherwise would need toggle_expand_dir). /// For undo redo the paths themselves do not matter so this is fine async fn paste(&mut self, directory: &str) { select_path(&self.panel, &format!("workspace/{directory}"), &mut self.cx); self.panel.update_in(&mut self.cx, |panel, window, cx| { panel.paste(&Default::default(), window, cx); }); self.cx.run_until_parked(); } async fn trash(&mut self, paths: &[&str]) { paths.iter().for_each(|p| { project_panel_tests::select_path_with_mark( &self.panel, &format!("workspace/{p}"), &mut self.cx, ) }); self.panel.update_in(&mut self.cx, |panel, window, cx| { panel.trash(&Trash { skip_prompt: true }, window, cx); }); self.cx.run_until_parked(); } /// The test tree is: /// ```txt /// a.txt /// b.txt /// ``` /// a and b are empty, x has the text "content" inside async fn new(cx: &mut gpui::TestAppContext) -> TestContext { Self::new_with_tree( cx, json!({ "a.txt": "", "b.txt": "", }), ) .await } async fn new_with_tree(cx: &mut gpui::TestAppContext, tree: Value) -> TestContext { project_panel_tests::init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree("/workspace", tree).await; let project = Project::test(fs.clone(), ["/workspace".as_ref()], cx).await; let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); let workspace = window .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); let mut cx = VisualTestContext::from_window(window.into(), cx); let panel = workspace.update_in(&mut cx, ProjectPanel::new); workspace.update_in(&mut cx, |workspace, window, cx| { workspace.add_panel(panel.clone(), window, cx); workspace.focus_panel::<ProjectPanel>(window, cx); }); cx.run_until_parked(); TestContext { panel, fs, cx } } fn update_app<R>(&mut self, update: impl FnOnce(&mut App) -> R) -> R { self.cx.cx.update(update) }} #[gpui::test]async fn rename_undo_redo(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; cx.rename("a.txt", "renamed.txt").await; cx.assert_fs_state_is(&["b.txt", "renamed.txt"]); cx.undo().await; cx.assert_fs_state_is(&["a.txt", "b.txt"]); cx.redo().await; cx.assert_fs_state_is(&["b.txt", "renamed.txt"]);} #[gpui::test]async fn create_undo_redo(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; let path = path("/workspace/c.txt"); cx.create_file("c.txt").await; cx.assert_exists("c.txt"); // We'll now insert some content into `c.txt` in order to ensure that, after // undoing the trash operation, i.e., when the file is restored, the actual // file's contents are preserved instead of a new one with the same path // being created. cx.fs.write(Path::new(&path), b"Hello!").await.unwrap(); cx.undo().await; cx.assert_not_exists("c.txt"); cx.redo().await; cx.assert_exists("c.txt"); assert_eq!(cx.fs.load(Path::new(&path)).await.unwrap(), "Hello!");} #[gpui::test]async fn create_dir_undo(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; cx.create_directory("new_dir").await; cx.assert_exists("new_dir"); cx.undo().await; cx.assert_not_exists("new_dir");} #[gpui::test]async fn cut_paste_undo(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; cx.create_directory("files").await; cx.cut("a.txt").await; cx.paste("files").await; cx.assert_fs_state_is(&["b.txt", "files/", "files/a.txt"]); cx.undo().await; cx.assert_fs_state_is(&["a.txt", "b.txt", "files/"]); cx.redo().await; cx.assert_fs_state_is(&["b.txt", "files/", "files/a.txt"]);} #[gpui::test]async fn drag_undo_redo(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; cx.create_directory("src").await; cx.create_file("src/a.rs").await; cx.assert_fs_state_is(&["a.txt", "b.txt", "src/", "src/a.rs"]); cx.drag(&["src/a.rs"], ""); cx.assert_fs_state_is(&["a.txt", "b.txt", "a.rs", "src/"]); cx.undo().await; cx.assert_fs_state_is(&["a.txt", "b.txt", "src/", "src/a.rs"]); cx.redo().await; cx.assert_fs_state_is(&["a.txt", "b.txt", "a.rs", "src/"]);} #[gpui::test]async fn drag_multiple_undo_redo(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; cx.create_directory("src").await; cx.create_file("src/x.rs").await; cx.create_file("src/y.rs").await; cx.drag(&["src/x.rs", "src/y.rs"], ""); cx.assert_fs_state_is(&["a.txt", "b.txt", "x.rs", "y.rs", "src/"]); cx.undo().await; cx.assert_fs_state_is(&["a.txt", "b.txt", "src/", "src/x.rs", "src/y.rs"]); cx.redo().await; cx.assert_fs_state_is(&["a.txt", "b.txt", "x.rs", "y.rs", "src/"]);} #[gpui::test]async fn two_sequential_undos(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; cx.rename("a.txt", "x.txt").await; cx.create_file("y.txt").await; cx.assert_fs_state_is(&["b.txt", "x.txt", "y.txt"]); cx.undo().await; cx.assert_fs_state_is(&["b.txt", "x.txt"]); cx.undo().await; cx.assert_fs_state_is(&["a.txt", "b.txt"]);} #[gpui::test]async fn undo_without_history(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; // Undoing without any history should just result in the filesystem state // remaining unchanged. cx.undo().await; cx.assert_fs_state_is(&["a.txt", "b.txt"])} #[gpui::test]async fn trash_undo_redo(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; cx.trash(&["a.txt", "b.txt"]).await; cx.assert_fs_state_is(&[]); cx.undo().await; cx.assert_fs_state_is(&["a.txt", "b.txt"]); cx.redo().await; cx.assert_fs_state_is(&[]);} #[gpui::test]async fn trash_directory_undo_redo(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new_with_tree( cx, json!({ "a.txt": "", "dir": { "b.txt": "File B's Content", }, }), ) .await; cx.assert_fs_state_is(&["a.txt", "dir/", "dir/b.txt"]); cx.trash(&["dir"]).await; cx.assert_fs_state_is(&["a.txt"]); cx.undo().await; cx.assert_fs_state_is(&["a.txt", "dir/", "dir/b.txt"]); assert_eq!( cx.fs .load(Path::new(&path("/workspace/dir/b.txt"))) .await .unwrap(), "File B's Content", ); cx.redo().await; cx.assert_fs_state_is(&["a.txt"]);} #[gpui::test]async fn trash_continues_when_one_entry_fails(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new_with_tree( cx, json!({ "0_dir": {}, "a.txt": "", "b.txt": "", }), ) .await; cx.fs .set_remove_dir_error(path("/workspace/0_dir"), "simulated failure".into()); cx.trash(&["0_dir", "a.txt", "b.txt"]).await; cx.assert_fs_state_is(&["0_dir/"]); cx.undo().await; cx.assert_fs_state_is(&["0_dir/", "a.txt", "b.txt"]);} #[gpui::test]async fn record_via_collab(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; // Manually update the `UndoManager::is_via_collab` field in order to // simulate a Project Panel's Undo Manager in a collab scenario, acting as a // client. cx.panel.update(&mut cx.cx, |panel, _cx| { panel.undo_manager.set_is_via_collab(true); }); // Even though the rename operation should succeed, no operation should be // recorded so calling undo or redo should not update the filesystem state. cx.rename("a.txt", "renamed.txt").await; cx.assert_fs_state_is(&["b.txt", "renamed.txt"]); cx.undo().await; cx.assert_fs_state_is(&["b.txt", "renamed.txt"]); cx.redo().await; cx.assert_fs_state_is(&["b.txt", "renamed.txt"]);} #[gpui::test]async fn undo_redo_unavailable_for_read_only_collab_guest(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; let focus_handle = cx .panel .read_with(&cx.cx, |panel, _| panel.focus_handle.clone()); cx.cx.update(|window, _cx| { assert!(window.is_action_available_in(&crate::Undo, &focus_handle)); assert!(window.is_action_available_in(&crate::Redo, &focus_handle)); }); // In order to simulate a read-only project, we mark it both as a collab // session as well as being a guest, which only has read access. // This is currently a bit redundant, seeing as these actions are already // disabled in collab either way. However, we'll want to enable undo/redo in // collab in the future and this test will ensure that, at that point, we // continue to not allow undo/redo in read-only projects. cx.panel.update(&mut cx.cx, |panel, cx| { panel.project.update(cx, |project, cx| { project.mark_as_collab_for_testing(); project.set_role(proto::ChannelRole::Guest, cx); }); assert!(panel.project.read(cx).is_read_only(cx)); cx.notify(); });Lines 1–500 of 545. Load more
#![cfg(test)] use client::proto; use collections::HashSet; use fs::{FakeFs, Fs}; use gpui::{App, BorrowAppContext, Entity, VisualTestContext}; use project::Project; use serde_json::{Value, json}; use settings::SettingsStore; use std::path::Path; use std::sync::Arc; use workspace::{MultiWorkspace, register_project_item}; use crate::project_panel_tests::{self, TestProjectItemView, find_project_entry, select_path}; use crate::{NewDirectory, NewFile, ProjectPanel, Redo, Rename, Trash, Undo}; struct TestContext { panel: Entity<ProjectPanel>, fs: Arc<FakeFs>, cx: VisualTestContext, } // Using the `util::path` macro requires a string literal, which would mean that // callers of, for example, `rename`, would now need to know about `/` and // use `path!` in tests. // // As such, we define it as a function here to make the helper methods more // ergonomic for our use case. fn path(path: impl AsRef<str>) -> String { let path = path.as_ref(); #[cfg(target_os = "windows")] { let mut path = path.replace("/", "\\"); if path.starts_with("\\") { path = format!("C:{}", &path); } path } #[cfg(not(target_os = "windows"))] { path.to_string() } } impl TestContext { async fn undo(&mut self) { self.panel.update_in(&mut self.cx, |panel, window, cx| { panel.undo(&Undo, window, cx); }); self.cx.run_until_parked(); } async fn redo(&mut self) { self.panel.update_in(&mut self.cx, |panel, window, cx| { panel.redo(&Redo, window, cx); }); self.cx.run_until_parked(); } /// Note this only works when every file has an extension fn assert_fs_state_is(&mut self, state: &[&str]) { let state: HashSet<_> = state .into_iter() .map(|s| path(format!("/workspace/{s}"))) .chain([path("/workspace"), path("/")]) .map(|s| Path::new(&s).to_path_buf()) .collect(); let dirs: HashSet<_> = state .iter() .map(|p| match p.extension() { Some(_) => p.parent().unwrap_or(Path::new(&path("/"))).to_owned(), None => p.clone(), }) .collect(); assert_eq!( self.fs .directories(true) .into_iter() .collect::<HashSet<_>>(), dirs ); assert_eq!( self.fs.paths(true).into_iter().collect::<HashSet<_>>(), state ); } fn assert_exists(&mut self, file: &str) { assert!( find_project_entry(&self.panel, &format!("workspace/{file}"), &mut self.cx).is_some(), "{file} should exist" ); } fn assert_not_exists(&mut self, file: &str) { assert_eq!( find_project_entry(&self.panel, &format!("workspace/{file}"), &mut self.cx), None, "{file} should not exist" ); } async fn rename(&mut self, from: &str, to: &str) { let from = format!("workspace/{from}"); let Self { panel, cx, .. } = self; select_path(&panel, &from, cx); panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); cx.run_until_parked(); let confirm = panel.update_in(cx, |panel, window, cx| { panel .filename_editor .update(cx, |editor, cx| editor.set_text(to, window, cx)); panel.confirm_edit(true, window, cx).unwrap() }); confirm.await.unwrap(); cx.run_until_parked(); } async fn create_file(&mut self, path: &str) { let Self { panel, cx, .. } = self; select_path(&panel, "workspace", cx); panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); cx.run_until_parked(); let confirm = panel.update_in(cx, |panel, window, cx| { panel .filename_editor .update(cx, |editor, cx| editor.set_text(path, window, cx)); panel.confirm_edit(true, window, cx).unwrap() }); confirm.await.unwrap(); cx.run_until_parked(); } async fn create_directory(&mut self, path: &str) { let Self { panel, cx, .. } = self; select_path(&panel, "workspace", cx); panel.update_in(cx, |panel, window, cx| { panel.new_directory(&NewDirectory, window, cx) }); cx.run_until_parked(); let confirm = panel.update_in(cx, |panel, window, cx| { panel .filename_editor .update(cx, |editor, cx| editor.set_text(path, window, cx)); panel.confirm_edit(true, window, cx).unwrap() }); confirm.await.unwrap(); cx.run_until_parked(); } /// Drags the `files` to the provided `directory`. fn drag(&mut self, files: &[&str], directory: &str) { self.panel .update(&mut self.cx, |panel, _| panel.marked_entries.clear()); files.into_iter().for_each(|file| { project_panel_tests::select_path_with_mark( &self.panel, &format!("workspace/{file}"), &mut self.cx, ) }); project_panel_tests::drag_selection_to( &self.panel, &format!("workspace/{directory}"), false, &mut self.cx, ); } /// Only supports files in root (otherwise would need toggle_expand_dir). /// For undo redo the paths themselves do not matter so this is fine async fn cut(&mut self, file: &str) { project_panel_tests::select_path_with_mark( &self.panel, &format!("workspace/{file}"), &mut self.cx, ); self.panel.update_in(&mut self.cx, |panel, window, cx| { panel.cut(&Default::default(), window, cx); }); } /// Only supports files in root (otherwise would need toggle_expand_dir). /// For undo redo the paths themselves do not matter so this is fine async fn paste(&mut self, directory: &str) { select_path(&self.panel, &format!("workspace/{directory}"), &mut self.cx); self.panel.update_in(&mut self.cx, |panel, window, cx| { panel.paste(&Default::default(), window, cx); }); self.cx.run_until_parked(); } async fn trash(&mut self, paths: &[&str]) { paths.iter().for_each(|p| { project_panel_tests::select_path_with_mark( &self.panel, &format!("workspace/{p}"), &mut self.cx, ) }); self.panel.update_in(&mut self.cx, |panel, window, cx| { panel.trash(&Trash { skip_prompt: true }, window, cx); }); self.cx.run_until_parked(); } /// The test tree is: /// ```txt /// a.txt /// b.txt /// ``` /// a and b are empty, x has the text "content" inside async fn new(cx: &mut gpui::TestAppContext) -> TestContext { Self::new_with_tree( cx, json!({ "a.txt": "", "b.txt": "", }), ) .await } async fn new_with_tree(cx: &mut gpui::TestAppContext, tree: Value) -> TestContext { project_panel_tests::init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree("/workspace", tree).await; let project = Project::test(fs.clone(), ["/workspace".as_ref()], cx).await; let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); let workspace = window .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); let mut cx = VisualTestContext::from_window(window.into(), cx); let panel = workspace.update_in(&mut cx, ProjectPanel::new); workspace.update_in(&mut cx, |workspace, window, cx| { workspace.add_panel(panel.clone(), window, cx); workspace.focus_panel::<ProjectPanel>(window, cx); }); cx.run_until_parked(); TestContext { panel, fs, cx } } fn update_app<R>(&mut self, update: impl FnOnce(&mut App) -> R) -> R { self.cx.cx.update(update) } } #[gpui::test] async fn rename_undo_redo(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; cx.rename("a.txt", "renamed.txt").await; cx.assert_fs_state_is(&["b.txt", "renamed.txt"]); cx.undo().await; cx.assert_fs_state_is(&["a.txt", "b.txt"]); cx.redo().await; cx.assert_fs_state_is(&["b.txt", "renamed.txt"]); } #[gpui::test] async fn create_undo_redo(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; let path = path("/workspace/c.txt"); cx.create_file("c.txt").await; cx.assert_exists("c.txt"); // We'll now insert some content into `c.txt` in order to ensure that, after // undoing the trash operation, i.e., when the file is restored, the actual // file's contents are preserved instead of a new one with the same path // being created. cx.fs.write(Path::new(&path), b"Hello!").await.unwrap(); cx.undo().await; cx.assert_not_exists("c.txt"); cx.redo().await; cx.assert_exists("c.txt"); assert_eq!(cx.fs.load(Path::new(&path)).await.unwrap(), "Hello!"); } #[gpui::test] async fn create_dir_undo(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; cx.create_directory("new_dir").await; cx.assert_exists("new_dir"); cx.undo().await; cx.assert_not_exists("new_dir"); } #[gpui::test] async fn cut_paste_undo(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; cx.create_directory("files").await; cx.cut("a.txt").await; cx.paste("files").await; cx.assert_fs_state_is(&["b.txt", "files/", "files/a.txt"]); cx.undo().await; cx.assert_fs_state_is(&["a.txt", "b.txt", "files/"]); cx.redo().await; cx.assert_fs_state_is(&["b.txt", "files/", "files/a.txt"]); } #[gpui::test] async fn drag_undo_redo(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; cx.create_directory("src").await; cx.create_file("src/a.rs").await; cx.assert_fs_state_is(&["a.txt", "b.txt", "src/", "src/a.rs"]); cx.drag(&["src/a.rs"], ""); cx.assert_fs_state_is(&["a.txt", "b.txt", "a.rs", "src/"]); cx.undo().await; cx.assert_fs_state_is(&["a.txt", "b.txt", "src/", "src/a.rs"]); cx.redo().await; cx.assert_fs_state_is(&["a.txt", "b.txt", "a.rs", "src/"]); } #[gpui::test] async fn drag_multiple_undo_redo(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; cx.create_directory("src").await; cx.create_file("src/x.rs").await; cx.create_file("src/y.rs").await; cx.drag(&["src/x.rs", "src/y.rs"], ""); cx.assert_fs_state_is(&["a.txt", "b.txt", "x.rs", "y.rs", "src/"]); cx.undo().await; cx.assert_fs_state_is(&["a.txt", "b.txt", "src/", "src/x.rs", "src/y.rs"]); cx.redo().await; cx.assert_fs_state_is(&["a.txt", "b.txt", "x.rs", "y.rs", "src/"]); } #[gpui::test] async fn two_sequential_undos(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; cx.rename("a.txt", "x.txt").await; cx.create_file("y.txt").await; cx.assert_fs_state_is(&["b.txt", "x.txt", "y.txt"]); cx.undo().await; cx.assert_fs_state_is(&["b.txt", "x.txt"]); cx.undo().await; cx.assert_fs_state_is(&["a.txt", "b.txt"]); } #[gpui::test] async fn undo_without_history(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; // Undoing without any history should just result in the filesystem state // remaining unchanged. cx.undo().await; cx.assert_fs_state_is(&["a.txt", "b.txt"]) } #[gpui::test] async fn trash_undo_redo(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; cx.trash(&["a.txt", "b.txt"]).await; cx.assert_fs_state_is(&[]); cx.undo().await; cx.assert_fs_state_is(&["a.txt", "b.txt"]); cx.redo().await; cx.assert_fs_state_is(&[]); } #[gpui::test] async fn trash_directory_undo_redo(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new_with_tree( cx, json!({ "a.txt": "", "dir": { "b.txt": "File B's Content", }, }), ) .await; cx.assert_fs_state_is(&["a.txt", "dir/", "dir/b.txt"]); cx.trash(&["dir"]).await; cx.assert_fs_state_is(&["a.txt"]); cx.undo().await; cx.assert_fs_state_is(&["a.txt", "dir/", "dir/b.txt"]); assert_eq!( cx.fs .load(Path::new(&path("/workspace/dir/b.txt"))) .await .unwrap(), "File B's Content", ); cx.redo().await; cx.assert_fs_state_is(&["a.txt"]); } #[gpui::test] async fn trash_continues_when_one_entry_fails(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new_with_tree( cx, json!({ "0_dir": {}, "a.txt": "", "b.txt": "", }), ) .await; cx.fs .set_remove_dir_error(path("/workspace/0_dir"), "simulated failure".into()); cx.trash(&["0_dir", "a.txt", "b.txt"]).await; cx.assert_fs_state_is(&["0_dir/"]); cx.undo().await; cx.assert_fs_state_is(&["0_dir/", "a.txt", "b.txt"]); } #[gpui::test] async fn record_via_collab(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; // Manually update the `UndoManager::is_via_collab` field in order to // simulate a Project Panel's Undo Manager in a collab scenario, acting as a // client. cx.panel.update(&mut cx.cx, |panel, _cx| { panel.undo_manager.set_is_via_collab(true); }); // Even though the rename operation should succeed, no operation should be // recorded so calling undo or redo should not update the filesystem state. cx.rename("a.txt", "renamed.txt").await; cx.assert_fs_state_is(&["b.txt", "renamed.txt"]); cx.undo().await; cx.assert_fs_state_is(&["b.txt", "renamed.txt"]); cx.redo().await; cx.assert_fs_state_is(&["b.txt", "renamed.txt"]); } #[gpui::test] async fn undo_redo_unavailable_for_read_only_collab_guest(cx: &mut gpui::TestAppContext) { let mut cx = TestContext::new(cx).await; let focus_handle = cx .panel .read_with(&cx.cx, |panel, _| panel.focus_handle.clone()); cx.cx.update(|window, _cx| { assert!(window.is_action_available_in(&crate::Undo, &focus_handle)); assert!(window.is_action_available_in(&crate::Redo, &focus_handle)); }); // In order to simulate a read-only project, we mark it both as a collab // session as well as being a guest, which only has read access. // This is currently a bit redundant, seeing as these actions are already // disabled in collab either way. However, we'll want to enable undo/redo in // collab in the future and this test will ensure that, at that point, we // continue to not allow undo/redo in read-only projects. cx.panel.update(&mut cx.cx, |panel, cx| { panel.project.update(cx, |project, cx| { project.mark_as_collab_for_testing(); project.set_role(proto::ChannelRole::Guest, cx); }); assert!(panel.project.read(cx).is_read_only(cx)); cx.notify(); });