Files
Marcos Paulo 15d7f5e873 feat: editor blocks and lint, vault knowledge, calendar plugin, macOS menu
- 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>
2026-10-18 12:36:00 -03:00

187 lines
8.1 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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(), "");
}
}