280 lines
8.1 KiB
Rust
280 lines
8.1 KiB
Rust
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<Command> {
|
|
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<Command> {
|
|
move |query, commands| {
|
|
let query = query.to_lowercase();
|
|
let matched: Vec<Command> = 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<CommandPaletteState>,
|
|
}
|
|
|
|
impl Render for Host {
|
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
div().size_full().child(self.palette.clone())
|
|
}
|
|
}
|
|
|
|
type Executed = Rc<RefCell<Vec<SharedString>>>;
|
|
|
|
struct PaletteHost {
|
|
cx: TestAppContext,
|
|
state: Entity<CommandPaletteState>,
|
|
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<RefCell<Option<Entity<CommandPaletteState>>>> = 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")]);
|
|
}
|