use std::cell::RefCell; use std::rc::Rc; use gpui::{ AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, SharedString, Styled as _, TestAppContext, VisualTestContext, Window, div, }; use gpui_native_core::command_palette::{Command, CommandPalette, CommandPaletteState}; use gpui_native_core::init; fn commands() -> Vec { vec![ Command::new("open-file", "Open File").hint("Cmd+O"), Command::new("close-tab", "Close Tab"), Command::new("save-all", "Save All").hint("Cmd+Alt+S"), Command::new("toggle-theme", "Toggle Theme"), ] } /// The caller-owned matcher: case-insensitive substring. `reverse` flips the /// match order, exercising that ranking belongs to the caller. fn substring_matcher(reverse: bool) -> impl Fn(&str, &[Command]) -> Vec { move |query, commands| { let query = query.to_lowercase(); let matched: Vec = commands .iter() .filter(|command| command.label.to_lowercase().contains(&query)) .cloned() .collect(); if reverse { matched.into_iter().rev().collect() } else { matched } } } struct Host { palette: Entity, } impl Render for Host { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { div().size_full().child(self.palette.clone()) } } type Executed = Rc>>; struct PaletteHost { cx: TestAppContext, state: Entity, executed: Executed, visual: VisualTestContext, } fn palette_host(reverse: bool) -> PaletteHost { let cx = TestAppContext::single(); // `init` registers the input bindings and then the palette's, in the // load-bearing order. cx.update(init); let executed: Executed = Rc::new(RefCell::new(Vec::new())); let state_cell: Rc>>> = Rc::new(RefCell::new(None)); let window = cx.update(|cx| { let executed_for_builder = executed.clone(); let cell = state_cell.clone(); cx.open_window(gpui::WindowOptions::default(), |window, cx| { let palette = CommandPalette::new("test-palette", commands(), substring_matcher(reverse)) .on_execute(move |command: &Command, _, _| { executed_for_builder.borrow_mut().push(command.id.clone()) }); let state = cx.new(|cx| CommandPaletteState::new(palette, window, cx)); cell.borrow_mut().replace(state.clone()); cx.new(|_| Host { palette: state }) }) .unwrap() }); let state = state_cell.borrow().clone().expect("state captured"); let mut visual = VisualTestContext::from_window(window.into(), &cx); draw(&mut visual); PaletteHost { cx, state, executed, visual, } } fn draw(visual: &mut VisualTestContext) { visual.update(|window, cx| { window.draw(cx).clear(cx); }); } fn open(host: &mut PaletteHost) { let state = host.state.clone(); host.visual.update(|window, cx| { state.update(cx, |state, cx| state.open(window, cx)); }); } fn type_text(host: &mut PaletteHost, text: &str) { for letter in text.chars() { host.visual.simulate_keystrokes(&letter.to_string()); } draw(&mut host.visual); } #[test] fn open_lists_the_callers_commands() { let mut host = palette_host(false); open(&mut host); draw(&mut host.visual); assert!(host.visual.debug_bounds("test-palette-command-0").is_some()); assert!(host.visual.debug_bounds("test-palette-command-3").is_some()); assert!(host.visual.debug_bounds("test-palette-panel").is_some()); assert!(host.cx.update(|cx| host.state.read(cx).is_open())); } #[test] fn enter_runs_the_highlighted_command_and_closes() { let mut host = palette_host(false); open(&mut host); host.visual.simulate_keystrokes("enter"); draw(&mut host.visual); assert_eq!( *host.executed.borrow(), vec![SharedString::from("open-file")], "the first row starts highlighted" ); assert!(host.visual.debug_bounds("test-palette-panel").is_none()); } #[test] fn rows_follow_the_callers_match_order() { let mut host = palette_host(true); open(&mut host); host.visual.simulate_keystrokes("enter"); draw(&mut host.visual); assert_eq!( *host.executed.borrow(), vec![SharedString::from("toggle-theme")], "enter runs the first row, and the caller's matcher owns the row order" ); } #[test] fn typing_filters_through_the_caller_matcher() { let mut host = palette_host(false); open(&mut host); type_text(&mut host, "save"); assert!(host.visual.debug_bounds("test-palette-command-0").is_some()); assert!( host.visual.debug_bounds("test-palette-command-1").is_none(), "only the matching command stays listed" ); host.visual.simulate_keystrokes("enter"); draw(&mut host.visual); assert_eq!( *host.executed.borrow(), vec![SharedString::from("save-all")] ); } #[test] fn escape_closes_without_running() { let mut host = palette_host(false); open(&mut host); host.visual.simulate_keystrokes("escape"); draw(&mut host.visual); assert!(host.visual.debug_bounds("test-palette-panel").is_none()); assert!(host.executed.borrow().is_empty()); assert!(!host.cx.update(|cx| host.state.read(cx).is_open())); } #[test] fn arrow_keys_move_the_highlight() { let mut host = palette_host(false); open(&mut host); host.visual.simulate_keystrokes("down"); host.visual.simulate_keystrokes("enter"); draw(&mut host.visual); assert_eq!( *host.executed.borrow(), vec![SharedString::from("close-tab")] ); // Up from the top wraps to the last row. open(&mut host); host.visual.simulate_keystrokes("up"); host.visual.simulate_keystrokes("enter"); draw(&mut host.visual); assert_eq!( *host.executed.borrow(), vec![ SharedString::from("close-tab"), SharedString::from("toggle-theme") ] ); } #[test] fn clicking_a_row_runs_it() { let mut host = palette_host(false); open(&mut host); let row = host .visual .debug_bounds("test-palette-command-2") .expect("save-all row missing"); host.visual .simulate_click(row.center(), gpui::Modifiers::default()); draw(&mut host.visual); assert_eq!( *host.executed.borrow(), vec![SharedString::from("save-all")] ); assert!(host.visual.debug_bounds("test-palette-panel").is_none()); } #[test] fn clicking_the_scrim_closes() { let mut host = palette_host(false); open(&mut host); draw(&mut host.visual); host.visual.simulate_click( gpui::point(gpui::px(10.), gpui::px(10.)), gpui::Modifiers::default(), ); draw(&mut host.visual); assert!(host.visual.debug_bounds("test-palette-panel").is_none()); assert!(host.executed.borrow().is_empty()); } #[test] fn reopen_starts_from_a_fresh_query() { let mut host = palette_host(false); open(&mut host); type_text(&mut host, "zzzz"); assert!( host.visual.debug_bounds("test-palette-command-0").is_none(), "nothing matches nonsense" ); host.visual.simulate_keystrokes("escape"); open(&mut host); draw(&mut host.visual); assert!( host.visual.debug_bounds("test-palette-command-0").is_some(), "a fresh open clears the query" ); } #[test] fn set_commands_replaces_the_registry() { let mut host = palette_host(false); host.cx.update(|cx| { host.state.update(cx, |state, cx| { state.set_commands(vec![Command::new("only", "Only Command")], cx) }); }); open(&mut host); draw(&mut host.visual); assert!(host.visual.debug_bounds("test-palette-command-0").is_some()); assert!(host.visual.debug_bounds("test-palette-command-1").is_none()); host.visual.simulate_keystrokes("enter"); draw(&mut host.visual); assert_eq!(*host.executed.borrow(), vec![SharedString::from("only")]); }