diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index f1fa4738e30e5ed24e7815b61571b03e5a16252e..a25c02c1b5f72f1e85f532fcee244f0165a8a48e 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -1514,7 +1514,6 @@ impl PickerDelegate for DebugDelegate { let highlighted_location = HighlightedMatch { text: hit.string.clone(), highlight_positions: hit.positions.clone(), - char_count: hit.string.chars().count(), color: Color::Default, }; diff --git a/crates/picker/src/highlighted_match_with_paths.rs b/crates/picker/src/highlighted_match_with_paths.rs index 255e0150e8d6d9684b4f5b1315d4975f037ace48..6e91b997da2dab2ac61befd2f596e6f3a4207c85 100644 --- a/crates/picker/src/highlighted_match_with_paths.rs +++ b/crates/picker/src/highlighted_match_with_paths.rs @@ -10,36 +10,36 @@ pub struct HighlightedMatchWithPaths { pub struct HighlightedMatch { pub text: String, pub highlight_positions: Vec, - pub char_count: usize, pub color: Color, } impl HighlightedMatch { pub fn join(components: impl Iterator, separator: &str) -> Self { - let mut char_count = 0; - let separator_char_count = separator.chars().count(); + // Track a running byte offset and insert separators between parts. + let mut first = true; + let mut byte_offset = 0; let mut text = String::new(); let mut highlight_positions = Vec::new(); for component in components { - if char_count != 0 { + if !first { text.push_str(separator); - char_count += separator_char_count; + byte_offset += separator.len(); } + first = false; highlight_positions.extend( component .highlight_positions .iter() - .map(|position| position + char_count), + .map(|position| position + byte_offset), ); text.push_str(&component.text); - char_count += component.text.chars().count(); + byte_offset += component.text.len(); } Self { text, highlight_positions, - char_count, color: Color::Default, } } @@ -73,3 +73,36 @@ impl RenderOnce for HighlightedMatchWithPaths { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn join_offsets_positions_by_bytes_not_chars() { + // "αβγ" is 3 Unicode scalar values, 6 bytes in UTF-8. + let left_text = "αβγ".to_string(); + let right_text = "label".to_string(); + let left = HighlightedMatch { + text: left_text, + highlight_positions: vec![], + color: Color::Default, + }; + let right = HighlightedMatch { + text: right_text, + highlight_positions: vec![0, 1], + color: Color::Default, + }; + let joined = HighlightedMatch::join([left, right].into_iter(), ""); + + assert!( + joined + .highlight_positions + .iter() + .all(|&p| joined.text.is_char_boundary(p)), + "join produced non-boundary positions {:?} for text {:?}", + joined.highlight_positions, + joined.text + ); + } +} diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 2b011638218dd58b758f3af2e46836614e1c6780..ad7270d98c2597d77a71945c4aed97374cc6d8da 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -463,8 +463,7 @@ impl PickerDelegate for RecentProjectsDelegate { .map(|path| { let highlighted_text = highlights_for_path(path.as_ref(), &hit.positions, path_start_offset); - - path_start_offset += highlighted_text.1.char_count; + path_start_offset += highlighted_text.1.text.len(); highlighted_text }) .unzip(); @@ -590,34 +589,33 @@ fn highlights_for_path( path_start_offset: usize, ) -> (Option, HighlightedMatch) { let path_string = path.to_string_lossy(); - let path_char_count = path_string.chars().count(); + let path_text = path_string.to_string(); + let path_byte_len = path_text.len(); // Get the subset of match highlight positions that line up with the given path. // Also adjusts them to start at the path start let path_positions = match_positions .iter() .copied() .skip_while(|position| *position < path_start_offset) - .take_while(|position| *position < path_start_offset + path_char_count) + .take_while(|position| *position < path_start_offset + path_byte_len) .map(|position| position - path_start_offset) .collect::>(); // Again subset the highlight positions to just those that line up with the file_name // again adjusted to the start of the file_name let file_name_text_and_positions = path.file_name().map(|file_name| { - let text = file_name.to_string_lossy(); - let char_count = text.chars().count(); - let file_name_start = path_char_count - char_count; + let file_name_text = file_name.to_string_lossy().to_string(); + let file_name_start_byte = path_byte_len - file_name_text.len(); let highlight_positions = path_positions .iter() .copied() - .skip_while(|position| *position < file_name_start) - .take_while(|position| *position < file_name_start + char_count) - .map(|position| position - file_name_start) + .skip_while(|position| *position < file_name_start_byte) + .take_while(|position| *position < file_name_start_byte + file_name_text.len()) + .map(|position| position - file_name_start_byte) .collect::>(); HighlightedMatch { - text: text.to_string(), + text: file_name_text, highlight_positions, - char_count, color: Color::Default, } }); @@ -625,9 +623,8 @@ fn highlights_for_path( ( file_name_text_and_positions, HighlightedMatch { - text: path_string.to_string(), + text: path_text, highlight_positions: path_positions, - char_count: path_char_count, color: Color::Default, }, ) diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 3522e9522a6d32d729e7f0dca6731b2052f63f94..3b669e5a4d88405d32c77d88abf336c4c65f30c0 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -482,7 +482,6 @@ impl PickerDelegate for TasksModalDelegate { let highlighted_location = HighlightedMatch { text: hit.string.clone(), highlight_positions: hit.positions.clone(), - char_count: hit.string.chars().count(), color: Color::Default, }; let icon = match source_kind {