fix(composer): use wrap_input_lines_for_mouse, clamp selection to input length
- Replace visible_line_char_ranges with wrap_input_lines_for_mouse for accurate mouse selection - Clamp selection_anchor and cursor_position to char_count - Clear selection on history navigation to prevent stale highlights - Add test for history-navigation-clears-stale-selection
This commit is contained in:
@@ -3774,8 +3774,9 @@ impl App {
|
||||
/// Return the (start, end) of the active selection, or `None`.
|
||||
/// `start` is inclusive, `end` is exclusive; both are char indices.
|
||||
pub fn selection_range(&self) -> Option<(usize, usize)> {
|
||||
let anchor = self.selection_anchor?;
|
||||
let cursor = self.cursor_position;
|
||||
let total = char_count(&self.input);
|
||||
let anchor = self.selection_anchor?.min(total);
|
||||
let cursor = self.cursor_position.min(total);
|
||||
if anchor == cursor {
|
||||
return None;
|
||||
}
|
||||
@@ -4510,6 +4511,7 @@ impl App {
|
||||
self.history_index = Some(new_index);
|
||||
self.input = self.input_history[new_index].clone();
|
||||
self.cursor_position = char_count(&self.input);
|
||||
self.selection_anchor = None;
|
||||
self.selected_attachment_index = None;
|
||||
self.slash_menu_hidden = false;
|
||||
self.paste_burst.clear_after_explicit_paste();
|
||||
@@ -4526,6 +4528,7 @@ impl App {
|
||||
self.history_index = Some(i + 1);
|
||||
self.input = self.input_history[i + 1].clone();
|
||||
self.cursor_position = char_count(&self.input);
|
||||
self.selection_anchor = None;
|
||||
self.selected_attachment_index = None;
|
||||
self.slash_menu_hidden = false;
|
||||
self.paste_burst.clear_after_explicit_paste();
|
||||
@@ -4534,6 +4537,7 @@ impl App {
|
||||
if let Some(draft) = self.history_navigation_draft.take() {
|
||||
self.input = draft.input;
|
||||
self.cursor_position = draft.cursor.min(char_count(&self.input));
|
||||
self.selection_anchor = None;
|
||||
self.selected_attachment_index = None;
|
||||
self.slash_menu_hidden = false;
|
||||
self.paste_burst.clear_after_explicit_paste();
|
||||
@@ -5914,6 +5918,22 @@ mod tests {
|
||||
assert!(app.history_index.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_history_navigation_clears_stale_selection() {
|
||||
let mut app = App::new(test_options(false), &Config::default());
|
||||
app.input_history.push("previous input".to_string());
|
||||
app.input = "hello world".to_string();
|
||||
app.cursor_position = "hello ".chars().count();
|
||||
app.selection_anchor = Some(app.input.chars().count());
|
||||
|
||||
app.history_up();
|
||||
assert_eq!(app.input, "previous input");
|
||||
assert!(app.selection_anchor.is_none());
|
||||
|
||||
app.insert_char('x');
|
||||
assert_eq!(app.input, "previous inputx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_history_restores_empty_draft_at_end_of_navigation() {
|
||||
let mut app = App::new(test_options(false), &Config::default());
|
||||
|
||||
@@ -667,12 +667,13 @@ impl Renderable for ComposerWidget<'_> {
|
||||
Style::default().fg(palette::TEXT_MUTED).italic(),
|
||||
)));
|
||||
} else if let Some((sel_start, sel_end)) = self.app.selection_range() {
|
||||
let line_ranges = visible_line_char_ranges(
|
||||
&self.app.input,
|
||||
&visible_lines,
|
||||
content_width,
|
||||
scroll_offset,
|
||||
);
|
||||
let line_ranges: Vec<(usize, usize)> =
|
||||
wrap_input_lines_for_mouse(&self.app.input, content_width)
|
||||
.into_iter()
|
||||
.skip(scroll_offset)
|
||||
.take(visible_lines.len())
|
||||
.map(|(start, text)| (start, start + text.chars().count()))
|
||||
.collect();
|
||||
for (line_text, (line_start, line_end)) in visible_lines.iter().zip(line_ranges.iter())
|
||||
{
|
||||
let spans = line_spans_with_selection(
|
||||
@@ -2443,58 +2444,6 @@ fn wrap_text(text: &str, width: usize) -> Vec<String> {
|
||||
lines
|
||||
}
|
||||
|
||||
/// Compute the (char_start, char_end) range for each visible wrapped line.
|
||||
/// `char_start` is inclusive, `char_end` is exclusive.
|
||||
/// `scroll_offset` is the number of wrapped lines skipped from the top.
|
||||
fn visible_line_char_ranges(
|
||||
input: &str,
|
||||
visible_lines: &[String],
|
||||
width: usize,
|
||||
scroll_offset: usize,
|
||||
) -> Vec<(usize, usize)> {
|
||||
if input.is_empty() || width == 0 {
|
||||
return vec![(0, 0); visible_lines.len()];
|
||||
}
|
||||
|
||||
let mut ranges = Vec::new();
|
||||
let mut char_idx = 0usize;
|
||||
let mut line_start = 0usize;
|
||||
let mut line_width = 0usize;
|
||||
|
||||
for g in input.graphemes(true) {
|
||||
if g == "\n" {
|
||||
ranges.push((line_start, char_idx));
|
||||
char_idx += 1;
|
||||
line_start = char_idx;
|
||||
line_width = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
let gw = g.width();
|
||||
if line_width + gw > width && line_width > 0 {
|
||||
ranges.push((line_start, char_idx));
|
||||
line_start = char_idx;
|
||||
line_width = 0;
|
||||
}
|
||||
char_idx += g.chars().count();
|
||||
line_width += gw;
|
||||
if line_width >= width {
|
||||
ranges.push((line_start, char_idx));
|
||||
line_start = char_idx;
|
||||
line_width = 0;
|
||||
}
|
||||
}
|
||||
ranges.push((line_start, char_idx));
|
||||
|
||||
// Use the actual scroll_offset to align with visible_lines.
|
||||
let start = scroll_offset.min(ranges.len());
|
||||
ranges
|
||||
.into_iter()
|
||||
.skip(start)
|
||||
.take(visible_lines.len())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn line_spans_with_selection<'a>(
|
||||
line: &'a str,
|
||||
line_start: usize,
|
||||
|
||||
Reference in New Issue
Block a user