diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index 201890ad..d8068956 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -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()); diff --git a/crates/tui/src/tui/widgets/mod.rs b/crates/tui/src/tui/widgets/mod.rs index 58cdffb7..2c478a29 100644 --- a/crates/tui/src/tui/widgets/mod.rs +++ b/crates/tui/src/tui/widgets/mod.rs @@ -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 { 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,