Files
Marcos Paulo 67bb5fe06f
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: Cmd/Ctrl+Enter toggles a task in the Source view
Ticks or unticks a box, adds a box to a list item, makes a plain or empty line a task.
Round 5 tasks 36-37 added; the Visual block editor's version (37) is open.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 16:22:41 -03:00

214 lines
7.9 KiB
Rust

//! Tables for the pure key behaviours: list continuation, Tab/Shift+Tab,
//! wrap selection, auto-close and skip-over (with multi-byte characters).
use notescraft_editor::keys::{self, Edit, Indent};
fn applied(text: &str, edit: &Edit) -> String {
let mut out = String::from(text);
out.replace_range(edit.range.clone(), &edit.insert);
out
}
#[test]
fn enter_bullet_continues() {
let text = "- one";
let at = text.len();
let edit = keys::on_enter(text, at..at).unwrap();
assert_eq!(edit.insert, "\n- ");
}
#[test]
fn enter_star_and_quote_continues() {
let text = "* item";
let at = text.len();
assert_eq!(keys::on_enter(text, at..at).map(|e| e.insert), Some(String::from("\n* ")));
let text = "> quote";
let at = text.len();
assert_eq!(keys::on_enter(text, at..at).map(|e| e.insert), Some(String::from("\n> ")));
}
#[test]
fn enter_numbered_increments() {
let text = "12. item";
let at = text.len();
assert_eq!(keys::on_enter(text, at..at).map(|e| e.insert), Some(String::from("\n13. ")));
}
#[test]
fn enter_task_continues_unchecked() {
let text = "- [x] done";
let at = text.len();
let edit = keys::on_enter(text, at..at).unwrap();
assert_eq!(edit.insert, "\n- [ ] ");
}
#[test]
fn enter_nested_keeps_indent() {
let text = "- a\n - b";
let at = text.len();
let edit = keys::on_enter(text, at..at).unwrap();
assert_eq!(edit.insert, "\n - ");
}
#[test]
fn enter_empty_item_ends_list() {
// Cursor right after "- " on an otherwise empty item.
let text = "a\n- ";
let at = text.len();
let edit = keys::on_enter(text, at..at).unwrap();
assert_eq!(edit.insert, "");
let new_text = applied(text, &edit);
assert_eq!(new_text, "a\n");
}
#[test]
fn enter_ordinary_line_returns_none() {
let text = "plain words";
let at = text.len();
assert!(keys::on_enter(text, at..at).is_none());
let text = "";
assert!(keys::on_enter(text, 0..0).is_none());
}
#[test]
fn tab_indents_cursor_and_selection() {
let text = "a\nb\nc";
let line2 = text.find('b').unwrap();
let edit = keys::on_tab(text, line2..line2, false, Indent::default()).unwrap();
assert_eq!(edit.insert, " ");
// Selection across lines b and c re-indents both.
let line3_end = text.len();
let block = keys::on_tab(text, line2..line3_end, false, Indent::default()).unwrap();
let new_text = applied(text, &block);
assert_eq!(new_text, "a\n b\n c");
}
#[test]
fn shift_tab_outdents() {
let text = "a\n b";
let line2 = text.find('b').unwrap();
let edit = keys::on_tab(text, line2..line2, true, Indent::default()).unwrap();
assert_eq!(applied(text, &edit), "a\nb");
// Nothing to outdent on line 1.
assert!(keys::on_tab("a", 0..0, true, Indent::default()).is_none());
}
#[test]
fn wrap_selection_bold_and_empty() {
let text = "say hi";
let hi = text.find("hi").unwrap()..text.len();
let edit = keys::wrap_selection(text, hi, "**", "**").unwrap();
assert_eq!(applied(text, &edit), "say **hi**");
assert_eq!(edit.sel, 6..8);
// Empty selection inserts the pair with the caret between.
let at = text.len();
let empty = keys::wrap_selection(text, at..at, "**", "**").unwrap();
assert_eq!(applied(text, &empty), "say hi****");
assert_eq!(empty.sel, empty.range.start + 2..empty.range.start + 2);
}
#[test]
fn wrap_selection_wikilink_via_k() {
let text = "Some note";
let edit = keys::wrap_selection(text, 0..text.len(), "[[", "]]").unwrap();
assert_eq!(applied(text, &edit), "[[Some note]]");
}
#[test]
fn auto_close_pairs() {
let text = "";
for (typed, expect) in [('(', "()"), ('[', "[]"), ('"', "\"\""), ('`', "``")] {
let edit = keys::auto_close(text, 0..0, typed).unwrap();
assert_eq!(edit.insert, expect, "typed {typed}");
assert_eq!(edit.sel, 1..1);
}
}
#[test]
fn auto_close_lone_star_and_bold_pair() {
// `*` after a normal char: no interference (plain insert).
let text = "a";
assert!(keys::auto_close(text, 1..1, '*').is_none());
// `*` right after another `*` completes the `**` opener (typed char plus
// the closer `**`).
let bold = "a*";
let edit = keys::auto_close(bold, bold.len()..bold.len(), '*').unwrap();
assert_eq!(applied(bold, &edit), "a****");
assert_eq!(edit.sel, 3..3);
}
#[test]
fn skip_over_closer() {
// Cursor sits before the auto-closed `]`; typing `]` skips over it.
let text = "a[]b";
let before_close = text.find(']').unwrap();
let edit = keys::auto_close(text, before_close..before_close, ']').unwrap();
assert_eq!(edit.insert, "");
assert_eq!(edit.sel, before_close + 1..before_close + 1);
}
#[test]
fn multibyte_positions_stay_on_boundaries() {
// "é" is two bytes; positions come from `find`/`len`, never by hand.
let text = "éx";
let edit = keys::auto_close(text, text.len()..text.len(), '(').unwrap();
assert_eq!(applied(text, &edit), "éx()");
// Line continuation after a multi-byte first line.
let nested = "éé\n - b";
let at = nested.len();
let edit = keys::on_enter(nested, at..at).unwrap();
assert_eq!(edit.insert, "\n - ");
}
#[test]
fn bad_ranges_are_normalized_never_panic() {
// Inverted out-of-bounds selection: normalized to valid, never a panic.
let sel = usize::from(4u8)..usize::from(1u8); // 4..1 without a literal reversed range
let edit = keys::wrap_selection("abc", sel, "**", "**").unwrap();
assert_eq!(applied("abc", &edit), "a**bc**");
let tab = keys::on_tab("abc", 4..4, false, Indent::default()).unwrap();
assert_eq!(applied("abc", &tab), "abc ");
}
#[test]
fn tab_indents_with_the_chosen_unit_and_shift_tab_takes_one_level_back() {
let two = Indent { tabs: false, width: 2 };
let tabs = Indent { tabs: true, width: 4 };
assert_eq!(keys::on_tab("a", 0..0, false, two).map(|edit| edit.insert), Some(String::from(" ")));
assert_eq!(keys::on_tab("a", 0..0, false, tabs).map(|edit| edit.insert), Some(String::from("\t")));
let block = keys::on_tab("a\nb\n", 0..3, false, tabs).expect("block");
assert_eq!(block.insert, "\ta\n\tb\n");
let back = keys::on_tab("\ta", 2..2, true, tabs).expect("outdent a tab");
assert_eq!((back.range, back.insert.as_str()), (0..1, ""));
let back = keys::on_tab(" a", 5..5, true, two).expect("outdent one level of spaces");
assert_eq!(back.range, 0..2, "two spaces, not four");
assert!(keys::on_tab("a", 1..1, true, tabs).is_none(), "nothing to take back");
}
#[test]
fn cmd_enter_ticks_unticks_and_makes_tasks() {
let apply = |text: &str, pos: usize| {
let edit = keys::on_toggle_task(text, pos).expect("an edit");
let mut out = text.to_owned();
out.replace_range(edit.range.clone(), &edit.insert);
(out, edit.sel.start)
};
assert_eq!(apply("- [ ] buy milk", 8).0, "- [x] buy milk");
assert_eq!(apply("- [x] buy milk", 3).0, "- [ ] buy milk");
assert_eq!(apply("- [X] done", 0).0, "- [ ] done");
assert_eq!(apply(" * [ ] nested", 5).0, " * [x] nested");
assert_eq!(apply("1. [ ] first", 4).0, "1. [x] first");
assert_eq!(apply("- plain item", 4).0, "- [ ] plain item", "a list item gets a box");
assert_eq!(apply("12) numbered", 0).0, "12) [ ] numbered");
assert_eq!(apply("just text", 3).0, "- [ ] just text", "a plain line becomes a task");
assert_eq!(apply(" indented text", 4).0, " - [ ] indented text");
assert_eq!(apply("", 0).0, "- [ ] ", "an empty line too");
let (text, caret) = apply("just text", 3);
assert_eq!(text.get(caret..caret + 4), Some("t te"), "the caret stays in the same word: {text:?} {caret}");
let multi = "a\n- [ ] b\nc\n";
assert_eq!(apply(multi, multi.find('b').expect("offset")).0, "a\n- [x] b\nc\n", "only the caret line changes");
assert_eq!(apply("-no space", 0).0, "- [ ] -no space");
assert_eq!(apply("- [y] odd box", 0).0, "- [ ] [y] odd box", "an unknown box is text, so a task is made");
}