diff --git a/Cargo.lock b/Cargo.lock index 9b35fded9..5e8c3ff2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -363,12 +354,6 @@ dependencies = [ "wasip3", ] -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - [[package]] name = "hashbrown" version = "0.15.5" @@ -783,7 +768,6 @@ dependencies = [ "keybindings", "nu-ansi-term", "pretty_assertions", - "rstest", "rusqlite", "serde", "serde_json", @@ -796,68 +780,6 @@ dependencies = [ "unicode-width 0.2.2", ] -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "relative-path" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" - -[[package]] -name = "rstest" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" -dependencies = [ - "rstest_macros", - "rustc_version", -] - -[[package]] -name = "rstest_macros" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" -dependencies = [ - "cfg-if", - "glob", - "proc-macro2", - "quote", - "regex", - "relative-path", - "rustc_version", - "syn", - "unicode-ident", -] - [[package]] name = "rusqlite" version = "0.37.0" diff --git a/Cargo.toml b/Cargo.toml index afce7b1e6..0747a2e3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,6 @@ unicode-width = "0.2" [dev-dependencies] gethostname = "0.4.0" pretty_assertions = "1.4.0" -rstest = { version = "0.23.0", default-features = false } serde_json = "1.0" tempfile = "3.3.0" diff --git a/src/completion/history.rs b/src/completion/history.rs index a48cffca0..259993435 100644 --- a/src/completion/history.rs +++ b/src/completion/history.rs @@ -72,8 +72,6 @@ impl<'menu> HistoryCompleter<'menu> { #[cfg(test)] mod tests { - use rstest::rstest; - use super::*; use crate::*; @@ -134,26 +132,32 @@ mod tests { Ok(()) } - #[rstest] - #[case(vec![], "any", vec![])] - #[case(vec!["old match","recent match","between","recent match"], "match", vec!["recent match","old match"])] - #[case(vec!["a","b","c","a","b","c"], "", vec!["c","b","a"])] - fn complete_doesnt_return_duplicates( - #[case] history_items: Vec<&str>, - #[case] line: &str, - #[case] expected: Vec<&str>, - ) -> Result<()> { - let mut history = FileBackedHistory::new(history_items.len())?; - for history_item in history_items { - history.save(new_history_item(history_item))?; + #[test] + fn complete_doesnt_return_duplicates() -> Result<()> { + let cases = [ + (vec![], "any", vec![]), + ( + vec!["old match", "recent match", "between", "recent match"], + "match", + vec!["recent match", "old match"], + ), + (vec!["a", "b", "c", "a", "b", "c"], "", vec!["c", "b", "a"]), + ]; + + for (history_items, line, expected) in cases { + let mut history = FileBackedHistory::new(history_items.len())?; + for history_item in history_items { + history.save(new_history_item(history_item))?; + } + let mut sut = HistoryCompleter::new(&history); + let actual: Vec = sut + .complete(line, line.len()) + .into_iter() + .map(|suggestion| suggestion.value) + .collect(); + assert_eq!(actual, expected); } - let mut sut = HistoryCompleter::new(&history); - let actual: Vec = sut - .complete(line, line.len()) - .into_iter() - .map(|suggestion| suggestion.value) - .collect(); - assert_eq!(actual, expected); + Ok(()) } } diff --git a/src/core_editor/edit_stack.rs b/src/core_editor/edit_stack.rs index 0a0f60a48..9b85a4570 100644 --- a/src/core_editor/edit_stack.rs +++ b/src/core_editor/edit_stack.rs @@ -65,7 +65,6 @@ where mod test { use super::*; use pretty_assertions::assert_eq; - use rstest::rstest; fn edit_stack(values: &[T], index: usize) -> EditStack where @@ -77,37 +76,52 @@ mod test { } } - #[rstest] - #[case(edit_stack(&[1, 2, 3][..], 2), 2)] - #[case(edit_stack(&[1][..], 0), 1)] - fn undo_works(#[case] stack: EditStack, #[case] value_after_undo: isize) { - let mut stack = stack; + #[test] + fn undo_works() { + let cases = [ + (edit_stack(&[1, 2, 3][..], 2), 2), + (edit_stack(&[1][..], 0), 1), + ]; - let value = stack.undo(); - assert_eq!(*value, value_after_undo); + for (stack, value_after_undo) in cases { + let mut stack = stack; + + let value = stack.undo(); + assert_eq!(*value, value_after_undo); + } } - #[rstest] - #[case(edit_stack(&[1, 2, 3][..], 1), 3)] - #[case(edit_stack(&[1][..], 0), 1)] - fn redo_works(#[case] stack: EditStack, #[case] value_after_undo: isize) { - let mut stack = stack; + #[test] + fn redo_works() { + let cases = [ + (edit_stack(&[1, 2, 3][..], 1), 3), + (edit_stack(&[1][..], 0), 1), + ]; - let value = stack.redo(); - assert_eq!(*value, value_after_undo); + for (stack, value_after_undo) in cases { + let mut stack = stack; + + let value = stack.redo(); + assert_eq!(*value, value_after_undo); + } } - #[rstest] - #[case(edit_stack(&[1, 2, 3][..], 1), 4, edit_stack(&[1, 2, 4], 2))] - #[case(edit_stack(&[1, 2, 3][..], 2), 3, edit_stack(&[1, 2, 3, 3], 3))] - fn insert_works( - #[case] old_stack: EditStack, - #[case] value_to_insert: isize, - #[case] expected_stack: EditStack, - ) { - let mut stack = old_stack; - - stack.insert(value_to_insert); - assert_eq!(stack, expected_stack); + #[test] + fn insert_works() { + let cases = [ + (edit_stack(&[1, 2, 3][..], 1), 4, edit_stack(&[1, 2, 4], 2)), + ( + edit_stack(&[1, 2, 3][..], 2), + 3, + edit_stack(&[1, 2, 3, 3], 3), + ), + ]; + + for (old_stack, value_to_insert, expected_stack) in cases { + let mut stack = old_stack; + + stack.insert(value_to_insert); + assert_eq!(stack, expected_stack); + } } } diff --git a/src/core_editor/editor.rs b/src/core_editor/editor.rs index 0d7673ba8..e5450a025 100644 --- a/src/core_editor/editor.rs +++ b/src/core_editor/editor.rs @@ -1153,7 +1153,6 @@ fn insert_clipboard_content_before(line_buffer: &mut LineBuffer, clipboard: &mut mod test { use super::*; use pretty_assertions::assert_eq; - use rstest::rstest; fn editor_with(buffer: &str) -> Editor { let mut editor = Editor::default(); @@ -1161,80 +1160,84 @@ mod test { editor } - #[rstest] - #[case("abc def ghi", 11, "abc def ")] - #[case("abc def-ghi", 11, "abc def-")] - #[case("abc def.ghi", 11, "abc ")] - fn test_cut_word_left(#[case] input: &str, #[case] position: usize, #[case] expected: &str) { - let mut editor = editor_with(input); - editor.line_buffer.set_insertion_point(position); + #[test] + fn test_cut_word_left() { + let cases = [ + ("abc def ghi", 11, "abc def "), + ("abc def-ghi", 11, "abc def-"), + ("abc def.ghi", 11, "abc "), + ]; + + for (input, position, expected) in cases { + let mut editor = editor_with(input); + editor.line_buffer.set_insertion_point(position); - editor.cut_word_left(); + editor.cut_word_left(); - assert_eq!(editor.get_buffer(), expected); + assert_eq!(editor.get_buffer(), expected); + } } - #[rstest] - #[case("abc def ghi", 11, "abc def ")] - #[case("abc def-ghi", 11, "abc ")] - #[case("abc def.ghi", 11, "abc ")] - #[case("abc def gh ", 11, "abc def ")] - fn test_cut_big_word_left( - #[case] input: &str, - #[case] position: usize, - #[case] expected: &str, - ) { - let mut editor = editor_with(input); - editor.line_buffer.set_insertion_point(position); - - editor.cut_big_word_left(); - - assert_eq!(editor.get_buffer(), expected); - } - - #[rstest] - #[case("hello world", 0, 'l', 1, false, "lo world")] - #[case("hello world", 0, 'l', 1, true, "llo world")] - #[ignore = "Deleting two consecutive chars is not implemented correctly and needs the multiplier explicitly."] - #[case("hello world", 0, 'l', 2, false, "o world")] - #[case("hello world", 0, 'h', 1, false, "hello world")] - #[case("hello world", 0, 'l', 3, true, "ld")] - #[case("hello world", 4, 'o', 1, true, "hellorld")] - #[case("hello world", 4, 'w', 1, false, "hellorld")] - #[case("hello world", 4, 'o', 1, false, "hellrld")] - fn test_cut_right_until_char( - #[case] input: &str, - #[case] position: usize, - #[case] search_char: char, - #[case] repeat: usize, - #[case] before_char: bool, - #[case] expected: &str, - ) { - let mut editor = editor_with(input); - editor.line_buffer.set_insertion_point(position); - for _ in 0..repeat { - editor.cut_right_until_char(search_char, before_char, true); - } - assert_eq!(editor.get_buffer(), expected); - } - - #[rstest] - #[case("abc", 1, 'X', "aXc")] - #[case("abc", 1, '🔄', "a🔄c")] - #[case("a🔄c", 1, 'X', "aXc")] - #[case("a🔄c", 1, '🔀', "a🔀c")] - fn test_replace_char( - #[case] input: &str, - #[case] position: usize, - #[case] replacement: char, - #[case] expected: &str, - ) { - let mut editor = editor_with(input); - editor.line_buffer.set_insertion_point(position); + #[test] + fn test_cut_big_word_left() { + let cases = [ + ("abc def ghi", 11, "abc def "), + ("abc def-ghi", 11, "abc "), + ("abc def.ghi", 11, "abc "), + ("abc def gh ", 11, "abc def "), + ]; + + for (input, position, expected) in cases { + let mut editor = editor_with(input); + editor.line_buffer.set_insertion_point(position); - editor.replace_char(replacement); + editor.cut_big_word_left(); - assert_eq!(editor.get_buffer(), expected); + assert_eq!(editor.get_buffer(), expected); + } + } + + #[test] + fn test_cut_right_until_char() { + let cases = [ + ("hello world", 0, 'l', 1, false, "lo world"), + ("hello world", 0, 'l', 1, true, "llo world"), + // Deleting two consecutive chars is not implemented correctly and needs the multiplier explicitly + // ("hello world", 0, 'l', 2, false, "o world"), + ("hello world", 0, 'h', 1, false, "hello world"), + ("hello world", 0, 'l', 3, true, "ld"), + ("hello world", 4, 'o', 1, true, "hellorld"), + ("hello world", 4, 'w', 1, false, "hellorld"), + ("hello world", 4, 'o', 1, false, "hellrld"), + ]; + + for (input, position, search_char, repeat, before_char, expected) in cases { + let mut editor = editor_with(input); + editor.line_buffer.set_insertion_point(position); + for _ in 0..repeat { + editor.cut_right_until_char(search_char, before_char, true); + } + assert_eq!(editor.get_buffer(), expected); + } + } + + #[test] + fn test_replace_char() { + let cases = [ + ("abc", 1, 'X', "aXc"), + ("abc", 1, '🔄', "a🔄c"), + ("a🔄c", 1, 'X', "aXc"), + ("a🔄c", 1, '🔀', "a🔀c"), + ]; + + for (input, position, replacement, expected) in cases { + let mut editor = editor_with(input); + editor.line_buffer.set_insertion_point(position); + + editor.replace_char(replacement); + + assert_eq!(editor.get_buffer(), expected); + } } fn str_to_edit_commands(s: &str) -> Vec { @@ -1784,215 +1787,250 @@ mod test { assert_eq!(editor.cut_buffer.get().0, "hello"); } - #[rstest] - #[case("hello world test", 7, "hello test", 6, "world")] // cursor inside word - #[case("hello world test", 6, "hello test", 6, "world")] // cursor at start of word - #[case("hello world test", 10, "hello test", 6, "world")] // cursor at end of word - fn test_cut_inside_word( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] expected_buffer: &str, - #[case] expected_cursor: usize, - #[case] expected_cut: &str, - ) { - let mut editor = editor_with(input); - editor.move_to_position(cursor_pos, false); - editor.cut_text_object(TextObject { - scope: TextObjectScope::Inner, - object_type: TextObjectType::Word, - }); - assert_eq!(editor.get_buffer(), expected_buffer); - assert_eq!(editor.insertion_point(), expected_cursor); - assert_eq!(editor.cut_buffer.get().0, expected_cut); - } - - #[rstest] - #[case("hello world test", 7, "world")] // cursor inside word - #[case("hello world test", 6, "world")] // cursor at start of word - #[case("hello world test", 10, "world")] // cursor at end of word - fn test_yank_inside_word( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] expected_yank: &str, - ) { - let mut editor = editor_with(input); - editor.move_to_position(cursor_pos, false); - editor.copy_text_object(TextObject { - scope: TextObjectScope::Inner, - object_type: TextObjectType::Word, - }); - assert_eq!(editor.get_buffer(), input); // Buffer shouldn't change - assert_eq!(editor.insertion_point(), cursor_pos); // Cursor should return to original position - assert_eq!(editor.cut_buffer.get().0, expected_yank); - } - - #[rstest] - #[case("hello world test", 7, "hello test", 6, "world ")] // word with following space - #[case("hello world", 7, "hello", 5, " world")] // word at end, gets preceding space - #[case("word test", 2, "test", 0, "word ")] // first word with following space - #[case("hello word", 7, "hello", 5, " word")] // last word gets preceding space - // Edge cases at end of string - #[case("word", 2, "", 0, "word")] // single word, no whitespace - #[case(" word", 2, "", 0, " word")] // word with only leading space - // Edge cases with punctuation boundaries - #[case("word.", 2, ".", 0, "word")] // word followed by punctuation - #[case(".word", 2, ".", 1, "word")] // word preceded by punctuation - #[case("(word)", 2, "()", 1, "word")] // word surrounded by punctuation - #[case("hello,world", 2, ",world", 0, "hello")] // word followed by punct+word - #[case("hello,world", 7, "hello,", 6, "world")] // word preceded by word+punct - fn test_cut_around_word( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] expected_buffer: &str, - #[case] expected_cursor: usize, - #[case] expected_cut: &str, - ) { - let mut editor = editor_with(input); - editor.move_to_position(cursor_pos, false); - editor.cut_text_object(TextObject { - scope: TextObjectScope::Around, - object_type: TextObjectType::Word, - }); - assert_eq!(editor.get_buffer(), expected_buffer); - assert_eq!(editor.insertion_point(), expected_cursor); - assert_eq!(editor.cut_buffer.get().0, expected_cut); - } - - #[rstest] - #[case("hello world test", 7, "world ")] // word with following space - #[case("hello world", 7, " world")] // word at end, gets preceding space - #[case("word test", 2, "word ")] // first word with following space - fn test_yank_around_word( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] expected_yank: &str, - ) { - let mut editor = editor_with(input); - editor.move_to_position(cursor_pos, false); - editor.copy_text_object(TextObject { - scope: TextObjectScope::Around, - object_type: TextObjectType::Word, - }); - assert_eq!(editor.get_buffer(), input); // Buffer shouldn't change - assert_eq!(editor.insertion_point(), cursor_pos); // Cursor should return to original position - assert_eq!(editor.cut_buffer.get().0, expected_yank); - } - - #[rstest] - #[case("hello big-word test", 10, "hello test", 6, "big-word")] // big word with punctuation - #[case("hello BIGWORD test", 10, "hello test", 6, "BIGWORD")] // simple big word - #[case("test@example.com file", 8, " file", 0, "test@example.com")] //cursor on email address - #[case("test@example.com file", 17, "test@example.com ", 17, "file")] // cursor at end of "file" - fn test_cut_inside_big_word( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] expected_buffer: &str, - #[case] expected_cursor: usize, - #[case] expected_cut: &str, - ) { - let mut editor = editor_with(input); - editor.move_to_position(cursor_pos, false); - editor.cut_text_object(TextObject { - scope: TextObjectScope::Inner, - object_type: TextObjectType::BigWord, - }); - - assert_eq!(editor.get_buffer(), expected_buffer); - assert_eq!(editor.insertion_point(), expected_cursor); - assert_eq!(editor.cut_buffer.get().0, expected_cut); - } - - #[rstest] - #[case("hello-world test", 2, "-world test", 0, "hello")] // cursor on "hello" - #[case("hello-world test", 5, "helloworld test", 5, "-")] // cursor on "-" - #[case("hello-world test", 8, "hello- test", 6, "world")] // cursor on "world" - #[case("a-b-c test", 0, "-b-c test", 0, "a")] // single char "a" - #[case("a-b-c test", 2, "a--c test", 2, "b")] // single char "b" - fn test_cut_inside_word_with_punctuation( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] expected_buffer: &str, - #[case] expected_cursor: usize, - #[case] expected_cut: &str, - ) { - let mut editor = editor_with(input); - editor.move_to_position(cursor_pos, false); - editor.cut_text_object(TextObject { - scope: TextObjectScope::Inner, - object_type: TextObjectType::Word, - }); - assert_eq!(editor.get_buffer(), expected_buffer); - assert_eq!(editor.insertion_point(), expected_cursor); - assert_eq!(editor.cut_buffer.get().0, expected_cut); - } - - #[rstest] - #[case("hello-world test", 2, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "-world test", "hello")] // small word gets just "hello" - #[case("hello-world test", 2, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::BigWord }, " test", "hello-world")] // big word gets "hello-word" - #[case("test@example.com", 6, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "test@", "example.com")] // small word in email (UAX#29 extends across punct) - #[case("test@example.com", 6, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::BigWord }, "", "test@example.com")] // big word gets entire email - fn test_word_vs_big_word_comparison( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] text_object: TextObject, - #[case] expected_buffer: &str, - #[case] expected_cut: &str, - ) { - let mut editor = editor_with(input); - editor.move_to_position(cursor_pos, false); - editor.cut_text_object(text_object); - assert_eq!(editor.get_buffer(), expected_buffer); - assert_eq!(editor.cut_buffer.get().0, expected_cut); - } - - #[rstest] - // Test inside operations (iw) at word boundaries - #[case("hello world", 0, "hello")] // start of first word - #[case("hello world", 4, "hello")] // end of first word - #[case("hello world", 6, "world")] // start of second word - #[case("hello world", 10, "world")] // end of second word - // Test at exact word boundaries with punctuation - #[case("hello-world", 4, "hello")] // just before punctuation - #[case("hello-world", 5, "-")] // on punctuation - #[case("hello-world", 6, "world")] // just after punctuation - fn test_cut_inside_word_boundaries( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] expected_cut: &str, - ) { - let mut editor = editor_with(input); - editor.move_to_position(cursor_pos, false); - editor.cut_text_object(TextObject { - scope: TextObjectScope::Inner, - object_type: TextObjectType::Word, - }); - assert_eq!(editor.cut_buffer.get().0, expected_cut); - } - - #[rstest] - // Test around operations (aw) at word boundaries - #[case("hello world", 0, "hello ")] // start of first word - #[case("hello world", 4, "hello ")] // end of first word - #[case("hello world", 6, " world")] // start of second word (gets preceding space) - #[case("hello world", 10, " world")] // end of second word - #[case("word", 0, "word")] // single word, no whitespace - #[case("word ", 0, "word ")] // word with trailing space - #[case(" word", 1, " word")] // word with leading space - fn test_cut_around_word_boundaries( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] expected_cut: &str, - ) { - let mut editor = editor_with(input); - editor.move_to_position(cursor_pos, false); - editor.cut_text_object(TextObject { - scope: TextObjectScope::Around, - object_type: TextObjectType::Word, - }); - assert_eq!(editor.cut_buffer.get().0, expected_cut); + #[test] + fn test_cut_inside_word() { + let cases = [ + ("hello world test", 7, "hello test", 6, "world"), // cursor inside word + ("hello world test", 6, "hello test", 6, "world"), // cursor at start of word + ("hello world test", 10, "hello test", 6, "world"), // cursor at end of word + ]; + + for (input, cursor_pos, expected_buffer, expected_cursor, expected_cut) in cases { + let mut editor = editor_with(input); + editor.move_to_position(cursor_pos, false); + editor.cut_text_object(TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Word, + }); + assert_eq!(editor.get_buffer(), expected_buffer); + assert_eq!(editor.insertion_point(), expected_cursor); + assert_eq!(editor.cut_buffer.get().0, expected_cut); + } } - #[rstest] + #[test] + fn test_yank_inside_word() { + let cases = [ + ("hello world test", 7, "world"), // cursor inside word + ("hello world test", 6, "world"), // cursor at start of word + ("hello world test", 10, "world"), // cursor at end of word + ]; + + for (input, cursor_pos, expected_yank) in cases { + let mut editor = editor_with(input); + editor.move_to_position(cursor_pos, false); + editor.copy_text_object(TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Word, + }); + assert_eq!(editor.get_buffer(), input); // Buffer shouldn't change + assert_eq!(editor.insertion_point(), cursor_pos); // Cursor should return to original position + assert_eq!(editor.cut_buffer.get().0, expected_yank); + } + } + + #[test] + fn test_cut_around_word() { + let cases = [ + ("hello world test", 7, "hello test", 6, "world "), // word with following space + ("hello world", 7, "hello", 5, " world"), // word at end, gets preceding space + ("word test", 2, "test", 0, "word "), // first word with following space + ("hello word", 7, "hello", 5, " word"), // last word gets preceding space + // Edge cases at end of string + ("word", 2, "", 0, "word"), // single word, no whitespace + (" word", 2, "", 0, " word"), // word with only leading space + // Edge cases with punctuation boundaries + ("word.", 2, ".", 0, "word"), // word followed by punctuation + (".word", 2, ".", 1, "word"), // word preceded by punctuation + ("(word)", 2, "()", 1, "word"), // word surrounded by punctuation + ("hello,world", 2, ",world", 0, "hello"), // word followed by punct+word + ("hello,world", 7, "hello,", 6, "world"), // word preceded by word+punct + ]; + + for (input, cursor_pos, expected_buffer, expected_cursor, expected_cut) in cases { + let mut editor = editor_with(input); + editor.move_to_position(cursor_pos, false); + editor.cut_text_object(TextObject { + scope: TextObjectScope::Around, + object_type: TextObjectType::Word, + }); + assert_eq!(editor.get_buffer(), expected_buffer); + assert_eq!(editor.insertion_point(), expected_cursor); + assert_eq!(editor.cut_buffer.get().0, expected_cut); + } + } + + #[test] + fn test_yank_around_word() { + let cases = [ + ("hello world test", 7, "world "), // word with following space + ("hello world", 7, " world"), // word at end, gets preceding space + ("word test", 2, "word "), // first word with following space + ]; + + for (input, cursor_pos, expected_yank) in cases { + let mut editor = editor_with(input); + editor.move_to_position(cursor_pos, false); + editor.copy_text_object(TextObject { + scope: TextObjectScope::Around, + object_type: TextObjectType::Word, + }); + assert_eq!(editor.get_buffer(), input); // Buffer shouldn't change + assert_eq!(editor.insertion_point(), cursor_pos); // Cursor should return to original position + assert_eq!(editor.cut_buffer.get().0, expected_yank); + } + } + + #[test] + fn test_cut_inside_big_word() { + let cases = [ + ("hello big-word test", 10, "hello test", 6, "big-word"), // big word with punctuation + ("hello BIGWORD test", 10, "hello test", 6, "BIGWORD"), // simple big word + ("test@example.com file", 8, " file", 0, "test@example.com"), //cursor on email address + ("test@example.com file", 17, "test@example.com ", 17, "file"), // cursor at end of "file" + ]; + + for (input, cursor_pos, expected_buffer, expected_cursor, expected_cut) in cases { + let mut editor = editor_with(input); + editor.move_to_position(cursor_pos, false); + editor.cut_text_object(TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::BigWord, + }); + + assert_eq!(editor.get_buffer(), expected_buffer); + assert_eq!(editor.insertion_point(), expected_cursor); + assert_eq!(editor.cut_buffer.get().0, expected_cut); + } + } + + #[test] + fn test_cut_inside_word_with_punctuation() { + let cases = [ + ("hello-world test", 2, "-world test", 0, "hello"), // cursor on "hello" + ("hello-world test", 5, "helloworld test", 5, "-"), // cursor on "-" + ("hello-world test", 8, "hello- test", 6, "world"), // cursor on "world" + ("a-b-c test", 0, "-b-c test", 0, "a"), // single char "a" + ("a-b-c test", 2, "a--c test", 2, "b"), // single char "b" + ]; + + for (input, cursor_pos, expected_buffer, expected_cursor, expected_cut) in cases { + let mut editor = editor_with(input); + editor.move_to_position(cursor_pos, false); + editor.cut_text_object(TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Word, + }); + assert_eq!(editor.get_buffer(), expected_buffer); + assert_eq!(editor.insertion_point(), expected_cursor); + assert_eq!(editor.cut_buffer.get().0, expected_cut); + } + } + + #[test] + fn test_word_vs_big_word_comparison() { + let cases = [ + ( + "hello-world test", + 2, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Word, + }, + "-world test", + "hello", + ), // small word gets just "hello" + ( + "hello-world test", + 2, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::BigWord, + }, + " test", + "hello-world", + ), // big word gets "hello-word" + ( + "test@example.com", + 6, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Word, + }, + "test@", + "example.com", + ), // small word in email (UAX#29 extends across punct) + ( + "test@example.com", + 6, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::BigWord, + }, + "", + "test@example.com", + ), // big word gets entire email + ]; + + for (input, cursor_pos, text_object, expected_buffer, expected_cut) in cases { + let mut editor = editor_with(input); + editor.move_to_position(cursor_pos, false); + editor.cut_text_object(text_object); + assert_eq!(editor.get_buffer(), expected_buffer); + assert_eq!(editor.cut_buffer.get().0, expected_cut); + } + } + + #[test] + fn test_cut_inside_word_boundaries() { + let cases = [ + // Test inside operations (iw) at word boundaries + ("hello world", 0, "hello"), // start of first word + ("hello world", 4, "hello"), // end of first word + ("hello world", 6, "world"), // start of second word + ("hello world", 10, "world"), // end of second word + // Test at exact word boundaries with punctuation + ("hello-world", 4, "hello"), // just before punctuation + ("hello-world", 5, "-"), // on punctuation + ("hello-world", 6, "world"), // just after punctuation + ]; + + for (input, cursor_pos, expected_cut) in cases { + let mut editor = editor_with(input); + editor.move_to_position(cursor_pos, false); + editor.cut_text_object(TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Word, + }); + assert_eq!(editor.cut_buffer.get().0, expected_cut); + } + } + + #[test] + fn test_cut_around_word_boundaries() { + let cases = [ + // Test around operations (aw) at word boundaries + ("hello world", 0, "hello "), // start of first word + ("hello world", 4, "hello "), // end of first word + ("hello world", 6, " world"), // start of second word (gets preceding space) + ("hello world", 10, " world"), // end of second word + ("word", 0, "word"), // single word, no whitespace + ("word ", 0, "word "), // word with trailing space + (" word", 1, " word"), // word with leading space + ]; + + for (input, cursor_pos, expected_cut) in cases { + let mut editor = editor_with(input); + editor.move_to_position(cursor_pos, false); + editor.cut_text_object(TextObject { + scope: TextObjectScope::Around, + object_type: TextObjectType::Word, + }); + assert_eq!(editor.cut_buffer.get().0, expected_cut); + } + } + + #[test] fn test_cut_text_object_unicode_safety() { let mut editor = editor_with("hello 🦀end"); editor.move_to_position(10, false); // Position after the emoji @@ -2006,166 +2044,472 @@ mod test { assert!(editor.line_buffer.is_valid()); // Should not panic or be invalid } - #[rstest] - // Test operations when cursor is IN WHITESPACE (middle of spaces) - #[case("hello world test", 5, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "helloworld test", 5, " ")] // single space - #[case("hello world", 6, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "helloworld", 5, " ")] // multiple spaces, cursor on second - #[case("hello world", 7, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "helloworld", 5, " ")] // multiple spaces, cursor on middle - #[case(" hello", 1, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "hello", 0, " ")] // leading spaces, cursor on middle - #[case("hello ", 7, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "hello", 5, " ")] // trailing spaces, cursor on middle - #[case("hello\tworld", 5, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "helloworld", 5, "\t")] // tab character - #[case("hello\nworld", 5, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "helloworld", 5, "\n")] // newline character - #[case("hello world test", 5, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::BigWord }, "helloworld test", 5, " ")] // single space (big word) - #[case("hello world", 6, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::BigWord }, "helloworld", 5, " ")] // multiple spaces (big word) - #[case(" ", 0, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "", 0, " ")] // only whitespace at start - #[case(" ", 1, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "", 0, " ")] // only whitespace at end - #[case("hello ", 5, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "hello", 5, " ")] // trailing whitespace at string end - #[case(" hello", 0, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "hello", 0, " ")] // leading whitespace at string start - fn test_text_object_in_whitespace( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] text_object: TextObject, - #[case] expected_buffer: &str, - #[case] expected_cursor: usize, - #[case] expected_cut: &str, - ) { - let mut editor = editor_with(input); - editor.move_to_position(cursor_pos, false); - editor.cut_text_object(text_object); - assert_eq!(editor.get_buffer(), expected_buffer); - assert_eq!(editor.insertion_point(), expected_cursor); - assert_eq!(editor.cut_buffer.get().0, expected_cut); - } - - #[rstest] - // Test text object jumping behavior in various scenarios - // Cursor inside empty pairs should operate on current pair (cursor stays, nothing cut) - #[case(r#"foo()bar"#, 4, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Brackets }, "foo()bar", 4, "")] // inside empty brackets - #[case(r#"foo""bar"#, 4, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Quote }, "foo\"\"bar", 4, "")] // inside empty quotes - // Cursor outside pairs should jump to next pair (even if empty) - #[case(r#"foo ()bar"#, 2, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Brackets }, "foo ()bar", 5, "")] // jump to empty brackets - #[case(r#"foo ""bar"#, 2, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Quote }, "foo \"\"bar", 5, "")] // jump to empty quote - #[case(r#"foo (content)bar"#, 2, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Brackets }, "foo ()bar", 5, "content")] // jump to non-empty brackets - #[case(r#"foo "content"bar"#, 2, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Quote }, "foo \"\"bar", 5, "content")] // jump to non-empty quotes - // Cursor between pairs should jump to next pair - #[case(r#"(first) (second)"#, 8, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Brackets }, "(first) ()", 9, "second")] // between brackets - #[case(r#""first" "second""#, 8, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Quote }, "\"first\"\"second\"", 7, " ")] // between quotes - // Around scope should include the pair characters - #[case(r#"foo (bar)"#, 2, TextObject { scope: TextObjectScope::Around, object_type: TextObjectType::Brackets }, "foo ", 4, "(bar)")] // around includes parentheses - #[case(r#"foo "bar""#, 2, TextObject { scope: TextObjectScope::Around, object_type: TextObjectType::Quote }, "foo ", 4, "\"bar\"")] // around includes quotes - fn test_text_object_jumping_behavior( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] text_object: TextObject, - #[case] expected_buffer: &str, - #[case] expected_cursor: usize, - #[case] expected_cut: &str, - ) { - let mut editor = editor_with(input); - editor.move_to_position(cursor_pos, false); - editor.cut_text_object(text_object); - assert_eq!(editor.get_buffer(), expected_buffer); - assert_eq!(editor.insertion_point(), expected_cursor); - assert_eq!(editor.cut_buffer.get().0, expected_cut); - } - - #[rstest] - // Test bracket_text_object_range with Inner scope - just the content inside brackets - #[case("foo(bar)baz", 5, TextObjectScope::Inner, Some(4..7))] // cursor inside brackets - #[case("foo[bar]baz", 5, TextObjectScope::Inner, Some(4..7))] // square brackets - #[case("foo{bar}baz", 5, TextObjectScope::Inner, Some(4..7))] // square brackets - #[case("foo()bar", 4, TextObjectScope::Inner, Some(4..4))] // empty brackets - #[case("(nested[inner]outer)", 8, TextObjectScope::Inner, Some(8..13))] // nested, innermost - #[case("(nested[mixed{inner}brackets]outer)", 8, TextObjectScope::Inner, Some(8..28))] // nested, innermost - #[case("next(nested[mixed{inner}brackets]outer)", 0, TextObjectScope::Inner, Some(5..38))] // next nested mixed - #[case("foo (bar)baz", 0, TextObjectScope::Inner, Some(5..8))] // next pair from line start - #[case(" (bar)baz", 1, TextObjectScope::Inner, Some(5..8))] // next pair from whitespace - #[case("foo(bar)baz", 2, TextObjectScope::Inner, Some(4..7))] // next pair from word - #[case("foo(bar\nbaz)qux", 8, TextObjectScope::Inner, Some(4..11))] // multi-line brackets - #[case("foo\n(bar\nbaz)qux", 0, TextObjectScope::Inner, Some(5..12))] // next multi-line brackets - #[case("foo\n(bar\nbaz)qux", 3, TextObjectScope::Around, Some(4..13))] // next multi-line brackets - #[case("{hello}", 3, TextObjectScope::Around, Some(0..7))] // includes curly brackets - #[case("foo()bar", 4, TextObjectScope::Around, Some(3..5))] // around empty brackets - #[case("(nested(inner)outer)", 8, TextObjectScope::Around, Some(7..14))] // nested around includes delimiters - #[case("start(nested(inner)outer)", 2, TextObjectScope::Around, Some(5..25))] // Next outer nested pair - #[case("(mixed{nested)brackets", 1, TextObjectScope::Inner, Some(1..13))] // mixed nesting - #[case("(unclosed(nested)brackets", 1, TextObjectScope::Inner, Some(10..16))] // unclosed bracket, find next closed - #[case("no brackets here", 5, TextObjectScope::Inner, None)] // no brackets found - #[case("(unclosed", 1, TextObjectScope::Inner, None)] // unclosed bracket - #[case("(mismatched}", 1, TextObjectScope::Inner, None)] // mismatched brackets - fn test_bracket_text_object_range( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] scope: TextObjectScope, - #[case] expected: Option>, - ) { - let mut editor = editor_with(input); - editor.move_to_position(cursor_pos, false); - let result = editor.bracket_text_object_range(scope); - assert_eq!(result, expected); - } - - #[rstest] - // Test quote_text_object_range with Inner scope - just the content inside quotes - #[case(r#"foo"bar"baz"#, 5, TextObjectScope::Inner, Some(4..7))] // cursor inside double quotes - #[case("foo'bar'baz", 5, TextObjectScope::Inner, Some(4..7))] // single quotes - #[case("foo`bar`baz", 5, TextObjectScope::Inner, Some(4..7))] // backticks - #[case(r#"foo""bar"#, 4, TextObjectScope::Inner, Some(4..4))] // empty quotes - #[case(r#""nested'inner'outer""#, 8, TextObjectScope::Inner, Some(8..13))] // nested, innermost - #[case(r#""nested`mixed'inner'backticks`outer""#, 8, TextObjectScope::Inner, Some(8..29))] // nested, innermost - #[case(r#"next"nested'mixed`inner`quotes'outer""#, 0, TextObjectScope::Inner, Some(5..36))] // next nested mixed - #[case(r#"foo "bar"baz"#, 0, TextObjectScope::Inner, Some(5..8))] // next pair - #[case(r#"foo"bar"baz"#, 2, TextObjectScope::Inner, Some(4..7))] // next from inside word - #[case(r#"foo"bar"baz"#, 4, TextObjectScope::Around, Some(3..8))] // around includes quotes - #[case(r#"foo"bar"baz"#, 3, TextObjectScope::Around, Some(3..8))] // around on opening quote - #[case(r#"foo"bar"baz"#, 2, TextObjectScope::Around, Some(3..8))] // around next quotes - #[case(r#"foo""bar"#, 4, TextObjectScope::Around, Some(3..5))] // around empty quotes - #[case(r#"foo""bar"#, 1, TextObjectScope::Around, Some(3..5))] // around empty quotes - #[case(r#""nested"inner"outer""#, 8, TextObjectScope::Around, Some(7..14))] // nested around includes delimiters - #[case(r#"start"nested'inner'outer""#, 2, TextObjectScope::Around, Some(5..25))] // Next outer nested pair - #[case("no quotes here", 5, TextObjectScope::Inner, None)] // no quotes found - #[case(r#"foo"bar"#, 1, TextObjectScope::Inner, None)] // unclosed quote - #[case("foo'bar\nbaz'qux", 5, TextObjectScope::Inner, None)] // quotes don't span multiple lines - #[case("foo'bar\nbaz'qux", 0, TextObjectScope::Inner, None)] // quotes don't span multiple lines - #[case("foobar\n`baz`qux", 6, TextObjectScope::Inner, None)] // quotes don't span multiple lines - #[case("foo\n(bar\nbaz)qux", 0, TextObjectScope::Inner, None)] // next multi-line brackets - #[case("foo\n(bar\nbaz)qux", 3, TextObjectScope::Around, None)] // next multi-line brackets - fn test_quote_text_object_range( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] scope: TextObjectScope, - #[case] expected: Option>, - ) { - let mut editor = editor_with(input); - editor.line_buffer.set_insertion_point(cursor_pos); - let result = editor.quote_text_object_range(scope); - assert_eq!(result, expected); - } - - #[rstest] - // Test edge cases and complex scenarios for both bracket and quote text objects - #[case("", 0, TextObjectScope::Inner, None, None)] // empty buffer - #[case("a", 0, TextObjectScope::Inner, None, None)] // single character - #[case("()", 1, TextObjectScope::Inner, Some(1..1), None)] // empty brackets, cursor inside - #[case(r#""""#, 1, TextObjectScope::Inner, None, Some(1..1))] // empty quotes, cursor inside - #[case("([{}])", 3, TextObjectScope::Inner, Some(3..3), None)] // deeply nested brackets - #[case(r#""'`text`'""#, 5, TextObjectScope::Inner, None, Some(3..7))] // deeply nested quotes - #[case("(text) and [more]", 5, TextObjectScope::Around, Some(0..6), None)] // multiple bracket types - #[case(r#""text" and 'more'"#, 5, TextObjectScope::Around, None, Some(0..6))] // multiple quote types - fn test_text_object_edge_cases( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] scope: TextObjectScope, - #[case] expected_bracket: Option>, - #[case] expected_quote: Option>, - ) { - let mut editor = editor_with(input); - editor.move_to_position(cursor_pos, false); + #[test] + fn test_text_object_in_whitespace() { + let cases = [ + // Test operations when cursor is IN WHITESPACE (middle of spaces) + ( + "hello world test", + 5, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Word, + }, + "helloworld test", + 5, + " ", + ), // single space + ( + "hello world", + 6, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Word, + }, + "helloworld", + 5, + " ", + ), // multiple spaces, cursor on second + ( + "hello world", + 7, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Word, + }, + "helloworld", + 5, + " ", + ), // multiple spaces, cursor on middle + ( + " hello", + 1, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Word, + }, + "hello", + 0, + " ", + ), // leading spaces, cursor on middle + ( + "hello ", + 7, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Word, + }, + "hello", + 5, + " ", + ), // trailing spaces, cursor on middle + ( + "hello\tworld", + 5, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Word, + }, + "helloworld", + 5, + "\t", + ), // tab character + ( + "hello\nworld", + 5, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Word, + }, + "helloworld", + 5, + "\n", + ), // newline character + ( + "hello world test", + 5, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::BigWord, + }, + "helloworld test", + 5, + " ", + ), // single space (big word) + ( + "hello world", + 6, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::BigWord, + }, + "helloworld", + 5, + " ", + ), // multiple spaces (big word) + ( + " ", + 0, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Word, + }, + "", + 0, + " ", + ), // only whitespace at start + ( + " ", + 1, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Word, + }, + "", + 0, + " ", + ), // only whitespace at end + ( + "hello ", + 5, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Word, + }, + "hello", + 5, + " ", + ), // trailing whitespace at string end + ( + " hello", + 0, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Word, + }, + "hello", + 0, + " ", + ), // leading whitespace at string start + ]; + + for (input, cursor_pos, text_object, expected_buffer, expected_cursor, expected_cut) in + cases + { + let mut editor = editor_with(input); + editor.move_to_position(cursor_pos, false); + editor.cut_text_object(text_object); + assert_eq!(editor.get_buffer(), expected_buffer); + assert_eq!(editor.insertion_point(), expected_cursor); + assert_eq!(editor.cut_buffer.get().0, expected_cut); + } + } - let bracket_result = editor.bracket_text_object_range(scope); - let quote_result = editor.quote_text_object_range(scope); + #[test] + fn test_text_object_jumping_behavior() { + let cases = [ + // Test text object jumping behavior in various scenarios + // Cursor inside empty pairs should operate on current pair (cursor stays, nothing cut) + ( + r#"foo()bar"#, + 4, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Brackets, + }, + "foo()bar", + 4, + "", + ), // inside empty brackets + ( + r#"foo""bar"#, + 4, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Quote, + }, + "foo\"\"bar", + 4, + "", + ), // inside empty quotes + // Cursor outside pairs should jump to next pair (even if empty) + ( + r#"foo ()bar"#, + 2, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Brackets, + }, + "foo ()bar", + 5, + "", + ), // jump to empty brackets + ( + r#"foo ""bar"#, + 2, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Quote, + }, + "foo \"\"bar", + 5, + "", + ), // jump to empty quote + ( + r#"foo (content)bar"#, + 2, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Brackets, + }, + "foo ()bar", + 5, + "content", + ), // jump to non-empty brackets + ( + r#"foo "content"bar"#, + 2, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Quote, + }, + "foo \"\"bar", + 5, + "content", + ), // jump to non-empty quotes + // Cursor between pairs should jump to next pair + ( + r#"(first) (second)"#, + 8, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Brackets, + }, + "(first) ()", + 9, + "second", + ), // between brackets + ( + r#""first" "second""#, + 8, + TextObject { + scope: TextObjectScope::Inner, + object_type: TextObjectType::Quote, + }, + "\"first\"\"second\"", + 7, + " ", + ), // between quotes + // Around scope should include the pair characters + ( + r#"foo (bar)"#, + 2, + TextObject { + scope: TextObjectScope::Around, + object_type: TextObjectType::Brackets, + }, + "foo ", + 4, + "(bar)", + ), // around includes parentheses + ( + r#"foo "bar""#, + 2, + TextObject { + scope: TextObjectScope::Around, + object_type: TextObjectType::Quote, + }, + "foo ", + 4, + "\"bar\"", + ), // around includes quotes + ]; + + for (input, cursor_pos, text_object, expected_buffer, expected_cursor, expected_cut) in + cases + { + let mut editor = editor_with(input); + editor.move_to_position(cursor_pos, false); + editor.cut_text_object(text_object); + assert_eq!(editor.get_buffer(), expected_buffer); + assert_eq!(editor.insertion_point(), expected_cursor); + assert_eq!(editor.cut_buffer.get().0, expected_cut); + } + } + + #[test] + fn test_bracket_text_object_range() { + let cases = [ + // Test bracket_text_object_range with Inner scope - just the content inside brackets + ("foo(bar)baz", 5, TextObjectScope::Inner, Some(4..7)), // cursor inside brackets + ("foo[bar]baz", 5, TextObjectScope::Inner, Some(4..7)), // square brackets + ("foo{bar}baz", 5, TextObjectScope::Inner, Some(4..7)), // square brackets + ("foo()bar", 4, TextObjectScope::Inner, Some(4..4)), // empty brackets + ( + "(nested[inner]outer)", + 8, + TextObjectScope::Inner, + Some(8..13), + ), // nested, innermost + ( + "(nested[mixed{inner}brackets]outer)", + 8, + TextObjectScope::Inner, + Some(8..28), + ), // nested, innermost + ( + "next(nested[mixed{inner}brackets]outer)", + 0, + TextObjectScope::Inner, + Some(5..38), + ), // next nested mixed + ("foo (bar)baz", 0, TextObjectScope::Inner, Some(5..8)), // next pair from line start + (" (bar)baz", 1, TextObjectScope::Inner, Some(5..8)), // next pair from whitespace + ("foo(bar)baz", 2, TextObjectScope::Inner, Some(4..7)), // next pair from word + ("foo(bar\nbaz)qux", 8, TextObjectScope::Inner, Some(4..11)), // multi-line brackets + ("foo\n(bar\nbaz)qux", 0, TextObjectScope::Inner, Some(5..12)), // next multi-line brackets + ( + "foo\n(bar\nbaz)qux", + 3, + TextObjectScope::Around, + Some(4..13), + ), // next multi-line brackets + ("{hello}", 3, TextObjectScope::Around, Some(0..7)), // includes curly brackets + ("foo()bar", 4, TextObjectScope::Around, Some(3..5)), // around empty brackets + ( + "(nested(inner)outer)", + 8, + TextObjectScope::Around, + Some(7..14), + ), // nested around includes delimiters + ( + "start(nested(inner)outer)", + 2, + TextObjectScope::Around, + Some(5..25), + ), // Next outer nested pair + ( + "(mixed{nested)brackets", + 1, + TextObjectScope::Inner, + Some(1..13), + ), // mixed nesting + ( + "(unclosed(nested)brackets", + 1, + TextObjectScope::Inner, + Some(10..16), + ), // unclosed bracket, find next closed + ("no brackets here", 5, TextObjectScope::Inner, None), // no brackets found + ("(unclosed", 1, TextObjectScope::Inner, None), // unclosed bracket + ("(mismatched}", 1, TextObjectScope::Inner, None), // mismatched brackets + ]; + + for (input, cursor_pos, scope, expected) in cases { + let mut editor = editor_with(input); + editor.move_to_position(cursor_pos, false); + let result = editor.bracket_text_object_range(scope); + assert_eq!(result, expected); + } + } - assert_eq!(bracket_result, expected_bracket); - assert_eq!(quote_result, expected_quote); + #[test] + fn test_quote_text_object_range() { + let cases = [ + // Test quote_text_object_range with Inner scope - just the content inside quotes + (r#"foo"bar"baz"#, 5, TextObjectScope::Inner, Some(4..7)), // cursor inside double quotes + ("foo'bar'baz", 5, TextObjectScope::Inner, Some(4..7)), // single quotes + ("foo`bar`baz", 5, TextObjectScope::Inner, Some(4..7)), // backticks + (r#"foo""bar"#, 4, TextObjectScope::Inner, Some(4..4)), // empty quotes + ( + r#""nested'inner'outer""#, + 8, + TextObjectScope::Inner, + Some(8..13), + ), // nested, innermost + ( + r#""nested`mixed'inner'backticks`outer""#, + 8, + TextObjectScope::Inner, + Some(8..29), + ), // nested, innermost + ( + r#"next"nested'mixed`inner`quotes'outer""#, + 0, + TextObjectScope::Inner, + Some(5..36), + ), // next nested mixed + (r#"foo "bar"baz"#, 0, TextObjectScope::Inner, Some(5..8)), // next pair + (r#"foo"bar"baz"#, 2, TextObjectScope::Inner, Some(4..7)), // next from inside word + (r#"foo"bar"baz"#, 4, TextObjectScope::Around, Some(3..8)), // around includes quotes + (r#"foo"bar"baz"#, 3, TextObjectScope::Around, Some(3..8)), // around on opening quote + (r#"foo"bar"baz"#, 2, TextObjectScope::Around, Some(3..8)), // around next quotes + (r#"foo""bar"#, 4, TextObjectScope::Around, Some(3..5)), // around empty quotes + (r#"foo""bar"#, 1, TextObjectScope::Around, Some(3..5)), // around empty quotes + ( + r#""nested"inner"outer""#, + 8, + TextObjectScope::Around, + Some(7..14), + ), // nested around includes delimiters + ( + r#"start"nested'inner'outer""#, + 2, + TextObjectScope::Around, + Some(5..25), + ), // Next outer nested pair + ("no quotes here", 5, TextObjectScope::Inner, None), // no quotes found + (r#"foo"bar"#, 1, TextObjectScope::Inner, None), // unclosed quote + ("foo'bar\nbaz'qux", 5, TextObjectScope::Inner, None), // quotes don't span multiple lines + ("foo'bar\nbaz'qux", 0, TextObjectScope::Inner, None), // quotes don't span multiple lines + ("foobar\n`baz`qux", 6, TextObjectScope::Inner, None), // quotes don't span multiple lines + ("foo\n(bar\nbaz)qux", 0, TextObjectScope::Inner, None), // next multi-line brackets + ("foo\n(bar\nbaz)qux", 3, TextObjectScope::Around, None), // next multi-line brackets + ]; + + for (input, cursor_pos, scope, expected) in cases { + let mut editor = editor_with(input); + editor.line_buffer.set_insertion_point(cursor_pos); + let result = editor.quote_text_object_range(scope); + assert_eq!(result, expected); + } + } + + #[test] + fn test_text_object_edge_cases() { + let cases = [ + // Test edge cases and complex scenarios for both bracket and quote text objects + ("", 0, TextObjectScope::Inner, None, None), // empty buffer + ("a", 0, TextObjectScope::Inner, None, None), // single character + ("()", 1, TextObjectScope::Inner, Some(1..1), None), // empty brackets, cursor inside + (r#""""#, 1, TextObjectScope::Inner, None, Some(1..1)), // empty quotes, cursor inside + ("([{}])", 3, TextObjectScope::Inner, Some(3..3), None), // deeply nested brackets + (r#""'`text`'""#, 5, TextObjectScope::Inner, None, Some(3..7)), // deeply nested quotes + ( + "(text) and [more]", + 5, + TextObjectScope::Around, + Some(0..6), + None, + ), // multiple bracket types + ( + r#""text" and 'more'"#, + 5, + TextObjectScope::Around, + None, + Some(0..6), + ), // multiple quote types + ]; + + for (input, cursor_pos, scope, expected_bracket, expected_quote) in cases { + let mut editor = editor_with(input); + editor.move_to_position(cursor_pos, false); + + let bracket_result = editor.bracket_text_object_range(scope); + let quote_result = editor.quote_text_object_range(scope); + + assert_eq!(bracket_result, expected_bracket); + assert_eq!(quote_result, expected_quote); + } } } diff --git a/src/core_editor/line_buffer.rs b/src/core_editor/line_buffer.rs index 1582d4960..873fa02a7 100644 --- a/src/core_editor/line_buffer.rs +++ b/src/core_editor/line_buffer.rs @@ -1055,7 +1055,6 @@ fn is_whitespace_str(s: &str) -> bool { mod test { use super::*; use pretty_assertions::assert_eq; - use rstest::rstest; fn buffer_with(content: &str) -> LineBuffer { let mut line_buffer = LineBuffer::new(); @@ -1109,76 +1108,85 @@ mod test { line_buffer.assert_valid(); } - #[rstest] - #[case("hello", 5, "hello\n", 6)] - #[case("hello", 0, "\nhello", 1)] - #[case("hello", 3, "hel\nlo", 4)] - #[case("line1\nline2", 11, "line1\nline2\n", 12)] - #[case("", 0, "\n", 1)] - fn insert_newline_inserts_lf_only( - #[case] input: &str, - #[case] in_location: usize, - #[case] output: &str, - #[case] out_location: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); - - line_buffer.insert_newline(); - - assert_eq!(line_buffer.get_buffer(), output); - assert!( - !line_buffer.get_buffer().contains('\r'), - "Buffer should never contain CR" - ); - assert_eq!(line_buffer.insertion_point(), out_location); - line_buffer.assert_valid(); + #[test] + fn insert_newline_inserts_lf_only() { + let cases = [ + ("hello", 5, "hello\n", 6), + ("hello", 0, "\nhello", 1), + ("hello", 3, "hel\nlo", 4), + ("line1\nline2", 11, "line1\nline2\n", 12), + ("", 0, "\n", 1), + ]; + + for (input, in_location, output, out_location) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); + + line_buffer.insert_newline(); + + assert_eq!(line_buffer.get_buffer(), output); + assert!( + !line_buffer.get_buffer().contains('\r'), + "Buffer should never contain CR" + ); + assert_eq!(line_buffer.insertion_point(), out_location); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("new string", 10)] - #[case("new line1\nnew line 2", 20)] - fn set_buffer_updates_insertion_point_to_new_buffer_length( - #[case] string_to_set: &str, - #[case] expected_insertion_point: usize, - ) { - let mut line_buffer = buffer_with("test string"); - let before_operation_location = 11; - assert_eq!(before_operation_location, line_buffer.insertion_point()); + #[test] + fn set_buffer_updates_insertion_point_to_new_buffer_length() { + let cases = [("new string", 10), ("new line1\nnew line 2", 20)]; + + for (string_to_set, expected_insertion_point) in cases { + let mut line_buffer = buffer_with("test string"); + let before_operation_location = 11; + assert_eq!(before_operation_location, line_buffer.insertion_point()); - line_buffer.set_buffer(string_to_set.to_string()); + line_buffer.set_buffer(string_to_set.to_string()); - assert_eq!(expected_insertion_point, line_buffer.insertion_point()); - line_buffer.assert_valid(); + assert_eq!(expected_insertion_point, line_buffer.insertion_point()); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("This is a test", "This is a tes")] - #[case("This is a test 😊", "This is a test ")] - #[case("", "")] - fn delete_left_grapheme_works(#[case] input: &str, #[case] expected: &str) { - let mut line_buffer = buffer_with(input); - line_buffer.delete_left_grapheme(); - - let expected_line_buffer = buffer_with(expected); + #[test] + fn delete_left_grapheme_works() { + let cases = [ + ("This is a test", "This is a tes"), + ("This is a test 😊", "This is a test "), + ("", ""), + ]; - assert_eq!(expected_line_buffer, line_buffer); - line_buffer.assert_valid(); - } + for (input, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.delete_left_grapheme(); - #[rstest] - #[case("This is a test", "This is a tes")] - #[case("This is a test 😊", "This is a test ")] - #[case("", "")] - fn delete_right_grapheme_works(#[case] input: &str, #[case] expected: &str) { - let mut line_buffer = buffer_with(input); - line_buffer.move_left(); - line_buffer.delete_right_grapheme(); + let expected_line_buffer = buffer_with(expected); - let expected_line_buffer = buffer_with(expected); + assert_eq!(expected_line_buffer, line_buffer); + line_buffer.assert_valid(); + } + } - assert_eq!(expected_line_buffer, line_buffer); - line_buffer.assert_valid(); + #[test] + fn delete_right_grapheme_works() { + let cases = [ + ("This is a test", "This is a tes"), + ("This is a test 😊", "This is a test "), + ("", ""), + ]; + + for (input, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.move_left(); + line_buffer.delete_right_grapheme(); + + let expected_line_buffer = buffer_with(expected); + + assert_eq!(expected_line_buffer, line_buffer); + line_buffer.assert_valid(); + } } #[test] @@ -1204,1004 +1212,1027 @@ mod test { line_buffer.assert_valid(); } - #[rstest] - #[case("", 0, 0)] // Basecase - #[case("word", 0, 3)] // Cursor on top of the last grapheme of the word - #[case("word and another one", 0, 3)] - #[case("word and another one", 3, 7)] // repeat calling will move - #[case("word and another one", 4, 7)] // Starting from whitespace works - #[case("word\nline two", 0, 3)] // Multiline... - #[case("word\nline two", 3, 8)] // ... continues to next word end - #[case("weirdö characters", 0, 5)] // Multibyte unicode at the word end (latin UTF-8 should be two bytes long) - #[case("weirdö characters", 5, 17)] // continue with unicode (latin UTF-8 should be two bytes long) - #[case("weirdö", 0, 5)] // Multibyte unicode at the buffer end is fine as well - #[case("weirdö", 5, 5)] // Multibyte unicode at the buffer end is fine as well - #[case("word😇 with emoji", 0, 3)] // (Emojis are a separate word) - #[case("word😇 with emoji", 3, 4)] // Moves to end of "emoji word" as it is one grapheme, on top of the first byte - #[case("😇", 0, 0)] // More UTF-8 shenanigans - fn test_move_word_right_end( - #[case] input: &str, - #[case] in_location: usize, - #[case] expected: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); - - line_buffer.move_word_right_end(); - - assert_eq!(line_buffer.insertion_point(), expected); - line_buffer.assert_valid(); + #[test] + fn test_move_word_right_end() { + let cases = [ + ("", 0, 0), // Basecase + ("word", 0, 3), // Cursor on top of the last grapheme of the word + ("word and another one", 0, 3), + ("word and another one", 3, 7), // repeat calling will move + ("word and another one", 4, 7), // Starting from whitespace works + ("word\nline two", 0, 3), // Multiline... + ("word\nline two", 3, 8), // ... continues to next word end + ("weirdö characters", 0, 5), // Multibyte unicode at the word end (latin UTF-8 should be two bytes long) + ("weirdö characters", 5, 17), // continue with unicode (latin UTF-8 should be two bytes long) + ("weirdö", 0, 5), // Multibyte unicode at the buffer end is fine as well + ("weirdö", 5, 5), // Multibyte unicode at the buffer end is fine as well + ("word😇 with emoji", 0, 3), // (Emojis are a separate word) + ("word😇 with emoji", 3, 4), // Moves to end of "emoji word" as it is one grapheme, on top of the first byte + ("😇", 0, 0), // More UTF-8 shenanigans + ]; + + for (input, in_location, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); + + line_buffer.move_word_right_end(); + + assert_eq!(line_buffer.insertion_point(), expected); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("This is a test", 13, "This is a tesT", 14)] - #[case("This is a test", 10, "This is a Test", 11)] - #[case("This is a test", 9, "This is a Test", 11)] - fn capitalize_char_works( - #[case] input: &str, - #[case] in_location: usize, - #[case] output: &str, - #[case] out_location: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); - line_buffer.capitalize_char(); - - let mut expected = buffer_with(output); - expected.set_insertion_point(out_location); - - assert_eq!(expected, line_buffer); - line_buffer.assert_valid(); + #[test] + fn capitalize_char_works() { + let cases = [ + ("This is a test", 13, "This is a tesT", 14), + ("This is a test", 10, "This is a Test", 11), + ("This is a test", 9, "This is a Test", 11), + ]; + + for (input, in_location, output, out_location) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); + line_buffer.capitalize_char(); + + let mut expected = buffer_with(output); + expected.set_insertion_point(out_location); + + assert_eq!(expected, line_buffer); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("This is a test", 13, "This is a TEST", 14)] - #[case("This is a test", 10, "This is a TEST", 14)] - #[case("", 0, "", 0)] - #[case("This", 0, "THIS", 4)] - #[case("This", 4, "THIS", 4)] - fn uppercase_word_works( - #[case] input: &str, - #[case] in_location: usize, - #[case] output: &str, - #[case] out_location: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); - line_buffer.uppercase_word(); - - let mut expected = buffer_with(output); - expected.set_insertion_point(out_location); - - assert_eq!(expected, line_buffer); - line_buffer.assert_valid(); + #[test] + fn uppercase_word_works() { + let cases = [ + ("This is a test", 13, "This is a TEST", 14), + ("This is a test", 10, "This is a TEST", 14), + ("", 0, "", 0), + ("This", 0, "THIS", 4), + ("This", 4, "THIS", 4), + ]; + + for (input, in_location, output, out_location) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); + line_buffer.uppercase_word(); + + let mut expected = buffer_with(output); + expected.set_insertion_point(out_location); + + assert_eq!(expected, line_buffer); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("This is a TEST", 13, "This is a test", 14)] - #[case("This is a TEST", 10, "This is a test", 14)] - #[case("", 0, "", 0)] - #[case("THIS", 0, "this", 4)] - #[case("THIS", 4, "this", 4)] - fn lowercase_word_works( - #[case] input: &str, - #[case] in_location: usize, - #[case] output: &str, - #[case] out_location: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); - line_buffer.lowercase_word(); - - let mut expected = buffer_with(output); - expected.set_insertion_point(out_location); - - assert_eq!(expected, line_buffer); - line_buffer.assert_valid(); + #[test] + fn lowercase_word_works() { + let cases = [ + ("This is a TEST", 13, "This is a test", 14), + ("This is a TEST", 10, "This is a test", 14), + ("", 0, "", 0), + ("THIS", 0, "this", 4), + ("THIS", 4, "this", 4), + ]; + + for (input, in_location, output, out_location) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); + line_buffer.lowercase_word(); + + let mut expected = buffer_with(output); + expected.set_insertion_point(out_location); + + assert_eq!(expected, line_buffer); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("", 0, "", 0)] - #[case("a test", 2, "a Test", 3)] - #[case("a Test", 2, "a test", 3)] - #[case("test", 0, "Test", 1)] - #[case("Test", 0, "test", 1)] - #[case("test", 3, "tesT", 4)] - #[case("tesT", 3, "test", 4)] - #[case("ß", 0, "ß", 2)] - fn switchcase_char( - #[case] input: &str, - #[case] in_location: usize, - #[case] output: &str, - #[case] out_location: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); - line_buffer.switchcase_char(); - - let mut expected = buffer_with(output); - expected.set_insertion_point(out_location); - - assert_eq!(expected, line_buffer); - line_buffer.assert_valid(); + #[test] + fn switchcase_char() { + let cases = [ + ("", 0, "", 0), + ("a test", 2, "a Test", 3), + ("a Test", 2, "a test", 3), + ("test", 0, "Test", 1), + ("Test", 0, "test", 1), + ("test", 3, "tesT", 4), + ("tesT", 3, "test", 4), + ("ß", 0, "ß", 2), + ]; + + for (input, in_location, output, out_location) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); + line_buffer.switchcase_char(); + + let mut expected = buffer_with(output); + expected.set_insertion_point(out_location); + + assert_eq!(expected, line_buffer); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("This is a test", 13, "This is a tets", 14)] - #[case("This is a test", 14, "This is a tets", 14)] // NOTE: Swapping works in opposite direction at last index - #[case("This is a test", 4, "Thi sis a test", 5)] // NOTE: Swaps space, moves right - #[case("This is a test", 0, "hTis is a test", 2)] - fn swap_graphemes_work( - #[case] input: &str, - #[case] in_location: usize, - #[case] output: &str, - #[case] out_location: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); + #[test] + fn swap_graphemes_work() { + let cases = [ + ("This is a test", 13, "This is a tets", 14), + ("This is a test", 14, "This is a tets", 14), // NOTE: Swapping works in opposite direction at last index + ("This is a test", 4, "Thi sis a test", 5), // NOTE: Swaps space, moves right + ("This is a test", 0, "hTis is a test", 2), + ]; + + for (input, in_location, output, out_location) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); - line_buffer.swap_graphemes(); + line_buffer.swap_graphemes(); - let mut expected = buffer_with(output); - expected.set_insertion_point(out_location); + let mut expected = buffer_with(output); + expected.set_insertion_point(out_location); - assert_eq!(line_buffer, expected); - line_buffer.assert_valid(); + assert_eq!(line_buffer, expected); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("This is a test", 8, "This is test a", 8)] - #[case("This is a test", 0, "is This a test", 0)] - #[case("This is a test", 14, "This is a test", 14)] - fn swap_words_works( - #[case] input: &str, - #[case] in_location: usize, - #[case] output: &str, - #[case] out_location: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); + #[test] + fn swap_words_works() { + let cases = [ + ("This is a test", 8, "This is test a", 8), + ("This is a test", 0, "is This a test", 0), + ("This is a test", 14, "This is a test", 14), + ]; - line_buffer.swap_words(); + for (input, in_location, output, out_location) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); - let mut expected = buffer_with(output); - expected.set_insertion_point(out_location); + line_buffer.swap_words(); - assert_eq!(line_buffer, expected); - line_buffer.assert_valid(); + let mut expected = buffer_with(output); + expected.set_insertion_point(out_location); + + assert_eq!(line_buffer, expected); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("line 1\nline 2", 7, 0)] - #[case("line 1\nline 2", 8, 1)] - #[case("line 1\nline 2", 0, 0)] - #[case("line\nlong line", 14, 4)] - #[case("line\nlong line", 8, 3)] - #[case("line 1\n😇line 2", 11, 1)] - #[case("line\n\nline", 8, 5)] - fn moving_up_works( - #[case] input: &str, - #[case] in_location: usize, - #[case] out_location: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); - - line_buffer.move_line_up(); - - let mut expected = buffer_with(input); - expected.set_insertion_point(out_location); - - assert_eq!(line_buffer, expected); - line_buffer.assert_valid(); + #[test] + fn moving_up_works() { + let cases = [ + ("line 1\nline 2", 7, 0), + ("line 1\nline 2", 8, 1), + ("line 1\nline 2", 0, 0), + ("line\nlong line", 14, 4), + ("line\nlong line", 8, 3), + ("line 1\n😇line 2", 11, 1), + ("line\n\nline", 8, 5), + ]; + + for (input, in_location, out_location) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); + + line_buffer.move_line_up(); + + let mut expected = buffer_with(input); + expected.set_insertion_point(out_location); + + assert_eq!(line_buffer, expected); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("line 1", 0, 0)] - #[case("line 1\nline 2", 0, 7)] - #[case("line 1\n😇line 2", 1, 11)] - #[case("line 😇 1\nline 2 long", 9, 18)] - #[case("line 1\nline 2", 7, 7)] - #[case("long line\nline", 8, 14)] - #[case("long line\nline", 4, 14)] - #[case("long line\nline", 3, 13)] - #[case("long line\nline\nline", 8, 14)] - #[case("line\n\nline", 3, 5)] - fn moving_down_works( - #[case] input: &str, - #[case] in_location: usize, - #[case] out_location: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); - - line_buffer.move_line_down(); - - let mut expected = buffer_with(input); - expected.set_insertion_point(out_location); - - assert_eq!(line_buffer, expected); - line_buffer.assert_valid(); + #[test] + fn moving_down_works() { + let cases = [ + ("line 1", 0, 0), + ("line 1\nline 2", 0, 7), + ("line 1\n😇line 2", 1, 11), + ("line 😇 1\nline 2 long", 9, 18), + ("line 1\nline 2", 7, 7), + ("long line\nline", 8, 14), + ("long line\nline", 4, 14), + ("long line\nline", 3, 13), + ("long line\nline\nline", 8, 14), + ("line\n\nline", 3, 5), + ]; + + for (input, in_location, out_location) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); + + line_buffer.move_line_down(); + + let mut expected = buffer_with(input); + expected.set_insertion_point(out_location); + + assert_eq!(line_buffer, expected); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("line", 4, true)] - #[case("line 1\nline 2\nline 3", 0, true)] - #[case("line 1\nline 2\nline 3", 6, true)] - #[case("line 1\nline 2\nline 3", 8, false)] - fn test_first_line_detection( - #[case] input: &str, - #[case] in_location: usize, - #[case] expected: bool, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); - line_buffer.assert_valid(); + #[test] + fn test_first_line_detection() { + let cases = [ + ("line", 4, true), + ("line 1\nline 2\nline 3", 0, true), + ("line 1\nline 2\nline 3", 6, true), + ("line 1\nline 2\nline 3", 8, false), + ]; + + for (input, in_location, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); + line_buffer.assert_valid(); + + assert_eq!(line_buffer.is_cursor_at_first_line(), expected); + } + } - assert_eq!(line_buffer.is_cursor_at_first_line(), expected); - } - - #[rstest] - #[case("line", 4, true)] - #[case("line\nline", 9, true)] - #[case("line 1\nline 2\nline 3", 8, false)] - #[case("line 1\nline 2\nline 3", 13, false)] - #[case("line 1\nline 2\nline 3", 14, true)] - #[case("line 1\nline 2\nline 3", 20, true)] - #[case("line 1\nline 2\nline 3\n", 20, false)] - #[case("line 1\nline 2\nline 3\n", 21, true)] - fn test_last_line_detection( - #[case] input: &str, - #[case] in_location: usize, - #[case] expected: bool, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); - line_buffer.assert_valid(); + #[test] + fn test_last_line_detection() { + let cases = [ + ("line", 4, true), + ("line\nline", 9, true), + ("line 1\nline 2\nline 3", 8, false), + ("line 1\nline 2\nline 3", 13, false), + ("line 1\nline 2\nline 3", 14, true), + ("line 1\nline 2\nline 3", 20, true), + ("line 1\nline 2\nline 3\n", 20, false), + ("line 1\nline 2\nline 3\n", 21, true), + ]; + + for (input, in_location, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); + line_buffer.assert_valid(); + + assert_eq!(line_buffer.is_cursor_at_last_line(), expected); + } + } - assert_eq!(line_buffer.is_cursor_at_last_line(), expected); - } - - #[rstest] - #[case("abc def ghi", 0, 'c', true, 2)] - #[case("abc def ghi", 0, 'a', true, 0)] - #[case("abc def ghi", 0, 'z', true, 0)] - #[case("a😇c", 0, 'c', true, 5)] - #[case("😇bc", 0, 'c', true, 5)] - #[case("abc\ndef", 0, 'f', true, 0)] - #[case("abc\ndef", 3, 'f', true, 3)] - #[case("abc\ndef", 0, 'f', false, 6)] - #[case("abc\ndef", 3, 'f', false, 6)] - fn test_move_right_until( - #[case] input: &str, - #[case] position: usize, - #[case] c: char, - #[case] current_line: bool, - #[case] expected: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(position); - - line_buffer.move_right_until(c, current_line); - - assert_eq!(line_buffer.insertion_point(), expected); - line_buffer.assert_valid(); + #[test] + fn test_move_right_until() { + let cases = [ + ("abc def ghi", 0, 'c', true, 2), + ("abc def ghi", 0, 'a', true, 0), + ("abc def ghi", 0, 'z', true, 0), + ("a😇c", 0, 'c', true, 5), + ("😇bc", 0, 'c', true, 5), + ("abc\ndef", 0, 'f', true, 0), + ("abc\ndef", 3, 'f', true, 3), + ("abc\ndef", 0, 'f', false, 6), + ("abc\ndef", 3, 'f', false, 6), + ]; + + for (input, position, c, current_line, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + + line_buffer.move_right_until(c, current_line); + + assert_eq!(line_buffer.insertion_point(), expected); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("abc def ghi", 0, 'd', true, 3)] - #[case("abc def ghi", 3, 'd', true, 3)] - #[case("a😇c", 0, 'c', true, 1)] - #[case("😇bc", 0, 'c', true, 4)] - fn test_move_right_before( - #[case] input: &str, - #[case] position: usize, - #[case] c: char, - #[case] current_line: bool, - #[case] expected: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(position); - - line_buffer.move_right_before(c, current_line); - - assert_eq!(line_buffer.insertion_point(), expected); - line_buffer.assert_valid(); + #[test] + fn test_move_right_before() { + let cases = [ + ("abc def ghi", 0, 'd', true, 3), + ("abc def ghi", 3, 'd', true, 3), + ("a😇c", 0, 'c', true, 1), + ("😇bc", 0, 'c', true, 4), + ]; + + for (input, position, c, current_line, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + + line_buffer.move_right_before(c, current_line); + + assert_eq!(line_buffer.insertion_point(), expected); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("abc def ghi", 0, 'd', true, "ef ghi")] - #[case("abc def ghi", 0, 'i', true, "")] - #[case("abc def ghi", 0, 'z', true, "abc def ghi")] - #[case("abc def ghi", 0, 'a', true, "abc def ghi")] - fn test_delete_until( - #[case] input: &str, - #[case] position: usize, - #[case] c: char, - #[case] current_line: bool, - #[case] expected: &str, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(position); - - line_buffer.delete_right_until_char(c, current_line); - - assert_eq!(line_buffer.lines, expected); - line_buffer.assert_valid(); + #[test] + fn test_delete_until() { + let cases = [ + ("abc def ghi", 0, 'd', true, "ef ghi"), + ("abc def ghi", 0, 'i', true, ""), + ("abc def ghi", 0, 'z', true, "abc def ghi"), + ("abc def ghi", 0, 'a', true, "abc def ghi"), + ]; + + for (input, position, c, current_line, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + + line_buffer.delete_right_until_char(c, current_line); + + assert_eq!(line_buffer.lines, expected); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("abc def ghi", 0, 'b', true, "bc def ghi")] - #[case("abc def ghi", 0, 'i', true, "i")] - #[case("abc def ghi", 0, 'z', true, "abc def ghi")] - fn test_delete_before( - #[case] input: &str, - #[case] position: usize, - #[case] c: char, - #[case] current_line: bool, - #[case] expected: &str, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(position); + #[test] + fn test_delete_before() { + let cases = [ + ("abc def ghi", 0, 'b', true, "bc def ghi"), + ("abc def ghi", 0, 'i', true, "i"), + ("abc def ghi", 0, 'z', true, "abc def ghi"), + ]; - line_buffer.delete_right_before_char(c, current_line); + for (input, position, c, current_line, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); - assert_eq!(line_buffer.lines, expected); - line_buffer.assert_valid(); + line_buffer.delete_right_before_char(c, current_line); + + assert_eq!(line_buffer.lines, expected); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("abc def ghi", 4, 'c', true, 2)] - #[case("abc def ghi", 0, 'a', true, 0)] - #[case("abc def ghi", 6, 'a', true, 0)] - fn test_move_left_until( - #[case] input: &str, - #[case] position: usize, - #[case] c: char, - #[case] current_line: bool, - #[case] expected: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(position); + #[test] + fn test_move_left_until() { + let cases = [ + ("abc def ghi", 4, 'c', true, 2), + ("abc def ghi", 0, 'a', true, 0), + ("abc def ghi", 6, 'a', true, 0), + ]; - line_buffer.move_left_until(c, current_line); + for (input, position, c, current_line, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); - assert_eq!(line_buffer.insertion_point(), expected); - line_buffer.assert_valid(); + line_buffer.move_left_until(c, current_line); + + assert_eq!(line_buffer.insertion_point(), expected); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("abc def ghi", 4, 'c', true, 3)] - #[case("abc def ghi", 0, 'a', true, 0)] - #[case("abc def ghi", 6, 'a', true, 1)] - fn test_move_left_before( - #[case] input: &str, - #[case] position: usize, - #[case] c: char, - #[case] current_line: bool, - #[case] expected: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(position); + #[test] + fn test_move_left_before() { + let cases = [ + ("abc def ghi", 4, 'c', true, 3), + ("abc def ghi", 0, 'a', true, 0), + ("abc def ghi", 6, 'a', true, 1), + ]; + + for (input, position, c, current_line, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); - line_buffer.move_left_before(c, current_line); + line_buffer.move_left_before(c, current_line); - assert_eq!(line_buffer.insertion_point(), expected); - line_buffer.assert_valid(); + assert_eq!(line_buffer.insertion_point(), expected); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("abc def ghi", 5, 'b', true, "aef ghi")] - #[case("abc def ghi", 5, 'e', true, "abc def ghi")] - #[case("abc def ghi", 10, 'a', true, "i")] - #[case("z\nabc def ghi", 10, 'z', true, "z\nabc def ghi")] - #[case("z\nabc def ghi", 12, 'z', false, "i")] - fn test_delete_until_left( - #[case] input: &str, - #[case] position: usize, - #[case] c: char, - #[case] current_line: bool, - #[case] expected: &str, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(position); - - line_buffer.delete_left_until_char(c, current_line); - - assert_eq!(line_buffer.lines, expected); - line_buffer.assert_valid(); + #[test] + fn test_delete_until_left() { + let cases = [ + ("abc def ghi", 5, 'b', true, "aef ghi"), + ("abc def ghi", 5, 'e', true, "abc def ghi"), + ("abc def ghi", 10, 'a', true, "i"), + ("z\nabc def ghi", 10, 'z', true, "z\nabc def ghi"), + ("z\nabc def ghi", 12, 'z', false, "i"), + ]; + + for (input, position, c, current_line, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + + line_buffer.delete_left_until_char(c, current_line); + + assert_eq!(line_buffer.lines, expected); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("abc def ghi", 5, 'b', true, "abef ghi")] - #[case("abc def ghi", 5, 'e', true, "abc def ghi")] - #[case("abc def ghi", 10, 'a', true, "ai")] - fn test_delete_before_left( - #[case] input: &str, - #[case] position: usize, - #[case] c: char, - #[case] current_line: bool, - #[case] expected: &str, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(position); + #[test] + fn test_delete_before_left() { + let cases = [ + ("abc def ghi", 5, 'b', true, "abef ghi"), + ("abc def ghi", 5, 'e', true, "abc def ghi"), + ("abc def ghi", 10, 'a', true, "ai"), + ]; - line_buffer.delete_left_before_char(c, current_line); + for (input, position, c, current_line, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); - assert_eq!(line_buffer.lines, expected); - line_buffer.assert_valid(); + line_buffer.delete_left_before_char(c, current_line); + + assert_eq!(line_buffer.lines, expected); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("line", 0, 4)] - #[case("line\nline", 1, 4)] - #[case("line\nline", 7, 9)] - // TODO: Check if this behavior is desired for full vi consistency - #[case("line\n", 4, 4)] - #[case("line\n", 5, 5)] - // Platform agnostic - #[case("\n", 0, 0)] - #[case("\r\n", 0, 0)] - #[case("line\r\nword", 1, 4)] - #[case("line\r\nword", 7, 10)] - fn test_find_current_line_end( - #[case] input: &str, - #[case] in_location: usize, - #[case] expected: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); - line_buffer.assert_valid(); + #[test] + fn test_find_current_line_end() { + let cases = [ + ("line", 0, 4), + ("line\nline", 1, 4), + ("line\nline", 7, 9), + // TODO: Check if this behavior is desired for full vi consistency + ("line\n", 4, 4), + ("line\n", 5, 5), + // Platform agnostic + ("\n", 0, 0), + ("\r\n", 0, 0), + ("line\r\nword", 1, 4), + ("line\r\nword", 7, 10), + ]; + + for (input, in_location, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); + line_buffer.assert_valid(); + + assert_eq!(line_buffer.find_current_line_end(), expected); + } + } - assert_eq!(line_buffer.find_current_line_end(), expected); - } - - #[rstest] - #[case("", 0, 0)] - #[case("\n", 0, 0)] - #[case("\n", 1, 1)] - #[case("a\nb", 0, 0)] - #[case("a\nb", 1, 0)] - #[case("a\nb", 2, 1)] - #[case("a\nbc", 3, 1)] - #[case("a\r\nb", 3, 1)] - #[case("a\r\nbc", 4, 1)] - fn test_current_line_num( - #[case] input: &str, - #[case] in_location: usize, - #[case] expected: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); - line_buffer.assert_valid(); + #[test] + fn test_current_line_num() { + let cases = [ + ("", 0, 0), + ("\n", 0, 0), + ("\n", 1, 1), + ("a\nb", 0, 0), + ("a\nb", 1, 0), + ("a\nb", 2, 1), + ("a\nbc", 3, 1), + ("a\r\nb", 3, 1), + ("a\r\nbc", 4, 1), + ]; + + for (input, in_location, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); + line_buffer.assert_valid(); + + assert_eq!(line_buffer.line(), expected); + } + } - assert_eq!(line_buffer.line(), expected); + #[test] + fn test_num_lines() { + let cases = [ + ("", 0, 1), + ("line", 0, 1), + ("\n", 0, 2), + ("line\n", 0, 2), + ("a\nb", 0, 2), + ]; + + for (input, in_location, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); + line_buffer.assert_valid(); + + assert_eq!(line_buffer.num_lines(), expected); + } } - #[rstest] - #[case("", 0, 1)] - #[case("line", 0, 1)] - #[case("\n", 0, 2)] - #[case("line\n", 0, 2)] - #[case("a\nb", 0, 2)] - fn test_num_lines(#[case] input: &str, #[case] in_location: usize, #[case] expected: usize) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); - line_buffer.assert_valid(); + #[test] + fn test_move_to_line_end() { + let cases = [ + ("", 0, 0), + ("line", 0, 4), + ("\n", 0, 0), + ("line\n", 0, 4), + ("a\nb", 2, 3), + ("a\nb", 0, 1), + ("a\r\nb", 0, 1), + ]; + + for (input, in_location, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); + + line_buffer.move_to_line_end(); + + assert_eq!(line_buffer.insertion_point(), expected); + line_buffer.assert_valid(); + } + } + + #[test] + fn test_move_to_line_start() { + let cases = [ + ("", 0, 0), + ("line", 3, 0), + ("\n", 1, 1), + ("\n", 0, 0), + ("\nline", 3, 1), + ("a\nb", 2, 2), + ("a\nb", 3, 2), + ("a\r\nb", 3, 3), + ]; + + for (input, in_location, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); + + line_buffer.move_to_line_start(); + + assert_eq!(line_buffer.insertion_point(), expected); + line_buffer.assert_valid(); + } + } - assert_eq!(line_buffer.num_lines(), expected); + #[test] + fn test_current_line_range() { + let cases = [ + ("", 0, 0..0), + ("line", 0, 0..4), + ("line\n", 0, 0..5), + ("line\n", 4, 0..5), + ("line\r\n", 0, 0..6), + ("line\r\n", 4, 0..6), // Position 5 would be invalid from a grapheme perspective + ("line\nsecond", 5, 5..11), + ("line\r\nsecond", 7, 6..12), + ]; + + for (input, in_location, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); + line_buffer.assert_valid(); + + assert_eq!(line_buffer.current_line_range(), expected); + } } - #[rstest] - #[case("", 0, 0)] - #[case("line", 0, 4)] - #[case("\n", 0, 0)] - #[case("line\n", 0, 4)] - #[case("a\nb", 2, 3)] - #[case("a\nb", 0, 1)] - #[case("a\r\nb", 0, 1)] - fn test_move_to_line_end( - #[case] input: &str, - #[case] in_location: usize, - #[case] expected: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); + #[test] + fn test_clear_to_line_end() { + let cases = [ + ("This is a test", 7, "This is", 7), + ("This is a test\nunrelated", 7, "This is\nunrelated", 7), + ("This is a test\r\nunrelated", 7, "This is\r\nunrelated", 7), + ]; - line_buffer.move_to_line_end(); + for (input, in_location, output, out_location) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(in_location); - assert_eq!(line_buffer.insertion_point(), expected); - line_buffer.assert_valid(); + line_buffer.clear_to_line_end(); + + let mut expected = buffer_with(output); + expected.set_insertion_point(out_location); + + assert_eq!(expected, line_buffer); + line_buffer.assert_valid(); + } } - #[rstest] - #[case("", 0, 0)] - #[case("line", 3, 0)] - #[case("\n", 1, 1)] - #[case("\n", 0, 0)] - #[case("\nline", 3, 1)] - #[case("a\nb", 2, 2)] - #[case("a\nb", 3, 2)] - #[case("a\r\nb", 3, 3)] - fn test_move_to_line_start( - #[case] input: &str, - #[case] in_location: usize, - #[case] expected: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); - - line_buffer.move_to_line_start(); - - assert_eq!(line_buffer.insertion_point(), expected); - line_buffer.assert_valid(); + #[test] + fn test_word_left_index() { + let cases = [ + ("abc def ghi", 10, 8), + ("abc def-ghi", 10, 8), + ("abc def.ghi", 10, 4), + ]; + + for (input, position, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + + let index = line_buffer.word_left_index(); + + assert_eq!(index, expected); + } } - #[rstest] - #[case("", 0, 0..0)] - #[case("line", 0, 0..4)] - #[case("line\n", 0, 0..5)] - #[case("line\n", 4, 0..5)] - #[case("line\r\n", 0, 0..6)] - #[case("line\r\n", 4, 0..6)] // Position 5 would be invalid from a grapheme perspective - #[case("line\nsecond", 5, 5..11)] - #[case("line\r\nsecond", 7, 6..12)] - fn test_current_line_range( - #[case] input: &str, - #[case] in_location: usize, - #[case] expected: Range, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); - line_buffer.assert_valid(); + #[test] + fn test_big_word_left_index() { + let cases = [ + ("abc def ghi", 10, 8), + ("abc def-ghi", 10, 4), + ("abc def.ghi", 10, 4), + ("abc def i", 10, 4), + ]; + + for (input, position, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + + let index = line_buffer.big_word_left_index(); - assert_eq!(line_buffer.current_line_range(), expected); + assert_eq!(index, expected,); + } } - #[rstest] - #[case("This is a test", 7, "This is", 7)] - #[case("This is a test\nunrelated", 7, "This is\nunrelated", 7)] - #[case("This is a test\r\nunrelated", 7, "This is\r\nunrelated", 7)] - fn test_clear_to_line_end( - #[case] input: &str, - #[case] in_location: usize, - #[case] output: &str, - #[case] out_location: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(in_location); + #[test] + fn test_word_right_start_index() { + let cases = [ + ("abc def ghi", 0, 4), + ("abc-def ghi", 0, 3), + ("abc.def ghi", 0, 8), + ]; - line_buffer.clear_to_line_end(); + for (input, position, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); - let mut expected = buffer_with(output); - expected.set_insertion_point(out_location); + let index = line_buffer.word_right_start_index(); - assert_eq!(expected, line_buffer); - line_buffer.assert_valid(); + assert_eq!(index, expected); + } } - #[rstest] - #[case("abc def ghi", 10, 8)] - #[case("abc def-ghi", 10, 8)] - #[case("abc def.ghi", 10, 4)] - fn test_word_left_index(#[case] input: &str, #[case] position: usize, #[case] expected: usize) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(position); - - let index = line_buffer.word_left_index(); - - assert_eq!(index, expected); - } - - #[rstest] - #[case("abc def ghi", 10, 8)] - #[case("abc def-ghi", 10, 4)] - #[case("abc def.ghi", 10, 4)] - #[case("abc def i", 10, 4)] - fn test_big_word_left_index( - #[case] input: &str, - #[case] position: usize, - #[case] expected: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(position); - - let index = line_buffer.big_word_left_index(); - - assert_eq!(index, expected,); - } - - #[rstest] - #[case("abc def ghi", 0, 4)] - #[case("abc-def ghi", 0, 3)] - #[case("abc.def ghi", 0, 8)] - fn test_word_right_start_index( - #[case] input: &str, - #[case] position: usize, - #[case] expected: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(position); - - let index = line_buffer.word_right_start_index(); - - assert_eq!(index, expected); - } - - #[rstest] - #[case("abc def ghi", 0, 4)] - #[case("abc-def ghi", 0, 8)] - #[case("abc.def ghi", 0, 8)] - fn test_big_word_right_start_index( - #[case] input: &str, - #[case] position: usize, - #[case] expected: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(position); - - let index = line_buffer.big_word_right_start_index(); - - assert_eq!(index, expected); - } - - #[rstest] - #[case("abc def ghi", 0, 2)] - #[case("abc-def ghi", 0, 2)] - #[case("abc.def ghi", 0, 6)] - #[case("abc", 1, 2)] - #[case("abc", 2, 2)] - #[case("abc def", 2, 6)] - fn test_word_right_end_index( - #[case] input: &str, - #[case] position: usize, - #[case] expected: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(position); - - let index = line_buffer.word_right_end_index(); - - assert_eq!(index, expected); - } - - #[rstest] - #[case("abc def ghi", 0, 2)] - #[case("abc-def ghi", 0, 6)] - #[case("abc-def ghi", 5, 6)] - #[case("abc-def ghi", 6, 10)] - #[case("abc.def ghi", 0, 6)] - #[case("abc", 1, 2)] - #[case("abc", 2, 2)] - #[case("abc def", 2, 6)] - #[case("abc-def", 6, 6)] - fn test_big_word_right_end_index( - #[case] input: &str, - #[case] position: usize, - #[case] expected: usize, - ) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(position); - - let index = line_buffer.big_word_right_end_index(); - - assert_eq!(index, expected); - } - - #[rstest] - #[case("abc def", 0, 3)] - #[case("abc def ghi", 3, 7)] - #[case("abc", 1, 3)] - fn test_next_whitespace(#[case] input: &str, #[case] position: usize, #[case] expected: usize) { - let mut line_buffer = buffer_with(input); - line_buffer.set_insertion_point(position); - - let index = line_buffer.next_whitespace(); - - assert_eq!(index, expected); - } - - #[rstest] - #[case("abc", 0, 1)] // Basic ASCII - #[case("abc", 1, 2)] // From middle position - #[case("abc", 2, 3)] // From last char - #[case("abc", 3, 3)] // From end of string - #[case("🦀rust", 0, 4)] // Unicode emoji - #[case("🦀rust", 4, 5)] // After emoji - #[case("é́", 0, 4)] // Combining characters - fn test_grapheme_right_index_from_pos( - #[case] input: &str, - #[case] position: usize, - #[case] expected: usize, - ) { - let mut line = LineBuffer::new(); - line.insert_str(input); - assert_eq!( - line.grapheme_right_index_from_pos(position), - expected, - "input: {input:?}, pos: {position}" - ); + #[test] + fn test_big_word_right_start_index() { + let cases = [ + ("abc def ghi", 0, 4), + ("abc-def ghi", 0, 8), + ("abc.def ghi", 0, 8), + ]; + + for (input, position, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + + let index = line_buffer.big_word_right_start_index(); + + assert_eq!(index, expected); + } + } + + #[test] + fn test_word_right_end_index() { + let cases = [ + ("abc def ghi", 0, 2), + ("abc-def ghi", 0, 2), + ("abc.def ghi", 0, 6), + ("abc", 1, 2), + ("abc", 2, 2), + ("abc def", 2, 6), + ]; + + for (input, position, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + + let index = line_buffer.word_right_end_index(); + + assert_eq!(index, expected); + } + } + + #[test] + fn test_big_word_right_end_index() { + let cases = [ + ("abc def ghi", 0, 2), + ("abc-def ghi", 0, 6), + ("abc-def ghi", 5, 6), + ("abc-def ghi", 6, 10), + ("abc.def ghi", 0, 6), + ("abc", 1, 2), + ("abc", 2, 2), + ("abc def", 2, 6), + ("abc-def", 6, 6), + ]; + + for (input, position, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + + let index = line_buffer.big_word_right_end_index(); + + assert_eq!(index, expected); + } + } + + #[test] + fn test_next_whitespace() { + let cases = [("abc def", 0, 3), ("abc def ghi", 3, 7), ("abc", 1, 3)]; + + for (input, position, expected) in cases { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + + let index = line_buffer.next_whitespace(); + + assert_eq!(index, expected); + } + } + + #[test] + fn test_grapheme_right_index_from_pos() { + let cases = [ + ("abc", 0, 1), // Basic ASCII + ("abc", 1, 2), // From middle position + ("abc", 2, 3), // From last char + ("abc", 3, 3), // From end of string + ("🦀rust", 0, 4), // Unicode emoji + ("🦀rust", 4, 5), // After emoji + ("é́", 0, 4), // Combining characters + ]; + for (input, position, expected) in cases { + let mut line = LineBuffer::new(); + line.insert_str(input); + assert_eq!( + line.grapheme_right_index_from_pos(position), + expected, + "input: {input:?}, pos: {position}" + ); + } } const BRACKET_PAIRS: &[(char, char); 3] = &[('(', ')'), ('[', ']'), ('{', '}')]; const QUOTE_PAIRS: &[(char, char); 3] = &[('"', '"'), ('\'', '\''), ('`', '`')]; // Tests for range_inside_current_quote - cursor inside or on the boundary - #[rstest] - #[case("foo(bar)baz", 5, BRACKET_PAIRS, Some(4..7))] // cursor on 'a' in "bar" - #[case("foo[bar]baz", 5, BRACKET_PAIRS, Some(4..7))] // square brackets - #[case("foo{bar}baz", 5, BRACKET_PAIRS, Some(4..7))] // curly brackets - #[case("foo(bar(baz)qux)end", 9, BRACKET_PAIRS, Some(8..11))] // cursor on 'a' in "baz", finds inner - #[case("foo(bar(baz)qux)end", 5, BRACKET_PAIRS, Some(4..15))] // cursor on 'a' in "bar", finds outer - #[case("foo([bar])baz", 6, BRACKET_PAIRS, Some(5..8))] // mixed bracket types, cursor on 'a' - should find [bar], not (...) - #[case("foo[(bar)]baz", 6, BRACKET_PAIRS, Some(5..8))] // reversed nesting, cursor on 'a' - should find (bar), not [...] - #[case("foo(bar)baz", 4, BRACKET_PAIRS, Some(4..7))] // cursor just after opening bracket - #[case("foo(bar)baz", 7, BRACKET_PAIRS, Some(4..7))] // cursor just before closing bracket - #[case("foo[]bar", 4, BRACKET_PAIRS, Some(4..4))] // empty square brackets - #[case("(content)", 0, BRACKET_PAIRS, Some(1..8))] // brackets at buffer start/end - #[case("a(b)c", 2, BRACKET_PAIRS, Some(2..3))] // minimal case - cursor inside brackets - #[case(r#"foo("bar")baz"#, 6, BRACKET_PAIRS, Some(4..9))] // quotes inside brackets - #[case(r#"foo"(bar)"baz"#, 6, BRACKET_PAIRS, Some(5..8))] // brackets inside quotes - #[case("())", 1, BRACKET_PAIRS, Some(1..1))] // extra closing bracket - #[case("", 0, BRACKET_PAIRS, None)] // empty buffer - #[case("(", 0, BRACKET_PAIRS, None)] // single opening bracket - #[case(")", 0, BRACKET_PAIRS, None)] // single closing bracket - #[case("", 0, BRACKET_PAIRS, None)] // empty buffer - #[case(r#"foo"bar"baz"#, 5, QUOTE_PAIRS, Some(4..7))] // cursor on 'a' in "bar" - #[case("foo'bar'baz", 5, QUOTE_PAIRS, Some(4..7))] // single quotes - #[case("foo`bar`baz", 5, QUOTE_PAIRS, Some(4..7))] // backticks - #[case(r#"'foo"baz`bar`taz"baz'"#, 0, QUOTE_PAIRS, Some(1..20))] // backticks - #[case(r#""foo"'bar'`baz`"#, 0, QUOTE_PAIRS, Some(1..4))] // cursor at start, should find first (double) - #[case("no quotes here", 5, QUOTE_PAIRS, None)] // no quotes in buffer - #[case(r#"unclosed "quotes"#, 10, QUOTE_PAIRS, None)] // unmatched quotes - #[case("", 0, QUOTE_PAIRS, None)] // empty buffer - fn test_range_inside_current_pair_group( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] pairs: &[(char, char); 3], - #[case] expected: Option>, - ) { - let mut buf = LineBuffer::from(input); - buf.set_insertion_point(cursor_pos); - assert_eq!(buf.range_inside_current_pair_in_group(pairs), expected); + #[test] + fn test_range_inside_current_pair_group() { + let cases = [ + ("foo(bar)baz", 5, BRACKET_PAIRS, Some(4..7)), // cursor on 'a' in "bar" + ("foo[bar]baz", 5, BRACKET_PAIRS, Some(4..7)), // square brackets + ("foo{bar}baz", 5, BRACKET_PAIRS, Some(4..7)), // curly brackets + ("foo(bar(baz)qux)end", 9, BRACKET_PAIRS, Some(8..11)), // cursor on 'a' in "baz", finds inner + ("foo(bar(baz)qux)end", 5, BRACKET_PAIRS, Some(4..15)), // cursor on 'a' in "bar", finds outer + ("foo([bar])baz", 6, BRACKET_PAIRS, Some(5..8)), // mixed bracket types, cursor on 'a' - should find [bar], not (...) + ("foo[(bar)]baz", 6, BRACKET_PAIRS, Some(5..8)), // reversed nesting, cursor on 'a' - should find (bar), not [...] + ("foo(bar)baz", 4, BRACKET_PAIRS, Some(4..7)), // cursor just after opening bracket + ("foo(bar)baz", 7, BRACKET_PAIRS, Some(4..7)), // cursor just before closing bracket + ("foo[]bar", 4, BRACKET_PAIRS, Some(4..4)), // empty square brackets + ("(content)", 0, BRACKET_PAIRS, Some(1..8)), // brackets at buffer start/end + ("a(b)c", 2, BRACKET_PAIRS, Some(2..3)), // minimal case - cursor inside brackets + (r#"foo("bar")baz"#, 6, BRACKET_PAIRS, Some(4..9)), // quotes inside brackets + (r#"foo"(bar)"baz"#, 6, BRACKET_PAIRS, Some(5..8)), // brackets inside quotes + ("())", 1, BRACKET_PAIRS, Some(1..1)), // extra closing bracket + ("", 0, BRACKET_PAIRS, None), // empty buffer + ("(", 0, BRACKET_PAIRS, None), // single opening bracket + (")", 0, BRACKET_PAIRS, None), // single closing bracket + ("", 0, BRACKET_PAIRS, None), // empty buffer + (r#"foo"bar"baz"#, 5, QUOTE_PAIRS, Some(4..7)), // cursor on 'a' in "bar" + ("foo'bar'baz", 5, QUOTE_PAIRS, Some(4..7)), // single quotes + ("foo`bar`baz", 5, QUOTE_PAIRS, Some(4..7)), // backticks + (r#"'foo"baz`bar`taz"baz'"#, 0, QUOTE_PAIRS, Some(1..20)), // backticks + (r#""foo"'bar'`baz`"#, 0, QUOTE_PAIRS, Some(1..4)), // cursor at start, should find first (double) + ("no quotes here", 5, QUOTE_PAIRS, None), // no quotes in buffer + (r#"unclosed "quotes"#, 10, QUOTE_PAIRS, None), // unmatched quotes + ("", 0, QUOTE_PAIRS, None), // empty buffer + ]; + + for (input, cursor_pos, pairs, expected) in cases { + let mut buf = LineBuffer::from(input); + buf.set_insertion_point(cursor_pos); + assert_eq!(buf.range_inside_current_pair_in_group(pairs), expected); + } } // Tests for range_inside_next_pair_in_group - cursor before pairs, return range inside next pair if exists - #[rstest] - #[case("foo (bar)baz", 1, BRACKET_PAIRS, Some(5..8))] // cursor before brackets - #[case("foo []bar", 1, BRACKET_PAIRS, Some(5..5))] // cursor before empty brackets - #[case("(first)(second)", 4, BRACKET_PAIRS, Some(8..14))] // inside first, should find second - #[case("foo{bar[baz]qux}end", 0, BRACKET_PAIRS, Some(4..15))] // cursor at start, finds outermost - #[case("foo{bar[baz]qux}end", 1, BRACKET_PAIRS, Some(4..15))] // cursor before nested, finds innermost - #[case("foo{bar[baz]qux}end", 4, BRACKET_PAIRS, Some(8..11))] // cursor before nested, finds innermost - #[case("(){}[]", 0, BRACKET_PAIRS, Some(1..1))] // cursor at start, finds first empty pair - #[case("(){}[]", 2, BRACKET_PAIRS, Some(3..3))] // cursor between pairs, finds next - #[case("no brackets here", 5, BRACKET_PAIRS, None)] // no brackets found - #[case("", 0, BRACKET_PAIRS, None)] // empty buffer - #[case(r#"foo "'bar'" baz"#, 1, QUOTE_PAIRS, Some(5..10))] // cursor before nested quotes - #[case(r#"foo '' "bar" baz"#, 1, QUOTE_PAIRS, Some(5..5))] // cursor before first quotes - #[case(r#""foo"'bar`b'az`"#, 1, QUOTE_PAIRS, Some(6..11))] // cursor inside first quotes, find single quotes - #[case(r#""foo"'bar'`baz`"#, 6, QUOTE_PAIRS, Some(11..14))] // cursor after second quotes, find backticks - #[case(r#"zaz'foo"b`a`r"baz'zaz"#, 3, QUOTE_PAIRS, Some(4..17))] // range inside outermost nested quotes - #[case(r#""""#, 0, QUOTE_PAIRS, Some(1..1))] // single quote pair (empty) - should find it ahead - #[case(r#"""asdf"#, 0, QUOTE_PAIRS, Some(1..1))] // unmatched trailing quote - #[case(r#""foo"'bar'`baz`"#, 0, QUOTE_PAIRS, Some(1..4))] // cursor at start, should find first quotes - #[case(r#"foo'bar""#, 1, QUOTE_PAIRS, None)] // mismatched quotes - #[case("no quotes here", 5, QUOTE_PAIRS, None)] // no quotes in buffer - #[case("", 0, QUOTE_PAIRS, None)] // empty buffer - fn test_range_inside_next_pair_in_group( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] pairs: &[(char, char); 3], - #[case] expected: Option>, - ) { - let mut buf = LineBuffer::from(input); - buf.set_insertion_point(cursor_pos); - assert_eq!(buf.range_inside_next_pair_in_group(pairs), expected); + #[test] + fn test_range_inside_next_pair_in_group() { + let cases = [ + ("foo (bar)baz", 1, BRACKET_PAIRS, Some(5..8)), // cursor before brackets + ("foo []bar", 1, BRACKET_PAIRS, Some(5..5)), // cursor before empty brackets + ("(first)(second)", 4, BRACKET_PAIRS, Some(8..14)), // inside first, should find second + ("foo{bar[baz]qux}end", 0, BRACKET_PAIRS, Some(4..15)), // cursor at start, finds outermost + ("foo{bar[baz]qux}end", 1, BRACKET_PAIRS, Some(4..15)), // cursor before nested, finds innermost + ("foo{bar[baz]qux}end", 4, BRACKET_PAIRS, Some(8..11)), // cursor before nested, finds innermost + ("(){}[]", 0, BRACKET_PAIRS, Some(1..1)), // cursor at start, finds first empty pair + ("(){}[]", 2, BRACKET_PAIRS, Some(3..3)), // cursor between pairs, finds next + ("no brackets here", 5, BRACKET_PAIRS, None), // no brackets found + ("", 0, BRACKET_PAIRS, None), // empty buffer + (r#"foo "'bar'" baz"#, 1, QUOTE_PAIRS, Some(5..10)), // cursor before nested quotes + (r#"foo '' "bar" baz"#, 1, QUOTE_PAIRS, Some(5..5)), // cursor before first quotes + (r#""foo"'bar`b'az`"#, 1, QUOTE_PAIRS, Some(6..11)), // cursor inside first quotes, find single quotes + (r#""foo"'bar'`baz`"#, 6, QUOTE_PAIRS, Some(11..14)), // cursor after second quotes, find backticks + (r#"zaz'foo"b`a`r"baz'zaz"#, 3, QUOTE_PAIRS, Some(4..17)), // range inside outermost nested quotes + (r#""""#, 0, QUOTE_PAIRS, Some(1..1)), // single quote pair (empty) - should find it ahead + (r#"""asdf"#, 0, QUOTE_PAIRS, Some(1..1)), // unmatched trailing quote + (r#""foo"'bar'`baz`"#, 0, QUOTE_PAIRS, Some(1..4)), // cursor at start, should find first quotes + (r#"foo'bar""#, 1, QUOTE_PAIRS, None), // mismatched quotes + ("no quotes here", 5, QUOTE_PAIRS, None), // no quotes in buffer + ("", 0, QUOTE_PAIRS, None), // empty buffer + ]; + + for (input, cursor_pos, pairs, expected) in cases { + let mut buf = LineBuffer::from(input); + buf.set_insertion_point(cursor_pos); + assert_eq!(buf.range_inside_next_pair_in_group(pairs), expected); + } } // Tests for range_inside_current_pair - when cursor is inside a pair - #[rstest] - #[case("(abc)", 1, '(', ')', Some(1..4))] // cursor inside simple pair - #[case("foo(bar)baz", 3, '(', ')', Some(4..7))] // cursor inside pair - #[case("[abc]", 1, '[', ']', Some(1..4))] // square brackets - #[case("{abc}", 1, '{', '}', Some(1..4))] // curly brackets - #[case("foo(🦀bar)baz", 8, '(', ')', Some(4..11))] // emoji inside brackets - cursor inside (on 'b') - #[case("🦀(bar)🦀", 6, '(', ')', Some(5..8))] // emoji outside brackets - cursor inside - #[case("()", 1, '(', ')', Some(1..1))] // empty pair - #[case("foo()bar", 4, '(', ')', Some(4..4))] // empty pair - cursor inside - // Cases where cursor is not inside any pair - #[case("(abc)", 0, '(', ')', Some(1..4))] // cursor at start, not inside - #[case("foo(bar)baz", 2, '(', ')', None)] // cursor before pair - #[case("foo(bar)baz", 0, '(', ')', None)] // cursor at start of buffer - #[case("", 0, '(', ')', None)] // empty string - #[case("no brackets", 5, '(', ')', None)] // no brackets - #[case("(unclosed", 1, '(', ')', None)] // unclosed bracket - #[case("unclosed)", 1, '(', ')', None)] // unclosed bracket - #[case("end of line", 11, '(', ')', None)] // unclosed bracket - fn test_range_inside_current_pair( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] open_char: char, - #[case] close_char: char, - #[case] expected: Option>, - ) { - let mut buf = LineBuffer::from(input); - buf.set_insertion_point(cursor_pos); - let result = buf.range_inside_current_pair(open_char, close_char); - assert_eq!( - result, expected, - "Failed for input: '{}', cursor: {}, chars: '{}' '{}'", - input, cursor_pos, open_char, close_char - ); + #[test] + fn test_range_inside_current_pair() { + let cases = [ + ("(abc)", 1, '(', ')', Some(1..4)), // cursor inside simple pair + ("foo(bar)baz", 3, '(', ')', Some(4..7)), // cursor inside pair + ("[abc]", 1, '[', ']', Some(1..4)), // square brackets + ("{abc}", 1, '{', '}', Some(1..4)), // curly brackets + ("foo(🦀bar)baz", 8, '(', ')', Some(4..11)), // emoji inside brackets - cursor inside (on 'b') + ("🦀(bar)🦀", 6, '(', ')', Some(5..8)), // emoji outside brackets - cursor inside + ("()", 1, '(', ')', Some(1..1)), // empty pair + ("foo()bar", 4, '(', ')', Some(4..4)), // empty pair - cursor inside + // Cases where cursor is not inside any pair + ("(abc)", 0, '(', ')', Some(1..4)), // cursor at start, not inside + ("foo(bar)baz", 2, '(', ')', None), // cursor before pair + ("foo(bar)baz", 0, '(', ')', None), // cursor at start of buffer + ("", 0, '(', ')', None), // empty string + ("no brackets", 5, '(', ')', None), // no brackets + ("(unclosed", 1, '(', ')', None), // unclosed bracket + ("unclosed)", 1, '(', ')', None), // unclosed bracket + ("end of line", 11, '(', ')', None), // unclosed bracket + ]; + + for (input, cursor_pos, open_char, close_char, expected) in cases { + let mut buf = LineBuffer::from(input); + buf.set_insertion_point(cursor_pos); + let result = buf.range_inside_current_pair(open_char, close_char); + assert_eq!( + result, expected, + "Failed for input: '{}', cursor: {}, chars: '{}' '{}'", + input, cursor_pos, open_char, close_char + ); + } } // Tests for range_inside_next_pair - when looking for the next pair forward - #[rstest] - #[case("(abc)", 0, '(', ')', Some(1..4))] // cursor at start, find first pair - #[case("foo(bar)baz", 2, '(', ')', Some(4..7))] // cursor before pair - #[case("(first)(second)", 4, '(', ')', Some(8..14))] // inside first, should find second - #[case("()", 0, '(', ')', Some(1..1))] // empty pair - #[case("foo()bar", 2, '(', ')', Some(4..4))] // empty pair - #[case("[abc]", 0, '[', ']', Some(1..4))] // square brackets - #[case("{abc}", 0, '{', '}', Some(1..4))] // curly brackets - #[case("foo(🦀bar)baz", 0, '(', ')', Some(4..11))] // emoji inside brackets - find from start - #[case("🦀(bar)🦀", 0, '(', ')', Some(5..8))] // emoji outside brackets - find from start - #[case("", 0, '(', ')', None)] // empty string - #[case("no brackets", 5, '(', ')', None)] // no brackets - #[case("(unclosed", 1, '(', ')', None)] // unclosed bracket - #[case("(abc)", 4, '(', ')', None)] // cursor after pair, no more pairs - #[case(r#""""#, 0, '"', '"', Some(1..1))] // single quote pair (empty) - should find it ahead - #[case(r#"""asdf"#, 0, '"', '"', Some(1..1))] // unmatched quote - should find it ahead - #[case(r#""foo"'bar'`baz`"#, 0, '"', '"', Some(1..4))] // cursor at start, should find first quotes - fn test_range_inside_next_pair( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] open_char: char, - #[case] close_char: char, - #[case] expected: Option>, - ) { - let mut buf = LineBuffer::from(input); - buf.set_insertion_point(cursor_pos); - let result = buf.range_inside_next_pair(open_char, close_char); - assert_eq!( - result, expected, - "Failed for input: '{}', cursor: {}, chars: '{}' '{}'", - input, cursor_pos, open_char, close_char - ); + #[test] + fn test_range_inside_next_pair() { + let cases = [ + ("(abc)", 0, '(', ')', Some(1..4)), // cursor at start, find first pair + ("foo(bar)baz", 2, '(', ')', Some(4..7)), // cursor before pair + ("(first)(second)", 4, '(', ')', Some(8..14)), // inside first, should find second + ("()", 0, '(', ')', Some(1..1)), // empty pair + ("foo()bar", 2, '(', ')', Some(4..4)), // empty pair + ("[abc]", 0, '[', ']', Some(1..4)), // square brackets + ("{abc}", 0, '{', '}', Some(1..4)), // curly brackets + ("foo(🦀bar)baz", 0, '(', ')', Some(4..11)), // emoji inside brackets - find from start + ("🦀(bar)🦀", 0, '(', ')', Some(5..8)), // emoji outside brackets - find from start + ("", 0, '(', ')', None), // empty string + ("no brackets", 5, '(', ')', None), // no brackets + ("(unclosed", 1, '(', ')', None), // unclosed bracket + ("(abc)", 4, '(', ')', None), // cursor after pair, no more pairs + (r#""""#, 0, '"', '"', Some(1..1)), // single quote pair (empty) - should find it ahead + (r#"""asdf"#, 0, '"', '"', Some(1..1)), // unmatched quote - should find it ahead + (r#""foo"'bar'`baz`"#, 0, '"', '"', Some(1..4)), // cursor at start, should find first quotes + ]; + + for (input, cursor_pos, open_char, close_char, expected) in cases { + let mut buf = LineBuffer::from(input); + buf.set_insertion_point(cursor_pos); + let result = buf.range_inside_next_pair(open_char, close_char); + assert_eq!( + result, expected, + "Failed for input: '{}', cursor: {}, chars: '{}' '{}'", + input, cursor_pos, open_char, close_char + ); + } } - #[rstest] + #[test] // Test next quote is restricted to single line - #[case("line1\n\"quote\"", 7, '"', '"', None)] // Inside second line quote, no quotes after - #[case("\"quote\"\nline2", 2, '"', '"', None)] // No next quote on current line - #[case("line1\n\"quote\"", 6, '"', '"', Some(7..12))] // cursor at start of line 2 - #[case("line1\n\"quote\"", 0, '"', '"', None)] // cursor line 1 doesn't find quote on line 2 - #[case("line1\n\"quote\"", 5, '"', '"', None)] // cursor at end of line 1 - fn test_multiline_next_quote_multiline( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] open_char: char, - #[case] close_char: char, - #[case] expected: Option>, - ) { - let mut buf = LineBuffer::from(input); - buf.set_insertion_point(cursor_pos); - let result = buf.range_inside_next_pair(open_char, close_char); - assert_eq!( - result, - expected, - "MULTILINE TEST - Input: {:?}, cursor: {}, chars: '{}' '{}', lines: {:?}", - input, - cursor_pos, - open_char, - close_char, - input.lines().collect::>() - ); + fn test_multiline_next_quote_multiline() { + let cases = [ + ("line1\n\"quote\"", 7, '"', '"', None), // Inside second line quote, no quotes after + ("\"quote\"\nline2", 2, '"', '"', None), // No next quote on current line + ("line1\n\"quote\"", 6, '"', '"', Some(7..12)), // cursor at start of line 2 + ("line1\n\"quote\"", 0, '"', '"', None), // cursor line 1 doesn't find quote on line 2 + ("line1\n\"quote\"", 5, '"', '"', None), // cursor at end of line 1 + ]; + + for (input, cursor_pos, open_char, close_char, expected) in cases { + let mut buf = LineBuffer::from(input); + buf.set_insertion_point(cursor_pos); + let result = buf.range_inside_next_pair(open_char, close_char); + assert_eq!( + result, + expected, + "MULTILINE TEST - Input: {:?}, cursor: {}, chars: '{}' '{}', lines: {:?}", + input, + cursor_pos, + open_char, + close_char, + input.lines().collect::>() + ); + } } // Test that range_inside_current_pair work across multiple lines - #[rstest] - #[case("line1\n(bracket)", 7, '(', ')', Some(7..14))] // cursor at bracket start on line 2 - #[case("(bracket)\nline2", 2, '(', ')', Some(1..8))] // cursor inside bracket on line 1 - #[case("line1\n(bracket)", 5, '(', ')', None)] // cursor end of line 1 - #[case("(1\ninner\n3)", 4, '(', ')', Some(1..10))] // bracket spanning 3 lines - #[case("(1\ninner\n3)", 2, '(', ')', Some(1..10))] // bracket spanning 3 lines, cursor end of line 1 - #[case("outer(\ninner(\ndeep\n)\nback\n)", 15, '(', ')', Some(13..19))] // nested multiline brackets - #[case("outer(\ninner(\ndeep\n)\nback\n)", 8, '(', ')', Some(6..26))] // nested multiline brackets - #[case("{\nkey: [\n value\n]\n}", 10, '[', ']', Some(8..17))] // mixed bracket types across lines - fn test_multiline_bracket_behavior( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] open_char: char, - #[case] close_char: char, - #[case] expected: Option>, - ) { - let mut buf = LineBuffer::from(input); - buf.set_insertion_point(cursor_pos); - let result = buf.range_inside_current_pair(open_char, close_char); - assert_eq!( - result, - expected, - "MULTILINE BRACKET TEST - Input: {:?}, cursor: {}, chars: '{}' '{}', lines: {:?}", - input, - cursor_pos, - open_char, - close_char, - input.lines().collect::>() - ); + #[test] + fn test_multiline_bracket_behavior() { + let cases = [ + ("line1\n(bracket)", 7, '(', ')', Some(7..14)), // cursor at bracket start on line 2 + ("(bracket)\nline2", 2, '(', ')', Some(1..8)), // cursor inside bracket on line 1 + ("line1\n(bracket)", 5, '(', ')', None), // cursor end of line 1 + ("(1\ninner\n3)", 4, '(', ')', Some(1..10)), // bracket spanning 3 lines + ("(1\ninner\n3)", 2, '(', ')', Some(1..10)), // bracket spanning 3 lines, cursor end of line 1 + ( + "outer(\ninner(\ndeep\n)\nback\n)", + 15, + '(', + ')', + Some(13..19), + ), // nested multiline brackets + ("outer(\ninner(\ndeep\n)\nback\n)", 8, '(', ')', Some(6..26)), // nested multiline brackets + ("{\nkey: [\n value\n]\n}", 10, '[', ']', Some(8..17)), // mixed bracket types across lines + ]; + + for (input, cursor_pos, open_char, close_char, expected) in cases { + let mut buf = LineBuffer::from(input); + buf.set_insertion_point(cursor_pos); + let result = buf.range_inside_current_pair(open_char, close_char); + assert_eq!( + result, + expected, + "MULTILINE BRACKET TEST - Input: {:?}, cursor: {}, chars: '{}' '{}', lines: {:?}", + input, + cursor_pos, + open_char, + close_char, + input.lines().collect::>() + ); + } } // Test next brackets work across multiple lines (unlike quotes which are line-restricted) - #[rstest] - #[case("line1\n(bracket)", 2, '(', ')', Some(7..14))] // cursor at bracket start on line 2 - #[case("line1\n(bracket)", 5, '(', ')', Some(7..14))] // cursor end of line 1 - #[case("outer(\ninner(\ndeep\n)\nback\n)", 0, '(', ')', Some(6..26))] // nested multiline brackets - #[case("outer(\ninner(\ndeep\n)\nback\n)", 8, '(', ')', Some(13..19))] // nested multiline brackets - fn test_multiline_next_bracket_behavior( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] open_char: char, - #[case] close_char: char, - #[case] expected: Option>, - ) { - let mut buf = LineBuffer::from(input); - buf.set_insertion_point(cursor_pos); - let result = buf.range_inside_next_pair(open_char, close_char); - assert_eq!( - result, - expected, - "MULTILINE BRACKET TEST - Input: {:?}, cursor: {}, chars: '{}' '{}', lines: {:?}", - input, - cursor_pos, - open_char, - close_char, - input.lines().collect::>() - ); + #[test] + fn test_multiline_next_bracket_behavior() { + let cases = [ + ("line1\n(bracket)", 2, '(', ')', Some(7..14)), // cursor at bracket start on line 2 + ("line1\n(bracket)", 5, '(', ')', Some(7..14)), // cursor end of line 1 + ("outer(\ninner(\ndeep\n)\nback\n)", 0, '(', ')', Some(6..26)), // nested multiline brackets + ( + "outer(\ninner(\ndeep\n)\nback\n)", + 8, + '(', + ')', + Some(13..19), + ), // nested multiline brackets + ]; + + for (input, cursor_pos, open_char, close_char, expected) in cases { + let mut buf = LineBuffer::from(input); + buf.set_insertion_point(cursor_pos); + let result = buf.range_inside_next_pair(open_char, close_char); + assert_eq!( + result, + expected, + "MULTILINE BRACKET TEST - Input: {:?}, cursor: {}, chars: '{}' '{}', lines: {:?}", + input, + cursor_pos, + open_char, + close_char, + input.lines().collect::>() + ); + } } // Unicode safety tests for core pair-finding functionality - #[rstest] - #[case("(🦀)", 1, '(', ')', Some(1..5))] // emoji inside brackets - #[case("🦀(text)🦀", 5, '(', ')', Some(5..9))] // emojis outside brackets - #[case("(multi👨‍👩‍👧‍👦family)", 1, '(', ')', Some(1..37))] // complex emoji family inside (25 bytes) - #[case("(åëïöü)", 1, '(', ')', Some(1..11))] // accented characters - #[case("(mixed🦀åëïtext)", 1, '(', ')', Some(1..20))] // mixed unicode content - #[case("'🦀emoji🦀'", 1, '\'', '\'', Some(1..14))] // emojis in quotes - #[case("'mixed👨‍👩‍👧‍👦åëï'", 1, '\'', '\'', Some(1..37))] // complex 25 byte family emoji - fn test_range_inside_current_pair_unicode_safety( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] open_char: char, - #[case] close_char: char, - #[case] expected: Option>, - ) { - let mut buf = LineBuffer::from(input); - buf.set_insertion_point(cursor_pos); - let result = buf.range_inside_current_pair(open_char, close_char); - assert_eq!(result, expected); - // Verify buffer remains valid after operations - assert!(buf.is_valid()); - } - - #[rstest] - #[case("start🦀(content)end", 0, '(', ')', Some(10..17))] // emoji before brackets - #[case("start(🦀)end", 0, '(', ')', Some(6..10))] // emoji inside brackets to find - #[case("🦀'text'🦀", 0, '\'', '\'', Some(5..9))] // emoji before quotes - #[case("start'🦀text🦀'", 0, '\'', '\'', Some(6..18))] // emoji before quotes - #[case("start'multi👨‍👩‍👧‍👦family'end", 0, '\'', '\'', Some(6..42))] // complex 25 byte family emoji - #[case("start'👨‍👩‍👧‍👦multifamily'end", 0, '\'', '\'', Some(6..42))] // complex 25 byte family emoji - fn test_range_inside_next_pair_unicode_safety( - #[case] input: &str, - #[case] cursor_pos: usize, - #[case] open_char: char, - #[case] close_char: char, - #[case] expected: Option>, - ) { - let mut buf = LineBuffer::from(input); - buf.set_insertion_point(cursor_pos); - let result = buf.range_inside_next_pair(open_char, close_char); - assert_eq!(result, expected); - // Verify buffer remains valid after operations - assert!(buf.is_valid()); + #[test] + fn test_range_inside_current_pair_unicode_safety() { + let cases = [ + ("(🦀)", 1, '(', ')', Some(1..5)), // emoji inside brackets + ("🦀(text)🦀", 5, '(', ')', Some(5..9)), // emojis outside brackets + ("(multi👨‍👩‍👧‍👦family)", 1, '(', ')', Some(1..37)), // complex emoji family inside (25 bytes) + ("(åëïöü)", 1, '(', ')', Some(1..11)), // accented characters + ("(mixed🦀åëïtext)", 1, '(', ')', Some(1..20)), // mixed unicode content + ("'🦀emoji🦀'", 1, '\'', '\'', Some(1..14)), // emojis in quotes + ("'mixed👨‍👩‍👧‍👦åëï'", 1, '\'', '\'', Some(1..37)), // complex 25 byte family emoji + ]; + + for (input, cursor_pos, open_char, close_char, expected) in cases { + let mut buf = LineBuffer::from(input); + buf.set_insertion_point(cursor_pos); + let result = buf.range_inside_current_pair(open_char, close_char); + assert_eq!(result, expected); + // Verify buffer remains valid after operations + assert!(buf.is_valid()); + } + } + + #[test] + fn test_range_inside_next_pair_unicode_safety() { + let cases = [ + ("start🦀(content)end", 0, '(', ')', Some(10..17)), // emoji before brackets + ("start(🦀)end", 0, '(', ')', Some(6..10)), // emoji inside brackets to find + ("🦀'text'🦀", 0, '\'', '\'', Some(5..9)), // emoji before quotes + ("start'🦀text🦀'", 0, '\'', '\'', Some(6..18)), // emoji before quotes + ("start'multi👨‍👩‍👧‍👦family'end", 0, '\'', '\'', Some(6..42)), // complex 25 byte family emoji + ("start'👨‍👩‍👧‍👦multifamily'end", 0, '\'', '\'', Some(6..42)), // complex 25 byte family emoji + ]; + + for (input, cursor_pos, open_char, close_char, expected) in cases { + let mut buf = LineBuffer::from(input); + buf.set_insertion_point(cursor_pos); + let result = buf.range_inside_next_pair(open_char, close_char); + assert_eq!(result, expected); + // Verify buffer remains valid after operations + assert!(buf.is_valid()); + } } } diff --git a/src/edit_mode/helix/mod.rs b/src/edit_mode/helix/mod.rs index 2ed4091da..e194c4711 100644 --- a/src/edit_mode/helix/mod.rs +++ b/src/edit_mode/helix/mod.rs @@ -77,7 +77,6 @@ mod tests { use super::*; use crate::enums::EditCommand; use crossterm::event::{Event, KeyEvent, KeyEventKind, KeyEventState}; - use rstest::rstest; fn key_press(code: KeyCode, modifiers: KeyModifiers) -> ReedlineRawEvent { Event::Key(KeyEvent { @@ -175,27 +174,31 @@ mod tests { ); } - #[rstest] - #[case(KeyCode::Char('h'))] - #[case(KeyCode::Left)] - fn pressing_left_key_or_h_in_normal_mode_moves_cursor_left(#[case] key_code: KeyCode) { - let mut helix_mode = Helix::new(PromptViMode::Normal); + #[test] + fn pressing_left_key_or_h_in_normal_mode_moves_cursor_left() { + let cases = [(KeyCode::Char('h')), (KeyCode::Left)]; - assert_eq!( - helix_mode.parse_event(key_press(key_code, KeyModifiers::NONE)), - ReedlineEvent::Edit(vec![EditCommand::MoveLeft { select: false }]) - ); + for key_code in cases { + let mut helix_mode = Helix::new(PromptViMode::Normal); + + assert_eq!( + helix_mode.parse_event(key_press(key_code, KeyModifiers::NONE)), + ReedlineEvent::Edit(vec![EditCommand::MoveLeft { select: false }]) + ); + } } - #[rstest] - #[case(KeyCode::Char('l'))] - #[case(KeyCode::Right)] - fn pressing_right_key_or_l_in_normal_mode_moves_cursor_right(#[case] key_code: KeyCode) { - let mut helix_mode = Helix::new(PromptViMode::Normal); + #[test] + fn pressing_right_key_or_l_in_normal_mode_moves_cursor_right() { + let cases = [(KeyCode::Char('l')), (KeyCode::Right)]; - assert_eq!( - helix_mode.parse_event(key_press(key_code, KeyModifiers::NONE)), - ReedlineEvent::Edit(vec![EditCommand::MoveRight { select: false }]) - ); + for key_code in cases { + let mut helix_mode = Helix::new(PromptViMode::Normal); + + assert_eq!( + helix_mode.parse_event(key_press(key_code, KeyModifiers::NONE)), + ReedlineEvent::Edit(vec![EditCommand::MoveRight { select: false }]) + ); + } } } diff --git a/src/edit_mode/vi/parser.rs b/src/edit_mode/vi/parser.rs index da0839cd0..4100d8e34 100644 --- a/src/edit_mode/vi/parser.rs +++ b/src/edit_mode/vi/parser.rs @@ -204,7 +204,6 @@ where mod tests { use super::*; use pretty_assertions::assert_eq; - use rstest::rstest; fn vi_parse(input: &[char]) -> ParsedViSequence { parse(ViMode::Normal, &mut input.iter().peekable()) @@ -478,191 +477,588 @@ mod tests { assert_eq!(output.is_complete(ViMode::Visual), true); } - #[rstest] - #[case(&['2', 'k'], ReedlineEvent::Multiple(vec![ReedlineEvent::UntilFound(vec![ - ReedlineEvent::MenuUp, - ReedlineEvent::Up, - ]), ReedlineEvent::UntilFound(vec![ - ReedlineEvent::MenuUp, - ReedlineEvent::Up, - ])]))] - #[case(&['k'], ReedlineEvent::Multiple(vec![ReedlineEvent::UntilFound(vec![ - ReedlineEvent::MenuUp, - ReedlineEvent::Up, - ])]))] - #[case(&['w'], - ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveWordRightStart{select:false}])]))] - #[case(&['W'], - ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveBigWordRightStart{select:false}])]))] - #[case(&['2', 'l'], ReedlineEvent::Multiple(vec![ - ReedlineEvent::UntilFound(vec![ - ReedlineEvent::HistoryHintComplete, - ReedlineEvent::MenuRight, - ReedlineEvent::Edit(vec![EditCommand::MoveRight{select:false}]), - ]),ReedlineEvent::UntilFound(vec![ - ReedlineEvent::HistoryHintComplete, - ReedlineEvent::MenuRight, - ReedlineEvent::Edit(vec![EditCommand::MoveRight{select:false}]), - ]) ]))] - #[case(&['l'], ReedlineEvent::Multiple(vec![ReedlineEvent::UntilFound(vec![ - ReedlineEvent::HistoryHintComplete, - ReedlineEvent::MenuRight, - ReedlineEvent::Edit(vec![EditCommand::MoveRight{select:false}]), - ])]))] - #[case(&['0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveToLineStart{select:false}])]))] - #[case(&['$'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveToLineEnd{select:false}])]))] - #[case(&['i'], ReedlineEvent::Multiple(vec![ReedlineEvent::Repaint]))] - #[case(&['p'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::PasteCutBufferAfter])]))] - #[case(&['2', 'p'], ReedlineEvent::Multiple(vec![ - ReedlineEvent::Edit(vec![EditCommand::PasteCutBufferAfter]), - ReedlineEvent::Edit(vec![EditCommand::PasteCutBufferAfter]) - ]))] - #[case(&['u'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::Undo])]))] - #[case(&['2', 'u'], ReedlineEvent::Multiple(vec![ - ReedlineEvent::Edit(vec![EditCommand::Undo]), - ReedlineEvent::Edit(vec![EditCommand::Undo]) - ]))] - #[case(&['d', 'd'], ReedlineEvent::Multiple(vec![ - ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine])]))] - #[case(&['d', 'w'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordRightToNext])]))] - #[case(&['d', 'W'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordRightToNext])]))] - #[case(&['d', 'e'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordRight])]))] - #[case(&['d', 'b'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordLeft])]))] - #[case(&['d', 'B'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordLeft])]))] - #[case(&['c', 'c'], ReedlineEvent::Multiple(vec![ - ReedlineEvent::Edit(vec![EditCommand::MoveToLineStart { select: false }]), ReedlineEvent::Edit(vec![EditCommand::CutToLineEnd]), ReedlineEvent::Repaint]))] - #[case(&['c', 'w'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordRight]), ReedlineEvent::Repaint]))] - #[case(&['c', 'W'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordRight]), ReedlineEvent::Repaint]))] - #[case(&['c', 'e'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordRight]), ReedlineEvent::Repaint]))] - #[case(&['c', 'b'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordLeft]), ReedlineEvent::Repaint]))] - #[case(&['c', 'B'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordLeft]), ReedlineEvent::Repaint]))] - #[case(&['d', 'h'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::Backspace])]))] - #[case(&['d', 'l'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::Delete])]))] - #[case(&['2', 'd', 'd'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine]), ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine])]))] - // #[case(&['d', 'j'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine]), ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine])]))] - // #[case(&['d', 'k'], ReedlineEvent::Multiple(vec![ReedlineEvent::Up, ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine]), ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine])]))] - #[case(&['d', 'E'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordRight])]))] - #[case(&['d', '0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart])]))] - #[case(&['d', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineNonBlankStart])]))] - #[case(&['d', '$'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutToLineEnd])]))] - #[case(&['d', 'f', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')])]))] - #[case(&['d', 't', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightBefore('a')])]))] - #[case(&['d', 'F', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')])]))] - #[case(&['d', 'T', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftBefore('a')])]))] - #[case(&['d', 'g', 'g'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromStartLinewise { leave_blank_line: false }])]))] - #[case(&['d', 'G'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutToEndLinewise { leave_blank_line: false }])]))] - #[case(&['c', 'E'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordRight]), ReedlineEvent::Repaint]))] - #[case(&['c', '0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart]), ReedlineEvent::Repaint]))] - #[case(&['c', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineNonBlankStart]), ReedlineEvent::Repaint]))] - #[case(&['c', '$'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutToLineEnd]), ReedlineEvent::Repaint]))] - #[case(&['c', 'f', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')]), ReedlineEvent::Repaint]))] - #[case(&['c', 't', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightBefore('a')]), ReedlineEvent::Repaint]))] - #[case(&['c', 'F', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')]), ReedlineEvent::Repaint]))] - #[case(&['c', 'T', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftBefore('a')]), ReedlineEvent::Repaint]))] - #[case(&['c', 'g', 'g'], ReedlineEvent::Multiple(vec![ - ReedlineEvent::Edit(vec![EditCommand::CutFromStartLinewise { leave_blank_line: true }]), - ReedlineEvent::Repaint, - ]))] - #[case(&['c', 'G'], ReedlineEvent::Multiple(vec![ - ReedlineEvent::Edit(vec![EditCommand::CutToEndLinewise { leave_blank_line: true }]), - ReedlineEvent::Repaint, - ]))] - #[case(&['y', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CopyFromLineNonBlankStart])]))] - #[case(&['y', 'g', 'g'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CopyFromStartLinewise])]))] - #[case(&['y', 'G'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CopyToEndLinewise])]))] - fn test_reedline_move(#[case] input: &[char], #[case] expected: ReedlineEvent) { - let mut vi = Vi::default(); - let res = vi_parse(input); - let output = res.to_reedline_event(&mut vi); - - assert_eq!(output, expected); + #[test] + fn test_reedline_move() { + let cases: &[(&[char], ReedlineEvent)] = &[ + ( + &['2', 'k'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::UntilFound(vec![ReedlineEvent::MenuUp, ReedlineEvent::Up]), + ReedlineEvent::UntilFound(vec![ReedlineEvent::MenuUp, ReedlineEvent::Up]), + ]), + ), + ( + &['k'], + ReedlineEvent::Multiple(vec![ReedlineEvent::UntilFound(vec![ + ReedlineEvent::MenuUp, + ReedlineEvent::Up, + ])]), + ), + ( + &['w'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::MoveWordRightStart { select: false }, + ])]), + ), + ( + &['W'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::MoveBigWordRightStart { select: false }, + ])]), + ), + ( + &['2', 'l'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::UntilFound(vec![ + ReedlineEvent::HistoryHintComplete, + ReedlineEvent::MenuRight, + ReedlineEvent::Edit(vec![EditCommand::MoveRight { select: false }]), + ]), + ReedlineEvent::UntilFound(vec![ + ReedlineEvent::HistoryHintComplete, + ReedlineEvent::MenuRight, + ReedlineEvent::Edit(vec![EditCommand::MoveRight { select: false }]), + ]), + ]), + ), + ( + &['l'], + ReedlineEvent::Multiple(vec![ReedlineEvent::UntilFound(vec![ + ReedlineEvent::HistoryHintComplete, + ReedlineEvent::MenuRight, + ReedlineEvent::Edit(vec![EditCommand::MoveRight { select: false }]), + ])]), + ), + ( + &['0'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::MoveToLineStart { select: false }, + ])]), + ), + ( + &['$'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::MoveToLineEnd { select: false }, + ])]), + ), + ( + &['i'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Repaint]), + ), + ( + &['p'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::PasteCutBufferAfter, + ])]), + ), + ( + &['2', 'p'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::PasteCutBufferAfter]), + ReedlineEvent::Edit(vec![EditCommand::PasteCutBufferAfter]), + ]), + ), + ( + &['u'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::Undo])]), + ), + ( + &['2', 'u'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::Undo]), + ReedlineEvent::Edit(vec![EditCommand::Undo]), + ]), + ), + ( + &['d', 'd'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutCurrentLine, + ])]), + ), + ( + &['d', 'w'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutWordRightToNext, + ])]), + ), + ( + &['d', 'W'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutBigWordRightToNext, + ])]), + ), + ( + &['d', 'e'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordRight])]), + ), + ( + &['d', 'b'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordLeft])]), + ), + ( + &['d', 'B'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutBigWordLeft, + ])]), + ), + ( + &['c', 'c'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::MoveToLineStart { select: false }]), + ReedlineEvent::Edit(vec![EditCommand::CutToLineEnd]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['c', 'w'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutWordRight]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['c', 'W'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutBigWordRight]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['c', 'e'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutWordRight]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['c', 'b'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutWordLeft]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['c', 'B'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutBigWordLeft]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['d', 'h'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::Backspace])]), + ), + ( + &['d', 'l'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::Delete])]), + ), + ( + &['2', 'd', 'd'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine]), + ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine]), + ]), + ), + // (&['d', 'j'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine]), ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine])])), + // (&['d', 'k'], ReedlineEvent::Multiple(vec![ReedlineEvent::Up, ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine]), ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine])])), + ( + &['d', 'E'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutBigWordRight, + ])]), + ), + ( + &['d', '0'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutFromLineStart, + ])]), + ), + ( + &['d', '^'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutFromLineNonBlankStart, + ])]), + ), + ( + &['d', '$'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutToLineEnd])]), + ), + ( + &['d', 'f', 'a'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutRightUntil('a'), + ])]), + ), + ( + &['d', 't', 'a'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutRightBefore('a'), + ])]), + ), + ( + &['d', 'F', 'a'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutLeftUntil('a'), + ])]), + ), + ( + &['d', 'T', 'a'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutLeftBefore('a'), + ])]), + ), + ( + &['d', 'g', 'g'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutFromStartLinewise { + leave_blank_line: false, + }, + ])]), + ), + ( + &['d', 'G'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutToEndLinewise { + leave_blank_line: false, + }, + ])]), + ), + ( + &['c', 'E'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutBigWordRight]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['c', '0'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['c', '^'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutFromLineNonBlankStart]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['c', '$'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutToLineEnd]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['c', 'f', 'a'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['c', 't', 'a'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutRightBefore('a')]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['c', 'F', 'a'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['c', 'T', 'a'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutLeftBefore('a')]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['c', 'g', 'g'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutFromStartLinewise { + leave_blank_line: true, + }]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['c', 'G'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutToEndLinewise { + leave_blank_line: true, + }]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['y', '^'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CopyFromLineNonBlankStart, + ])]), + ), + ( + &['y', 'g', 'g'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CopyFromStartLinewise, + ])]), + ), + ( + &['y', 'G'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CopyToEndLinewise, + ])]), + ), + ]; + + for (input, expected) in cases { + let mut vi = Vi::default(); + let res = vi_parse(input); + let output = res.to_reedline_event(&mut vi); + + assert_eq!(output, *expected); + } } - #[rstest] - #[case(&['f', 'a'], &[';'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveRightUntil{c: 'a',select: false}])]))] - #[case(&['f', 'a'], &[','], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveLeftUntil{c: 'a', select: false}])]))] - #[case(&['F', 'a'], &[','], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveRightUntil{c: 'a', select: false}])]))] - #[case(&['F', 'a'], &[';'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveLeftUntil{c: 'a', select: false}])]))] - #[case(&['f', 'a'], &['d', ';'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')])]))] - #[case(&['f', 'a'], &['d', ','], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')])]))] - #[case(&['F', 'a'], &['d', ','], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')])]))] - #[case(&['F', 'a'], &['d', ';'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')])]))] - #[case(&['f', 'a'], &['c', ';'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')]), ReedlineEvent::Repaint]))] - #[case(&['f', 'a'], &['c', ','], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')]), ReedlineEvent::Repaint]))] - #[case(&['F', 'a'], &['c', ','], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')]), ReedlineEvent::Repaint]))] - #[case(&['F', 'a'], &['c', ';'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')]), ReedlineEvent::Repaint]))] - fn test_reedline_memory_move( - #[case] before: &[char], - #[case] now: &[char], - #[case] expected: ReedlineEvent, - ) { - let mut vi = Vi::default(); - let _ = vi_parse(before).to_reedline_event(&mut vi); - let output = vi_parse(now).to_reedline_event(&mut vi); - - assert_eq!(output, expected); + #[test] + fn test_reedline_memory_move() { + let cases: &[(&[char], &[char], ReedlineEvent)] = &[ + ( + &['f', 'a'], + &[';'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::MoveRightUntil { + c: 'a', + select: false, + }, + ])]), + ), + ( + &['f', 'a'], + &[','], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::MoveLeftUntil { + c: 'a', + select: false, + }, + ])]), + ), + ( + &['F', 'a'], + &[','], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::MoveRightUntil { + c: 'a', + select: false, + }, + ])]), + ), + ( + &['F', 'a'], + &[';'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::MoveLeftUntil { + c: 'a', + select: false, + }, + ])]), + ), + ( + &['f', 'a'], + &['d', ';'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutRightUntil('a'), + ])]), + ), + ( + &['f', 'a'], + &['d', ','], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutLeftUntil('a'), + ])]), + ), + ( + &['F', 'a'], + &['d', ','], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutRightUntil('a'), + ])]), + ), + ( + &['F', 'a'], + &['d', ';'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutLeftUntil('a'), + ])]), + ), + ( + &['f', 'a'], + &['c', ';'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['f', 'a'], + &['c', ','], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['F', 'a'], + &['c', ','], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')]), + ReedlineEvent::Repaint, + ]), + ), + ( + &['F', 'a'], + &['c', ';'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')]), + ReedlineEvent::Repaint, + ]), + ), + ]; + + for (before, now, expected) in cases { + let mut vi = Vi::default(); + let _ = vi_parse(before).to_reedline_event(&mut vi); + let output = vi_parse(now).to_reedline_event(&mut vi); + + assert_eq!(output, *expected); + } } - #[rstest] - #[case(&['c', 'w'], &['c', 'e'])] - #[case(&['c', 'W'], &['c', 'E'])] - fn test_reedline_move_synonm(#[case] synonym: &[char], #[case] original: &[char]) { - let mut vi = Vi::default(); - let output = vi_parse(synonym).to_reedline_event(&mut vi); - let expected = vi_parse(original).to_reedline_event(&mut vi); + #[test] + fn test_reedline_move_synonm() { + let cases = [(&['c', 'w'], &['c', 'e']), (&['c', 'W'], &['c', 'E'])]; + + for (synonym, original) in cases { + let mut vi = Vi::default(); + let output = vi_parse(synonym).to_reedline_event(&mut vi); + let expected = vi_parse(original).to_reedline_event(&mut vi); - assert_eq!(output, expected); + assert_eq!(output, expected); + } } - #[rstest] - #[case(&['2', 'k'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ - EditCommand::MoveLineUp { select: true }, - ]), ReedlineEvent::Edit(vec![ - EditCommand::MoveLineUp { select: true }, - ])]))] - #[case(&['k'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveLineUp { select: true }])]))] - #[case(&['w'], - ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveWordRightStart{select:true}])]))] - #[case(&['W'], - ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveBigWordRightStart{select:true}])]))] - #[case(&['2', 'l'], ReedlineEvent::Multiple(vec![ - ReedlineEvent::UntilFound(vec![ - ReedlineEvent::HistoryHintComplete, - ReedlineEvent::MenuRight, - ReedlineEvent::Edit(vec![EditCommand::MoveRight{select:true}]), - ]),ReedlineEvent::UntilFound(vec![ - ReedlineEvent::HistoryHintComplete, - ReedlineEvent::MenuRight, - ReedlineEvent::Edit(vec![EditCommand::MoveRight{select:true}]), - ]) ]))] - #[case(&['l'], ReedlineEvent::Multiple(vec![ReedlineEvent::UntilFound(vec![ - ReedlineEvent::HistoryHintComplete, - ReedlineEvent::MenuRight, - ReedlineEvent::Edit(vec![EditCommand::MoveRight{select:true}]), - ])]))] - #[case(&['0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveToLineStart{select:true}])]))] - #[case(&['$'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveToLineEnd{select:true}])]))] - #[case(&['i'], ReedlineEvent::Multiple(vec![ReedlineEvent::Repaint]))] - #[case(&['p'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::PasteCutBufferAfter])]))] - #[case(&['2', 'p'], ReedlineEvent::Multiple(vec![ - ReedlineEvent::Edit(vec![EditCommand::PasteCutBufferAfter]), - ReedlineEvent::Edit(vec![EditCommand::PasteCutBufferAfter]) - ]))] - #[case(&['u'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::Undo])]))] - #[case(&['2', 'u'], ReedlineEvent::Multiple(vec![ - ReedlineEvent::Edit(vec![EditCommand::Undo]), - ReedlineEvent::Edit(vec![EditCommand::Undo]) - ]))] - #[case(&['d'], ReedlineEvent::Multiple(vec![ - ReedlineEvent::Edit(vec![EditCommand::CutSelection])]))] - fn test_reedline_move_in_visual_mode(#[case] input: &[char], #[case] expected: ReedlineEvent) { - let mut vi = Vi { - mode: ViMode::Visual, - ..Default::default() - }; - let res = vi_parse(input); - let output = res.to_reedline_event(&mut vi); - - assert_eq!(output, expected); + #[test] + fn test_reedline_move_in_visual_mode() { + let cases: &[(&[char], ReedlineEvent)] = &[ + ( + &['2', 'k'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::MoveLineUp { select: true }]), + ReedlineEvent::Edit(vec![EditCommand::MoveLineUp { select: true }]), + ]), + ), + ( + &['k'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveLineUp { + select: true, + }])]), + ), + ( + &['w'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::MoveWordRightStart { select: true }, + ])]), + ), + ( + &['W'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::MoveBigWordRightStart { select: true }, + ])]), + ), + ( + &['2', 'l'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::UntilFound(vec![ + ReedlineEvent::HistoryHintComplete, + ReedlineEvent::MenuRight, + ReedlineEvent::Edit(vec![EditCommand::MoveRight { select: true }]), + ]), + ReedlineEvent::UntilFound(vec![ + ReedlineEvent::HistoryHintComplete, + ReedlineEvent::MenuRight, + ReedlineEvent::Edit(vec![EditCommand::MoveRight { select: true }]), + ]), + ]), + ), + ( + &['l'], + ReedlineEvent::Multiple(vec![ReedlineEvent::UntilFound(vec![ + ReedlineEvent::HistoryHintComplete, + ReedlineEvent::MenuRight, + ReedlineEvent::Edit(vec![EditCommand::MoveRight { select: true }]), + ])]), + ), + ( + &['0'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::MoveToLineStart { select: true }, + ])]), + ), + ( + &['$'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::MoveToLineEnd { select: true }, + ])]), + ), + ( + &['i'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Repaint]), + ), + ( + &['p'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::PasteCutBufferAfter, + ])]), + ), + ( + &['2', 'p'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::PasteCutBufferAfter]), + ReedlineEvent::Edit(vec![EditCommand::PasteCutBufferAfter]), + ]), + ), + ( + &['u'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::Undo])]), + ), + ( + &['2', 'u'], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::Undo]), + ReedlineEvent::Edit(vec![EditCommand::Undo]), + ]), + ), + ( + &['d'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutSelection])]), + ), + ]; + + for (input, expected) in cases { + let mut vi = Vi { + mode: ViMode::Visual, + ..Default::default() + }; + let res = vi_parse(input); + let output = res.to_reedline_event(&mut vi); + + assert_eq!(output, *expected); + } } } diff --git a/src/engine.rs b/src/engine.rs index 528004d7e..06ddc5a9f 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1699,20 +1699,6 @@ impl Reedline { /// Executes [`EditCommand`] actions by modifying the internal state appropriately. Does not output itself. pub fn run_edit_commands(&mut self, commands: &[EditCommand]) { if self.input_mode == InputMode::HistoryTraversal { - if matches!( - self.history_cursor.get_navigation(), - HistoryNavigationQuery::Normal(_) - ) { - if let Some(string) = self.history_cursor.string_at_cursor() { - // NOTE: `set_buffer` resets the insertion point, - // which we should avoid during history navigation through the same buffer - // https://github.com/nushell/reedline/pull/899 - if string != self.editor.get_buffer() { - self.editor - .set_buffer(string, UndoBehavior::HistoryNavigation); - } - } - } self.input_mode = InputMode::Regular; } @@ -2230,6 +2216,7 @@ impl Reedline { Ok(messages) } + #[allow(unused_variables)] fn submit_buffer(&mut self, prompt: &dyn Prompt) -> io::Result { let buffer = self.editor.get_buffer().to_string(); self.hide_hints = true; @@ -2238,6 +2225,8 @@ impl Reedline { self.repaint(transient_prompt.as_ref())?; self.transient_prompt = Some(transient_prompt); } else { + // Don't repaint when running unit test as it will fill up stdout with garbage + #[cfg(not(test))] self.repaint(prompt)?; } if !buffer.is_empty() { @@ -2270,8 +2259,7 @@ impl Reedline { mod tests { use super::*; use crate::terminal_extensions::semantic_prompt::PromptKind; - use crate::DefaultPrompt; - use rstest::rstest; + use crate::{ColumnarMenu, DefaultPrompt, MenuBuilder}; #[test] fn test_cursor_position_after_multiline_history_navigation() { @@ -2524,42 +2512,50 @@ mod tests { assert_eq!(reedline.current_buffer_contents(), "sudo git commit"); } - #[rstest] - #[case("\"hello gc", false)] - #[case("'hello gc", false)] - #[case("\"hello\" gc", true)] - #[case("'Сегодня хороший gc", false)] - #[case("'Сегодня' gc", true)] - #[case("'今日はいい日だ gc", false)] - #[case("'🔥🎉 gc", false)] - fn abbreviation_string_detection_with_override( - #[case] buffer: &str, - #[case] should_expand: bool, - ) { - let mut reedline = reedline_with_abbrevs_and_string_lit_override(&[("gc", "git commit")]); - set_buffer_at_end(&mut reedline, buffer); - assert_eq!( - reedline.try_expand_abbreviation_at_cursor(true).is_some(), - should_expand - ); + #[test] + fn abbreviation_string_detection_with_override() { + let cases = [ + ("\"hello gc", false), + ("'hello gc", false), + ("\"hello\" gc", true), + ("'Сегодня хороший gc", false), + ("'Сегодня' gc", true), + ("'今日はいい日だ gc", false), + ("'🔥🎉 gc", false), + ]; + + for (buffer, should_expand) in cases { + let mut reedline = + reedline_with_abbrevs_and_string_lit_override(&[("gc", "git commit")]); + set_buffer_at_end(&mut reedline, buffer); + assert_eq!( + reedline.try_expand_abbreviation_at_cursor(true).is_some(), + should_expand + ); + } } - #[rstest] - #[case("\"hello gc")] - #[case("'hello gc")] - #[case("\"hello\" gc")] - #[case("'Сегодня хороший gc")] - #[case("'Сегодня' gc")] - #[case("'今日はいい日だ gc")] - #[case("'🔥🎉 gc")] - fn abbreviation_string_detection_default(#[case] buffer: &str) { - let mut reedline = - reedline_with_abbrevs_and_default_string_lit_check(&[("gc", "git commit")]); - set_buffer_at_end(&mut reedline, buffer); - assert!( - reedline.try_expand_abbreviation_at_cursor(true).is_some(), - "must expand when highlighter does not override is_inside_string_literal" - ); + #[test] + fn abbreviation_string_detection_default() { + let cases = [ + ("\"hello gc"), + ("'hello gc"), + ("\"hello\" gc"), + ("'Сегодня хороший gc"), + ("'Сегодня' gc"), + ("'今日はいい日だ gc"), + ("'🔥🎉 gc"), + ]; + + for buffer in cases { + let mut reedline = + reedline_with_abbrevs_and_default_string_lit_check(&[("gc", "git commit")]); + set_buffer_at_end(&mut reedline, buffer); + assert!( + reedline.try_expand_abbreviation_at_cursor(true).is_some(), + "must expand when highlighter does not override is_inside_string_literal" + ); + } } #[test] @@ -2585,9 +2581,9 @@ mod tests { } #[cfg(feature = "bashisms")] - fn reedline_with_history_and_string_lit_check(entries: &[&str]) -> Reedline { + fn reedline_with_history_default(entries: &[&str]) -> Reedline { let mut reedline = - Reedline::create().with_highlighter(Box::new(ExampleHighlighter::default())); + Reedline::create().with_highlighter(Box::new(SimpleMatchHighlighter::default())); for entry in entries { reedline .history @@ -2597,52 +2593,179 @@ mod tests { reedline } + #[test] #[cfg(feature = "bashisms")] - fn reedline_with_history_default(entries: &[&str]) -> Reedline { - let mut reedline = - Reedline::create().with_highlighter(Box::new(SimpleMatchHighlighter::default())); - for entry in entries { + fn bang_always_expands_without_override() { + let cases = [ + "\"echo !!", + "'echo !!", + "'echo' !!", + "\"echo !git", + "'echo !git", + "'Сегодня !!", + "'今日は !!", + "'🔥 !!", + ]; + + for buffer in cases { + let mut reedline = reedline_with_history_default(&["git status"]); + set_buffer_at_end(&mut reedline, buffer); + assert!( + reedline.parse_bang_command().is_some(), + "must expand when highlighter does not override is_inside_string_literal" + ); + } + } + + #[test] + fn test_move_to_line_start() { + let cases = [ + "", + "line of text", +"longgggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line of text" + ]; + + for input in cases { + let mut reedline = Reedline::create(); + let prompt = DefaultPrompt::default(); + + let insert_input_event = + ReedlineEvent::Edit(vec![EditCommand::InsertString(input.to_string())]); + let move_to_line_start_event = + ReedlineEvent::Edit(vec![EditCommand::MoveToLineStart { select: false }]); + + // Have to resize, or painting.utils.estimate_single_line_wraps panics with divide-by-zero. reedline - .history - .save(HistoryItem::from_command_line(*entry)) - .expect("failed to save history"); + .handle_event(&prompt, ReedlineEvent::Resize(u16::MAX, u16::MAX)) + .unwrap(); + + // Write the string, and then move to the start of the line. + reedline.handle_event(&prompt, insert_input_event).unwrap(); + reedline + .handle_event(&prompt, move_to_line_start_event.clone()) + .unwrap(); + assert_eq!(reedline.editor.line_buffer().insertion_point(), 0); + + // Enter the string into history, then scroll back up and move to the start of the line. + reedline + .handle_event(&prompt, ReedlineEvent::Enter) + .unwrap(); + reedline.handle_event(&prompt, ReedlineEvent::Up).unwrap(); + reedline + .handle_event(&prompt, move_to_line_start_event) + .unwrap(); + assert_eq!(reedline.editor.line_buffer().insertion_point(), 0); } - reedline } - #[rstest] - #[case("!!", true)] - #[case("\"echo !!", false)] - #[case("'echo !!", false)] - #[case("'echo' !!", true)] - #[case("\"echo !git", false)] - #[case("'echo !git", false)] - #[case("'Сегодня !!", false)] - #[case("'今日は !!", false)] - #[case("'🔥 !!", false)] - #[cfg(feature = "bashisms")] - fn bang_string_detection_with_override(#[case] buffer: &str, #[case] should_expand: bool) { - let mut reedline = reedline_with_history_and_string_lit_check(&["git status"]); - set_buffer_at_end(&mut reedline, buffer); - assert_eq!(reedline.parse_bang_command().is_some(), should_expand); - } - - #[rstest] - #[case("\"echo !!")] - #[case("'echo !!")] - #[case("'echo' !!")] - #[case("\"echo !git")] - #[case("'echo !git")] - #[case("'Сегодня !!")] - #[case("'今日は !!")] - #[case("'🔥 !!")] - #[cfg(feature = "bashisms")] - fn bang_always_expands_without_override(#[case] buffer: &str) { - let mut reedline = reedline_with_history_default(&["git status"]); - set_buffer_at_end(&mut reedline, buffer); - assert!( - reedline.parse_bang_command().is_some(), - "must expand when highlighter does not override is_inside_string_literal" - ); + #[test] + fn test_move_to_line_start_multiline() { + let cases = [ + ("a\nb", 2, 2), + ("123456789\n123456789\n123456789", 10, 20), + ("0\n1\n2\n3\n4\n5\n6\n7\n8\n9", 2, 18), + ]; + + for (input, second_line_start, last_line_start) in cases { + let mut reedline = Reedline::create(); + let prompt = DefaultPrompt::default(); + + let insert_input_event = + ReedlineEvent::Edit(vec![EditCommand::InsertString(input.to_string())]); + let move_to_line_start_event = + ReedlineEvent::Edit(vec![EditCommand::MoveToLineStart { select: false }]); + let move_to_end_event = + ReedlineEvent::Edit(vec![EditCommand::MoveToEnd { select: false }]); + + // Have to resize, or painting.utils.estimate_single_line_wraps panics with divide-by-zero. + reedline + .handle_event(&prompt, ReedlineEvent::Resize(u16::MAX, u16::MAX)) + .unwrap(); + + // Write the string, and then move to the start of the last line. + reedline.handle_event(&prompt, insert_input_event).unwrap(); + reedline + .handle_event(&prompt, move_to_line_start_event.clone()) + .unwrap(); + assert_eq!( + reedline.editor.line_buffer().insertion_point(), + last_line_start + ); + + // Enter the string into history, then scroll back up and move to the start of the first line. + reedline + .handle_event(&prompt, ReedlineEvent::Enter) + .unwrap(); + reedline.handle_event(&prompt, ReedlineEvent::Up).unwrap(); + reedline + .handle_event(&prompt, move_to_line_start_event.clone()) + .unwrap(); + assert_eq!(reedline.editor.line_buffer().insertion_point(), 0); + + // Enter the string again, then scroll up in history, move down one line, + // and move to the start of the second line. + reedline + .handle_event(&prompt, ReedlineEvent::Enter) + .unwrap(); + reedline.handle_event(&prompt, ReedlineEvent::Up).unwrap(); + reedline.handle_event(&prompt, ReedlineEvent::Down).unwrap(); + reedline + .handle_event(&prompt, move_to_line_start_event.clone()) + .unwrap(); + assert_eq!( + reedline.editor.line_buffer().insertion_point(), + second_line_start + ); + + // Enter the string again, then scroll up in history, move to the end of the text, + // and move to the start of the last line. + reedline + .handle_event(&prompt, ReedlineEvent::Enter) + .unwrap(); + reedline.handle_event(&prompt, ReedlineEvent::Up).unwrap(); + reedline.handle_event(&prompt, move_to_end_event).unwrap(); + reedline + .handle_event(&prompt, move_to_line_start_event) + .unwrap(); + assert_eq!( + reedline.editor.line_buffer().insertion_point(), + last_line_start + ); + } + } + + #[test] + fn test_complete_line_from_history() { + let history = Box::new(FileBackedHistory::new(1).unwrap()); + let completer = Box::new(DefaultCompleter::new(vec!["67".into()])); + let completion_menu = ReedlineMenu::EngineCompleter(Box::new( + ColumnarMenu::default().with_name("completion_menu"), + )); + + let prompt = DefaultPrompt::default(); + let mut reedline = Reedline::create() + .with_quick_completions(true) + .with_history(history) + .with_completer(completer) + .with_menu(completion_menu); + + let insert_6 = ReedlineEvent::Edit(vec![EditCommand::InsertString("6".into())]); + let submit = ReedlineEvent::Submit; + let up = ReedlineEvent::Up; + let tab = ReedlineEvent::Menu("completion_menu".into()); + let insert_x = ReedlineEvent::Edit(vec![EditCommand::InsertString("x".into())]); + + // Insert 6, press enter, then re-select it from history. + // Press tab to automatically complete 67 using quick completion without actually showing the menu. + // Anything typed after this should be appended to the end of the string rather than replacing the completion. + // 67 + x should be 67x and not 6x + + reedline.handle_event(&prompt, insert_6).unwrap(); + reedline.handle_event(&prompt, submit).unwrap(); + reedline.handle_event(&prompt, up).unwrap(); + reedline.handle_event(&prompt, tab).unwrap(); + reedline.handle_event(&prompt, insert_x).unwrap(); + + assert_eq!(reedline.editor.get_buffer(), "67x"); } } diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index ba49ac68b..618a78350 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -1076,228 +1076,202 @@ mod tests { use super::*; use pretty_assertions::assert_eq; - use rstest::rstest; - - #[rstest] - #[case( - "", - 10, - vec![] - )] - #[case( - "description", - 15, - vec![ - "description".into(), - ] - )] - #[case( - "this is a description", - 10, - vec![ - "this is a".into(), - "descriptio".into(), - "n".into(), - ] - )] - #[case( - "this is another description", - 2, - vec![ - "th".into(), - "is".into(), - "is".into(), - "an".into(), - "ot".into(), - "he".into(), - "r".into(), - "de".into(), - "sc".into(), - "ri".into(), - "pt".into(), - "io".into(), - "n".into(), - ] - )] - #[case( - "this is a description", - 10, - vec![ - "this is a".into(), - "descriptio".into(), - "n".into(), - ] - )] - #[case( - "this is a description", - 10, - vec![ - "this is a".into(), - "descriptio".into(), - "n".into(), - ] - )] - #[case( - "this is a description", - 12, - vec![ - "this is a".into(), - "description".into(), - ] - )] - #[case( - "test", - 1, - vec![ - "t".into(), - "e".into(), - "s".into(), - "t".into(), - ] - )] - #[case( - "😊a😊 😊bc de😊fg", - 2, - vec![ - "😊".into(), - "a".into(), - "😊".into(), - "😊".into(), - "bc".into(), - "de".into(), - "😊".into(), - "fg".into(), - ] - )] - #[case( - "😊", - 1, - vec![], - )] - #[case( - "t😊e😊s😊t", - 1, - vec![ - "t".into(), - "e".into(), - "s".into(), - "t".into(), - ] - )] - - fn test_split_string( - #[case] input: &str, - #[case] max_width: usize, - #[case] expected: Vec, - ) { - let result = split_string(input, max_width); - assert_eq!(result, expected) + #[test] + fn test_split_string() { + let cases = [ + ("", 10, vec![]), + ("description", 15, vec!["description".to_string()]), + ( + "this is a description", + 10, + vec![ + "this is a".to_string(), + "descriptio".to_string(), + "n".to_string(), + ], + ), + ( + "this is another description", + 2, + vec![ + "th".to_string(), + "is".to_string(), + "is".to_string(), + "an".to_string(), + "ot".to_string(), + "he".to_string(), + "r".to_string(), + "de".to_string(), + "sc".to_string(), + "ri".to_string(), + "pt".to_string(), + "io".to_string(), + "n".to_string(), + ], + ), + ( + "this is a description", + 10, + vec![ + "this is a".to_string(), + "descriptio".to_string(), + "n".to_string(), + ], + ), + ( + "this is a description", + 10, + vec![ + "this is a".to_string(), + "descriptio".to_string(), + "n".to_string(), + ], + ), + ( + "this is a description", + 12, + vec!["this is a".to_string(), "description".to_string()], + ), + ( + "test", + 1, + vec![ + "t".to_string(), + "e".to_string(), + "s".to_string(), + "t".to_string(), + ], + ), + ( + "😊a😊 😊bc de😊fg", + 2, + vec![ + "😊".to_string(), + "a".to_string(), + "😊".to_string(), + "😊".to_string(), + "bc".to_string(), + "de".to_string(), + "😊".to_string(), + "fg".to_string(), + ], + ), + ("😊", 1, vec![]), + ( + "t😊e😊s😊t", + 1, + vec![ + "t".to_string(), + "e".to_string(), + "s".to_string(), + "t".to_string(), + ], + ), + ]; + + for (input, max_width, expected) in cases { + let result = split_string(input, max_width); + + assert_eq!(result, expected) + } } - #[rstest] - #[case( - &mut vec![ - "this is a description".into(), - "that will be truncate".into(), - "d".into(), - ], - "...", - vec![ - "this is a description".into(), - "that will be trunca..".into(), - ".".into(), - ] - )] - #[case( - &mut vec![ - "this is a description".into(), - "that will be truncate".into(), - "d".into(), - ], - "....", - vec![ - "this is a description".into(), - "that will be trunc...".into(), - ".".into(), - ] - )] - #[case( - &mut vec![ - "😊a😊 😊bc de😊fg".into(), - "😊a😊 😊bc de😊fg".into(), - "😊a😊 😊bc de😊fg".into(), - ], - "...", - vec![ - "😊a😊 😊bc de😊fg".into(), - "😊a😊 😊bc de😊fg".into(), - "😊a😊 😊bc de...".into(), - ] - )] - #[case( - &mut vec![ - "t".into(), - "e".into(), - "s".into(), - "t".into(), - ], - "..", - vec![ - "t".into(), - "e".into(), - ".".into(), - ".".into(), - ] - )] - #[case( - &mut vec![ - "😊".into(), - "😊".into(), - "s".into(), - "t".into(), - ], - "..😊", - vec![ - "😊".into(), - ".".into(), - ".".into(), - "😊".into(), - ] - )] - #[case( - &mut vec![ - "".into(), - ], - "test", - vec![ - "".into() - ], - )] - #[case( - &mut vec![ - "t".into(), - "e".into(), - "s".into(), - "t".into() - ], - "", - vec![ - "t".into(), - "e".into(), - "s".into(), - "t".into() - ], - )] - - fn test_truncate_list_string( - #[case] input: &mut Vec, - #[case] truncation_chars: &str, - #[case] expected: Vec, - ) { - truncate_string_list(input, truncation_chars); + #[test] + fn test_truncate_list_string() { + let cases = [ + ( + &mut vec![ + "this is a description".to_string(), + "that will be truncate".to_string(), + "d".to_string(), + ], + "...", + vec![ + "this is a description".to_string(), + "that will be trunca..".to_string(), + ".".to_string(), + ], + ), + ( + &mut vec![ + "this is a description".to_string(), + "that will be truncate".to_string(), + "d".to_string(), + ], + "....", + vec![ + "this is a description".to_string(), + "that will be trunc...".to_string(), + ".".to_string(), + ], + ), + ( + &mut vec![ + "😊a😊 😊bc de😊fg".to_string(), + "😊a😊 😊bc de😊fg".to_string(), + "😊a😊 😊bc de😊fg".to_string(), + ], + "...", + vec![ + "😊a😊 😊bc de😊fg".to_string(), + "😊a😊 😊bc de😊fg".to_string(), + "😊a😊 😊bc de...".to_string(), + ], + ), + ( + &mut vec![ + "t".to_string(), + "e".to_string(), + "s".to_string(), + "t".to_string(), + ], + "..", + vec![ + "t".to_string(), + "e".to_string(), + ".".to_string(), + ".".to_string(), + ], + ), + ( + &mut vec![ + "😊".to_string(), + "😊".to_string(), + "s".to_string(), + "t".to_string(), + ], + "..😊", + vec![ + "😊".to_string(), + ".".to_string(), + ".".to_string(), + "😊".to_string(), + ], + ), + (&mut vec!["".to_string()], "test", vec!["".to_string()]), + ( + &mut vec![ + "t".to_string(), + "e".to_string(), + "s".to_string(), + "t".to_string(), + ], + "", + vec![ + "t".to_string(), + "e".to_string(), + "s".to_string(), + "t".to_string(), + ], + ), + ]; - assert_eq!(*input, expected) + for (input, truncation_chars, expected) in cases { + truncate_string_list(input, truncation_chars); + + assert_eq!(*input, expected) + } } macro_rules! partial_completion_tests { diff --git a/src/menu/menu_functions.rs b/src/menu/menu_functions.rs index d883fc54c..9446d23be 100644 --- a/src/menu/menu_functions.rs +++ b/src/menu/menu_functions.rs @@ -686,7 +686,6 @@ mod tests { use super::*; use crate::{EditCommand, LineBuffer, Span}; use nu_ansi_term::Color; - use rstest::rstest; #[test] fn parse_row_test() { @@ -927,130 +926,194 @@ mod tests { assert_eq!(res, (1, "e")); } - #[rstest] - #[case::ascii(vec!["nushell", "null"], 2)] - #[case::non_ascii(vec!["nushell", "null"], 6)] - // https://github.com/nushell/nushell/pull/16765#issuecomment-3384411809 - #[case::unsorted(vec!["a", "b", "ab"], 0)] - #[case::should_be_case_sensitive(vec!["a", "A"], 0)] - #[case::first_suggestion_longest(vec!["foobar", "foo"], 3)] - fn test_find_common_string(#[case] input: Vec<&str>, #[case] expected: usize) { - let input: Vec<_> = input - .into_iter() - .map(|s| Suggestion { - value: s.into(), - ..Default::default() - }) - .collect(); - let (_, len) = find_common_string(&input).unwrap(); - - assert!(len == expected); + #[test] + fn test_find_common_string() { + let cases = [ + (vec!["nushell", "null"], 2), + (vec!["nushell", "null"], 6), + (vec!["a", "b", "ab"], 0), + (vec!["a", "A"], 0), + (vec!["foobar", "foo"], 3), + ]; + + for (input, expected) in cases { + let input: Vec<_> = input + .into_iter() + .map(|s| Suggestion { + value: s.into(), + ..Default::default() + }) + .collect(); + let (_, len) = find_common_string(&input).unwrap(); + + assert!(len == expected); + } } - #[rstest] - #[case("foobar", 6, None, false, "foobar", 6)] - #[case("foo\r\nbar", 5, None, false, "foo\r\n", 5)] - #[case("foo\nbar", 4, None, false, "foo\n", 4)] - #[case("foobar", 6, None, true, "", 6)] - #[case("foobar", 3, Some("foobar"), true, "", 3)] - #[case("foobar", 6, Some("foo"), true, "bar", 6)] - #[case("foobar", 6, Some("for"), true, "oba", 5)] - fn test_completer_input( - #[case] buffer: String, - #[case] insertion_point: usize, - #[case] prev_input: Option<&str>, - #[case] only_buffer_difference: bool, - #[case] output: String, - #[case] pos: usize, - ) { - assert_eq!( - (output, pos), - completer_input(&buffer, insertion_point, prev_input, only_buffer_difference) - ) + #[test] + fn test_completer_input() { + let cases = [ + ("foobar", 6, None, false, "foobar", 6), + ("foo\r\nbar", 5, None, false, "foo\r\n", 5), + ("foo\nbar", 4, None, false, "foo\n", 4), + ("foobar", 6, None, true, "", 6), + ("foobar", 3, Some("foobar"), true, "", 3), + ("foobar", 6, Some("foo"), true, "bar", 6), + ("foobar", 6, Some("for"), true, "oba", 5), + ]; + + for (buffer, insertion_point, prev_input, only_buffer_difference, output, pos) in cases { + assert_eq!( + (output.to_string(), pos), + completer_input(buffer, insertion_point, prev_input, only_buffer_difference) + ) + } } - #[rstest] - #[case("foobar baz", 6, "foobleh baz", 7, "bleh", 3, 6)] - #[case("foobar baz", 6, "foo baz", 3, "", 3, 6)] - #[case("foobar baz", 10, "foobleh", 7, "bleh", 3, 1000)] - fn test_replace_in_buffer( - #[case] orig_buffer: &str, - #[case] orig_insertion_point: usize, - #[case] new_buffer: &str, - #[case] new_insertion_point: usize, - #[case] value: String, - #[case] start: usize, - #[case] end: usize, - ) { - let mut editor = Editor::default(); - let mut line_buffer = LineBuffer::new(); - line_buffer.set_buffer(orig_buffer.to_owned()); - line_buffer.set_insertion_point(orig_insertion_point); - editor.set_line_buffer(line_buffer, UndoBehavior::CreateUndoPoint); - replace_in_buffer( - Some(Suggestion { - value, - span: Span::new(start, end), - ..Default::default() - }), - &mut editor, - ); - assert_eq!(new_buffer, editor.get_buffer()); - assert_eq!(new_insertion_point, editor.insertion_point()); + #[test] + fn test_replace_in_buffer() { + let cases = [ + ("foobar baz", 6, "foobleh baz", 7, "bleh", 3, 6), + ("foobar baz", 6, "foo baz", 3, "", 3, 6), + ("foobar baz", 10, "foobleh", 7, "bleh", 3, 1000), + ]; - editor.run_edit_command(&EditCommand::Undo); - assert_eq!(orig_buffer, editor.get_buffer()); - assert_eq!(orig_insertion_point, editor.insertion_point()); + for ( + orig_buffer, + orig_insertion_point, + new_buffer, + new_insertion_point, + value, + start, + end, + ) in cases + { + let mut editor = Editor::default(); + let mut line_buffer = LineBuffer::new(); + line_buffer.set_buffer(orig_buffer.to_owned()); + line_buffer.set_insertion_point(orig_insertion_point); + editor.set_line_buffer(line_buffer, UndoBehavior::CreateUndoPoint); + replace_in_buffer( + Some(Suggestion { + value: value.to_string(), + span: Span::new(start, end), + ..Default::default() + }), + &mut editor, + ); + assert_eq!(new_buffer, editor.get_buffer()); + assert_eq!(new_insertion_point, editor.insertion_point()); + + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(orig_buffer, editor.get_buffer()); + assert_eq!(orig_insertion_point, editor.insertion_point()); + } } - #[rstest] - #[case::plain("Foo", vec![AnsiSegment { escape: None, text: "Foo" }])] - #[case::unterminated("\x1b[", vec![AnsiSegment { escape: None, text: "\x1b[" }])] - #[case::invalid( - "\x1b[\x1b[mFoo", - vec![ - AnsiSegment { escape: None, text: "\x1b[" }, - AnsiSegment { escape: None, text: "Foo" }, - ] - )] - #[case::no_args_reset( - "\x1b[3m\x1b[m\x1b[2mFoo", - vec![ - AnsiSegment { escape: None, text: "" }, - AnsiSegment { escape: Some("2m"), text: "Foo" }, - ] - )] - #[case::empty_reset_with_args_afterwards( - "\x1b[3m\x1b[1;;20mFoo", - vec![ - AnsiSegment { escape: None, text: "" }, - AnsiSegment { escape: Some("20m"), text: "Foo" }, - ] - )] - #[case::empty_reset_without_args_afterwards( - "\x1b[3m\x1b[1;mFoo", - vec![ - AnsiSegment { escape: None, text: "" }, - AnsiSegment { escape: None, text: "Foo" }, - ] - )] - #[case::zero_reset_without_args_afterwards( - "\x1b[3m\x1b[10;0mFoo", - vec![ - AnsiSegment { escape: None, text: "" }, - AnsiSegment { escape: None, text: "Foo" }, - ] - )] - #[case::multiple( - "Foo\x1b[1;0;2m\x1b[2;3m\x1b[Bar\x1b[1;2m\x1b[2;3mBaz", - vec![ - AnsiSegment { escape: None, text: "Foo" }, - AnsiSegment { escape: Some("2m\x1b[2;3m"), text: "\x1b[Bar" }, - AnsiSegment { escape: Some("1;2m\x1b[2;3m"), text: "Baz" }, - ] - )] - fn test_parse_ansi(#[case] s: &str, #[case] expected: Vec) { - assert_eq!(parse_ansi(s), expected); + #[test] + fn test_parse_ansi() { + let cases = [ + ( + "Foo", + vec![AnsiSegment { + escape: None, + text: "Foo", + }], + ), + ( + "\x1b[", + vec![AnsiSegment { + escape: None, + text: "\x1b[", + }], + ), + ( + "\x1b[\x1b[mFoo", + vec![ + AnsiSegment { + escape: None, + text: "\x1b[", + }, + AnsiSegment { + escape: None, + text: "Foo", + }, + ], + ), + ( + "\x1b[3m\x1b[m\x1b[2mFoo", + vec![ + AnsiSegment { + escape: None, + text: "", + }, + AnsiSegment { + escape: Some("2m"), + text: "Foo", + }, + ], + ), + ( + "\x1b[3m\x1b[1;;20mFoo", + vec![ + AnsiSegment { + escape: None, + text: "", + }, + AnsiSegment { + escape: Some("20m"), + text: "Foo", + }, + ], + ), + ( + "\x1b[3m\x1b[1;mFoo", + vec![ + AnsiSegment { + escape: None, + text: "", + }, + AnsiSegment { + escape: None, + text: "Foo", + }, + ], + ), + ( + "\x1b[3m\x1b[10;0mFoo", + vec![ + AnsiSegment { + escape: None, + text: "", + }, + AnsiSegment { + escape: None, + text: "Foo", + }, + ], + ), + ( + "Foo\x1b[1;0;2m\x1b[2;3m\x1b[Bar\x1b[1;2m\x1b[2;3mBaz", + vec![ + AnsiSegment { + escape: None, + text: "Foo", + }, + AnsiSegment { + escape: Some("2m\x1b[2;3m"), + text: "\x1b[Bar", + }, + AnsiSegment { + escape: Some("1;2m\x1b[2;3m"), + text: "Baz", + }, + ], + ), + ]; + + for (s, expected) in cases { + assert_eq!(parse_ansi(s), expected); + } } #[test] @@ -1141,25 +1204,25 @@ mod tests { ); } - #[rstest] - #[case::no_ansi_shorter("asdf", 5, "asdf")] - #[case::with_ansi_shorter( - "\x1b[1;2;3;ma\x1b[1;15;ms\x1b[1;md\x1b[1;mf", - 5, - "\x1b[1;2;3;ma\x1b[1;15;ms\x1b[1;md\x1b[1;mf" - )] - // H has width 2 - #[case::no_ansi_one_longer("asdfH", 5, "as...")] - #[case::no_ansi_result_thinner_than_max("aHHH", 5, "a...")] - #[case::with_ansi_exact_width("\x1b[2masd\x1b[2;3;mH", 5, "\x1b[2masd\x1b[2;3;mH")] - #[case::no_ansi_nothing_left("foobar", 3, "...")] - #[case::trunc_with_short_segments("foobar\x1b[1;ma\x1b[2;mb\x1b[3;mc", 8, "fooba...")] - #[case::trunc_with_long_segment("foo\x1b[1;mBarbaz\x1b[2;mExtra", 8, "foo\x1b[0mBa...")] - fn test_truncate_with_ansi( - #[case] value: &str, - #[case] max_width: usize, - #[case] expected: &str, - ) { - assert_eq!(expected, truncate_with_ansi(value, max_width)); + #[test] + fn test_truncate_with_ansi() { + let cases = [ + ("asdf", 5, "asdf"), + ( + "\x1b[1;2;3;ma\x1b[1;15;ms\x1b[1;md\x1b[1;mf", + 5, + "\x1b[1;2;3;ma\x1b[1;15;ms\x1b[1;md\x1b[1;mf", + ), + ("asdfH", 5, "as..."), + ("aHHH", 5, "a..."), + ("\x1b[2masd\x1b[2;3;mH", 5, "\x1b[2masd\x1b[2;3;mH"), + ("foobar", 3, "..."), + ("foobar\x1b[1;ma\x1b[2;mb\x1b[3;mc", 8, "fooba..."), + ("foo\x1b[1;mBarbaz\x1b[2;mExtra", 8, "foo\x1b[0mBa..."), + ]; + + for (value, max_width, expected) in cases { + assert_eq!(expected, truncate_with_ansi(value, max_width)); + } } } diff --git a/src/painting/prompt_lines.rs b/src/painting/prompt_lines.rs index 6d7597cc1..3bc188601 100644 --- a/src/painting/prompt_lines.rs +++ b/src/painting/prompt_lines.rs @@ -173,85 +173,35 @@ impl<'prompt> PromptLines<'prompt> { mod tests { use super::*; use pretty_assertions::assert_eq; - use rstest::rstest; - #[rstest] - #[case( - "~/path/", - "❯ ", - "", - 100, - (9, 0) - )] - #[case( - "~/longer/path/\n", - "❯ ", - "test", - 100, - (6, 0) - )] - #[case( - "~/longer/path/", - "\n❯ ", - "test", - 100, - (6, 0) - )] - #[case( - "~/longer/path/\n", - "\n❯ ", - "test", - 100, - (6, 0) - )] - #[case( - "~/path/", - "❯ ", - "very long input that does not fit in a single line", - 40, - (19, 1) - )] - #[case( - "~/path/\n", - "\n❯\n ", - "very long input that does not fit in a single line", - 10, - (1, 5) - )] - #[case( - "~/path/", - "❯ ", - "this is a text that contains newlines\n::: and a multiline prompt", - 40, - (26, 2) - )] - #[case( - "~/path/", - "❯ ", - "this is a text that contains newlines\n::: and very loooooooooooooooong text that wraps", - 40, - (8, 3) - )] - - fn test_cursor_pos( - #[case] prompt_str_left: &str, - #[case] prompt_indicator: &str, - #[case] before_cursor: &str, - #[case] terminal_columns: u16, - #[case] expected: (u16, u16), - ) { - let prompt_lines = PromptLines { - prompt_str_left: Cow::Borrowed(prompt_str_left), - prompt_str_right: Cow::Borrowed(""), - prompt_indicator: Cow::Borrowed(prompt_indicator), - before_cursor: Cow::Borrowed(before_cursor), - after_cursor: Cow::Borrowed(""), - hint: Cow::Borrowed(""), - right_prompt_on_last_line: false, - }; - - let pos = prompt_lines.cursor_pos(terminal_columns); - - assert_eq!(pos, expected); + #[test] + fn test_cursor_pos() { + let cases=[ + ( "~/path/", "❯ ", "", 100, (9, 0) ), + ( "~/longer/path/\n", "❯ ", "test", 100, (6, 0) ), + ( "~/longer/path/", "\n❯ ", "test", 100, (6, 0) ), + ( "~/longer/path/\n", "\n❯ ", "test", 100, (6, 0) ), + ( "~/path/", "❯ ", "very long input that does not fit in a single line", 40, (19, 1) ), + ( "~/path/\n", "\n❯\n ", "very long input that does not fit in a single line", 10, (1, 5) ), + ( "~/path/", "❯ ", "this is a text that contains newlines\n::: and a multiline prompt", 40, (26, 2) ), + ( "~/path/", "❯ ", "this is a text that contains newlines\n::: and very loooooooooooooooong text that wraps", 40, (8, 3) ), + ]; + + for (prompt_str_left, prompt_indicator, before_cursor, terminal_columns, expected) in cases + { + let prompt_lines = PromptLines { + prompt_str_left: Cow::Borrowed(prompt_str_left), + prompt_str_right: Cow::Borrowed(""), + prompt_indicator: Cow::Borrowed(prompt_indicator), + before_cursor: Cow::Borrowed(before_cursor), + after_cursor: Cow::Borrowed(""), + hint: Cow::Borrowed(""), + right_prompt_on_last_line: false, + }; + + let pos = prompt_lines.cursor_pos(terminal_columns); + + assert_eq!(pos, expected); + } } } diff --git a/src/painting/utils.rs b/src/painting/utils.rs index 2595fe221..fd80cbf51 100644 --- a/src/painting/utils.rs +++ b/src/painting/utils.rs @@ -90,25 +90,29 @@ pub(crate) fn line_width(line: &str) -> usize { mod test { use super::*; use pretty_assertions::assert_eq; - use rstest::rstest; - - #[rstest] - #[case("sentence\nsentence", "sentence\r\nsentence")] - #[case("sentence\r\nsentence", "sentence\r\nsentence")] - #[case("sentence\nsentence\n", "sentence\r\nsentence\r\n")] - #[case("😇\nsentence", "😇\r\nsentence")] - #[case("sentence\n😇", "sentence\r\n😇")] - #[case("\n", "\r\n")] - #[case("", "")] - fn test_coerce_crlf(#[case] input: &str, #[case] expected: &str) { - let result = coerce_crlf(input); - - assert_eq!(result, expected); - - assert!( - input != expected || matches!(result, Cow::Borrowed(_)), - "Unnecessary allocation" - ) + + #[test] + fn test_coerce_crlf() { + let cases = [ + ("sentence\nsentence", "sentence\r\nsentence"), + ("sentence\r\nsentence", "sentence\r\nsentence"), + ("sentence\nsentence\n", "sentence\r\nsentence\r\n"), + ("😇\nsentence", "😇\r\nsentence"), + ("sentence\n😇", "sentence\r\n😇"), + ("\n", "\r\n"), + ("", ""), + ]; + + for (input, expected) in cases { + let result = coerce_crlf(input); + + assert_eq!(result, expected); + + assert!( + input != expected || matches!(result, Cow::Borrowed(_)), + "Unnecessary allocation" + ) + } } /// Narrow-terminal regression: a zero-column terminal used to panic @@ -120,16 +124,17 @@ mod test { assert_eq!(estimate_single_line_wraps("", 0), 0); } - #[rstest] - #[case("", 80, 0)] - #[case("hello", 80, 0)] - #[case("abcdefghij", 5, 1)] - #[case("abcdefghijk", 5, 2)] - fn estimate_single_line_wraps_basic( - #[case] line: &str, - #[case] columns: u16, - #[case] expected: usize, - ) { - assert_eq!(estimate_single_line_wraps(line, columns), expected); + #[test] + fn estimate_single_line_wraps_basic() { + let cases = [ + ("", 80, 0), + ("hello", 80, 0), + ("abcdefghij", 5, 1), + ("abcdefghijk", 5, 2), + ]; + + for (line, columns, expected) in cases { + assert_eq!(estimate_single_line_wraps(line, columns), expected); + } } } diff --git a/src/validator/default.rs b/src/validator/default.rs index f53b186eb..7c9ebd624 100644 --- a/src/validator/default.rs +++ b/src/validator/default.rs @@ -38,16 +38,20 @@ fn incomplete_brackets(line: &str) -> bool { #[cfg(test)] mod test { use super::*; - use rstest::rstest; - #[rstest] - #[case("(([[]]))", false)] - #[case("(([[]]", true)] - #[case("{[}]", true)] - #[case("{[]}{()}", false)] - fn test_incomplete_brackets(#[case] input: &str, #[case] expected: bool) { - let result = incomplete_brackets(input); + #[test] + fn test_incomplete_brackets() { + let cases = [ + ("(([[]]))", false), + ("(([[]]", true), + ("{[}]", true), + ("{[]}{()}", false), + ]; - assert_eq!(result, expected); + for (input, expected) in cases { + let result = incomplete_brackets(input); + + assert_eq!(result, expected); + } } }