use editor::Editor;
use gpui::{Entity, Focusable, TestAppContext, VisualTestContext, WindowHandle, px, size};
const SOURCE: &str = "# Title\n\nA paragraph long enough that it has to wrap more than once inside the pane it is painted into, which is what makes it worth testing.\n\n- first\n- second\n\n> a quote";
fn open(cx: &mut TestAppContext) -> (Entity<Editor>, WindowHandle<Editor>, VisualTestContext) {
open_with(SOURCE, cx)
}
fn open_with(
source: &str,
cx: &mut TestAppContext,
) -> (Entity<Editor>, WindowHandle<Editor>, VisualTestContext) {
cx.update(|cx| {
theme::Theme::install(theme::Appearance::Dark, cx);
editor::init(cx);
});
let window = cx.add_window(|_, cx| Editor::new(source, cx));
let editor = window.root(cx).unwrap();
let mut visual = VisualTestContext::from_window(window.into(), cx);
visual.simulate_resize(size(px(360.0), px(600.0)));
visual.update(|window, cx| {
let handle = editor.read(cx).focus_handle(cx);
window.focus(&handle, cx);
});
visual.run_until_parked();
(editor, window, visual)
}
fn head(editor: &Entity<Editor>, cx: &mut VisualTestContext) -> markdown::Cursor {
cx.update(|_, cx| editor.read(cx).selection().head)
}
fn source(editor: &Entity<Editor>, cx: &mut VisualTestContext) -> String {
cx.update(|_, cx| editor.read(cx).source())
}
fn go_to_block(editor: &Entity<Editor>, cx: &mut VisualTestContext, block: usize) {
for _ in 0..20 {
if head(editor, cx).block == block {
return;
}
cx.simulate_keystrokes("down");
}
panic!("never reached block {block}");
}
#[gpui::test]
fn typing_reaches_the_document(cx: &mut TestAppContext) {
let (editor, _window, mut cx) = open(cx);
cx.simulate_input("Xy");
assert!(
source(&editor, &mut cx).starts_with("# XyTitle"),
"typed text lands at the caret"
);
}
#[gpui::test]
fn enter_splits_and_backspace_merges(cx: &mut TestAppContext) {
let (editor, _window, mut cx) = open(cx);
cx.simulate_keystrokes("right right enter");
assert!(
source(&editor, &mut cx).starts_with("# Ti\n\ntle"),
"a heading splits into a heading and body text"
);
cx.simulate_keystrokes("backspace");
assert!(
source(&editor, &mut cx).starts_with("# Title"),
"and backspace at the seam puts it back"
);
}
#[gpui::test]
fn down_crosses_every_block_boundary(cx: &mut TestAppContext) {
let (editor, _window, mut cx) = open(cx);
let mut seen = vec![head(&editor, &mut cx)];
for _ in 0..12 {
cx.simulate_keystrokes("down");
seen.push(head(&editor, &mut cx));
}
let blocks: Vec<usize> = seen.iter().map(|at| at.block).collect();
assert!(
blocks.windows(2).all(|pair| pair[1] >= pair[0]),
"the caret never goes backwards: {blocks:?}"
);
assert_eq!(
*blocks.last().unwrap(),
4,
"and it reaches the last block: {blocks:?}"
);
}
#[gpui::test]
fn down_does_not_stick_inside_a_wrapped_block(cx: &mut TestAppContext) {
let (editor, _window, mut cx) = open(cx);
cx.simulate_keystrokes("down");
let mut rows = Vec::new();
for _ in 0..4 {
cx.simulate_keystrokes("down");
rows.push(head(&editor, &mut cx));
}
assert!(
rows.windows(2).all(|pair| pair[0] != pair[1]),
"every step moves: {rows:?}"
);
}
#[gpui::test]
fn up_retraces_the_path_down(cx: &mut TestAppContext) {
let (editor, _window, mut cx) = open(cx);
let start = head(&editor, &mut cx);
cx.simulate_keystrokes("down down down");
assert_ne!(head(&editor, &mut cx), start);
cx.simulate_keystrokes("up up up");
assert_eq!(
head(&editor, &mut cx),
start,
"the goal column is held across the whole run"
);
}
#[gpui::test]
fn the_menu_opens_on_a_slash_and_turns_the_block(cx: &mut TestAppContext) {
let (editor, _window, mut cx) = open(cx);
cx.simulate_keystrokes("end enter");
cx.simulate_input("/");
cx.simulate_keystrokes("down enter");
assert!(
source(&editor, &mut cx).starts_with("# Title\n\n# "),
"Enter took Heading 1 from the menu: {:?}",
source(&editor, &mut cx)
);
}
#[gpui::test]
fn the_menu_actually_paints(cx: &mut TestAppContext) {
let (_editor, _window, mut cx) = open(cx);
assert!(
cx.debug_bounds(editor::SLASH_MENU).is_none(),
"nothing is open yet"
);
cx.simulate_keystrokes("end enter");
cx.simulate_input("/");
cx.run_until_parked();
assert!(
cx.debug_bounds(editor::SLASH_MENU).is_some(),
"the menu reached the screen, not just the state"
);
}
#[gpui::test]
fn escape_closes_the_menu_and_gives_enter_back(cx: &mut TestAppContext) {
let (editor, _window, mut cx) = open(cx);
cx.simulate_keystrokes("end enter");
cx.simulate_input("/");
cx.simulate_keystrokes("escape enter");
assert!(
source(&editor, &mut cx).starts_with("# Title\n\n/"),
"with the menu shut, Enter splits and the slash stays literal: {:?}",
source(&editor, &mut cx)
);
}
#[cfg(target_os = "macos")]
const PRIMARY: &str = "cmd";
#[cfg(not(target_os = "macos"))]
const PRIMARY: &str = "ctrl";
#[gpui::test]
fn a_selection_survives_a_mark_and_round_trips(cx: &mut TestAppContext) {
let (editor, _window, mut cx) = open(cx);
cx.simulate_keystrokes("shift-right shift-right shift-right");
assert!(
!cx.update(|_, cx| editor.read(cx).selection().is_collapsed()),
"shift+arrow extends"
);
cx.simulate_keystrokes(&format!("{PRIMARY}-b"));
assert!(
source(&editor, &mut cx).starts_with("# **Tit**le"),
"cmd-B marks the selection: {:?}",
source(&editor, &mut cx)
);
}
#[gpui::test]
fn undo_gives_back_a_run_of_typing_at_once(cx: &mut TestAppContext) {
let (editor, _window, mut cx) = open(cx);
let before = source(&editor, &mut cx);
cx.simulate_input("hello");
assert_ne!(source(&editor, &mut cx), before);
cx.simulate_keystrokes(&format!("{PRIMARY}-z"));
assert_eq!(
source(&editor, &mut cx),
before,
"one step takes the whole word"
);
}
#[gpui::test]
fn tab_indents_a_list_item_and_shift_tab_puts_it_back(cx: &mut TestAppContext) {
let (editor, _window, mut cx) = open(cx);
go_to_block(&editor, &mut cx, 3);
cx.simulate_keystrokes("tab");
assert!(
source(&editor, &mut cx).contains("- first\n - second"),
"tab nests it under the item above: {:?}",
source(&editor, &mut cx)
);
cx.simulate_keystrokes("shift-tab");
assert!(
source(&editor, &mut cx).contains("- first\n- second"),
"and shift-tab lifts it back"
);
}
#[gpui::test]
fn tab_indents_with_focus_traversal_installed(cx: &mut TestAppContext) {
use gpui::{Context, IntoElement, Render, Window, div, prelude::*};
struct Host(Entity<Editor>);
impl Render for Host {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
ui::focus::traversal(div())
.size_full()
.child(self.0.clone())
}
}
cx.update(|cx| {
theme::Theme::install(theme::Appearance::Dark, cx);
editor::init(cx);
ui::focus::init(cx);
});
let window = cx.add_window(|_, cx| Host(cx.new(|cx| Editor::new(SOURCE, cx))));
let editor = cx.update(|cx| window.root(cx).unwrap().read(cx).0.clone());
let mut cx = VisualTestContext::from_window(window.into(), cx);
cx.simulate_resize(size(px(360.0), px(600.0)));
cx.update(|window, cx| {
let handle = editor.read(cx).focus_handle(cx);
window.focus(&handle, cx);
});
cx.run_until_parked();
go_to_block(&editor, &mut cx, 3);
cx.simulate_keystrokes("tab");
assert!(
source(&editor, &mut cx).contains("- first\n - second"),
"tab nests the item rather than moving focus: {:?}",
source(&editor, &mut cx)
);
cx.simulate_keystrokes("shift-tab");
assert!(
source(&editor, &mut cx).contains("- first\n- second"),
"and shift-tab lifts it back rather than stepping the other way"
);
assert!(
cx.update(|window, cx| editor.read(cx).focus_handle(cx).is_focused(window)),
"the document still holds focus"
);
}
#[gpui::test]
fn the_handle_follows_the_caret(cx: &mut TestAppContext) {
let (editor, _window, mut cx) = open(cx);
let at = cx
.debug_bounds(editor::BLOCK_HANDLE)
.expect("a focused document shows a handle without being hovered");
go_to_block(&editor, &mut cx, 2);
cx.run_until_parked();
let moved = cx
.debug_bounds(editor::BLOCK_HANDLE)
.expect("and still shows one");
assert!(
moved.origin.y > at.origin.y,
"it followed the caret down the document"
);
}
#[gpui::test]
fn the_handle_moves_in_with_an_indented_block(cx: &mut TestAppContext) {
let (editor, _window, mut cx) = open_with("- first\n- second", &mut *cx);
go_to_block(&editor, &mut cx, 1);
cx.run_until_parked();
let before = cx
.debug_bounds(editor::BLOCK_HANDLE)
.expect("a handle")
.origin;
cx.simulate_keystrokes("tab");
cx.update(|window, cx| window.simulate_next_frame(cx));
cx.run_until_parked();
let after = cx
.debug_bounds(editor::BLOCK_HANDLE)
.expect("still a handle")
.origin;
assert_eq!(
after.x - before.x,
px(22.0),
"the handle followed the block in by one indent"
);
assert_eq!(after.y, before.y, "and stayed on the same row");
}
#[gpui::test]
fn an_empty_block_after_an_atomic_one_deletes(cx: &mut TestAppContext) {
for (name, source) in [
("a rule", "a\n\n---\n\nx"),
("a fence", "a\n\n```rs\nk\n```\n\nx"),
("an image", "a\n\n\n\nx"),
("a table", "a\n\n| h |\n| - |\n| c |\n\nx"),
] {
let (editor, _window, mut cx) = open_with(source, &mut *cx);
for _ in 0..25 {
cx.simulate_keystrokes("down");
}
cx.simulate_keystrokes("end backspace");
let before = cx.update(|_, cx| editor.read(cx).doc().blocks.len());
cx.simulate_keystrokes("backspace");
assert_eq!(
cx.update(|_, cx| editor.read(cx).doc().blocks.len()),
before - 1,
"an empty paragraph after {name} is deletable"
);
}
}