//! 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"); }