use std::borrow::Cow;
use std::fmt::{Display, Error, Formatter};
use crate::engine::util::{
map_byte_range_to_original, map_char_indices_to_original, normalize_with_byte_mapping, normalize_with_char_mapping,
};
use crate::{CaseMatching, MatchEngine, MatchEngineFactory, MatchRange, MatchResult, SkimItem};
pub struct NormalizedEngine {
inner: Box<dyn MatchEngine>,
}
impl NormalizedEngine {
pub fn new(inner: Box<dyn MatchEngine>) -> Self {
Self { inner }
}
}
impl MatchEngine for NormalizedEngine {
fn match_item(&self, item: &dyn SkimItem) -> Option<MatchResult> {
let item_text = item.text();
let (normalized_text, char_mapping) = normalize_with_char_mapping(&item_text);
let (_, byte_mapping) = normalize_with_byte_mapping(&item_text);
let normalized_item: &dyn SkimItem = &NormalizedItem(normalized_text);
let mut result = self.inner.match_item(normalized_item)?;
result.matched_range = match result.matched_range {
MatchRange::Chars(indices) => MatchRange::Chars(map_char_indices_to_original(&indices, &char_mapping)),
MatchRange::CharRange(start, end) => {
let orig_start = char_mapping.get(start).copied().unwrap_or(start);
let orig_end = if end > 0 {
char_mapping.get(end - 1).copied().map_or(end, |e| e + 1)
} else {
0
};
MatchRange::CharRange(orig_start, orig_end)
}
MatchRange::ByteRange(start, end) => {
let (orig_start, orig_end) = map_byte_range_to_original(start, end, &byte_mapping, &item_text);
MatchRange::ByteRange(orig_start, orig_end)
}
};
Some(result)
}
}
impl Display for NormalizedEngine {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
write!(f, "(Normalized: {})", self.inner)
}
}
struct NormalizedItem(String);
impl SkimItem for NormalizedItem {
fn text(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.0)
}
}
pub struct NormalizedEngineFactory {
inner: Box<dyn MatchEngineFactory>,
}
impl NormalizedEngineFactory {
pub fn new(inner: impl MatchEngineFactory + 'static) -> Self {
Self { inner: Box::new(inner) }
}
}
impl MatchEngineFactory for NormalizedEngineFactory {
fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
let (normalized_query, _) = normalize_with_char_mapping(query);
let inner_engine = self.inner.create_engine_with_case(&normalized_query, case);
Box::new(NormalizedEngine::new(inner_engine))
}
}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod tests {
use super::*;
use crate::engine::exact::{ExactEngine, ExactMatchingParam};
use crate::prelude::ExactOrFuzzyEngineFactory;
#[test]
fn matches_through_diacritics() {
let inner = Box::new(ExactEngine::builder("cafe", ExactMatchingParam::default()).build());
let engine = NormalizedEngine::new(inner);
let result = engine.match_item(&"café".to_string());
assert!(result.is_some());
}
#[test]
fn no_match_returns_none() {
let inner = Box::new(ExactEngine::builder("zzz", ExactMatchingParam::default()).build());
let engine = NormalizedEngine::new(inner);
assert!(engine.match_item(&"café".to_string()).is_none());
}
#[test]
fn display_includes_inner_engine() {
let inner = Box::new(ExactEngine::builder("x", ExactMatchingParam::default()).build());
let engine = NormalizedEngine::new(inner);
assert!(format!("{engine}").starts_with("(Normalized:"));
}
#[test]
fn factory_creates_normalized_engine() {
let factory = NormalizedEngineFactory::new(ExactOrFuzzyEngineFactory::builder().build());
let engine = factory.create_engine_with_case("cafe", CaseMatching::Smart);
assert!(engine.match_item(&"café".to_string()).is_some());
}
struct CharRangeStub(usize, usize);
impl MatchEngine for CharRangeStub {
fn match_item(&self, _item: &dyn SkimItem) -> Option<MatchResult> {
Some(MatchResult {
rank: crate::Rank::default(),
matched_range: MatchRange::CharRange(self.0, self.1),
})
}
}
impl Display for CharRangeStub {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
write!(f, "CharRangeStub")
}
}
#[test]
fn char_range_is_mapped_back_to_original() {
let engine = NormalizedEngine::new(Box::new(CharRangeStub(1, 3)));
let result = engine.match_item(&"café".to_string()).unwrap();
assert_eq!(result.matched_range, MatchRange::CharRange(1, 3));
}
#[test]
fn empty_char_range_maps_to_zero() {
let engine = NormalizedEngine::new(Box::new(CharRangeStub(0, 0)));
let result = engine.match_item(&"café".to_string()).unwrap();
assert_eq!(result.matched_range, MatchRange::CharRange(0, 0));
}
struct CharsStub(Vec<usize>);
impl MatchEngine for CharsStub {
fn match_item(&self, _item: &dyn SkimItem) -> Option<MatchResult> {
Some(MatchResult {
rank: crate::Rank::default(),
matched_range: MatchRange::Chars(self.0.clone()),
})
}
}
impl Display for CharsStub {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
write!(f, "CharsStub")
}
}
#[test]
fn chars_indices_are_mapped_back_to_original() {
let engine = NormalizedEngine::new(Box::new(CharsStub(vec![0, 2])));
let result = engine.match_item(&"café".to_string()).unwrap();
assert_eq!(result.matched_range, MatchRange::Chars(vec![0, 2]));
}
}