67bb5fe06f
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>
214 lines
7.9 KiB
Rust
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");
|
|
}
|