Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 217 additions & 94 deletions src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -2230,6 +2216,7 @@ impl Reedline {
Ok(messages)
}

#[allow(unused_variables)]
fn submit_buffer(&mut self, prompt: &dyn Prompt) -> io::Result<EventStatus> {
let buffer = self.editor.get_buffer().to_string();
self.hide_hints = true;
Expand All @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand All @@ -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");
}
}
Loading