//! Long lists draw in view, load more, pick by key. use std::{cell::RefCell, rc::Rc}; use gpui::{ Context, IntoElement, KeyBinding, KeyUpEvent, Keystroke, ParentElement, Render, Styled, TestAppContext, VisualTestContext, Window, div, px, }; use super::{ Blame, BlameView, Branch, BranchList, Commit, CommitList, FileHistory, GitTag, Stash, StashList, TagList, }; use crate::{forms, primitives::FocusNext, theme::Theme}; fn commit(n: usize) -> Commit { Commit { id: format!("{n:040}").into(), parents: vec![format!("{:040}", n + 1).into()], subject: format!("change {n}").into(), author: "Ann".into(), when: "today".into(), refs: Vec::new(), } } /// What a list asked for. #[derive(Default)] struct Heard { picked: Vec, ends: usize, } struct Lists { count: usize, heard: Rc>, which: &'static str, } impl Render for Lists { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { let commits: Vec = (0..self.count).map(commit).collect(); let (pick, end) = (self.heard.clone(), self.heard.clone()); let frame = div().w(px(600.0)).h(px(240.0)); match self.which { "commits" => frame.child( CommitList::new("commits", Rc::new(commits)) .on_pick(move |id, _, _| pick.borrow_mut().picked.push(id.to_string())) .on_end(move |_| end.borrow_mut().ends += 1), ), "history" => frame.child( FileHistory::new( "history", Rc::new(commits.into_iter().map(|c| (c, None)).collect()), ) .on_pick(move |id, _, _| pick.borrow_mut().picked.push(id.to_string())), ), "refs" => { let branch = |name: &str, current| Branch { name: name.to_string().into(), remote: false, current, ahead: 0, behind: 0, subject: "s".into(), when: "today".into(), }; let tag = GitTag { name: "v1".into(), message: None, commit: "a1".into(), when: "today".into(), }; let (tagged, switched, deleted) = (self.heard.clone(), self.heard.clone(), self.heard.clone()); frame .child( BranchList::new("branches", [branch("main", true), branch("side", false)]) .on_pick(move |name, _, _| { pick.borrow_mut().picked.push(name.to_string()) }) .on_switch(move |name, _, _| { switched.borrow_mut().picked.push(format!("switch {name}")) }) .on_delete(move |name, _, _| { deleted.borrow_mut().picked.push(format!("delete {name}")) }), ) .child(TagList::new("tags", [tag]).on_pick(move |name, _, _| { tagged.borrow_mut().picked.push(name.to_string()) })) } "stashes" => { let stash = |message: &str| Stash { message: message.to_string().into(), branch: "main".into(), when: "today".into(), }; let acted = self.heard.clone(); frame .child(StashList::new("bare", [stash("unhandled")])) .child( StashList::new("stashes", [stash("one"), stash("two")]) .selected(1) .on_pick(move |ix, _, _| pick.borrow_mut().picked.push(format!("{ix}"))) .on_action(move |ix, what, _, _| { acted.borrow_mut().picked.push(format!("{what:?} {ix}")) }), ) } _ => { let blame = |commit: &str| Blame { commit: commit.into(), author: "Ann".into(), when: "today".into(), subject: "first".into(), age: 0.0, }; frame.child( BlameView::new( "blame", "one\ntwo\nthree", [0, 0, 1], [blame("a1"), blame("b2")], ) .on_commit(move |id, _, _| pick.borrow_mut().picked.push(id.to_string())), ) } } } } fn shown<'a>( cx: &'a mut TestAppContext, count: usize, which: &'static str, ) -> (Rc>, &'a mut VisualTestContext) { cx.update(|cx| { Theme::init(cx); forms::bind_keys(cx); cx.bind_keys([KeyBinding::new("tab", FocusNext, None)]); }); let heard = Rc::new(RefCell::new(Heard::default())); let held = heard.clone(); let (_, cx) = cx.add_window_view(move |_, _| Lists { count, heard: held, which, }); cx.update(|window, _| window.activate_window()); settle(cx); (heard, cx) } fn settle(cx: &mut VisualTestContext) { cx.run_until_parked(); cx.update(|window, _| window.refresh()); cx.run_until_parked(); } /// Tab to the first stop and press Enter. fn press_first(cx: &mut VisualTestContext) { cx.update(|window, cx| window.focus_next(cx)); settle(cx); cx.simulate_keystrokes("enter"); settle(cx); cx.simulate_event(KeyUpEvent { keystroke: Keystroke::parse("enter").expect("a key"), }); settle(cx); } #[gpui::test] fn a_long_list_asks_for_more_only_near_its_end(cx: &mut TestAppContext) { let (heard, _) = shown(cx, 500, "commits"); assert_eq!(heard.borrow().ends, 0, "500 rows, few in view"); } #[gpui::test] fn a_short_list_asks_for_more(cx: &mut TestAppContext) { let (heard, _) = shown(cx, 12, "commits"); assert!(heard.borrow().ends > 0); } #[gpui::test] fn a_commit_picks_from_the_keyboard(cx: &mut TestAppContext) { let (heard, cx) = shown(cx, 500, "commits"); press_first(cx); assert_eq!(heard.borrow().picked, [format!("{:040}", 0)]); } #[gpui::test] fn a_file_history_row_picks_from_the_keyboard(cx: &mut TestAppContext) { let (heard, cx) = shown(cx, 300, "history"); press_first(cx); assert_eq!(heard.borrow().picked, [format!("{:040}", 0)]); } #[gpui::test] fn a_blame_run_picks_its_commit_from_the_keyboard(cx: &mut TestAppContext) { let (heard, cx) = shown(cx, 0, "blame"); press_first(cx); cx.update(|window, cx| window.focus_next(cx)); settle(cx); cx.simulate_keystrokes("enter"); settle(cx); cx.simulate_event(KeyUpEvent { keystroke: Keystroke::parse("enter").expect("a key"), }); settle(cx); assert_eq!( heard.borrow().picked, ["a1", "b2"], "each run's head, no stop between" ); } #[gpui::test] fn branches_and_tags_pick_from_the_keyboard(cx: &mut TestAppContext) { let (heard, cx) = shown(cx, 0, "refs"); press_first(cx); // Step through the stops until the tag is picked. for _ in 0..8 { if heard.borrow().picked.iter().any(|name| name == "v1") { break; } press_first(cx); } let picked = heard.borrow().picked.clone(); assert_eq!(picked.first().map(String::as_str), Some("main")); assert!( picked.contains(&"side".to_string()) && picked.contains(&"v1".to_string()), "{picked:?}" ); let acted = |what: &str| picked.iter().any(|said| said == what); assert!( acted("switch side") && acted("delete side"), "row actions answer: {picked:?}" ); } #[gpui::test] fn stashes_pick_and_act_from_the_keyboard(cx: &mut TestAppContext) { let (heard, cx) = shown(cx, 0, "stashes"); for _ in 0..4 { press_first(cx); } assert_eq!( heard.borrow().picked, ["0", "Apply 0", "Pop 0", "Drop 0"], "a list with no owner draws no stops" ); }