mirror of
https://github.com/openai/codex.git
synced 2026-09-15 12:08:01 +00:00
Fix fuzzy match scoring within Unicode lowercase expansions (#45475)
## Why Matches starting inside a lowercase expansion such as `İ` → `i̇` could receive an incorrect prefix bonus or gap penalty, causing strings that lowercase identically to rank differently. ## What changed Track the first matched position in the lowercased text directly when calculating scores. Preserve original character indices for highlighting. ## Testing Add a skill popup regression test and snapshot covering ranking and highlighting for matches beginning at the combining dot in expanded and already-lowercase names. GitOrigin-RevId: 78a8b79f1defca9f76fde5df945b0b1ff4af25da
This commit is contained in:
@@ -331,6 +331,33 @@ mod tests {
|
||||
insta::assert_snapshot!("skill_popup_scrolled", render_popup(&popup, /*width*/ 72));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lowercase_expansion_preserves_match_ranking_and_highlighting() {
|
||||
let mut popup = SkillPopup::new(vec![
|
||||
named_mention_item("İx", &[]),
|
||||
named_mention_item("i\u{0307}x", &[]),
|
||||
named_mention_item("aİx", &[]),
|
||||
named_mention_item("ai\u{0307}x", &[]),
|
||||
]);
|
||||
popup.set_query("\u{0307}x");
|
||||
|
||||
// Each pair lowercases identically. The dot starts a contiguous match,
|
||||
// but not a prefix, even when it came from the expansion of 'İ'.
|
||||
assert_eq!(
|
||||
popup.filtered(),
|
||||
vec![
|
||||
(3, Some(vec![2, 3]), 0),
|
||||
(2, Some(vec![1, 2]), 0),
|
||||
(1, Some(vec![1, 2]), 0),
|
||||
(0, Some(vec![0, 1]), 0),
|
||||
]
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
"skill_popup_lowercase_expansion",
|
||||
render_popup(&popup, /*width*/ 48)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_name_match_sorting_beats_worse_secondary_search_term_matches() {
|
||||
let mut popup = SkillPopup::new(vec![
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/skill_popup.rs
|
||||
expression: "render_popup(&popup, 48)"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 48, height: 6 },
|
||||
content: [
|
||||
" ai̇x [Skill] ",
|
||||
" aİx [Skill] ",
|
||||
" i̇x [Skill] ",
|
||||
" İx [Skill] ",
|
||||
" ",
|
||||
" Press enter to insert or esc to close ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 0, fg: Cyan, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 14, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 3, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 5, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 7, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 14, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 4, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 7, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 14, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 4, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 7, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 14, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 8, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 13, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 27, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 30, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
/// lowercased haystack back to the original character index in `haystack`.
|
||||
/// This ensures the returned indices can be safely used with
|
||||
/// `str::chars().enumerate()` consumers for highlighting, even when
|
||||
/// lowercasing expands certain characters (e.g., ß → ss, İ → i̇).
|
||||
/// lowercasing expands certain characters (e.g., İ → i̇).
|
||||
pub fn fuzzy_match(haystack: &str, needle: &str) -> Option<(Vec<usize>, i32)> {
|
||||
if needle.is_empty() {
|
||||
return Some((Vec::new(), i32::MAX));
|
||||
@@ -26,6 +26,7 @@ pub fn fuzzy_match(haystack: &str, needle: &str) -> Option<(Vec<usize>, i32)> {
|
||||
let lowered_needle: Vec<char> = needle.to_lowercase().chars().collect();
|
||||
|
||||
let mut result_orig_indices: Vec<usize> = Vec::with_capacity(lowered_needle.len());
|
||||
let mut first_lower_pos: Option<usize> = None;
|
||||
let mut last_lower_pos: Option<usize> = None;
|
||||
let mut cur = 0usize;
|
||||
for &nc in lowered_needle.iter() {
|
||||
@@ -40,18 +41,12 @@ pub fn fuzzy_match(haystack: &str, needle: &str) -> Option<(Vec<usize>, i32)> {
|
||||
}
|
||||
let pos = found_at?;
|
||||
result_orig_indices.push(lowered_to_orig_char_idx[pos]);
|
||||
first_lower_pos.get_or_insert(pos);
|
||||
last_lower_pos = Some(pos);
|
||||
}
|
||||
|
||||
let first_lower_pos = if result_orig_indices.is_empty() {
|
||||
0usize
|
||||
} else {
|
||||
let target_orig = result_orig_indices[0];
|
||||
lowered_to_orig_char_idx
|
||||
.iter()
|
||||
.position(|&oi| oi == target_orig)
|
||||
.unwrap_or(0)
|
||||
};
|
||||
// Score using the actual lowered positions, even within a character expansion.
|
||||
let first_lower_pos = first_lower_pos?;
|
||||
// last defaults to first for single-hit; score = extra span between first/last hit
|
||||
// minus needle len (≥0).
|
||||
// Strongly reward prefix matches by subtracting 100 when the first hit is at index 0.
|
||||
|
||||
Reference in New Issue
Block a user