diff --git a/src/app/state/find.rs b/src/app/state/find.rs index b057577..3d9cd1c 100644 --- a/src/app/state/find.rs +++ b/src/app/state/find.rs @@ -26,11 +26,33 @@ impl TextEditor { }; let mut start = 0; - while let Some(pos) = search_content[start..].find(&query) { - let absolute_pos = start + pos; - self.find_matches - .push((absolute_pos, absolute_pos + query.len())); - start = absolute_pos + 1; + while start < search_content.len() { + let search_slice = if search_content.is_char_boundary(start) { + &search_content[start..] + } else { + // Find next valid boundary + while start < search_content.len() && !search_content.is_char_boundary(start) { + start += 1; + } + if start >= search_content.len() { + break; + } + &search_content[start..] + }; + + if let Some(pos) = search_slice.find(&query) { + let absolute_pos = start + pos; + self.find_matches + .push((absolute_pos, absolute_pos + query.len())); + + // Advance to next valid character boundary instead of just +1 + start = absolute_pos + 1; + while start < search_content.len() && !search_content.is_char_boundary(start) { + start += 1; + } + } else { + break; + } } if !self.find_matches.is_empty() { @@ -94,8 +116,8 @@ impl TextEditor { if let Some(active_tab) = self.get_active_tab() { let content = &active_tab.content; - let start_char = content[..start_byte.min(content.len())].chars().count(); - let end_char = content[..end_byte.min(content.len())].chars().count(); + let start_char = Self::safe_slice_to_pos(content, start_byte).chars().count(); + let end_char = Self::safe_slice_to_pos(content, end_byte).chars().count(); let text_edit_id = egui::Id::new("main_text_editor"); if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) { @@ -132,8 +154,7 @@ impl TextEditor { self.update_find_matches(); if let Some(active_tab) = self.get_active_tab() { - let replacement_end_char = active_tab.content - [..replacement_end.min(active_tab.content.len())] + let replacement_end_char = Self::safe_slice_to_pos(&active_tab.content, replacement_end) .chars() .count(); diff --git a/src/app/state/processing.rs b/src/app/state/processing.rs index 09cfff7..7d27afd 100644 --- a/src/app/state/processing.rs +++ b/src/app/state/processing.rs @@ -2,7 +2,7 @@ use super::editor::{TextEditor, TextProcessingResult}; use eframe::egui; impl TextEditor { - fn safe_slice_to_pos(content: &str, pos: usize) -> &str { + pub(crate) fn safe_slice_to_pos(content: &str, pos: usize) -> &str { let pos = pos.min(content.len()); let mut boundary_pos = pos; while boundary_pos > 0 && !content.is_char_boundary(boundary_pos) { diff --git a/src/ui/central_panel/find_highlight.rs b/src/ui/central_panel/find_highlight.rs index f0082ea..f8f04f8 100644 --- a/src/ui/central_panel/find_highlight.rs +++ b/src/ui/central_panel/find_highlight.rs @@ -1,5 +1,15 @@ use eframe::egui; +/// Safely get a string slice up to a byte position, ensuring UTF-8 boundaries +fn safe_slice_to_pos(content: &str, pos: usize) -> &str { + let pos = pos.min(content.len()); + let mut boundary_pos = pos; + while boundary_pos > 0 && !content.is_char_boundary(boundary_pos) { + boundary_pos -= 1; + } + &content[..boundary_pos] +} + pub(super) fn draw_find_highlights( ui: &mut egui::Ui, content: &str, @@ -44,7 +54,7 @@ fn draw_single_highlight( font_id: &egui::FontId, is_current_match: bool, ) { - let text_up_to_start = &content[..start_pos.min(content.len())]; + let text_up_to_start = safe_slice_to_pos(content, start_pos); let start_line = text_up_to_start.chars().filter(|&c| c == '\n').count(); if start_line >= galley.rows.len() { @@ -52,8 +62,8 @@ fn draw_single_highlight( } let line_start_byte_pos = text_up_to_start.rfind('\n').map(|pos| pos + 1).unwrap_or(0); - let line_start_char_pos = content[..line_start_byte_pos].chars().count(); - let start_char_pos = content[..start_pos].chars().count(); + let line_start_char_pos = safe_slice_to_pos(content, line_start_byte_pos).chars().count(); + let start_char_pos = safe_slice_to_pos(content, start_pos).chars().count(); let start_col = start_char_pos - line_start_char_pos; let lines: Vec<&str> = content.lines().collect();