pub use super::*;
#[cfg(test)]
mod reviewer_battery {
use super::*;
#[test]
fn counts_multiply_non_operator_commands() {
let mut e = Editor::new(Buffer::from_text("abcde\n"));
e.feed_text("2x");
assert_eq!(e.buf().rope.to_string(), "cde\n");
let mut e = Editor::new(Buffer::from_text("a\nb\nc\n"));
e.feed_text("dd");
e.feed_text("dd");
assert_eq!(e.buf().rope.to_string(), "c\n");
e.feed_text("2u");
assert_eq!(e.buf().rope.to_string(), "a\nb\nc\n");
let mut e = Editor::new(Buffer::from_text("abcde\n"));
e.feed_text("3rx");
assert_eq!(e.buf().rope.to_string(), "xxxde\n"); let mut e = Editor::new(Buffer::from_text("ab\n"));
e.feed_text("yiw");
e.feed_text("2p");
assert_eq!(e.buf().rope.to_string(), "aababb\n"); }
#[test]
fn counted_insert_repeats_text() {
let mut e = Editor::new(Buffer::from_text("x\n"));
e.feed_text("3iZ");
e.feed(crate::editor::Key::Esc);
assert_eq!(e.buf().rope.to_string(), "ZZZx\n"); e.feed_text("u"); assert_eq!(e.buf().rope.to_string(), "x\n");
}
#[test]
fn cc_keeps_the_line_and_indent() {
let mut e = Editor::new(Buffer::from_text("one\n two\nthree\n"));
e.feed_text("jccX");
e.feed(crate::editor::Key::Esc);
assert_eq!(e.buf().rope.to_string(), "one\n X\nthree\n");
e.feed_text("u");
assert_eq!(e.buf().rope.to_string(), "one\n two\nthree\n");
let mut e = Editor::new(Buffer::from_text("one\ntwo\nthree\n"));
e.feed_text("jcc");
e.feed(crate::editor::Key::Esc);
assert_eq!(e.buf().rope.to_string(), "one\n\nthree\n");
}
#[test]
fn c_enters_insert_at_deletion_start() {
let mut e = Editor::new(Buffer::from_text("hello world\n"));
e.feed_text("wCthere");
e.feed(crate::editor::Key::Esc);
assert_eq!(e.buf().rope.to_string(), "hello there\n");
let mut e = Editor::new(Buffer::from_text("abcdef\n"));
e.feed_text("3lCX");
e.feed(crate::editor::Key::Esc);
assert_eq!(e.buf().rope.to_string(), "abcX\n");
}
#[test]
fn i_caret_tilde_s_work() {
let mut e = Editor::new(Buffer::from_text(" let x = 1;\n"));
e.feed_text("0^"); assert_eq!(e.head(), 4);
let mut e = Editor::new(Buffer::from_text(" let x = 1;\n"));
e.feed_text("I"); assert_eq!(e.mode, crate::editor::Mode::Insert);
assert_eq!(e.head(), 4);
e.feed(crate::editor::Key::Esc);
let mut e = Editor::new(Buffer::from_text("abc\n"));
e.feed_text("~~");
assert_eq!(e.buf().rope.to_string(), "ABc\n");
let mut e = Editor::new(Buffer::from_text("one\ntwo\n"));
e.feed_text("SX");
e.feed(crate::editor::Key::Esc);
assert_eq!(e.buf().rope.to_string(), "X\ntwo\n");
}
#[test]
fn unknown_bare_keys_say_so() {
let mut e = Editor::new(Buffer::from_text("x\n"));
e.feed_text("="); assert!(e.message.contains("not an editor command"));
}
}
#[cfg(test)]
mod scratch_tests {
use super::*;
#[test]
fn first_open_replaces_the_scratch_buffer() {
let dir = tempfile::tempdir().unwrap();
let f = dir.path().join("scratch-test.rs");
std::fs::write(&f, "fn a() {}\n").unwrap();
let mut e = Editor::new(Buffer::from_text(""));
e.open_buffer(f.to_str().unwrap()).unwrap();
assert_eq!(e.docs.len(), 1, "scratch replaced, not stacked");
assert_eq!(e.buf().path.as_deref(), f.to_str());
e.feed_text(":q\r");
assert!(e.should_quit, "one :q quits");
}
#[test]
fn view_marks_readonly_and_edits_refuse() {
let mut e = Editor::new(Buffer::from_text("one\ntwo\n"));
e.feed_text(":view\r");
assert!(e.buf().readonly);
e.feed_text("x");
assert_eq!(e.buf().rope.to_string(), "one\ntwo\n", "no edit landed");
assert!(e.message.contains("readonly"));
}
#[test]
fn edited_scratch_survives() {
let mut e = Editor::new(Buffer::from_text(""));
e.feed_text("ix"); e.feed(crate::editor::Key::Esc);
let dir = tempfile::tempdir().unwrap();
let f = dir.path().join("scratch-test.rs");
std::fs::write(&f, "fn a() {}\n").unwrap();
e.open_buffer(f.to_str().unwrap()).unwrap();
assert_eq!(e.docs.len(), 2, "edited scratch is real work");
}
}
#[cfg(test)]
mod edit_tests {
use super::*;
fn editor_with(text: &str) -> Editor {
Editor::new(Buffer::from_text(text))
}
fn text(e: &Editor) -> String {
e.buf().rope.to_string()
}
#[test]
fn named_registers_yank_and_paste() {
let mut e = editor_with("alpha\nbeta\ngamma\n");
e.feed_text("\"ayy"); assert_eq!(e.register(Some('a')).0, "alpha\n");
e.feed_text("j");
e.feed_text("\"ap"); assert_eq!(text(&e), "alpha\nbeta\nalpha\ngamma\n");
assert!(e.register(None).0.is_empty());
}
#[test]
fn space_y_yanks_motion_to_system_register() {
let mut e = Editor::new(Buffer::from_text("hello world\n"));
e.feed_text(" yw");
assert_eq!(e.register(Some('+')).0, "hello ");
assert!(e.osc52.is_some(), "OSC52 payload staged for the TUI");
}
#[test]
fn visual_space_y_yanks_selection_to_system_register() {
let mut e = Editor::new(Buffer::from_text("hello world\n"));
e.feed_text("vl y");
assert_eq!(e.register(Some('+')).0, "he");
assert!(e.osc52.is_some());
}
#[test]
fn clipboard_paste_inserts_read_result() {
let mut e = Editor::new(Buffer::from_text("ab\n"));
e.clip_paste_pending = Some(false);
e.clip_tx.send(Some("XY".into())).unwrap();
e.drain_clipboard();
assert_eq!(e.buf().rope.to_string(), "aXYb\n");
}
#[test]
fn clipboard_paste_reports_missing_provider() {
let mut e = Editor::new(Buffer::from_text("ab\n"));
e.clip_paste_pending = Some(false);
e.clip_tx.send(None).unwrap();
e.drain_clipboard();
assert!(e.message.contains("clipboard"));
assert_eq!(e.buf().rope.to_string(), "ab\n");
}
#[test]
fn alias_verbs() {
let mut e = editor_with("let edge = hone;\n");
e.feed_text("0wD"); assert_eq!(text(&e), "let \n");
let mut e = editor_with("let x = 1;\n");
e.feed_text("0wY"); assert_eq!(e.register(None).0, "let x = 1;\n");
let mut e = editor_with("abc\n");
e.feed_text("sZ"); e.feed(crate::editor::Key::Esc);
assert_eq!(text(&e), "Zbc\n");
}
#[test]
fn replace_char_and_join() {
let mut e = editor_with("abc\ndef\n");
e.feed_text("rX");
assert_eq!(text(&e), "Xbc\ndef\n");
e.feed_text("J");
assert_eq!(text(&e), "Xbc def\n");
}
#[test]
fn indent_and_dedent() {
let mut e = editor_with("a\nb\nc\n");
e.feed_text("2>>");
assert_eq!(text(&e), " a\n b\nc\n");
e.feed_text("0<<");
assert_eq!(text(&e), "a\n b\nc\n");
}
#[test]
fn dot_repeat_replays_insert() {
let mut e = editor_with("one\ntwo\n");
e.feed_text("A!");
e.feed(crate::editor::Key::Esc);
e.feed_text("j.");
assert_eq!(text(&e), "one!\ntwo!\n");
}
#[test]
fn visual_line_deletes_whole_lines() {
let mut e = editor_with("a\nb\nc\nd\n");
e.feed_text("Vjd");
assert_eq!(text(&e), "c\nd\n");
assert!(e.register(None).1); e.feed_text("P");
assert_eq!(text(&e), "a\nb\nc\nd\n"); }
#[test]
fn paste_is_one_undo_unit() {
let mut e = Editor::new(Buffer::from_text("hello world\n"));
e.feed_text("yiw"); e.feed_text("ep");
assert_eq!(e.buf().rope.to_string(), "hellohello world\n");
e.feed_text("u");
assert_eq!(e.buf().rope.to_string(), "hello world\n");
}
#[test]
fn semicolon_and_comma_repeat_find() {
let mut e = Editor::new(Buffer::from_text("a.b.c.d\n"));
e.feed_text("f."); assert_eq!(e.head(), 1);
e.feed_text(";");
assert_eq!(e.head(), 3);
e.feed_text(";");
assert_eq!(e.head(), 5);
e.feed_text(","); assert_eq!(e.head(), 3);
}
#[test]
fn star_searches_word_under_cursor_whole_word() {
let mut e = Editor::new(Buffer::from_text("hone honed hone\n"));
e.feed_text("*"); assert_eq!(e.head(), 11);
e.feed_text("n"); assert_eq!(e.head(), 0);
e.feed_text("#"); assert_eq!(e.head(), 11);
let mut e = Editor::new(Buffer::from_text("hone honed\n"));
e.feed_text("*");
assert_eq!(e.head(), 0, "honed is not a whole-word match for hone");
}
#[test]
fn count_motions_and_ex_line_jump() {
let mut e = Editor::new(Buffer::from_text("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n"));
e.feed_text("10j");
assert_eq!(e.buf().line_of(e.head()), 10);
e.feed_text(":30\r");
assert_eq!(e.buf().line_of(e.head()), 11);
e.feed_text(":4\r");
assert_eq!(e.buf().line_of(e.head()), 3);
}
#[test]
fn visual_indent_and_dedent() {
let mut e = Editor::new(Buffer::from_text("a\nb\nc\n"));
e.feed_text("Vj>");
assert_eq!(e.buf().rope.to_string(), " a\n b\nc\n");
e.feed_text("u");
assert_eq!(e.buf().rope.to_string(), "a\nb\nc\n");
e.feed_text("Vj>");
e.feed_text("Vj<");
assert_eq!(e.buf().rope.to_string(), "a\nb\nc\n");
}
#[test]
fn noh_clears_search_highlight() {
let mut e = Editor::new(Buffer::from_text("foo bar\n"));
e.feed_text("/foo\r");
assert!(e.last_search.is_some());
e.feed_text(":noh\r");
assert!(e.last_search.is_none());
}
#[test]
fn dot_repeats_delete_and_change() {
let mut e = Editor::new(Buffer::from_text("one\ntwo\nthree\n"));
e.feed_text("dd");
assert_eq!(e.buf().rope.to_string(), "two\nthree\n");
e.feed_text(".");
assert_eq!(e.buf().rope.to_string(), "three\n");
let mut e = Editor::new(Buffer::from_text("aa bb\ncc dd\n"));
e.feed_text("cwX");
e.feed(crate::editor::Key::Esc);
assert_eq!(e.buf().rope.to_string(), "X bb\ncc dd\n");
e.feed_text("j"); e.feed_text(".");
assert_eq!(e.buf().rope.to_string(), "X bb\nX dd\n");
}
#[test]
fn last_search_highlights_persistently() {
let mut e = Editor::new(Buffer::from_text("foo bar foo\n"));
e.feed_text("/foo\r");
assert!(e.search_pattern().is_none());
assert_eq!(e.last_search.as_ref().unwrap().pattern, "foo");
let frame = crate::headless::frame_string(&mut e, 40, 8);
assert!(frame.contains("foo bar foo"));
}
#[test]
fn empty_search_repeats_last() {
let mut e = Editor::new(Buffer::from_text("aa line\nbb line\ncc line\n"));
e.feed_text("/line\r");
assert_eq!(e.buf().line_of(e.head()), 0);
e.feed_text("/\r");
assert_eq!(e.buf().line_of(e.head()), 1, "empty / repeats forward");
e.feed_text("/\r");
assert_eq!(e.buf().line_of(e.head()), 2);
e.feed_text("?\r");
assert_eq!(e.buf().line_of(e.head()), 1, "empty ? reverses");
assert!(e.message.is_empty());
}
#[test]
fn x_on_multibyte_char_deletes_the_whole_char() {
let mut e = Editor::new(Buffer::from_text("héllo\n"));
e.feed_text("lx"); assert_eq!(e.buf().rope.to_string(), "hllo\n");
e.feed_text("u");
assert_eq!(e.buf().rope.to_string(), "héllo\n");
e.feed_text("a"); assert!(e.buf().is_boundary(e.head()));
}
#[test]
fn arrows_speak_hjkl() {
let mut e = Editor::new(Buffer::from_text("one\ntwo\nthree\n"));
e.feed(crate::editor::Key::Down);
assert_eq!(e.buf().line_of(e.head()), 1, "Down is j");
e.feed(crate::editor::Key::Right);
assert_eq!(e.head(), e.buf().line_start(1) + 1, "Right is l");
e.feed(crate::editor::Key::Up);
assert_eq!(e.buf().line_of(e.head()), 0, "Up is k");
e.feed(crate::editor::Key::Left);
assert_eq!(e.head(), 0, "Left is h");
}
#[test]
fn wq_never_closes_a_failed_save() {
let dir = tempfile::tempdir().unwrap();
let f = dir.path().join("wq.txt");
std::fs::write(&f, "one\n").unwrap();
let mut e = Editor::new(Buffer::open(f.to_str().unwrap()).unwrap());
e.feed_text("ix");
e.feed(crate::editor::Key::Esc);
std::fs::write(&f, "external\n").unwrap(); e.feed_text(":wq\r");
assert!(!e.should_quit, "failed save must not close");
assert!(e.buf().dirty, "still dirty");
assert!(e.message.contains("changed on disk"));
assert_eq!(std::fs::read_to_string(&f).unwrap(), "external\n");
e.feed_text(":wq!\r");
assert!(e.should_quit, "forced write quits");
assert_eq!(std::fs::read_to_string(&f).unwrap(), "xone\n");
}
#[test]
fn ex_open_and_close_buffers() {
std::fs::write("/tmp/strop-test-b.rs", "second\n").unwrap();
let mut e = editor_with("first\n");
e.feed_text(":e /tmp/strop-test-b.rs<cr>");
assert_eq!(e.docs.len(), 2);
assert_eq!(text(&e), "second\n");
e.feed_text(":q<cr>");
assert_eq!(e.docs.len(), 1);
assert_eq!(text(&e), "first\n");
e.feed_text("ix");
e.feed(crate::editor::Key::Esc);
e.feed_text(":q<cr>");
assert_eq!(e.docs.len(), 1);
assert!(e.message.contains("unsaved"));
e.feed_text(":q!<cr>");
assert!(e.should_quit);
}
}
#[cfg(test)]
mod indent_tests {
use super::*;
#[test]
fn enter_copies_and_deepens_indent() {
let mut e = Editor::new(Buffer::from_text("fn f() {\n let x = 1;\n}\n"));
e.feed_text("j$"); e.feed(crate::editor::Key::Char('a'));
e.feed(crate::editor::Key::Enter);
e.feed_text("let y = 2;");
assert_eq!(
e.buf().rope.to_string(),
"fn f() {\n let x = 1;\n let y = 2;\n}\n"
);
e.feed(crate::editor::Key::Esc);
e.feed_text("gg$");
e.feed(crate::editor::Key::Char('a'));
e.feed(crate::editor::Key::Enter);
e.feed_text("// body");
let got = e.buf().rope.to_string();
assert!(got.starts_with("fn f() {\n // body"), "got: {got:?}");
}
#[test]
fn o_auto_indents() {
let mut e = Editor::new(Buffer::from_text("fn f() {\n}\n"));
e.feed_text("o");
e.feed_text("let x = 1;");
assert_eq!(e.buf().rope.to_string(), "fn f() {\n let x = 1;\n}\n");
}
#[test]
fn tab_size_from_config() {
let mut e = Editor::new(Buffer::from_text("a\nb\n"));
e.config = crate::config::Config {
tab_size: 2,
..Default::default()
};
e.feed_text(">>");
assert_eq!(e.buf().rope.to_string(), " a\nb\n");
e.feed_text("<<");
assert_eq!(e.buf().rope.to_string(), "a\nb\n");
}
#[test]
fn new_file_opens_empty_and_saves() {
let path = "/tmp/strop-newfile-test.rs";
std::fs::remove_file(path).ok();
let mut e = Editor::new(Buffer::open(path).expect("missing file is a new buffer"));
assert_eq!(e.buf().len_bytes(), 0);
e.feed_text("ifresh");
e.feed(crate::editor::Key::Esc);
e.feed_text(":w<cr>");
assert_eq!(std::fs::read_to_string(path).unwrap(), "fresh");
std::fs::remove_file(path).ok();
}
}
#[cfg(test)]
mod alignment_tests {
use super::*;
#[test]
fn document_set_stays_honest() {
let dir = tempfile::tempdir().unwrap();
let a = dir.path().join("align-a.rs");
let b = dir.path().join("align-b.rs");
std::fs::write(&a, "a\n").unwrap();
std::fs::write(&b, "b\n").unwrap();
let mut e = Editor::new(Buffer::open(a.to_str().unwrap()).unwrap());
e.open_buffer(b.to_str().unwrap()).unwrap();
assert_eq!(e.docs.len(), 2);
e.open_diff_surface("delta", "f.rs", vec![], None);
assert_eq!(e.docs.len(), 3);
assert!(e.cur().surface.is_some());
e.close_buffer(true);
assert_eq!(e.docs.len(), 2);
e.close_buffer(true);
e.close_buffer(true);
assert!(e.should_quit, "closing the last document quits");
}
}
#[cfg(test)]
mod smartindent_tests {
use super::*;
#[test]
fn closer_dedents_on_indent_only_line() {
let mut e = Editor::new(Buffer::from_text("fn f() {\n}\n"));
e.feed_text("o"); assert_eq!(e.buf().line_text(1), " ");
e.feed_text("}"); assert_eq!(e.buf().rope.to_string(), "fn f() {\n}\n}\n");
}
#[test]
fn closer_noop_with_real_text_before() {
let mut e = Editor::new(Buffer::from_text("fn f() {\n}\n"));
e.feed_text("o"); e.feed_text("let x = 1;"); e.feed_text("}"); assert_eq!(e.buf().line_text(1), " let x = 1;}");
}
}
#[cfg(test)]
mod undo_tests {
use super::*;
#[test]
fn insert_session_undoes_as_one_unit() {
let mut e = Editor::new(Buffer::from_text("hello\n"));
e.feed_text("A world"); e.feed(crate::editor::Key::Esc);
assert_eq!(e.buf().rope.to_string(), "hello world\n");
e.feed_text("u");
assert_eq!(e.buf().rope.to_string(), "hello\n");
e.feed(crate::editor::Key::CtrlR);
assert_eq!(e.buf().rope.to_string(), "hello world\n");
}
#[test]
fn change_op_holds_one_undo_unit() {
let mut e = Editor::new(Buffer::from_text("say [old] now\n"));
e.feed_text("w"); e.feed_text("ci["); e.feed_text("new");
e.feed(crate::editor::Key::Esc);
assert_eq!(e.buf().rope.to_string(), "say [new] now\n");
e.feed_text("u"); assert_eq!(e.buf().rope.to_string(), "say [old] now\n");
}
#[test]
fn n_repeats_search_and_wraps() {
let mut e = Editor::new(Buffer::from_text("foo bar\nfoo baz\n"));
e.feed_text("/foo\r"); assert_eq!(e.head(), 8);
e.feed_text("n"); assert_eq!(e.head(), 0);
e.feed_text("N"); assert_eq!(e.head(), 8);
}
#[test]
fn edit_after_undo_forks_and_ctrlr_redoes_last_branch() {
let mut e = Editor::new(Buffer::from_text("ab\n"));
e.feed_text("rx"); e.feed_text("u");
e.feed_text("ry"); e.feed_text("u"); e.feed(crate::editor::Key::CtrlR); assert_eq!(e.buf().rope.to_string(), "yb\n");
e.feed(crate::editor::Key::CtrlR);
assert_eq!(e.buf().rope.to_string(), "yb\n");
}
#[test]
fn readonly_buffers_refuse_undo() {
let mut e = Editor::new(Buffer::from_text("x\n"));
e.buf_mut().readonly = true;
e.feed_text("u");
assert!(e.message.contains("readonly"));
assert_eq!(e.buf().rope.to_string(), "x\n");
}
}
#[cfg(test)]
mod surround_tests {
use super::*;
#[test]
fn ds_deletes_pair() {
let mut e = Editor::new(Buffer::from_text("say \"hi\" now\n"));
e.feed_text("w"); e.feed_text("ds\"");
assert_eq!(e.buf().rope.to_string(), "say hi now\n");
e.feed_text("u");
assert_eq!(e.buf().rope.to_string(), "say \"hi\" now\n");
}
#[test]
fn cs_changes_pair() {
let mut e = Editor::new(Buffer::from_text("call(a, b)\n"));
e.feed_text("f("); e.feed_text("cs(["); assert_eq!(e.buf().rope.to_string(), "call[a, b]\n");
e.feed_text("u");
assert_eq!(e.buf().rope.to_string(), "call(a, b)\n");
}
#[test]
fn ysiw_wraps_word() {
let mut e = Editor::new(Buffer::from_text("make it sharp\n"));
e.feed_text("w"); e.feed_text("ysiw\"");
assert_eq!(e.buf().rope.to_string(), "make \"it\" sharp\n");
e.feed_text("u");
assert_eq!(e.buf().rope.to_string(), "make it sharp\n");
}
#[test]
fn visual_s_wraps_selection() {
let mut e = Editor::new(Buffer::from_text("wrap me up\n"));
e.feed_text("ve"); e.feed_text("S(");
assert_eq!(e.buf().rope.to_string(), "(wrap) me up\n");
}
}
#[cfg(test)]
mod visual_object_tests {
use super::*;
#[test]
fn vi_paren_selects_inner() {
let mut e = Editor::new(Buffer::from_text("call(a, b)\n"));
e.feed_text("f("); e.feed_text("vi(");
let r = e.visual_range().expect("visual range");
assert_eq!(e.buf().slice_string(r), "a, b");
e.feed_text("d");
assert_eq!(e.buf().rope.to_string(), "call()\n");
}
#[test]
fn va_quote_includes_quotes() {
let mut e = Editor::new(Buffer::from_text("say \"hi\" now\n"));
e.feed_text("w"); e.feed_text("va\"");
let r = e.visual_range().expect("visual range");
assert_eq!(e.buf().slice_string(r), "\"hi\"");
}
}
#[cfg(test)]
mod hardening_tests {
use super::*;
#[test]
fn undo_after_visual_delete() {
let mut e = Editor::new(Buffer::from_text("say \"hi\" now\n"));
e.feed_text("ved"); assert_eq!(e.buf().rope.to_string(), " \"hi\" now\n");
e.feed_text("u");
assert_eq!(e.buf().rope.to_string(), "say \"hi\" now\n");
}
#[test]
fn quote_object_scans_forward_on_the_line() {
let mut e = Editor::new(Buffer::from_text("say \"hi\" now\n"));
e.feed_text("vi\"");
let r = e.visual_range().expect("selection");
assert_eq!(e.buf().slice_string(r), "hi");
}
#[test]
fn quit_leaves_editor_drain_safe() {
let mut e = Editor::new(Buffer::from_text("x\n"));
e.feed_text(":q!\r");
assert!(e.should_quit);
assert!(e.docs.is_empty());
e.drain_picker();
e.drain_git_jobs();
e.drain_lsp();
e.lsp_sync_changed();
}
#[test]
fn undo_lands_cursor_at_change_start() {
let mut e = Editor::new(Buffer::from_text("hello\n"));
e.feed_text("A world");
e.feed(crate::editor::Key::Esc);
e.feed_text("0"); e.feed_text("u");
assert_eq!(e.buf().rope.to_string(), "hello\n");
assert_eq!(e.head(), 4);
}
}
#[cfg(test)]
mod keybinds_tests {
use super::*;
#[test]
fn marks_set_and_jump() {
std::fs::write("/tmp/strop-mark-a.rs", "one\ntwo\nthree\n").unwrap();
std::fs::write("/tmp/strop-mark-b.rs", "alpha\nbeta\n").unwrap();
let mut e = Editor::new(Buffer::open("/tmp/strop-mark-a.rs").unwrap());
e.feed_text("jj"); e.feed_text("mb"); e.feed_text(":e /tmp/strop-mark-b.rs<cr>");
e.feed_text("'b"); assert_eq!(e.buf().path.as_deref(), Some("/tmp/strop-mark-a.rs"));
assert_eq!(e.buf().line_of(e.head()), 2);
std::fs::remove_file("/tmp/strop-mark-a.rs").ok();
std::fs::remove_file("/tmp/strop-mark-b.rs").ok();
}
#[test]
fn question_mark_is_search_backward() {
let mut e = Editor::new(Buffer::from_text("one two one two\n"));
e.feed_text("$"); e.feed_text("?one\r");
assert_eq!(
e.buf().col_of(e.head()),
8,
"backward search lands on the second 'one'"
);
}
#[test]
fn dw_leaves_exactly_one_cursor() {
let mut e = Editor::new(Buffer::from_text("one two three\n"));
e.feed_text("dw");
assert_eq!(e.sels().extra_heads().len(), 0);
assert_eq!(e.buf().rope.to_string(), "two three\n");
}
#[test]
fn arrows_consume_pending_counts() {
let mut e = Editor::new(Buffer::from_text("hello world\n"));
e.feed_text("2");
e.feed(crate::editor::Key::Right);
assert_eq!(e.buf().col_of(e.head()), 2);
e.feed_text("x");
assert_eq!(e.buf().rope.to_string(), "helo world\n");
}
#[test]
fn pathless_save_is_an_error_not_a_lie() {
let mut e = Editor::new(Buffer::from_text("unsaved\n"));
e.feed_text(":w\r");
assert!(e.message.contains("no file name"), "{}", e.message);
e.feed_text(":wq\r");
assert_eq!(e.buf().rope.to_string(), "unsaved\n");
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("named.txt");
e.feed_text(&format!(":w {}\r", p.display()));
assert_eq!(std::fs::read_to_string(&p).unwrap(), "unsaved\n");
assert_eq!(e.buf().path.as_deref(), Some(p.to_str().unwrap()));
}
#[test]
fn ctrl_c_warns_once_then_forces() {
let mut e = Editor::new(Buffer::from_text("dirty\n"));
e.feed_text("ix");
e.feed(crate::editor::Key::Esc);
assert!(!e.ctrl_c_quit());
assert!(e.message.contains("ctrl-c again"));
assert!(e.ctrl_c_quit());
let mut e = Editor::new(Buffer::from_text("clean\n"));
assert!(e.ctrl_c_quit());
}
#[test]
fn failed_pipe_never_touches_the_source() {
let mut e = Editor::new(Buffer::from_text("keep me\n"));
e.feed_text("V");
e.feed_text(" |false");
e.feed(crate::editor::Key::Enter);
for _ in 0..200 {
e.drain_shell();
if e.message.starts_with("pipe failed") {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert_eq!(e.buf().rope.to_string(), "keep me\n");
assert!(e.message.starts_with("pipe failed"), "{}", e.message);
}
fn tall_editor() -> Editor {
let text: String = (1..=100).map(|i| format!("line {i:03}\n")).collect();
let mut e = Editor::new(Buffer::from_text(&text));
e.view_rows = 20;
e
}
#[test]
fn ctrl_d_u_scroll_half_pages() {
let mut e = tall_editor();
e.feed_text("<c-d>");
assert_eq!(e.buf().line_of(e.head()), 10);
e.feed_text("<c-u>");
assert_eq!(e.buf().line_of(e.head()), 0);
e.feed_text("5<c-d>");
assert_eq!(e.buf().line_of(e.head()), 5);
}
#[test]
fn view_place_centers_and_edges() {
let mut e = tall_editor();
e.feed_text("50G");
e.view_place('z');
let line = e.buf().line_of(e.head());
assert_eq!(e.view_top() + 10, line);
e.view_place('t');
assert_eq!(e.view_top(), line);
e.view_place('b');
assert_eq!(e.view_top() + 20, line + 1);
}
#[test]
fn visible_jumps_and_counts() {
let mut e = tall_editor();
e.view_mut().view_top = 30;
e.feed_text("H");
assert_eq!(e.buf().line_of(e.head()), 30);
e.feed_text("3L");
assert_eq!(e.buf().line_of(e.head()), 30 + 20 - 1 - 2);
e.feed_text("M");
assert_eq!(e.buf().line_of(e.head()), 30 + 10);
}
#[test]
fn gv_reselects_and_gi_reinserts() {
let mut e = Editor::new(Buffer::from_text("hello world\nsecond\n"));
e.feed_text("vll");
e.feed(crate::editor::Key::Esc);
e.feed_text("gv");
assert_eq!(e.mode, crate::editor::Mode::Visual);
let p = e.sels().primary();
assert_eq!((p.anchor.min(p.head), p.anchor.max(p.head)), (0, 2));
e.feed(crate::editor::Key::Esc);
e.feed_text("2Gix");
e.feed(crate::editor::Key::Esc);
e.feed_text("gg");
e.feed_text("gi");
assert_eq!(e.mode, crate::editor::Mode::Insert);
assert_eq!(e.buf().line_of(e.head()), 1);
}
#[test]
fn change_list_walks_and_invalidates() {
let mut e = Editor::new(Buffer::from_text("aaa\nbbb\nccc\n"));
e.feed_text("ix");
e.feed(crate::editor::Key::Esc);
e.feed_text("G");
e.feed_text("Ay");
e.feed(crate::editor::Key::Esc);
e.feed_text("gg");
e.feed_text("g;");
assert_eq!(e.buf().line_of(e.head()), 2);
e.feed_text("g;");
assert_eq!(e.buf().line_of(e.head()), 0);
e.feed_text("g,");
assert_eq!(e.buf().line_of(e.head()), 2);
e.feed_text("ggiz");
e.feed(crate::editor::Key::Esc);
e.feed_text("g;");
assert_eq!(e.buf().line_of(e.head()), 0);
}
#[test]
fn alternate_buffer_round_trips() {
let dir = tempfile::tempdir().unwrap();
let a = dir.path().join("a.txt");
let b = dir.path().join("b.txt");
std::fs::write(&a, "aaa\n").unwrap();
std::fs::write(&b, "bbb\n").unwrap();
let mut e = Editor::new(Buffer::open(a.to_str().unwrap()).unwrap());
e.feed_text(&format!(":e {}\r", b.display()));
assert!(e.buf().path.as_deref().unwrap().ends_with("b.txt"));
e.feed_text("<c-^>");
assert!(e.buf().path.as_deref().unwrap().ends_with("a.txt"));
e.feed_text("<c-^>");
assert!(e.buf().path.as_deref().unwrap().ends_with("b.txt"));
}
#[test]
fn ex_ranges_and_substitute() {
let mut e = Editor::new(Buffer::from_text("foo one\nfoo two\nfoo three\n"));
e.feed_text(":%s/foo/bar/g\r");
assert_eq!(e.buf().rope.to_string(), "bar one\nbar two\nbar three\n");
e.feed_text("u");
assert_eq!(e.buf().rope.to_string(), "foo one\nfoo two\nfoo three\n");
e.feed_text(":2s/foo/only/\r");
assert_eq!(e.buf().rope.to_string(), "foo one\nonly two\nfoo three\n");
e.feed_text(":2,3d\r");
assert_eq!(e.buf().rope.to_string(), "foo one\n");
e.feed_text("u");
e.feed_text("gg:3\r");
assert_eq!(e.buf().line_of(e.head()), 2);
e.feed_text(":%s/nope/x/g\r");
assert!(e.message.contains("pattern not found"));
}
#[test]
fn diagnostic_jumps_wrap() {
use strop_lsp::Diag;
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("d.txt");
std::fs::write(&p, "aaa\nbbb\nccc\nddd\n").unwrap();
let mut e = Editor::new(Buffer::open(p.to_str().unwrap()).unwrap());
e.cwd = dir.path().to_path_buf();
let mk = |line: usize, msg: &str| Diag {
line,
col: 0,
end_line: line,
end_col: 1,
severity: 1,
message: msg.into(),
};
e.diags
.insert(p.clone(), vec![mk(1, "second"), mk(3, "fourth")]);
e.feed_text("]d");
assert_eq!(e.buf().line_of(e.head()), 1);
e.feed_text("]d");
assert_eq!(e.buf().line_of(e.head()), 3);
e.feed_text("]d"); assert_eq!(e.buf().line_of(e.head()), 1);
e.feed_text("[d"); assert_eq!(e.buf().line_of(e.head()), 3);
}
#[test]
fn dbg_unicode() {
let mut e = Editor::new(Buffer::from_text("café münchen\n"));
e.feed_text("llll");
eprintln!(
"after 4l: byte {} col {}",
e.head(),
e.buf().col_of(e.head())
);
e.feed_text("rX");
eprintln!("after rX: {:?}", e.buf().rope.to_string());
let mut e = Editor::new(Buffer::from_text("café münchen\n"));
e.feed_text("/mü\r");
eprintln!(
"search land: byte {} col {}",
e.head(),
e.buf().col_of(e.head())
);
}
#[test]
fn dbg_l_steps() {
let mut e = Editor::new(Buffer::from_text("café münchen\n"));
for i in 0..5 {
e.feed_text("l");
eprintln!("l{}: byte {}", i + 1, e.head());
}
}
#[test]
fn dbg_l_from_3() {
let mut e = Editor::new(Buffer::from_text("café münchen\n"));
e.set_head(3);
e.feed_text("l");
eprintln!(
"l from 3 -> {} (mode {:?}, msg {:?})",
e.head(),
e.mode,
e.message
);
e.feed_text("l");
eprintln!("again -> {}", e.head());
}
#[test]
fn bracketed_paste_is_one_text_unit() {
let mut e = Editor::new(Buffer::from_text("fn main() {}\n"));
e.feed_text("i");
e.paste_bracketed("// :q! not a command\n");
e.feed(crate::editor::Key::Esc);
assert!(e
.buf()
.rope
.to_string()
.starts_with("// :q! not a command\n"));
e.feed_text("u");
assert_eq!(e.buf().rope.to_string(), "fn main() {}\n");
let mut e = Editor::new(Buffer::from_text("ab\n"));
e.paste_bracketed("XY");
assert_eq!(e.buf().rope.to_string(), "aXYb\n");
}
#[test]
fn block_mode_ops() {
let mut e = Editor::new(Buffer::from_text("aa11bb\ncc22dd\nee33ff\n"));
e.feed_text("<c-v>lljx");
assert_eq!(e.buf().rope.to_string(), "1bb\n2dd\nee33ff\n");
let mut e = Editor::new(Buffer::from_text("aa\ncc\n"));
e.feed_text("<c-v>j");
e.feed_text("I");
e.feed_text(">>");
e.feed(crate::editor::Key::Esc);
assert_eq!(e.buf().rope.to_string(), ">>aa\n>>cc\n");
}
}