use escriba_buffer::BufferSet;
use escriba_core::{Mode, Position};
use escriba_keymap::Key;
use escriba_runtime::EditorState;
fn editor() -> EditorState {
let mut bufs = BufferSet::new();
let id = bufs.scratch("one\n");
EditorState::new_with_buffer(bufs, id)
}
#[derive(Debug, PartialEq, Eq)]
struct Observable {
text: String,
cursor: Position,
top_line: u32,
mode: Mode,
quit: bool,
}
fn observe(st: &EditorState) -> Observable {
Observable {
text: st
.buffers
.get(st.active)
.map(escriba_buffer::Buffer::to_string)
.unwrap_or_default(),
cursor: st.cursor(),
top_line: st.layout.active_window().map_or(0, |w| w.viewport.top_line),
mode: st.modal.mode(),
quit: st.quit_requested,
}
}
fn ex(st: &mut EditorState, line: &str) {
st.on_key(&Key::Char(':'));
for c in line.chars() {
st.on_key(&Key::Char(c));
}
st.on_key(&Key::Enter);
if st.modal.mode() == Mode::Command {
st.on_key(&Key::Esc);
}
}
fn edited(mut st: EditorState) -> EditorState {
st.on_key(&Key::Char('i'));
for _ in 0..4 {
st.on_key(&Key::Enter);
}
st.on_key(&Key::Esc);
st
}
#[test]
fn undo_by_key_and_by_command_agree() {
let mut by_key = edited(editor());
by_key.on_key(&Key::Char('u'));
let mut by_cmd = edited(editor());
ex(&mut by_cmd, "undo");
assert_eq!(
observe(&by_key),
observe(&by_cmd),
"`u` and `:undo` must leave the editor in the same state",
);
}
#[test]
fn redo_by_key_and_by_command_agree() {
let mut by_key = edited(editor());
by_key.on_key(&Key::Char('u'));
by_key.on_key(&Key::Ctrl('r'));
let mut by_cmd = edited(editor());
ex(&mut by_cmd, "undo");
ex(&mut by_cmd, "redo");
assert_eq!(observe(&by_key), observe(&by_cmd));
}
#[test]
fn an_undo_that_shrinks_the_buffer_never_strands_the_cursor() {
let check = |st: &EditorState, path: &str| {
let c = st.cursor();
let lines = st
.buffers
.get(st.active)
.map_or(0, escriba_buffer::Buffer::line_count);
assert!(
c.line < lines.max(1),
"{path}: cursor on line {} but the buffer has {lines} line(s) — an undo shrank it and nothing re-clamped",
c.line,
);
let w = st.layout.active_window().expect("a window");
assert!(
c.line >= w.viewport.top_line
&& c.line < w.viewport.top_line + w.viewport.visible_lines,
"{path}: cursor {c:?} outside viewport {:?}",
w.viewport,
);
};
let mut by_key = edited(editor());
for _ in 0..4 {
by_key.on_key(&Key::Char('u'));
check(&by_key, "u");
}
let mut by_cmd = edited(editor());
for _ in 0..4 {
ex(&mut by_cmd, "undo");
check(&by_cmd, ":undo");
}
}
#[test]
fn quit_by_key_and_by_command_agree() {
let mut by_cmd = editor();
ex(&mut by_cmd, "quit");
assert!(by_cmd.quit_requested, ":quit asks to exit");
}
#[test]
fn clearing_the_highlight_agrees_across_paths() {
let searched = || {
let mut st = editor();
for k in ['/', 'n'] {
st.on_key(&Key::Char(k));
}
st.on_key(&Key::Enter);
st
};
let mut by_cmd = searched();
assert!(!by_cmd.search.highlights().is_empty());
ex(&mut by_cmd, "noh");
assert!(by_cmd.search.highlights().is_empty());
assert_eq!(by_cmd.window_pattern(), Some("n".to_string()));
}
trait PatternPeek {
fn window_pattern(&self) -> Option<String>;
}
impl PatternPeek for EditorState {
fn window_pattern(&self) -> Option<String> {
use escriba_madoguchi::Snapshot;
self.window().search().pattern().map(str::to_string)
}
}
#[test]
fn lisp_effects_go_through_the_one_interpreter() {
let mut st = editor();
st.run_lisp(r#"(set-option "tabstop" "4")"#)
.expect("lisp evaluates");
assert_eq!(
st.options.get("tabstop").map(String::as_str),
Some("4"),
"a Lisp set-option must reach the same store defoption writes",
);
}
#[test]
fn a_lisp_message_lands_where_every_other_message_lands() {
let mut st = editor();
st.run_lisp(r#"(message "from lisp")"#).expect("evaluates");
assert_eq!(st.messages.last().map(String::as_str), Some("from lisp"));
}
#[test]
fn runaway_command_recursion_is_refused_not_fatal() {
use escriba_madoguchi::{Native, Negai, Outcome, View, caps, erase};
struct SelfCall;
impl Native for SelfCall {
type Reads = caps!();
fn run(_: &View<'_, Self::Reads>, _: &[String]) -> Outcome {
Outcome::did(vec![Negai::RunCommand {
name: "loop".to_string(),
args: Vec::new(),
}])
}
}
let mut st = editor();
st.commands.register(escriba_command::Command::native(
"loop",
"invokes itself",
erase::<SelfCall>(),
));
ex(&mut st, "loop");
assert!(
st.messages.iter().any(|m| m.contains("recursion too deep")),
"a self-invoking command must be refused and SAID: {:?}",
st.messages,
);
assert!(!st.quit_requested, "and the editor must survive it");
let mut st = editor();
for _ in 0..20 {
ex(&mut st, "buffer-info");
}
assert!(
!st.messages.iter().any(|m| m.contains("recursion too deep")),
"sequential commands must not exhaust the nesting budget: {:?}",
st.messages,
);
}