fix(tui): address transcript search review feedback

This commit is contained in:
Felipe Coury
2026-06-01 11:00:21 -03:00
parent ad01c93238
commit b972f2397d
2 changed files with 90 additions and 13 deletions
+3 -3
View File
@@ -1862,9 +1862,7 @@ impl TranscriptOverlay {
self.view.keymap.next_search_match.is_pressed(key_event);
let previous_match_pressed =
self.view.keymap.previous_search_match.is_pressed(key_event);
if self.view.keymap.start_search.is_pressed(key_event) {
self.activate_search();
} else if matches!(key_event.code, KeyCode::Esc) {
if matches!(key_event.code, KeyCode::Esc) {
self.cancel_search_mode();
} else if matches!(key_event.code, KeyCode::Backspace) {
self.search_backspace();
@@ -1888,6 +1886,8 @@ impl TranscriptOverlay {
text.push(ch);
self.append_search_text(&text);
}
} else if self.view.keymap.start_search.is_pressed(key_event) {
self.activate_search();
} else {
return self.handle_viewport_key_event(tui, key_event);
}
+87 -10
View File
@@ -1,5 +1,6 @@
use crate::history_cell::HistoryCell;
use crate::history_cell::HistoryRenderMode;
use crate::wrapping::wrap_ranges_trim;
use ratatui::text::Line;
use unicode_width::UnicodeWidthChar;
@@ -48,7 +49,9 @@ pub(crate) struct SearchLine {
scroll_line_index: usize,
width: u16,
folded_text: String,
plain_text: String,
display_cols_by_byte: Vec<u16>,
source_bytes_by_folded_byte: Vec<usize>,
owning_user_prompt_cell: Option<usize>,
}
@@ -71,13 +74,18 @@ impl SearchLine {
line_index,
scroll_line_index,
width,
folded_text: plain_text.to_lowercase(),
display_cols_by_byte: display_cols_by_byte(&plain_text),
folded_text: folded_text(&plain_text),
display_cols_by_byte: display_cols_by_folded_byte(&plain_text),
source_bytes_by_folded_byte: source_bytes_by_folded_byte(&plain_text),
plain_text,
owning_user_prompt_cell,
}
}
fn find_matches(&self, query: &str) -> Vec<SearchMatch> {
if query.is_empty() {
return Vec::new();
}
let mut matches = Vec::new();
let mut start = 0usize;
while let Some(found) = self.folded_text[start..].find(query) {
@@ -88,12 +96,15 @@ impl SearchLine {
line_index: self.line_index,
scroll_line_index: self
.scroll_line_index
.saturating_add(usize::from(self.display_col(match_start) / self.width)),
.saturating_add(self.wrapped_row(match_start)),
start_col: self.display_col(match_start),
end_col: self.display_col(match_end),
owning_user_prompt_cell: self.owning_user_prompt_cell,
});
start = match_start.saturating_add(1);
let Some(ch) = self.folded_text[match_start..].chars().next() else {
break;
};
start = match_start.saturating_add(ch.len_utf8());
}
matches
}
@@ -104,6 +115,18 @@ impl SearchLine {
.copied()
.unwrap_or_default()
}
fn wrapped_row(&self, folded_byte_index: usize) -> usize {
let source_byte_index = self
.source_bytes_by_folded_byte
.get(folded_byte_index)
.copied()
.unwrap_or(self.plain_text.len());
wrap_ranges_trim(&self.plain_text, usize::from(self.width.max(/*other*/ 1)))
.iter()
.position(|range| range.contains(&source_byte_index))
.unwrap_or_default()
}
}
pub(crate) fn transcript_search_lines(
@@ -171,13 +194,67 @@ fn push_search_lines(
}
}
fn display_cols_by_byte(text: &str) -> Vec<u16> {
let mut cols = vec![0; text.len().saturating_add(1)];
fn folded_text(text: &str) -> String {
text.chars().flat_map(char::to_lowercase).collect()
}
fn display_cols_by_folded_byte(text: &str) -> Vec<u16> {
let mut cols = vec![0];
let mut col = 0u16;
for (byte_index, ch) in text.char_indices() {
cols[byte_index] = col;
col = col.saturating_add(u16::try_from(ch.width().unwrap_or(0)).unwrap_or(u16::MAX));
for ch in text.chars() {
let next_col =
col.saturating_add(u16::try_from(ch.width().unwrap_or(0)).unwrap_or(u16::MAX));
cols.extend(std::iter::repeat_n(
next_col,
ch.to_lowercase().map(char::len_utf8).sum(),
));
col = next_col;
}
cols[text.len()] = col;
cols
}
fn source_bytes_by_folded_byte(text: &str) -> Vec<usize> {
let mut source_bytes = vec![0];
for (source_byte_index, ch) in text.char_indices() {
let next_source_byte = source_byte_index.saturating_add(ch.len_utf8());
source_bytes.extend(std::iter::repeat_n(
next_source_byte,
ch.to_lowercase().map(char::len_utf8).sum(),
));
}
source_bytes
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn search_match_iteration_advances_on_utf8_boundaries() {
let line = SearchLine::from_line(
/*renderable_index*/ 0,
/*line_index*/ 0,
/*scroll_line_index*/ 0,
/*width*/ 80,
&Line::from("éé"),
/*owning_user_prompt_cell*/ None,
);
assert_eq!(line.find_matches("é").len(), 2);
}
#[test]
fn search_match_scroll_offset_uses_word_wrapping() {
let line = SearchLine::from_line(
/*renderable_index*/ 0,
/*line_index*/ 0,
/*scroll_line_index*/ 0,
/*width*/ 10,
&Line::from("aaaaa aaaaa needle"),
/*owning_user_prompt_cell*/ None,
);
assert_eq!(line.find_matches("needle")[0].scroll_line_index, 2);
}
}