fix: match double-width roman characters (closes #1149)

This commit is contained in:
Loric ANDRE 2026-08-07 11:55:11 +02:00
parent b17e93890a
commit 1dead2b3c6
3 changed files with 50 additions and 3 deletions

View file

@ -1,6 +1,7 @@
//! Byte/Char helpers
use super::Score;
use super::constants::SEPARATOR_TABLE;
use crate::fuzzy_matcher::util::char_equal;
use memchr::{memchr, memrchr};
pub(super) trait Atom: PartialEq + Into<char> + Copy {
@ -112,9 +113,15 @@ impl Atom for u8 {
}
impl Atom for char {
#[inline(always)]
fn eq_ignore_case(self, b: Self) -> bool {
self.to_lowercase().eq(b.to_lowercase())
fn eq(self, other: Self, respect_case: bool) -> bool {
char_equal(self, other, respect_case)
}
#[inline(always)]
fn eq_ignore_case(self, b: Self) -> bool {
char_equal(self, b, false)
}
#[inline(always)]
fn is_lowercase(self) -> bool {
self.is_lowercase()
@ -179,6 +186,8 @@ mod tests {
fn char_atom_eq_and_case() {
assert!('a'.eq('A', false));
assert!(!'a'.eq('A', true));
assert!(''.eq('a', true));
assert!(''.eq('a', false));
assert!('a'.is_lowercase());
assert!(!'A'.is_lowercase());
// Default (non-SIMD) find impls for char.

View file

@ -27,6 +27,15 @@ fn test_match_or_not() {
);
}
#[test]
fn fullwidth_ascii_matches_ascii_query() {
let matcher = SkimMatcherV2::default();
let (_, indices) = matcher
.fuzzy_indices("", "abc")
.expect("fullwidth ASCII should match");
assert_eq!(indices, vec![0, 1, 2]);
}
#[test]
fn test_match_quality() {
let matcher = SkimMatcherV2::default().ignore_case();

View file

@ -22,15 +22,34 @@ pub fn cheap_matches(choice: &[char], pattern: &[char], case_sensitive: bool) ->
}
}
/// Given 2 character, check if they are equal (considering ascii case)
/// Convert the Unicode fullwidth form of an ASCII character to ASCII.
#[inline]
fn narrow_ascii_width(ch: char) -> char {
match ch {
'\u{3000}' => ' ',
'\u{FF01}'..='\u{FF5E}' => char::from_u32(ch as u32 - 0xFEE0).unwrap_or(ch),
_ => ch,
}
}
/// Given two characters, check if they are equal after folding ASCII width and,
/// when requested, case.
/// e.g. ('a', 'A', true) => false
/// e.g. ('a', 'A', false) => true
/// e.g. ('', 'a', true) => true
#[inline]
pub fn char_equal(a: char, b: char, case_sensitive: bool) -> bool {
if a == b {
return true;
}
let a = narrow_ascii_width(a);
let b = narrow_ascii_width(b);
if a == b {
return true;
}
if case_sensitive {
return false;
}
@ -156,6 +175,16 @@ mod tests {
assert!(!char_equal('a', 'b', false));
}
#[test]
fn char_equal_folds_fullwidth_ascii() {
assert!(char_equal('', 'a', true));
assert!(char_equal('', 'A', true));
assert!(!char_equal('', 'a', true));
assert!(char_equal('', 'a', false));
assert!(char_equal('', '1', true));
assert!(char_equal(' ', ' ', true));
}
#[test]
fn char_equal_multichar_lowercase_mismatch() {
// 'İ' (U+0130) lowercases to two chars ("i" + combining dot), so it is