diff --git a/src/engine.rs b/src/engine.rs index 528004d7..06ddc5a9 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"); } }