reader: unit-test try_apply_edit_to_autosuggestion

This commit is contained in:
Johannes Altmanninger
2026-01-24 15:07:02 +01:00
parent 9c3cb154d3
commit 2a3fe73a6d

View File

@@ -1995,52 +1995,61 @@ fn set_command_line_and_position(
self.edit_line_mut(elt).set_position(pos);
self.update_buff_pos(elt, Some(pos));
}
}
fn try_apply_edit_to_autosuggestion(&mut self, edit: &Edit) -> bool {
let autosuggestion = &self.autosuggestion;
if autosuggestion.is_empty() {
return false;
}
// Check to see if our autosuggestion still applies; if so, don't recompute it.
// Since the autosuggestion computation is asynchronous, this avoids "flashing" as you type into
// the autosuggestion.
// This is also the main mechanism by which readline commands that don't change the command line
// text avoid recomputing the autosuggestion.
assert!(string_prefixes_string_maybe_case_insensitive(
autosuggestion.icase,
&self.command_line.text()[autosuggestion.search_string_range.clone()],
&autosuggestion.text
));
let search_string_range = autosuggestion.search_string_range.clone();
// This is a heuristic with false negatives but that seems fine.
let Some(offset) = edit.range.start.checked_sub(search_string_range.start) else {
return false;
};
let Some(remaining) = autosuggestion.text.get(offset..) else {
return false;
};
if edit.range.end != search_string_range.end
|| !string_prefixes_string_maybe_case_insensitive(
autosuggestion.icase,
&edit.replacement,
remaining,
)
|| edit.replacement.len() == remaining.len()
{
return false;
}
self.autosuggestion.search_string_range.end = search_string_range.end
- edit.range.len().min(search_string_range.end)
+ edit.replacement.len();
true
fn try_apply_edit_to_autosuggestion(
autosuggestion: &mut Autosuggestion,
command_line_text: &wstr,
edit: &Edit,
) -> bool {
if autosuggestion.is_empty() {
return false;
}
// Check to see if our autosuggestion still applies; if so, don't recompute it.
// Since the autosuggestion computation is asynchronous, this avoids "flashing" as you type into
// the autosuggestion.
// This is also the main mechanism by which readline commands that don't change the command line
// text avoid recomputing the autosuggestion.
assert!(string_prefixes_string_maybe_case_insensitive(
autosuggestion.icase,
&command_line_text[autosuggestion.search_string_range.clone()],
&autosuggestion.text
));
let search_string_range = autosuggestion.search_string_range.clone();
// This is a heuristic with false negatives but that seems fine.
let Some(offset) = edit.range.start.checked_sub(search_string_range.start) else {
return false;
};
let Some(remaining) = autosuggestion.text.get(offset..) else {
return false;
};
if edit.range.end != search_string_range.end
|| !string_prefixes_string_maybe_case_insensitive(
autosuggestion.icase,
&edit.replacement,
remaining,
)
|| edit.replacement.len() == remaining.len()
{
return false;
}
autosuggestion.search_string_range.end = search_string_range.end
- edit.range.len().min(search_string_range.end)
+ edit.replacement.len();
true
}
impl ReaderData {
fn push_edit_internal(&mut self, elt: EditableLineTag, edit: Edit, allow_coalesce: bool) {
let mut autosuggestion_update = AutosuggestionUpdate::Remove;
if elt == EditableLineTag::Commandline {
let preserves_autosuggestion = self.try_apply_edit_to_autosuggestion(&edit);
let preserves_autosuggestion = try_apply_edit_to_autosuggestion(
&mut self.autosuggestion,
self.command_line.text(),
&edit,
);
if preserves_autosuggestion {
autosuggestion_update = AutosuggestionUpdate::Preserve
} else if !self.autosuggestion.is_empty()
@@ -5128,7 +5137,7 @@ fn exec_prompt(&mut self, full_prompt: bool, final_prompt: bool) {
}
}
#[derive(Default)]
#[derive(Default, Clone, PartialEq, Debug)]
pub(super) struct Autosuggestion {
/// The text to use, as an extension/replacement of the current line.
text: WString,
@@ -7309,4 +7318,99 @@ macro_rules! validate {
// See #6130
validate!(": (:^ ''", "", CompleteFlags::default(), false, ": (: ^''");
}
#[test]
fn test_try_apply_edit_to_autosuggestion() {
use super::Autosuggestion;
use super::try_apply_edit_to_autosuggestion;
use crate::editable_line::Edit;
macro_rules! validate {
(
$name:expr,
$autosuggestion:expr,
$command_line:expr,
$edit:expr,
$expected_autosuggestion:expr $(,)?
) => {
let mut autosuggestion = $autosuggestion;
let command_line = L!($command_line);
let edit = $edit;
let expected = $expected_autosuggestion;
let expect_success = expected.is_some();
assert_eq!(
try_apply_edit_to_autosuggestion(&mut autosuggestion, command_line, &edit),
expect_success,
"Test case '{}' failed: incorrect result",
$name
);
if expect_success {
assert_eq!(
autosuggestion,
expected.unwrap(),
"Test case '{}' failed: incorrect autosuggestion state",
$name
);
}
};
}
validate!(
"No autosuggestion",
Autosuggestion::default(),
"echo",
Edit::new(4..4, L!(" ").to_owned()),
None,
);
validate!(
"Matching edit",
Autosuggestion {
text: L!("echo hest").to_owned(),
search_string_range: 0..4,
icase: false,
is_whole_item_from_history: true,
},
"echo",
Edit::new(4..4, L!(" ").to_owned()),
Some(Autosuggestion {
text: L!("echo hest").to_owned(),
search_string_range: 0..5,
icase: false,
is_whole_item_from_history: true,
})
);
validate!(
"Non-matching edit",
Autosuggestion {
text: L!("echo hest").to_owned(),
search_string_range: 0..4,
icase: false,
is_whole_item_from_history: true,
},
"echo",
Edit::new(4..4, L!("f").to_owned()),
None,
);
validate!(
"Case-insensitive matching edit",
Autosuggestion {
text: L!("echo hest").to_owned(),
search_string_range: 0..4,
icase: true,
is_whole_item_from_history: true,
},
"echo",
Edit::new(4..4, L!(" H").to_owned()),
Some(Autosuggestion {
text: L!("echo hest").to_owned(),
search_string_range: 0..6,
icase: true,
is_whole_item_from_history: true,
})
);
}
}