15d7f5e873
- notescraft-lint (rules, safe fixes, learned style) and notescraft-blocks (block model and edits); engine lint.* and block.* commands with CLI and MCP parity - Lint tab, underlines, slash menu with preview in Source and Visual, block grip and menu, Settings > Editor and lint, Vim block keys - Visual view: tables, note embeds, %% comments, wrapping property chips - Vault knowledge (Related tab, assistant sources, Settings) and calendar plugin - Sidebar top padding fixed, dot grid follows PaperCraft spacing, right-card tabs no longer cover the collapse button Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
187 lines
8.1 KiB
Rust
187 lines
8.1 KiB
Rust
//! Host effects, register exchange, viewport hints and command status.
|
||
|
||
use notescraft_vim::{Doc, ExCommand, Key, KeyCode, Mode, Outcome, Vim};
|
||
|
||
fn feed(vim: &mut Vim, document: &mut Doc, input: &str) -> Outcome {
|
||
input.chars().fold(Outcome::default(), |_, character| vim.handle(Key::char(character), document))
|
||
}
|
||
|
||
fn special(vim: &mut Vim, document: &mut Doc, code: KeyCode) -> Outcome {
|
||
vim.handle(Key { code, ctrl: false, shift: false, alt: false }, document)
|
||
}
|
||
|
||
#[test]
|
||
fn ex_commands_return_host_effects_without_modifying_document() {
|
||
let cases = [
|
||
(":w", ExCommand::Write),
|
||
(":q", ExCommand::Quit),
|
||
(":q!", ExCommand::QuitForce),
|
||
(":wq", ExCommand::WriteQuit),
|
||
(":x", ExCommand::WriteQuit),
|
||
(":e notes/my note.md", ExCommand::Edit("notes/my note.md".into())),
|
||
(":bn", ExCommand::NextBuffer),
|
||
(":bp", ExCommand::PrevBuffer),
|
||
(":set number", ExCommand::Set("number".into())),
|
||
(":note.create", ExCommand::Command("note.create".into())),
|
||
(":editor.toggle_vim", ExCommand::Command("editor.toggle_vim".into())),
|
||
];
|
||
for (input, expected) in cases {
|
||
let mut vim = Vim::new();
|
||
let mut document = Doc::new("é猫🙂".into()).expect("valid document");
|
||
feed(&mut vim, &mut document, input);
|
||
assert_eq!(vim.mode(), Mode::CommandLine);
|
||
let outcome = special(&mut vim, &mut document, KeyCode::Enter);
|
||
assert_eq!(outcome.ex, Some(expected), "{input}");
|
||
assert_eq!(document.text, "é猫🙂");
|
||
assert!(outcome.mode_changed);
|
||
assert!(!outcome.text_changed);
|
||
assert_eq!(outcome.edits, []);
|
||
assert_eq!(vim.mode(), Mode::Normal);
|
||
assert_eq!(vim.command_line(), "");
|
||
assert_eq!(vim.pending(), "");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn invalid_ex_reports_error_and_preserves_document() {
|
||
for input in [":unknown", ":e", ":set", ":note..create", ":w extra", ":0", ":s/a/b/z"] {
|
||
let mut vim = Vim::new();
|
||
let mut document = Doc::new("abc".into()).expect("valid document");
|
||
feed(&mut vim, &mut document, input);
|
||
let outcome = special(&mut vim, &mut document, KeyCode::Enter);
|
||
assert!(outcome.message.is_some(), "{input}");
|
||
assert!(outcome.ex.is_none());
|
||
assert!(!outcome.text_changed);
|
||
assert_eq!(document.text, "abc");
|
||
assert_eq!(vim.mode(), Mode::Normal);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn excessive_counts_report_error_without_partial_edit() {
|
||
for input in ["100001x", "999999999999x", "1000d1000w"] {
|
||
let mut vim = Vim::new();
|
||
let mut document = Doc::new("abc".into()).expect("valid document");
|
||
let outcomes: Vec<Outcome> = input.chars().map(|character| vim.handle(Key::char(character), &mut document)).collect();
|
||
assert!(outcomes.iter().any(|outcome| outcome.message.is_some()), "{input}");
|
||
assert_eq!(document.text, "abc", "{input}");
|
||
assert_eq!(document.cursor, 0);
|
||
assert_eq!(vim.mode(), Mode::Normal);
|
||
assert_eq!(vim.pending(), "");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn markdown_commands_require_cursor_inside_link() {
|
||
let cases = [
|
||
("[[note]]", 2, "gf", Some(ExCommand::OpenWikilink)),
|
||
("[[é猫]]", 2, "gf", Some(ExCommand::OpenWikilink)),
|
||
("[label](note.md)", 3, "gx", Some(ExCommand::FollowLink)),
|
||
("[label](note.md)", 9, "gx", Some(ExCommand::FollowLink)),
|
||
("[[note]]", 3, "gx", Some(ExCommand::FollowLink)),
|
||
("outside [[note]]", 0, "gf", None),
|
||
("outside [note](path)", 0, "gx", None),
|
||
("# heading", 0, "za", Some(ExCommand::Fold)),
|
||
];
|
||
for (text, cursor, input, expected) in cases {
|
||
let mut vim = Vim::new();
|
||
let mut document = Doc::new(text.into()).expect("valid document");
|
||
document.cursor = cursor;
|
||
assert_eq!(feed(&mut vim, &mut document, input).ex, expected, "{text} {input}");
|
||
assert_eq!(document.text, text);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn clipboard_registers_exchange_text_with_host() {
|
||
for register in ['+', '*'] {
|
||
let mut vim = Vim::new();
|
||
let mut document = Doc::new("é猫🙂".into()).expect("valid document");
|
||
vim.set_clipboard_text("αβ").expect("clipboard import");
|
||
assert_eq!(vim.registers().get(register).expect("clipboard register").text, "αβ");
|
||
assert!(vim.registers().get('"').is_none());
|
||
feed(&mut vim, &mut document, &format!("\"{register}P"));
|
||
assert_eq!(document.text, "αβé猫🙂");
|
||
document.cursor = 0;
|
||
let outcome = feed(&mut vim, &mut document, &format!("\"{register}yl"));
|
||
assert_eq!(outcome.clipboard_set.as_deref(), Some("α"));
|
||
assert_eq!(vim.registers().get(register).expect("clipboard register").text, "α");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn registers_retain_yanks_and_record_small_deletes() {
|
||
let mut vim = Vim::new();
|
||
let mut document = Doc::new("abc\ndef".into()).expect("valid document");
|
||
feed(&mut vim, &mut document, "\"ayl");
|
||
assert_eq!(vim.registers().get('a').expect("named register").text, "a");
|
||
assert_eq!(vim.registers().get('0').expect("yank register").text, "a");
|
||
feed(&mut vim, &mut document, "lx");
|
||
assert_eq!(vim.registers().get('-').expect("small delete register").text, "b");
|
||
assert_eq!(vim.registers().get('0').expect("yank register").text, "a");
|
||
feed(&mut vim, &mut document, "\"_x");
|
||
assert_eq!(vim.registers().get('"').expect("unnamed register").text, "b");
|
||
feed(&mut vim, &mut document, "yy");
|
||
assert!(vim.registers().get('0').expect("line yank register").linewise);
|
||
}
|
||
|
||
#[test]
|
||
fn pending_counts_and_prompts_clear_on_escape() {
|
||
let mut vim = Vim::new();
|
||
let mut document = Doc::new("abc".into()).expect("valid document");
|
||
feed(&mut vim, &mut document, "2d");
|
||
assert_eq!(vim.pending(), "2d");
|
||
special(&mut vim, &mut document, KeyCode::Esc);
|
||
assert_eq!(vim.pending(), "");
|
||
feed(&mut vim, &mut document, ":note.create");
|
||
assert!(vim.command_line().contains("note.create"));
|
||
special(&mut vim, &mut document, KeyCode::Esc);
|
||
assert_eq!(vim.command_line(), "");
|
||
assert_eq!(vim.mode(), Mode::Normal);
|
||
assert_eq!(document.text, "abc");
|
||
}
|
||
|
||
#[test]
|
||
fn viewport_motions_use_host_hint_and_count() {
|
||
for (input, expected_line) in [("H", 2), ("2H", 3), ("M", 4), ("L", 6), ("2L", 5)] {
|
||
let mut vim = Vim::new();
|
||
let mut document = Doc::new("0\n1\n2\n3\n4\n5\n6\n7\n8\n9".into()).expect("valid document");
|
||
vim.set_viewport(2, 5).expect("valid viewport");
|
||
feed(&mut vim, &mut document, input);
|
||
assert_eq!(document.cursor, expected_line * 2, "{input}");
|
||
}
|
||
for (character, initial, expected) in [('d', 0, 4), ('u', 8, 4)] {
|
||
let mut vim = Vim::new();
|
||
let mut document = Doc::new("0\n1\n2\n3\n4\n5\n6\n7\n8\n9".into()).expect("valid document");
|
||
document.cursor = initial;
|
||
vim.set_viewport(0, 4).expect("valid viewport");
|
||
vim.handle(Key { code: KeyCode::Char(character), ctrl: true, shift: false, alt: false }, &mut document);
|
||
assert_eq!(document.cursor, expected);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn search_ranges_use_byte_offsets_and_noh_clears_them() {
|
||
let mut vim = Vim::new();
|
||
let mut document = Doc::new("α 猫 α".into()).expect("valid document");
|
||
feed(&mut vim, &mut document, "/α");
|
||
let outcome = special(&mut vim, &mut document, KeyCode::Enter);
|
||
assert_eq!(outcome.search, vec![0..2, 7..9]);
|
||
feed(&mut vim, &mut document, ":noh");
|
||
assert_eq!(special(&mut vim, &mut document, KeyCode::Enter).search, []);
|
||
}
|
||
|
||
#[test]
|
||
fn block_keys_ask_the_host_for_block_commands_and_leave_the_text_alone() {
|
||
for (input, id) in [("]]", "editor.block_next"), ("[[", "editor.block_prev"), ("gb", "editor.block_menu")] {
|
||
let mut vim = Vim::new();
|
||
let mut document = Doc::new("# a\n\nb".into()).expect("valid document");
|
||
let first = feed(&mut vim, &mut document, &input[..1]);
|
||
assert!(first.ex.is_none() && vim.pending() == &input[..1], "{input} waits for its second key");
|
||
let outcome = feed(&mut vim, &mut document, &input[1..]);
|
||
assert_eq!(outcome.ex, Some(ExCommand::Command(id.into())), "{input}");
|
||
assert_eq!(document.text, "# a\n\nb");
|
||
assert_eq!(vim.pending(), "");
|
||
}
|
||
}
|