use crate::{query::key_kind_allowed, QueryVariant, SearchKey};
use nucleo_matcher::{chars, Config as NucleoConfig, Matcher, Utf32Str};
use std::borrow::Cow;
use unicode_normalization::UnicodeNormalization;
const SCORE_MATCH: i64 = 160;
const SCORE_GAP_START: i64 = -30;
const SCORE_GAP_EXTENSION: i64 = -10;
const BONUS_BOUNDARY: i64 = 80;
const BONUS_BOUNDARY_WHITE: i64 = 100;
const BONUS_BOUNDARY_DELIMITER: i64 = 90;
const BONUS_CAMEL_OR_NUMBER: i64 = 70;
const BONUS_CONSECUTIVE: i64 = 40;
pub(crate) const BONUS_CASE_EXACT: i64 = 75;
const _: () = assert!(
BONUS_CASE_EXACT > BONUS_CAMEL_OR_NUMBER && BONUS_CASE_EXACT < BONUS_BOUNDARY,
"the exact-case bonus must break a camelCase tie without outranking a word boundary"
);
const BONUS_FIRST_CHAR_MULTIPLIER: i64 = 2;
const START_POSITION_PENALTY: i64 = 2;
const TEXT_LENGTH_PENALTY_DIVISOR: i64 = 8;
pub trait MatcherBackend {
fn score(&mut self, pattern: &str, text: &str) -> Option<i64>;
fn folds_case(&self) -> bool {
false
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct GreedyMatcher {
pub case_sensitive: bool,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ExactMatcher {
pub case_sensitive: bool,
}
impl GreedyMatcher {
pub fn new(case_sensitive: bool) -> Self {
Self { case_sensitive }
}
}
impl ExactMatcher {
pub fn new(case_sensitive: bool) -> Self {
Self { case_sensitive }
}
}
#[derive(Clone, Debug)]
pub struct NucleoMatcher {
matcher: Matcher,
pattern_buf: Vec<char>,
text_buf: Vec<char>,
folded: FoldedPattern,
}
impl NucleoMatcher {
pub fn new(case_sensitive: bool) -> Self {
let mut config = NucleoConfig::DEFAULT;
config.ignore_case = !case_sensitive;
Self {
matcher: Matcher::new(config),
pattern_buf: Vec::new(),
text_buf: Vec::new(),
folded: FoldedPattern::default(),
}
}
}
#[derive(Clone, Debug, Default)]
struct FoldedPattern {
source: String,
text: String,
needed: bool,
}
impl FoldedPattern {
#[inline]
fn pattern<'a>(&'a mut self, pattern: &'a str) -> &'a str {
if self.source != pattern {
self.source.clear();
self.source.push_str(pattern);
self.needed = pattern.chars().any(chars::is_upper_case);
if self.needed {
self.text.clear();
self.text.extend(pattern.chars().map(chars::to_lower_case));
}
}
if self.needed {
&self.text
} else {
pattern
}
}
}
impl Default for NucleoMatcher {
fn default() -> Self {
Self::new(false)
}
}
impl MatcherBackend for GreedyMatcher {
fn score(&mut self, pattern: &str, text: &str) -> Option<i64> {
score_text(pattern, text, self.case_sensitive)
}
fn folds_case(&self) -> bool {
!self.case_sensitive
}
}
impl MatcherBackend for ExactMatcher {
fn score(&mut self, pattern: &str, text: &str) -> Option<i64> {
score_exact_text(pattern, text, self.case_sensitive)
}
fn folds_case(&self) -> bool {
!self.case_sensitive
}
}
impl MatcherBackend for NucleoMatcher {
fn score(&mut self, pattern: &str, text: &str) -> Option<i64> {
let Self {
matcher,
pattern_buf,
text_buf,
folded,
} = self;
let pattern = if matcher.config.ignore_case {
folded.pattern(pattern)
} else {
pattern
};
let pattern = Utf32Str::new(pattern, pattern_buf);
let text = Utf32Str::new(text, text_buf);
matcher.fuzzy_match(text, pattern).map(i64::from)
}
fn folds_case(&self) -> bool {
false
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MatchPositions {
pub char_indices: Vec<usize>,
}
impl MatchPositions {
pub fn is_empty(&self) -> bool {
self.char_indices.is_empty()
}
}
pub fn score_key(variant: &QueryVariant, key: &SearchKey, case_sensitive: bool) -> Option<i64> {
if !key_kind_allowed(variant, key.kind) {
return None;
}
score_text(&variant.text, &key.text, case_sensitive)
.map(|score| score + i64::from(key.weight + variant.weight))
}
pub fn score_text(pattern: &str, text: &str, case_sensitive: bool) -> Option<i64> {
if pattern.is_empty() {
return Some(0);
}
if pattern.is_ascii() && text.is_ascii() {
return if case_sensitive {
score_ascii_text::<true>(pattern, text)
} else {
score_ascii_text::<false>(pattern, text)
};
}
if case_sensitive {
return score_unicode_text::<true>(pattern, text);
}
score_unicode_text::<false>(pattern, text).or_else(|| {
retry_with_rewritten_multi_char_lowercase(pattern, text, score_rewritten_unicode_text)
})
}
fn fold_char<const CASE_SENSITIVE: bool>(ch: char) -> char {
if CASE_SENSITIVE {
ch
} else if ch.is_ascii() {
ch.to_ascii_lowercase()
} else {
let mut lower = ch.to_lowercase();
match (lower.len(), lower.next()) {
(1, Some(lower)) => lower,
_ => ch,
}
}
}
pub(crate) fn fold_case_char(ch: char) -> char {
fold_char::<false>(ch)
}
pub(crate) const MULTI_CHAR_LOWERCASE: char = 'İ';
pub(crate) const MULTI_CHAR_LOWERCASE_EXPANSION: &str = "i\u{307}";
const MULTI_CHAR_LOWERCASE_LEAD_BYTE: u8 = 0xC4;
const _: () = assert!(
(MULTI_CHAR_LOWERCASE as u32) >= 0x80
&& (MULTI_CHAR_LOWERCASE as u32) < 0x800
&& MULTI_CHAR_LOWERCASE_LEAD_BYTE == 0xC0 | ((MULTI_CHAR_LOWERCASE as u32) >> 6) as u8,
"the lead byte must be the one a two-byte UTF-8 encoding of the character starts with"
);
fn retry_with_rewritten_multi_char_lowercase(
pattern: &str,
text: &str,
score: fn(&str, &str) -> Option<i64>,
) -> Option<i64> {
let pattern_composed = contains_multi_char_lowercase(pattern);
let text_composed = contains_multi_char_lowercase(text);
if !pattern_composed && !text_composed {
return None;
}
let composed_pattern = compose_multi_char_lowercase(pattern);
let composed_text = compose_multi_char_lowercase(text);
if composed_pattern.is_some() || composed_text.is_some() {
let composed = score(
composed_pattern.as_deref().unwrap_or(pattern),
composed_text.as_deref().unwrap_or(text),
);
if composed.is_some() {
return composed;
}
}
score(
&expand_multi_char_lowercase(pattern, pattern_composed),
&expand_multi_char_lowercase(text, text_composed),
)
}
fn contains_multi_char_lowercase(text: &str) -> bool {
text.as_bytes().contains(&MULTI_CHAR_LOWERCASE_LEAD_BYTE) && text.contains(MULTI_CHAR_LOWERCASE)
}
fn compose_multi_char_lowercase(text: &str) -> Option<String> {
if !text.contains(MULTI_CHAR_LOWERCASE_EXPANSION) {
return None;
}
let mut composed = [0u8; 4];
Some(text.replace(
MULTI_CHAR_LOWERCASE_EXPANSION,
MULTI_CHAR_LOWERCASE.encode_utf8(&mut composed),
))
}
fn expand_multi_char_lowercase(text: &str, contains: bool) -> Cow<'_, str> {
if contains {
Cow::Owned(text.replace(MULTI_CHAR_LOWERCASE, MULTI_CHAR_LOWERCASE_EXPANSION))
} else {
Cow::Borrowed(text)
}
}
const NAIVE_FOLDED_SCAN_BUDGET: usize = 4096;
fn naive_folded_scan_affordable(text_len: usize, pattern_len: usize) -> bool {
(text_len.saturating_sub(pattern_len) + 1).saturating_mul(pattern_len)
<= NAIVE_FOLDED_SCAN_BUDGET
}
thread_local! {
static FOLD_SCRATCH: std::cell::Cell<String> = const { std::cell::Cell::new(String::new()) };
}
const FOLD_SCRATCH_RETAINED_BYTES: usize = 64 * 1024;
fn find_folded_index(
text: impl Iterator<Item = char>,
pattern: impl Iterator<Item = char>,
) -> Option<usize> {
let mut scratch = FOLD_SCRATCH.take();
scratch.clear();
scratch.extend(text);
let split = scratch.len();
scratch.extend(pattern);
let (text, pattern) = scratch.split_at(split);
let found = text
.find(pattern)
.map(|offset| text[..offset].chars().count());
if scratch.capacity() > FOLD_SCRATCH_RETAINED_BYTES {
scratch.shrink_to(FOLD_SCRATCH_RETAINED_BYTES);
}
FOLD_SCRATCH.set(scratch);
found
}
fn case_exact_bonus<const CASE_SENSITIVE: bool>(case_exact: bool) -> i64 {
if !CASE_SENSITIVE && case_exact {
BONUS_CASE_EXACT
} else {
0
}
}
fn fold_ascii<const CASE_SENSITIVE: bool>(byte: u8) -> u8 {
if CASE_SENSITIVE {
byte
} else {
byte.to_ascii_lowercase()
}
}
fn score_unicode_text<const CASE_SENSITIVE: bool>(pattern: &str, text: &str) -> Option<i64> {
score_unicode_text_with::<CASE_SENSITIVE, true>(pattern, text)
}
fn score_rewritten_unicode_text(pattern: &str, text: &str) -> Option<i64> {
score_unicode_text_with::<false, false>(pattern, text)
}
fn score_unicode_text_with<const CASE_SENSITIVE: bool, const CASE_EXACT_ALLOWED: bool>(
pattern: &str,
text: &str,
) -> Option<i64> {
let pattern_chars: Vec<char> = pattern.chars().collect();
let text_chars: Vec<char> = text.chars().collect();
let compact_score = compact_char_match_score::<CASE_SENSITIVE, CASE_EXACT_ALLOWED>(
&pattern_chars,
&text_chars,
)?;
let exact_bonus = if CASE_SENSITIVE {
whole_text_bonus(pattern, text)
} else {
folded_whole_text_bonus(&pattern_chars, &text_chars)
};
Some(exact_bonus + compact_score)
}
fn whole_text_bonus(pattern: &str, text: &str) -> i64 {
if pattern == text {
10_000
} else if text.starts_with(pattern) {
8_000
} else if text.contains(pattern) {
6_000
} else {
0
}
}
fn folded_whole_text_bonus(pattern: &[char], text: &[char]) -> i64 {
if folded_chars_eq(pattern, text) {
10_000
} else if text.len() >= pattern.len() && folded_chars_eq(pattern, &text[..pattern.len()]) {
8_000
} else if folded_chars_contain(text, pattern) {
6_000
} else {
0
}
}
fn folded_chars_eq(left: &[char], right: &[char]) -> bool {
left.len() == right.len()
&& left
.iter()
.zip(right)
.all(|(left, right)| fold_char::<false>(*left) == fold_char::<false>(*right))
}
fn folded_chars_contain(text: &[char], pattern: &[char]) -> bool {
let Some(last_start) = text.len().checked_sub(pattern.len()) else {
return false;
};
if !naive_folded_scan_affordable(text.len(), pattern.len()) {
return find_folded_index(
text.iter().copied().map(fold_char::<false>),
pattern.iter().copied().map(fold_char::<false>),
)
.is_some();
}
(0..=last_start).any(|start| folded_chars_eq(pattern, &text[start..start + pattern.len()]))
}
fn compact_char_match_score<const CASE_SENSITIVE: bool, const CASE_EXACT_ALLOWED: bool>(
pattern: &[char],
text: &[char],
) -> Option<i64> {
if pattern.is_empty() {
return Some(0);
}
if pattern.len() > text.len() {
return None;
}
let mut pattern_index = 0usize;
let mut wanted = fold_char::<CASE_SENSITIVE>(pattern[0]);
let mut end = None;
for (text_index, &text_ch) in text.iter().enumerate() {
if fold_char::<CASE_SENSITIVE>(text_ch) == wanted {
pattern_index += 1;
if pattern_index == pattern.len() {
end = Some(text_index);
break;
}
wanted = fold_char::<CASE_SENSITIVE>(pattern[pattern_index]);
}
}
let mut text_index = end?;
let mut score = 1000;
let mut right_match: Option<usize> = None;
let mut first = 0usize;
let mut case_exact = true;
for pattern_index in (0..pattern.len()).rev() {
let wanted = fold_char::<CASE_SENSITIVE>(pattern[pattern_index]);
while fold_char::<CASE_SENSITIVE>(text[text_index]) != wanted {
if text_index == 0 {
return None;
}
text_index -= 1;
}
let position = text_index;
first = position;
if !CASE_SENSITIVE && text[position] != pattern[pattern_index] {
case_exact = false;
}
score += SCORE_MATCH;
let bonus = char_bonus_at(text, position);
if pattern_index == 0 {
score += bonus * BONUS_FIRST_CHAR_MULTIPLIER;
} else {
score += bonus;
}
if let Some(right_match) = right_match {
if right_match == position + 1 {
score += BONUS_CONSECUTIVE;
} else {
let gap = right_match.saturating_sub(position + 1) as i64;
score += SCORE_GAP_START + SCORE_GAP_EXTENSION * gap.saturating_sub(1);
}
}
right_match = Some(position);
if pattern_index > 0 {
if text_index == 0 {
return None;
}
text_index -= 1;
}
}
Some(
score + case_exact_bonus::<CASE_SENSITIVE>(CASE_EXACT_ALLOWED && case_exact)
- first as i64 * START_POSITION_PENALTY
- text.len() as i64 / TEXT_LENGTH_PENALTY_DIVISOR,
)
}
fn char_bonus_at(text: &[char], position: usize) -> i64 {
if position == 0 {
return BONUS_BOUNDARY_WHITE;
}
let previous = text[position - 1];
let current = text[position];
if previous.is_whitespace() {
BONUS_BOUNDARY_WHITE
} else if is_path_or_field_delimiter(previous) {
BONUS_BOUNDARY_DELIMITER
} else if !previous.is_alphanumeric() {
BONUS_BOUNDARY
} else if previous.is_lowercase() && current.is_uppercase()
|| !previous.is_numeric() && current.is_numeric()
{
BONUS_CAMEL_OR_NUMBER
} else {
0
}
}
pub fn match_positions(pattern: &str, text: &str, case_sensitive: bool) -> Option<MatchPositions> {
if pattern.is_empty() {
return Some(MatchPositions {
char_indices: Vec::new(),
});
}
let pattern = comparable_chars(pattern, case_sensitive);
let text_comparable = comparable_indexed_chars(text, case_sensitive);
let text_chars: Vec<char> = text.chars().collect();
contiguous_text_positions(&pattern, &text_comparable)
.or_else(|| best_subsequence_positions(&pattern, &text_comparable, &text_chars))
.map(|char_indices| MatchPositions { char_indices })
}
fn comparable_chars(text: &str, case_sensitive: bool) -> Vec<char> {
comparable_indexed_chars(text, case_sensitive)
.into_iter()
.map(|(_, ch)| ch)
.collect()
}
fn comparable_indexed_chars(text: &str, case_sensitive: bool) -> Vec<(usize, char)> {
let mut out = Vec::new();
for (char_index, ch) in text.chars().enumerate() {
for normalized in std::iter::once(ch).nfkc() {
if case_sensitive {
out.push((char_index, comparable_char(normalized)));
} else {
out.extend(
normalized
.to_lowercase()
.map(|lower| (char_index, comparable_char(lower))),
);
}
}
}
out
}
fn comparable_char(ch: char) -> char {
let folded = crate::normalize::fold_width_compatible_char(ch);
if folded != ch {
folded
} else if ('ァ'..='ヶ').contains(&ch) {
char::from_u32(ch as u32 - 0x60).unwrap_or(ch)
} else {
ch
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct PositionCandidate {
score: i64,
positions: Vec<usize>,
}
fn best_subsequence_positions(
pattern: &[char],
text_comparable: &[(usize, char)],
text_chars: &[char],
) -> Option<Vec<usize>> {
if pattern.len() > text_comparable.len() {
return None;
}
let mut states = Vec::new();
for &(text_index, text_ch) in text_comparable {
if pattern.first() == Some(&text_ch) {
states.push(Some(PositionCandidate {
score: match_position_score(text_chars, text_index) - text_index as i64 * 2,
positions: vec![text_index],
}));
} else {
states.push(None);
}
}
for &pattern_ch in &pattern[1..] {
let mut next_states = vec![None; text_comparable.len()];
for (text_offset, &(text_index, text_ch)) in text_comparable.iter().enumerate() {
if text_ch != pattern_ch {
continue;
}
let mut best = None;
for previous in states[..text_offset].iter().flatten() {
let Some(&previous_index) = previous.positions.last() else {
continue;
};
if previous_index >= text_index {
continue;
}
let mut positions = previous.positions.clone();
positions.push(text_index);
let gap = text_index.saturating_sub(previous_index + 1) as i64;
let consecutive_bonus = if text_index == previous_index + 1 {
160
} else {
0
};
let score = previous.score
+ match_position_score(text_chars, text_index)
+ consecutive_bonus
- gap * 4;
let candidate = PositionCandidate { score, positions };
if best
.as_ref()
.is_none_or(|current| better_position_candidate(&candidate, current))
{
best = Some(candidate);
}
}
next_states[text_offset] = best;
}
states = next_states;
}
states
.into_iter()
.flatten()
.max_by(compare_position_candidate)
.map(|candidate| candidate.positions)
}
fn match_position_score(text_chars: &[char], position: usize) -> i64 {
let boundary_bonus = if is_boundary(text_chars, position) {
90
} else {
0
};
100 + boundary_bonus
}
fn better_position_candidate(left: &PositionCandidate, right: &PositionCandidate) -> bool {
compare_position_candidate(left, right).is_gt()
}
fn compare_position_candidate(
left: &PositionCandidate,
right: &PositionCandidate,
) -> std::cmp::Ordering {
left.score
.cmp(&right.score)
.then_with(|| span_len(right).cmp(&span_len(left)))
.then_with(|| right.positions.cmp(&left.positions))
}
fn span_len(candidate: &PositionCandidate) -> usize {
match (candidate.positions.first(), candidate.positions.last()) {
(Some(first), Some(last)) => last - first + 1,
_ => 0,
}
}
fn contiguous_text_positions(
pattern: &[char],
text_comparable: &[(usize, char)],
) -> Option<Vec<usize>> {
if pattern.len() > text_comparable.len() {
return None;
}
text_comparable
.windows(pattern.len())
.find(|window| window.iter().map(|(_, ch)| ch).eq(pattern.iter()))
.map(|window| window.iter().map(|(index, _)| *index).collect())
}
fn score_ascii_text<const CASE_SENSITIVE: bool>(pattern: &str, text: &str) -> Option<i64> {
let pattern_bytes = pattern.as_bytes();
let text_bytes = text.as_bytes();
let compact_score = compact_ascii_match_score::<CASE_SENSITIVE>(pattern_bytes, text_bytes)?;
let exact_bonus = if CASE_SENSITIVE {
whole_text_bonus(pattern, text)
} else {
folded_ascii_whole_text_bonus(pattern_bytes, text_bytes)
};
Some(exact_bonus + compact_score)
}
fn folded_ascii_whole_text_bonus(pattern: &[u8], text: &[u8]) -> i64 {
if text.eq_ignore_ascii_case(pattern) {
10_000
} else if text.len() >= pattern.len() && text[..pattern.len()].eq_ignore_ascii_case(pattern) {
8_000
} else if find_ascii_ignore_case(text, pattern).is_some() {
6_000
} else {
0
}
}
fn find_ascii_ignore_case(text: &[u8], pattern: &[u8]) -> Option<usize> {
debug_assert!(text.is_ascii() && pattern.is_ascii());
let Some((&first, rest)) = pattern.split_first() else {
return Some(0);
};
let first = first.to_ascii_lowercase();
let last_start = text.len().checked_sub(pattern.len())?;
if rest.is_empty() {
return text
.iter()
.position(|byte| byte.to_ascii_lowercase() == first);
}
if !naive_folded_scan_affordable(text.len(), pattern.len()) {
return find_folded_index(
text.iter()
.map(|byte| char::from(byte.to_ascii_lowercase())),
pattern
.iter()
.map(|byte| char::from(byte.to_ascii_lowercase())),
);
}
(0..=last_start).find(|&start| {
text[start].to_ascii_lowercase() == first
&& text[start + 1..start + pattern.len()].eq_ignore_ascii_case(rest)
})
}
fn compact_ascii_match_score<const CASE_SENSITIVE: bool>(
pattern: &[u8],
text: &[u8],
) -> Option<i64> {
if pattern.is_empty() {
return Some(0);
}
if pattern.len() > text.len() {
return None;
}
let mut pattern_index = 0usize;
let mut wanted = fold_ascii::<CASE_SENSITIVE>(pattern[0]);
let mut end = None;
for (text_index, &text_byte) in text.iter().enumerate() {
if fold_ascii::<CASE_SENSITIVE>(text_byte) == wanted {
pattern_index += 1;
if pattern_index == pattern.len() {
end = Some(text_index);
break;
}
wanted = fold_ascii::<CASE_SENSITIVE>(pattern[pattern_index]);
}
}
let mut text_index = end?;
let mut score = 1000;
let mut right_match: Option<usize> = None;
let mut first = 0usize;
let mut case_exact = true;
for pattern_index in (0..pattern.len()).rev() {
let wanted = fold_ascii::<CASE_SENSITIVE>(pattern[pattern_index]);
while fold_ascii::<CASE_SENSITIVE>(text[text_index]) != wanted {
if text_index == 0 {
return None;
}
text_index -= 1;
}
let position = text_index;
first = position;
if !CASE_SENSITIVE && text[position] != pattern[pattern_index] {
case_exact = false;
}
score += SCORE_MATCH;
let bonus = ascii_bonus_at(text, position);
if pattern_index == 0 {
score += bonus * BONUS_FIRST_CHAR_MULTIPLIER;
} else {
score += bonus;
}
if let Some(right_match) = right_match {
if right_match == position + 1 {
score += BONUS_CONSECUTIVE;
} else {
let gap = right_match.saturating_sub(position + 1) as i64;
score += SCORE_GAP_START + SCORE_GAP_EXTENSION * gap.saturating_sub(1);
}
}
right_match = Some(position);
if pattern_index > 0 {
if text_index == 0 {
return None;
}
text_index -= 1;
}
}
Some(
score + case_exact_bonus::<CASE_SENSITIVE>(case_exact)
- first as i64 * START_POSITION_PENALTY
- text.len() as i64 / TEXT_LENGTH_PENALTY_DIVISOR,
)
}
fn ascii_bonus_at(text: &[u8], position: usize) -> i64 {
if position == 0 {
return BONUS_BOUNDARY_WHITE;
}
let previous = text[position - 1];
let current = text[position];
if previous.is_ascii_whitespace() {
BONUS_BOUNDARY_WHITE
} else if matches!(previous, b'/' | b'\\' | b',' | b':' | b';' | b'|') {
BONUS_BOUNDARY_DELIMITER
} else if !previous.is_ascii_alphanumeric() {
BONUS_BOUNDARY
} else if previous.is_ascii_lowercase() && current.is_ascii_uppercase()
|| !previous.is_ascii_digit() && current.is_ascii_digit()
{
BONUS_CAMEL_OR_NUMBER
} else {
0
}
}
pub fn score_exact_text(pattern: &str, text: &str, case_sensitive: bool) -> Option<i64> {
if let Some(score) = score_exact_folded_text(pattern, text, case_sensitive) {
return Some(score);
}
if case_sensitive {
return None;
}
retry_with_rewritten_multi_char_lowercase(pattern, text, score_rewritten_exact_text)
}
fn score_rewritten_exact_text(pattern: &str, text: &str) -> Option<i64> {
score_exact_folded_text_with(pattern, text, false, false)
}
fn score_exact_folded_text(pattern: &str, text: &str, case_sensitive: bool) -> Option<i64> {
score_exact_folded_text_with(pattern, text, case_sensitive, true)
}
fn score_exact_folded_text_with(
pattern: &str,
text: &str,
case_sensitive: bool,
case_exact_allowed: bool,
) -> Option<i64> {
if pattern.is_empty() {
return Some(0);
}
let (start, case_bonus) = if case_sensitive {
(text.find(pattern)?, 0)
} else {
let start = find_ignore_case(text, pattern)?;
(
start,
case_exact_bonus::<false>(case_exact_allowed && text[start..].starts_with(pattern)),
)
};
let whole_text = start == 0
&& if case_sensitive {
pattern == text
} else {
eq_ignore_case(pattern, text)
};
let exact_bonus = if whole_text {
10_000
} else if start == 0 {
8_000
} else {
6_000
};
Some(1000 + exact_bonus + case_bonus - start as i64 * 5 - text.chars().count() as i64)
}
fn find_ignore_case(text: &str, pattern: &str) -> Option<usize> {
if text.is_ascii() && pattern.is_ascii() {
return find_ascii_ignore_case(text.as_bytes(), pattern.as_bytes());
}
if !naive_folded_scan_affordable(text.len(), pattern.len()) {
let char_index = find_folded_index(
text.chars().map(fold_char::<false>),
pattern.chars().map(fold_char::<false>),
)?;
return text
.char_indices()
.nth(char_index)
.map(|(offset, _)| offset);
}
let first = fold_char::<false>(pattern.chars().next()?);
text.char_indices()
.filter(|&(_, ch)| fold_char::<false>(ch) == first)
.map(|(index, _)| index)
.find(|&index| starts_with_ignore_case(&text[index..], pattern))
}
fn starts_with_ignore_case(text: &str, pattern: &str) -> bool {
let mut text_chars = text.chars();
pattern.chars().all(|expected| {
text_chars.next().map(fold_char::<false>) == Some(fold_char::<false>(expected))
})
}
fn eq_ignore_case(left: &str, right: &str) -> bool {
left.chars()
.map(fold_char::<false>)
.eq(right.chars().map(fold_char::<false>))
}
fn is_boundary(text: &[char], position: usize) -> bool {
position == 0 || matches!(text[position - 1], '/' | '\\' | '_' | '-' | ' ' | '.')
}
fn is_path_or_field_delimiter(ch: char) -> bool {
matches!(ch, '/' | '\\' | ',' | ':' | ';' | '|')
}
#[cfg(test)]
mod tests;