use edamame::config::{Action, Theme};
use edamame::document::Buffer;
use edamame::editor::{edit_ops, EditorState, Mode};
const VP: usize = 40; const VW: usize = 80;
fn theme() -> &'static Theme {
Box::leak(Box::new(Theme::default()))
}
fn state(text: &str) -> EditorState {
EditorState::new(Buffer::from_str(text), theme())
}
fn apply(st: &mut EditorState, action: Action) -> bool {
edit_ops::apply(st, action, VP, VW)
}
fn apply_all(st: &mut EditorState, actions: &[Action]) {
for a in actions {
apply(st, a.clone());
}
}
#[test]
fn insert_char_enters_edit_mode() {
let mut st = state("");
assert_eq!(st.mode, Mode::Preview);
apply(&mut st, Action::InsertChar('a'));
assert_eq!(st.mode, Mode::Rendered);
assert_eq!(st.contents(), ""); assert_eq!(st.cursor.offset, 0);
apply(&mut st, Action::InsertChar('a'));
assert_eq!(st.contents(), "a");
assert_eq!(st.cursor.offset, 1);
}
#[test]
fn insert_multiple_chars() {
let mut st = state("");
st.mode = Mode::Rendered; apply_all(
&mut st,
&[
Action::InsertChar('h'),
Action::InsertChar('e'),
Action::InsertChar('l'),
Action::InsertChar('l'),
Action::InsertChar('o'),
],
);
assert_eq!(st.contents(), "hello");
assert_eq!(st.cursor.offset, 5);
}
#[test]
fn newline_splits_line() {
let mut st = state("");
st.mode = Mode::Rendered;
apply_all(
&mut st,
&[
Action::InsertChar('a'),
Action::InsertChar('b'),
Action::Newline,
Action::InsertChar('c'),
],
);
assert_eq!(st.contents(), "ab\nc");
assert_eq!(st.cursor.offset, 4);
}
#[test]
fn delete_char_back_basic() {
let mut st = state("hello");
st.cursor.offset = 5;
st.mode = Mode::Rendered;
apply(&mut st, Action::DeleteCharBack);
assert_eq!(st.contents(), "hell");
assert_eq!(st.cursor.offset, 4);
}
#[test]
fn delete_char_back_at_start_is_noop() {
let mut st = state("hello");
st.cursor.offset = 0;
st.mode = Mode::Rendered;
apply(&mut st, Action::DeleteCharBack);
assert_eq!(st.contents(), "hello");
assert_eq!(st.cursor.offset, 0);
}
#[test]
fn delete_char_forward_basic() {
let mut st = state("hello");
st.cursor.offset = 2;
st.mode = Mode::Rendered;
apply(&mut st, Action::DeleteCharForward);
assert_eq!(st.contents(), "helo");
assert_eq!(st.cursor.offset, 2);
}
#[test]
fn delete_char_forward_at_end_is_noop() {
let mut st = state("hello");
st.cursor.offset = 5;
st.mode = Mode::Rendered;
apply(&mut st, Action::DeleteCharForward);
assert_eq!(st.contents(), "hello");
assert_eq!(st.cursor.offset, 5);
}
#[test]
fn burst_of_inserts_coalesces_into_one_delta() {
let mut st = state("");
st.mode = Mode::Rendered;
let history_before = st.history.undo_depth();
let version_before = st.parsed_version;
let chars: Vec<char> = "aaaaaaaaaa".chars().collect();
edit_ops::apply_insert_run(&mut st, &chars, VP, VW);
assert_eq!(st.contents(), "aaaaaaaaaa");
assert_eq!(
st.history.undo_depth() - history_before,
1,
"one history entry for the whole run"
);
let bumps = st.parsed_version.wrapping_sub(version_before);
assert!(bumps <= 2, "expected ≤ 2 parsed_version bumps, got {bumps}");
}
#[test]
fn burst_breaks_on_kind_change() {
let mut st = state("");
st.mode = Mode::Rendered;
let history_before = st.history.undo_depth();
apply(&mut st, Action::InsertChar('a'));
apply(&mut st, Action::DeleteCharBack);
apply(&mut st, Action::InsertChar('b'));
assert_eq!(st.contents(), "b");
assert_eq!(st.history.undo_depth() - history_before, 3);
}
#[test]
fn delete_run_coalesces_into_one_delta() {
let mut st = state("abcdef");
st.mode = Mode::Rendered;
st.cursor.offset = 6;
let history_before = st.history.undo_depth();
edit_ops::apply_delete_run(&mut st, 4, true, VP, VW);
assert_eq!(st.contents(), "ab");
assert_eq!(st.cursor.offset, 2);
assert_eq!(st.history.undo_depth() - history_before, 1);
}
#[test]
fn forward_delete_run_coalesces_into_one_delta() {
let mut st = state("abcdef");
st.mode = Mode::Rendered;
st.cursor.offset = 1;
let history_before = st.history.undo_depth();
edit_ops::apply_delete_run(&mut st, 3, false, VP, VW);
assert_eq!(st.contents(), "aef");
assert_eq!(st.cursor.offset, 1);
assert_eq!(st.history.undo_depth() - history_before, 1);
}
#[test]
fn insert_run_escapes_pipe_inside_table() {
let mut st = state("|h|w|\n|-|-|\n|a|b|\n");
st.mode = Mode::Rendered;
st.cursor.offset = 14;
let before = st.contents();
edit_ops::apply_insert_run(&mut st, &['|', 'x'], VP, VW);
let after = st.contents();
assert!(
after.contains("\\|x"),
"expected \\| escape; got: before={:?} after={:?}",
before,
after
);
}
#[test]
fn insert_tab_inserts_four_spaces() {
let mut st = state("");
st.mode = Mode::Rendered;
apply(&mut st, Action::InsertTab);
assert_eq!(st.contents(), " ");
assert_eq!(st.cursor.offset, 4);
}
#[test]
fn undo_reverses_insert() {
let mut st = state("");
st.mode = Mode::Rendered;
apply(&mut st, Action::InsertChar('a'));
apply(&mut st, Action::InsertChar('b'));
assert_eq!(st.contents(), "ab");
apply(&mut st, Action::Undo);
assert_eq!(st.contents(), "");
}
#[test]
fn undo_merges_contiguous_inserts_across_character_classes() {
let mut st = state("");
st.mode = Mode::Rendered;
apply(&mut st, Action::InsertChar('a'));
apply(&mut st, Action::InsertChar(' '));
apply(&mut st, Action::InsertChar('b'));
assert_eq!(st.contents(), "a b");
apply(&mut st, Action::Undo);
assert_eq!(st.contents(), "");
}
#[test]
fn undo_past_empty_stack_is_noop() {
let mut st = state("hello");
st.mode = Mode::Rendered;
apply(&mut st, Action::Undo); assert_eq!(st.contents(), "hello");
}
#[test]
fn redo_after_undo() {
let mut st = state("");
st.mode = Mode::Rendered;
apply(&mut st, Action::InsertChar('x'));
apply(&mut st, Action::Undo);
assert_eq!(st.contents(), "");
apply(&mut st, Action::Redo);
assert_eq!(st.contents(), "x");
assert_eq!(st.cursor.offset, 1);
}
#[test]
fn redo_cleared_by_new_edit() {
let mut st = state("");
st.mode = Mode::Rendered;
apply(&mut st, Action::InsertChar('a'));
apply(&mut st, Action::Undo);
apply(&mut st, Action::InsertChar('b')); apply(&mut st, Action::Redo); assert_eq!(st.contents(), "b");
}
#[test]
fn undo_delete() {
let mut st = state("hello");
st.cursor.offset = 5;
st.mode = Mode::Rendered;
apply(&mut st, Action::DeleteCharBack); assert_eq!(st.contents(), "hell");
apply(&mut st, Action::Undo);
assert_eq!(st.contents(), "hello");
}
#[test]
fn move_left_right() {
let mut st = state("hello");
st.cursor.offset = 2;
st.mode = Mode::Rendered;
apply(&mut st, Action::MoveRight);
assert_eq!(st.cursor.offset, 3);
apply(&mut st, Action::MoveLeft);
assert_eq!(st.cursor.offset, 2);
}
#[test]
fn move_up_down() {
let mut st = state("hello\nworld\n");
st.cursor.offset = 2; st.cursor.preferred_col = 2;
st.mode = Mode::Rendered;
apply(&mut st, Action::MoveDown);
assert_eq!(st.cursor.offset, 8);
apply(&mut st, Action::MoveUp);
assert_eq!(st.cursor.offset, 2); }
#[test]
fn move_up_in_preview_mode_scrolls() {
let mut st = state("Hello\nWorld\n");
assert_eq!(st.mode, Mode::Preview);
st.scroll = 1;
apply(&mut st, Action::MoveUp);
assert_eq!(st.scroll, 0); assert_eq!(st.mode, Mode::Preview); }
#[test]
fn move_down_in_preview_mode_scrolls() {
let mut st = state("Hello\nWorld\n");
assert_eq!(st.mode, Mode::Preview);
apply(&mut st, Action::MoveDown);
assert_eq!(st.mode, Mode::Preview);
}
#[test]
fn move_line_start_end() {
let mut st = state("hello world");
st.cursor.offset = 5;
st.mode = Mode::Rendered;
apply(&mut st, Action::MoveLineEnd);
assert_eq!(st.cursor.offset, 11);
apply(&mut st, Action::MoveLineStart);
assert_eq!(st.cursor.offset, 0);
}
#[test]
fn move_word_left_right() {
let mut st = state("hello world");
st.cursor.offset = 11;
st.mode = Mode::Rendered;
apply(&mut st, Action::MoveWordLeft);
assert_eq!(st.cursor.offset, 6);
apply(&mut st, Action::MoveWordRight);
assert_eq!(st.cursor.offset, 11); }
#[test]
fn move_doc_start_end() {
let mut st = state("hello\nworld\n");
st.cursor.offset = 6;
st.mode = Mode::Rendered;
apply(&mut st, Action::MoveDocEnd);
assert_eq!(st.cursor.offset, 12);
apply(&mut st, Action::MoveDocStart);
assert_eq!(st.cursor.offset, 0);
}
#[test]
fn escape_exits_to_preview() {
let mut st = state("hello");
st.mode = Mode::Rendered;
apply(&mut st, Action::InsertChar('x'));
assert_eq!(st.mode, Mode::Rendered);
apply(&mut st, Action::ExitToPreview);
assert_eq!(st.mode, Mode::Preview);
}
#[test]
fn toggle_raw_mode_cycles() {
let mut st = state("hello");
st.mode = Mode::Rendered;
apply(&mut st, Action::InsertChar('x'));
assert_eq!(st.mode, Mode::Rendered);
apply(&mut st, Action::ToggleRawMode);
assert_eq!(st.mode, Mode::Raw);
apply(&mut st, Action::ToggleRawMode);
assert_eq!(st.mode, Mode::Rendered);
}
#[test]
fn enter_edit_mode_from_preview() {
let mut st = state("hello");
assert_eq!(st.mode, Mode::Preview);
apply(&mut st, Action::EnterEditMode);
assert_eq!(st.mode, Mode::Rendered);
}
#[test]
fn delete_word_back() {
let mut st = state("hello world");
st.cursor.offset = 11;
st.mode = Mode::Rendered;
apply(&mut st, Action::DeleteWordBack);
assert_eq!(st.contents(), "hello ");
assert_eq!(st.cursor.offset, 6);
}
#[test]
fn delete_word_forward() {
let mut st = state("hello world");
st.cursor.offset = 0;
st.mode = Mode::Rendered;
apply(&mut st, Action::DeleteWordForward);
assert_eq!(st.contents(), "world");
assert_eq!(st.cursor.offset, 0);
}
#[test]
fn delete_line() {
let mut st = state("line1\nline2\nline3\n");
st.cursor.offset = 6; st.mode = Mode::Rendered;
apply(&mut st, Action::DeleteLine);
assert_eq!(st.contents(), "line1\nline3\n");
}
#[test]
fn select_all_covers_buffer() {
let mut st = state("hello world");
st.mode = Mode::Rendered;
apply(&mut st, Action::SelectAll);
let sel = st.selection.unwrap();
assert_eq!(sel.anchor, 0);
assert_eq!(sel.active, 11);
}
#[test]
fn copy_sets_kill_ring() {
let mut st = state("hello world");
st.mode = Mode::Rendered;
apply(&mut st, Action::SelectAll);
apply(&mut st, Action::Copy);
assert_eq!(st.kill_ring, "hello world");
}
#[test]
fn cut_deletes_selection() {
let mut st = state("hello world");
st.mode = Mode::Rendered;
apply(&mut st, Action::SelectAll);
apply(&mut st, Action::Cut);
assert_eq!(st.contents(), "");
assert_eq!(st.kill_ring, "hello world");
}
#[test]
fn paste_from_kill_ring() {
let mut st = state("world");
st.mode = Mode::Rendered;
apply(&mut st, Action::SelectAll);
apply(&mut st, Action::Copy); apply(&mut st, Action::MoveDocEnd);
st.selection = None;
assert_eq!(st.kill_ring, "world");
}
#[test]
fn dirty_flag_set_on_insert() {
let mut st = state("hello");
st.mode = Mode::Rendered;
assert!(!st.dirty);
apply(&mut st, Action::InsertChar('!'));
assert!(st.dirty);
}
#[test]
fn save_clears_dirty_flag() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.md");
std::fs::write(&path, "hello").unwrap();
let buf = Buffer::load_file(&path).unwrap();
let mut st = EditorState::new(buf, theme());
st.mode = Mode::Rendered;
apply(&mut st, Action::MoveDocEnd);
apply(&mut st, Action::InsertChar('!'));
assert!(st.dirty);
assert_eq!(st.contents(), "hello!");
st.buffer.save_file().unwrap();
let saved = std::fs::read_to_string(&path).unwrap();
assert_eq!(saved, "hello!");
}
#[test]
fn insert_replaces_selection() {
let mut st = state("hello world");
st.mode = Mode::Rendered;
use edamame::document::Selection;
st.selection = Some(Selection {
anchor: 6,
active: 11,
});
st.cursor.offset = 11;
apply(&mut st, Action::InsertChar('X'));
assert_eq!(st.contents(), "hello X");
}
#[test]
fn scroll_actions_work_in_preview() {
let text = "hello\n\nhello\n\n".repeat(30);
let mut st = state(&text);
assert_eq!(st.scroll, 0);
apply(&mut st, Action::ScrollDown);
assert_eq!(st.scroll, 1);
apply(&mut st, Action::ScrollUp);
assert_eq!(st.scroll, 0);
apply(&mut st, Action::ScrollToBottom);
assert!(st.scroll > 0);
apply(&mut st, Action::ScrollToTop);
assert_eq!(st.scroll, 0);
}
#[test]
fn complex_edit_sequence() {
let mut st = state("The quick brown fox\n");
st.mode = Mode::Rendered;
st.cursor.offset = 4;
apply(&mut st, Action::DeleteWordForward);
assert_eq!(st.contents(), "The brown fox\n");
for ch in "slow ".chars() {
apply(&mut st, Action::InsertChar(ch));
}
assert_eq!(st.contents(), "The slow brown fox\n");
for _ in 0..6 {
apply(&mut st, Action::Undo);
}
assert_eq!(st.contents(), "The quick brown fox\n");
}
#[test]
fn cursor_lands_on_blank_line_between_paragraphs() {
let mut st = state("First\n\nSecond\n");
st.mode = Mode::Rendered;
st.cursor.offset = 5;
apply(&mut st, Action::MoveDown);
let (line, _col) = st.cursor.line_col(&st.buffer);
assert_eq!(
line, 1,
"cursor should be on the blank line (buffer line 1)"
);
apply(&mut st, Action::MoveDown);
let (line, _col) = st.cursor.line_col(&st.buffer);
assert_eq!(line, 2, "cursor should reach 'Second' (buffer line 2)");
}
#[test]
fn cursor_steps_through_each_blank_line() {
let mut st = state("A\n\n\nB\n");
st.mode = Mode::Rendered;
st.cursor.offset = 1;
apply(&mut st, Action::MoveDown);
assert_eq!(st.cursor.line_col(&st.buffer).0, 1);
apply(&mut st, Action::MoveDown);
assert_eq!(st.cursor.line_col(&st.buffer).0, 2);
apply(&mut st, Action::MoveDown);
assert_eq!(
st.cursor.line_col(&st.buffer).0,
3,
"should land on 'B' line"
);
}
#[test]
fn raw_mode_cursor_walks_through_table_pipes_one_char_at_a_time() {
let src = "| a | b |\n|---|---|\n| 1 | 2 |\n";
let mut st = state(src);
st.mode = Mode::Raw;
st.cursor.offset = 0;
for expected in 1..=5 {
apply(&mut st, Action::MoveRight);
assert_eq!(
st.cursor.offset, expected,
"raw mode MoveRight should advance one char at a time (got {} at step {expected})",
st.cursor.offset
);
}
}
#[test]
fn raw_mode_down_lands_on_alignment_row() {
let src = "| a | b |\n|---|---|\n| 1 | 2 |\n";
let mut st = state(src);
st.mode = Mode::Raw;
st.cursor.offset = 2;
apply(&mut st, Action::MoveDown);
assert_eq!(st.cursor.line_col(&st.buffer).0, 1);
}
#[test]
fn raw_mode_insert_pipe_is_literal_not_escaped() {
let src = "| a | b |\n|---|---|\n| 1 | 2 |\n";
let mut st = state(src);
st.mode = Mode::Raw;
st.cursor.offset = 2; apply(&mut st, Action::InsertChar('|'));
assert!(st.contents().contains("| |a | b |"));
assert!(!st.contents().contains(r"\|"));
}
#[test]
fn cursor_traverses_image_block_as_one_line() {
let src = "Above.\n\nBelow.\n";
let mut st = state(src);
st.mode = Mode::Rendered;
st.cursor.offset = st.buffer.line_to_char(0) + 6; let line_before = st.cursor.line_col(&st.buffer).0;
assert_eq!(line_before, 0);
apply(&mut st, Action::MoveDown);
let line_image = st.cursor.line_col(&st.buffer).0;
assert_eq!(line_image, 1, "MoveDown should land on image source line");
apply(&mut st, Action::MoveDown);
let line_after = st.cursor.line_col(&st.buffer).0;
assert_eq!(line_after, 2, "MoveDown should land on line below image");
}
fn image_block_rows(st: &EditorState, byte: usize) -> usize {
let idx = st
.parsed
.source_map
.block_for_byte(byte)
.expect("byte inside a block");
st.parsed.block_own_line_count(idx)
}
#[test]
fn revealed_image_collapses_to_its_source_line() {
let src = "Above.\n\n\n\nBelow.\n";
let byte = src.find("![cat]").expect("image in fixture");
let mut st = EditorState::new_with_config(Buffer::from_str(src), theme(), true, true, 12);
st.mode = Mode::Rendered;
assert_eq!(image_block_rows(&st, byte), 12);
st.cursor.offset = st.buffer.rope().byte_to_char(byte);
st.update_cursor_block();
st.cursor_block_entered_at = None;
assert!(st.sync_image_reveal(), "entering the block re-lays out");
assert_eq!(
image_block_rows(&st, byte),
1,
"a revealed image reserves one row for its one source line"
);
}
#[test]
fn leaving_an_image_restores_its_reservation() {
let src = "Above.\n\n\n\nBelow.\n";
let byte = src.find("![cat]").expect("image in fixture");
let mut st = EditorState::new_with_config(Buffer::from_str(src), theme(), true, true, 12);
st.mode = Mode::Rendered;
st.cursor.offset = st.buffer.rope().byte_to_char(byte);
st.update_cursor_block();
st.cursor_block_entered_at = None;
st.sync_image_reveal();
assert_eq!(image_block_rows(&st, byte), 1);
let below = src.find("Below.").expect("trailer in fixture");
st.cursor.offset = st.buffer.rope().byte_to_char(below);
st.update_cursor_block();
st.cursor_block_entered_at = None;
assert!(st.sync_image_reveal(), "leaving the block re-lays out");
assert_eq!(
image_block_rows(&st, byte),
12,
"the picture's rows come back once the cursor leaves"
);
}
#[test]
fn preview_mode_never_collapses_an_image() {
let src = "Above.\n\n\n\nBelow.\n";
let byte = src.find("![cat]").expect("image in fixture");
let mut st = EditorState::new_with_config(Buffer::from_str(src), theme(), true, true, 12);
st.mode = Mode::Preview;
st.cursor.offset = st.buffer.rope().byte_to_char(byte);
st.update_cursor_block();
st.cursor_block_entered_at = None;
assert!(!st.sync_image_reveal());
assert_eq!(image_block_rows(&st, byte), 12);
}
#[test]
fn revealing_one_image_leaves_a_duplicate_of_it_alone() {
let src = "\n\nMiddle.\n\n\n\nBelow.\n";
let first = src.find("![cat]").expect("first image in fixture");
let second = src.rfind("![cat]").expect("second image in fixture");
let mut st = EditorState::new_with_config(Buffer::from_str(src), theme(), true, true, 12);
st.mode = Mode::Rendered;
assert_eq!(image_block_rows(&st, first), 12);
assert_eq!(image_block_rows(&st, second), 12);
st.cursor.offset = st.buffer.rope().byte_to_char(first);
st.update_cursor_block();
st.cursor_block_entered_at = None;
assert!(st.sync_image_reveal(), "entering the block re-lays out");
assert_eq!(
image_block_rows(&st, first),
1,
"the revealed copy collapses"
);
assert_eq!(
image_block_rows(&st, second),
12,
"the duplicate keeps its picture's rows"
);
st.cursor.offset = st.buffer.rope().byte_to_char(second);
st.update_cursor_block();
st.cursor_block_entered_at = None;
assert!(st.sync_image_reveal(), "moving between them re-lays out");
assert_eq!(image_block_rows(&st, first), 12);
assert_eq!(image_block_rows(&st, second), 1);
}
#[test]
fn typing_next_to_image_block_keeps_source_map_consistent() {
let src = "Intro.\n\n\n\nEnd.\n";
let mut st = state(src);
st.mode = Mode::Rendered;
let last_line = st.buffer.line_count() - 2; let line_start = st.buffer.line_to_char(last_line);
st.cursor.offset = line_start + 4; apply_all(
&mut st,
&[
Action::InsertChar('!'),
Action::InsertChar(' '),
Action::InsertChar('X'),
],
);
let src_now = st.contents();
for b in 0..src_now.len() {
let range = st.parsed.source_map.rendered_lines_for_byte(b);
assert!(
!range.is_empty(),
"byte {} in {src_now:?} lost its rendered-line mapping",
b,
);
}
}
#[test]
fn editor_state_tracks_image_block_urls_in_parsed_doc() {
let src = "\n\n\n\nplain paragraph\n";
let st = state(src);
let urls: Vec<&str> = st
.parsed
.image_blocks
.iter()
.map(|b| b.url.as_str())
.collect();
assert_eq!(urls, vec!["a.png", "http://example.com/b.jpg"]);
}
#[test]
fn image_cache_survives_reparse_on_unrelated_edit() {
use edamame::image::DecodeStatus;
let src = "Intro.\n\n\n\nEnd.\n";
let mut st = state(src);
st.mode = Mode::Rendered;
st.images.request("cat.png");
st.images
.set_decoded("cat.png", image::DynamicImage::new_rgba8(1, 1));
assert!(matches!(
st.images.status("cat.png"),
Some(DecodeStatus::Ready(_))
));
st.cursor.offset = st.buffer.len_chars();
apply(&mut st, Action::InsertChar('Z'));
assert!(matches!(
st.images.status("cat.png"),
Some(DecodeStatus::Ready(_))
));
assert_eq!(st.parsed.image_blocks.len(), 1);
assert_eq!(st.parsed.image_blocks[0].url, "cat.png");
}
#[test]
fn down_arrow_past_block_comment_skips_hidden_bytes() {
let src = "Alpha.\n\n<!-- hidden -->\n\nBeta.\n";
let mut st = state(src);
apply(&mut st, Action::EnterEditMode);
let comment_byte = src.find("<!--").unwrap();
let blank_before_byte = comment_byte.saturating_sub(1);
st.cursor.offset = st.buffer.rope().byte_to_char(blank_before_byte);
st.update_cursor_block();
apply(&mut st, Action::MoveDown);
let rope = st.buffer.rope();
let cursor_byte = rope.char_to_byte(st.cursor.offset);
let block_idx = st
.parsed
.source_map
.block_for_byte(cursor_byte)
.expect("cursor byte must map to a block");
assert!(
st.parsed.block_own_line_count(block_idx) > 0,
"cursor landed in a hidden block at byte {cursor_byte}"
);
assert!(
cursor_byte > comment_byte,
"cursor did not move past the comment: at byte {cursor_byte}, comment starts at {comment_byte}"
);
}
#[test]
fn toggle_raw_to_rendered_snaps_cursor_out_of_comment() {
let src = "Alpha.\n\n<!-- hidden -->\n\nBeta.\n";
let mut st = state(src);
apply(&mut st, Action::EnterEditMode);
apply(&mut st, Action::ToggleRawMode);
assert_eq!(st.mode, Mode::Raw);
let comment_byte = src.find("<!--").unwrap();
st.cursor.offset = st.buffer.rope().byte_to_char(comment_byte);
st.update_cursor_block();
apply(&mut st, Action::ToggleRawMode);
assert_eq!(st.mode, Mode::Rendered);
let rope = st.buffer.rope();
let cursor_byte = rope.char_to_byte(st.cursor.offset);
let block_idx = st
.parsed
.source_map
.block_for_byte(cursor_byte)
.expect("cursor byte must map to a block");
assert!(
st.parsed.block_own_line_count(block_idx) > 0,
"cursor landed in a zero-own block after Raw→Rendered snap (byte {cursor_byte})"
);
}
#[test]
fn toggle_raw_mode_preserves_cursor_screen_row() {
let mut src = String::new();
for i in 0..30 {
src.push_str(&format!("line {i}\n"));
}
let mut st = state(&src);
apply(&mut st, Action::EnterEditMode);
st.cursor.offset = st.buffer.line_to_char(15);
st.update_cursor_block();
st.scroll = 12;
let row_before_first_toggle = st.cursor_screen_row(VW);
apply(&mut st, Action::ToggleRawMode);
assert_eq!(st.mode, Mode::Raw);
assert_eq!(st.cursor_screen_row(VW), row_before_first_toggle);
apply(&mut st, Action::ToggleRawMode);
assert_eq!(st.mode, Mode::Rendered);
assert_eq!(st.cursor_screen_row(VW), row_before_first_toggle);
}
#[test]
fn newline_at_viewport_bottom_scrolls_to_keep_cursor_visible() {
let mut src = String::new();
for i in 0..40 {
src.push_str(&format!("line {i}\n"));
}
let vp_h = 10;
let vp_w = 40;
let mut st = state(&src);
st.mode = Mode::Raw;
st.scroll = 5;
let cursor_line = st.scroll + vp_h - 1; let line_start = st.buffer.line_to_char(cursor_line);
let line_text = st.buffer.line(cursor_line).unwrap_or_default();
let line_len = line_text.trim_end_matches('\n').chars().count();
st.cursor.offset = line_start + line_len;
st.cursor.preferred_col = line_len;
st.update_cursor_block();
let scroll_before = st.scroll;
edit_ops::apply(&mut st, Action::Newline, vp_h, vp_w);
assert!(
st.scroll > scroll_before,
"scroll {} did not advance after newline at viewport bottom",
st.scroll
);
let (cursor_line_after, _) = st.cursor.line_col(&st.buffer);
assert!(cursor_line_after >= st.scroll && cursor_line_after < st.scroll + vp_h);
}
#[test]
fn type_at_viewport_bottom_wraps_and_scrolls_in_raw_mode() {
let mut src = String::new();
for _ in 0..40 {
src.push_str("short\n");
}
let vp_h = 10;
let vp_w = 20;
let mut st = state(&src);
st.mode = Mode::Raw;
st.scroll = 0;
let cursor_line = vp_h - 1;
let pad = "X".repeat(vp_w - "short".len());
let line_start = st.buffer.line_to_char(cursor_line);
st.buffer.insert(line_start + "short".len(), &pad);
let line_text = st.buffer.line(cursor_line).unwrap_or_default();
let line_len = line_text.trim_end_matches('\n').chars().count();
st.cursor.offset = line_start + line_len;
st.cursor.preferred_col = line_len;
st.update_cursor_block();
let scroll_before = st.scroll;
edit_ops::apply(&mut st, Action::InsertChar('Y'), vp_h, vp_w);
assert!(
st.scroll > scroll_before,
"scroll {} did not advance after a wrap-inducing keystroke (cursor on visual row past viewport bottom)",
st.scroll
);
}
#[test]
fn type_at_viewport_bottom_wraps_and_scrolls_in_rendered_mode() {
let vp_h = 10;
let vp_w = 20;
let mut src = String::new();
for i in 0..40 {
let prefix = format!("s{i}");
if i == vp_h - 1 {
src.push_str(&prefix);
src.push_str(&"X".repeat(vp_w - prefix.len()));
src.push('\n');
} else {
src.push_str(&prefix);
src.push('\n');
}
}
let mut st = state(&src);
st.set_reflow(false);
apply(&mut st, Action::EnterEditMode);
st.scroll = 0;
let cursor_buf_line = vp_h - 1;
let line_start = st.buffer.line_to_char(cursor_buf_line);
let line_text = st.buffer.line(cursor_buf_line).unwrap_or_default();
let line_len = line_text.trim_end_matches('\n').chars().count();
assert_eq!(line_len, vp_w, "line padding length is wrong");
st.cursor.offset = line_start + line_len;
st.cursor.preferred_col = line_len;
st.update_cursor_block();
let scroll_before = st.scroll;
edit_ops::apply(&mut st, Action::InsertChar('Y'), vp_h, vp_w);
assert!(
st.scroll > scroll_before,
"scroll {} did not advance after wrap-inducing keystroke in Rendered mode",
st.scroll
);
}
#[test]
fn delete_char_back_removes_full_zwj_grapheme() {
let mut st = state("a👨\u{200D}👩\u{200D}👧\u{200D}👦b");
st.mode = Mode::Rendered;
st.cursor.offset = 8; apply(&mut st, Action::DeleteCharBack);
assert_eq!(st.contents(), "ab");
assert_eq!(st.cursor.offset, 1);
}
#[test]
fn delete_char_forward_removes_full_zwj_grapheme() {
let mut st = state("a👨\u{200D}👩\u{200D}👧\u{200D}👦b");
st.mode = Mode::Rendered;
st.cursor.offset = 1; apply(&mut st, Action::DeleteCharForward);
assert_eq!(st.contents(), "ab");
assert_eq!(st.cursor.offset, 1);
}
#[test]
fn delete_char_back_removes_combining_mark_with_base() {
let mut st = state("e\u{0301}!");
st.mode = Mode::Rendered;
st.cursor.offset = 2; apply(&mut st, Action::DeleteCharBack);
assert_eq!(st.contents(), "!");
assert_eq!(st.cursor.offset, 0);
}
#[test]
fn move_right_in_raw_mode_steps_over_grapheme() {
let mut st = state("a👨\u{200D}👩\u{200D}👧\u{200D}👦b");
st.mode = Mode::Raw;
st.cursor.offset = 1; apply(&mut st, Action::MoveRight);
assert_eq!(st.cursor.offset, 8); }
fn select(st: &mut EditorState, anchor: usize, active: usize) {
use edamame::document::Selection;
st.selection = Some(Selection { anchor, active });
st.cursor.offset = active;
}
#[test]
fn bold_wraps_selection_and_reselects_inner() {
let mut st = state("hello world");
st.mode = Mode::Rendered;
select(&mut st, 6, 11); apply(&mut st, Action::BoldSelection);
assert_eq!(st.contents(), "hello **world**");
let sel = st.selection.expect("selection retained");
assert_eq!(sel.range(), (8, 13));
assert_eq!(sel.selected_text(&st.buffer), "world");
}
#[test]
fn italic_wraps_selection() {
let mut st = state("hello world");
st.mode = Mode::Rendered;
select(&mut st, 6, 11);
apply(&mut st, Action::ItalicizeSelection);
assert_eq!(st.contents(), "hello *world*");
}
#[test]
fn bold_then_italic_chains() {
let mut st = state("hello world");
st.mode = Mode::Rendered;
select(&mut st, 6, 11);
apply(&mut st, Action::BoldSelection);
apply(&mut st, Action::ItalicizeSelection);
assert_eq!(st.contents(), "hello ***world***");
}
#[test]
fn bold_toggles_off_when_selection_includes_markers() {
let mut st = state("hello **world**");
st.mode = Mode::Rendered;
select(&mut st, 6, 15); apply(&mut st, Action::BoldSelection);
assert_eq!(st.contents(), "hello world");
assert_eq!(st.selection.unwrap().range(), (6, 11));
}
#[test]
fn bold_strips_markers_just_outside_selection() {
let mut st = state("hello **world**");
st.mode = Mode::Rendered;
select(&mut st, 8, 13); apply(&mut st, Action::BoldSelection);
assert_eq!(st.contents(), "hello world");
assert_eq!(st.selection.unwrap().range(), (6, 11));
}
#[test]
fn italic_over_bold_wraps_rather_than_strips() {
let mut st = state("**bar**");
st.mode = Mode::Rendered;
select(&mut st, 2, 5); apply(&mut st, Action::ItalicizeSelection);
assert_eq!(st.contents(), "***bar***");
}
#[test]
fn bold_without_selection_is_noop() {
let mut st = state("hello");
st.mode = Mode::Rendered;
apply(&mut st, Action::BoldSelection);
assert_eq!(st.contents(), "hello");
}
#[test]
fn bold_refuses_multiline_selection() {
let mut st = state("foo\nbar");
st.mode = Mode::Rendered;
select(&mut st, 0, 7);
apply(&mut st, Action::BoldSelection);
assert_eq!(st.contents(), "foo\nbar");
}
#[test]
fn bold_with_interior_markers_wraps_not_unwraps() {
let mut st = state("**a** and **b**");
st.mode = Mode::Rendered;
select(&mut st, 0, 15);
apply(&mut st, Action::BoldSelection);
assert_eq!(st.contents(), "****a** and **b****");
}
#[test]
fn bold_wraps_mixed_selection_verbatim() {
let mut st = state("foo **x** baz");
st.mode = Mode::Rendered;
select(&mut st, 0, 13);
apply(&mut st, Action::BoldSelection);
assert_eq!(st.contents(), "**foo **x** baz**");
}
#[test]
fn inline_code_wraps_selection_and_reselects_inner() {
let mut st = state("hello world");
st.mode = Mode::Rendered;
select(&mut st, 6, 11); apply(&mut st, Action::InlineCodeSelection);
assert_eq!(st.contents(), "hello `world`");
let sel = st.selection.expect("selection retained");
assert_eq!(sel.selected_text(&st.buffer), "world");
}
#[test]
fn inline_code_toggles_off_when_selection_includes_markers() {
let mut st = state("hello `world`");
st.mode = Mode::Rendered;
select(&mut st, 6, 13); apply(&mut st, Action::InlineCodeSelection);
assert_eq!(st.contents(), "hello world");
assert_eq!(st.selection.unwrap().range(), (6, 11));
}
#[test]
fn strikethrough_wraps_selection() {
let mut st = state("hello world");
st.mode = Mode::Rendered;
select(&mut st, 6, 11);
apply(&mut st, Action::StrikethroughSelection);
assert_eq!(st.contents(), "hello ~~world~~");
}
#[test]
fn strikethrough_strips_markers_just_outside_selection() {
let mut st = state("hello ~~world~~");
st.mode = Mode::Rendered;
select(&mut st, 8, 13); apply(&mut st, Action::StrikethroughSelection);
assert_eq!(st.contents(), "hello world");
assert_eq!(st.selection.unwrap().range(), (6, 11));
}
#[test]
fn highlight_wraps_and_unwraps_selection() {
let mut st = state("hello world");
st.mode = Mode::Rendered;
select(&mut st, 6, 11);
apply(&mut st, Action::HighlightSelection);
assert_eq!(st.contents(), "hello ==world==");
apply(&mut st, Action::HighlightSelection);
assert_eq!(st.contents(), "hello world");
}
#[test]
fn inline_code_refuses_multiline_selection() {
let mut st = state("foo\nbar");
st.mode = Mode::Rendered;
select(&mut st, 0, 7);
apply(&mut st, Action::InlineCodeSelection);
assert_eq!(st.contents(), "foo\nbar");
}
fn put_cursor(st: &mut EditorState, byte: usize) {
st.cursor.offset = st.buffer.rope().byte_to_char(byte);
}
#[test]
fn insert_footnote_auto_numbers_and_undoes_atomically() {
let mut st = state("Claim.\n\n[^1]: prior note.\n");
st.mode = Mode::Rendered;
put_cursor(&mut st, "Claim.".len());
edit_ops::insert_footnote_at_cursor(&mut st, VP, VW);
assert!(
st.contents().starts_with("Claim.[^2]"),
"got: {:?}",
st.contents()
);
apply(&mut st, Action::Undo);
assert!(
st.contents().starts_with("Claim.\n"),
"got: {:?}",
st.contents()
);
}
#[test]
fn renumber_footnotes_action_reorders_by_reference() {
let mut st = state("A[^2] B[^1]\n\n[^2]: two\n[^1]: one\n");
st.mode = Mode::Rendered;
let changed = edit_ops::renumber_footnotes(&mut st, VP, VW);
assert!(changed);
assert_eq!(st.contents(), "A[^1] B[^2]\n\n[^1]: two\n[^2]: one\n");
assert!(!edit_ops::renumber_footnotes(&mut st, VP, VW));
}
#[test]
fn delete_footnote_action_removes_renumbers_and_undoes_atomically() {
let original = "A[^1] B[^2]\n\n[^1]: one\n[^2]: two\n";
let mut st = state(original);
st.mode = Mode::Rendered;
put_cursor(&mut st, 1); let removed = edit_ops::delete_footnote_at_cursor(&mut st, VP, VW);
assert!(removed);
assert_eq!(st.contents(), "A B[^1]\n\n[^1]: two\n");
apply(&mut st, Action::Undo);
assert_eq!(st.contents(), original);
}
#[test]
fn delete_footnote_off_a_footnote_is_noop() {
let mut st = state("plain text\n");
st.mode = Mode::Rendered;
put_cursor(&mut st, 3);
assert!(!edit_ops::delete_footnote_at_cursor(&mut st, VP, VW));
assert_eq!(st.contents(), "plain text\n");
}
#[test]
fn insert_link_snippet_selects_text_placeholder() {
let mut st = state("hello world\n");
st.mode = Mode::Rendered;
st.cursor.offset = 6; edit_ops::insert_link_at_cursor(&mut st, VP, VW);
assert_eq!(st.contents(), "hello [link text](file path or URL)world\n");
let (s, e) = st
.selection
.expect("placeholder should be selected")
.range();
assert_eq!(st.buffer.slice_to_string(s, e), "link text");
apply(&mut st, Action::InsertChar('x'));
assert_eq!(st.contents(), "hello [x](file path or URL)world\n");
}
#[test]
fn insert_image_wraps_selection_and_selects_url_placeholder() {
use edamame::document::Selection;
let mut st = state("hello world\n");
st.mode = Mode::Rendered;
st.selection = Some(Selection {
anchor: 6,
active: 11, });
st.cursor.offset = 11;
edit_ops::insert_image_at_cursor(&mut st, VP, VW);
assert_eq!(st.contents(), "hello \n");
let (s, e) = st
.selection
.expect("URL placeholder should be selected")
.range();
assert_eq!(st.buffer.slice_to_string(s, e), "file path or URL");
}
#[test]
fn insert_image_reference_lands_as_its_own_paragraph() {
let mut st = state("prose\n");
st.mode = Mode::Rendered;
st.cursor.offset = 6;
edit_ops::insert_image_reference_at_cursor(&mut st, "C:/x/y.png", VP, VW);
assert_eq!(st.contents(), "prose\n\n\n");
}
#[test]
fn insert_image_reference_splits_the_line_it_lands_on() {
let mut st = state("alpha\nbeta\n");
st.mode = Mode::Rendered;
st.cursor.offset = 2; edit_ops::insert_image_reference_at_cursor(&mut st, "img.png", VP, VW);
assert_eq!(st.contents(), "al\n\n\n\npha\nbeta\n");
assert_eq!(
st.cursor.offset,
"al\n\n![".len(),
"the cursor waits in the empty alt text"
);
}
#[test]
fn insert_image_reference_at_the_start_lands_in_the_alt_text() {
let mut st = state("");
st.mode = Mode::Rendered;
edit_ops::insert_image_reference_at_cursor(&mut st, "img.png", VP, VW);
assert_eq!(st.contents(), "\n");
assert_eq!(st.cursor.offset, "![".len());
assert!(st.selection.is_none());
}
#[test]
fn insert_image_reference_with_a_selection_lands_past_it() {
use edamame::document::Selection;
let mut st = state("a shot\n");
st.mode = Mode::Rendered;
st.selection = Some(Selection {
anchor: 0,
active: 6,
});
st.cursor.offset = 6;
edit_ops::insert_image_reference_at_cursor(&mut st, "img.png", VP, VW);
assert_eq!(st.contents(), "\n");
assert_eq!(st.cursor.offset, "".len());
}
#[test]
fn insert_image_reference_is_separated_from_the_paragraph_below() {
let mut st = state("prose\n\nnext\n");
st.mode = Mode::Rendered;
st.cursor.offset = 7; edit_ops::insert_image_reference_at_cursor(&mut st, "img.png", VP, VW);
assert_eq!(st.contents(), "prose\n\n\n\nnext\n");
}
#[test]
fn insert_image_reference_at_a_line_end_adds_the_blank_line_below() {
let mut st = state("prose\nnext\n");
st.mode = Mode::Rendered;
st.cursor.offset = 5; edit_ops::insert_image_reference_at_cursor(&mut st, "img.png", VP, VW);
assert_eq!(st.contents(), "prose\n\n\n\nnext\n");
}
#[test]
fn insert_image_reference_escapes_the_destination() {
let mut st = state("");
st.mode = Mode::Rendered;
edit_ops::insert_image_reference_at_cursor(&mut st, "a b/image (1).png", VP, VW);
assert_eq!(st.contents(), ".png>)\n");
}
#[test]
fn insert_image_reference_in_a_table_goes_inline_in_the_cell() {
let src = "| a | b |\n|---|---|\n| c | d |\n";
let mut st = state(src);
st.mode = Mode::Rendered;
st.cursor.offset = src.find("d |").unwrap();
edit_ops::insert_image_reference_at_cursor(&mut st, "img.png", VP, VW);
assert_eq!(
st.contents(),
"| a | b |\n|---|---|\n| c | d |\n"
);
assert_eq!(
st.cursor.offset,
src.find("d |").unwrap() + "![".len(),
"the cursor waits in the empty alt text"
);
}
#[test]
fn insert_image_reference_in_a_container_goes_inline_and_keeps_it_whole() {
for (src, at, expected) in [
("- item\n- two\n", "em", "- item\n- two\n"),
("- item\n- two\n", "\n- two", "- item\n- two\n"),
(
"1. one\n more\n",
"\n more",
"1. one\n more\n",
),
("> quote text\n", " text", "> quote text\n"),
(
"Note[^1].\n\n[^1]: the note\n",
" note",
"Note[^1].\n\n[^1]: the note\n",
),
] {
let mut st = state(src);
st.mode = Mode::Rendered;
st.cursor.offset = src.find(at).unwrap();
assert!(edit_ops::insert_image_reference_at_cursor(
&mut st, "img.png", VP, VW
));
assert_eq!(st.contents(), expected, "source was {src:?}");
assert_eq!(
st.cursor.offset,
src.find(at).unwrap() + "![".len(),
"the cursor waits in the empty alt text: {src:?}"
);
}
}
#[test]
fn can_insert_image_reference_in_preview_judges_the_synced_spot_without_moving() {
let src = "```\ncode\n```\n\nprose\n";
let mut st = state(src);
st.mode = Mode::Preview;
st.scroll = 0;
let offset = src.find("prose").unwrap();
st.cursor.offset = offset;
assert!(!edit_ops::can_insert_image_reference(&mut st, 1));
assert_eq!(st.cursor.offset, offset);
assert_eq!(st.mode, Mode::Preview);
}
#[test]
fn can_insert_image_reference_on_the_blank_line_after_a_block_that_absorbs_it() {
for (src, inside) in [
(" code\n\nprose\n", "code"),
("<div>x</div>\n\nprose\n", "x"),
] {
let mut st = state(src);
st.mode = Mode::Rendered;
st.cursor.offset = src.find("\n\n").unwrap() + 1;
assert!(
edit_ops::can_insert_image_reference(&mut st, 24),
"on the blank line in {src:?}"
);
st.cursor.offset = src.find(inside).unwrap();
assert!(
!edit_ops::can_insert_image_reference(&mut st, 24),
"inside the block in {src:?}"
);
}
}
#[test]
fn insert_image_reference_in_a_table_nested_in_a_quote_escapes_pipes() {
let src = "> | a |\n> |---|\n> | c |\n";
let mut st = state(src);
st.mode = Mode::Rendered;
st.cursor.offset = src.find("c |").unwrap() + 1;
edit_ops::insert_image_reference_at_cursor(&mut st, "a|b.png", VP, VW);
assert_eq!(st.contents(), "> | a |\n> |---|\n> | c |\n");
}
#[test]
fn insert_image_reference_in_a_table_escapes_pipes() {
let src = "| a |\n|---|\n| c |\n";
let mut st = state(src);
st.mode = Mode::Rendered;
st.cursor.offset = src.find("c |").unwrap() + 1;
edit_ops::insert_image_reference_at_cursor(&mut st, "a|b.png", VP, VW);
assert_eq!(st.contents(), "| a |\n|---|\n| c |\n");
}
#[test]
fn insert_image_reference_outside_a_table_leaves_pipes_alone() {
let mut st = state("");
st.mode = Mode::Rendered;
edit_ops::insert_image_reference_at_cursor(&mut st, "a|b.png", VP, VW);
assert_eq!(st.contents(), "\n");
}
#[test]
fn insert_image_reference_needs_no_extra_break_after_a_blank_line() {
let mut st = state("prose\n\n");
st.mode = Mode::Rendered;
st.cursor.offset = 7;
edit_ops::insert_image_reference_at_cursor(&mut st, "img.png", VP, VW);
assert_eq!(st.contents(), "prose\n\n\n");
}
#[test]
fn insert_link_wraps_multibyte_selection() {
use edamame::document::Selection;
let mut st = state("héllo wörld\n");
st.mode = Mode::Rendered;
st.selection = Some(Selection {
anchor: 6,
active: 11, });
st.cursor.offset = 11;
edit_ops::insert_link_at_cursor(&mut st, VP, VW);
assert_eq!(st.contents(), "héllo [wörld](file path or URL)\n");
let (s, e) = st
.selection
.expect("URL placeholder should be selected")
.range();
assert_eq!(st.buffer.slice_to_string(s, e), "file path or URL");
}
#[test]
fn insert_link_ignores_multiline_selection_without_destroying_it() {
use edamame::document::Selection;
let mut st = state("one\ntwo\n");
st.mode = Mode::Rendered;
st.selection = Some(Selection {
anchor: 0,
active: 7, });
st.cursor.offset = 7;
edit_ops::insert_link_at_cursor(&mut st, VP, VW);
assert_eq!(st.contents(), "one\ntwo[link text](file path or URL)\n");
}
#[test]
fn inline_snippet_guard_classifies_blocks() {
let src = "para\n\n```\ncode\n```\n\n\n\n---\n";
let mut st = state(src);
st.mode = Mode::Rendered;
st.cursor.offset = 2;
assert!(edit_ops::cursor_block_allows_inline_markdown(&mut st));
st.cursor.offset = 5;
assert!(edit_ops::cursor_block_allows_inline_markdown(&mut st));
st.cursor.offset = src.find("code").unwrap() + 1;
assert!(!edit_ops::cursor_block_allows_inline_markdown(&mut st));
st.cursor.offset = src.find("![a]").unwrap() + 2;
assert!(!edit_ops::cursor_block_allows_inline_markdown(&mut st));
st.cursor.offset = src.find("---").unwrap() + 1;
assert!(!edit_ops::cursor_block_allows_inline_markdown(&mut st));
}
#[test]
fn insert_image_from_preview_enters_rendered_mode() {
let mut st = state("\n");
assert_eq!(st.mode, Mode::Preview);
edit_ops::insert_image_at_cursor(&mut st, VP, VW);
assert_eq!(st.mode, Mode::Rendered);
assert_eq!(st.contents(), "\n");
}
#[test]
fn insert_link_denied_in_code_block_leaves_state_untouched() {
use edamame::document::Selection;
let src = "```\ncode\n```\n";
let mut st = state(src);
st.mode = Mode::Rendered;
st.selection = Some(Selection {
anchor: 4,
active: 8, });
st.cursor.offset = 8;
assert!(!edit_ops::insert_link_at_cursor(&mut st, VP, VW));
assert_eq!(st.contents(), src);
assert_eq!(st.mode, Mode::Rendered);
assert_eq!(st.selection.map(|s| s.range()), Some((4, 8)));
}
#[test]
fn insert_link_denied_when_selection_starts_in_code_block() {
use edamame::document::Selection;
let src = "```\ncode\n```\n\n";
let mut st = state(src);
st.mode = Mode::Rendered;
st.selection = Some(Selection {
anchor: 4,
active: 8, });
st.cursor.offset = 13;
assert!(!edit_ops::insert_link_at_cursor(&mut st, VP, VW));
assert_eq!(st.contents(), src);
}