use std::cell::RefCell;
use std::collections::HashMap;
use anyhow::{Result, anyhow};
use common::database::Store;
use common::database::rope_helpers::block_content_via_store;
use common::entities::Block;
use regex::{Regex, RegexBuilder};
use crate::matching::{self, FoldLocale, MatchOptions};
pub fn build_full_text_via_store(blocks: &[Block], store: &Store) -> String {
let mut out = String::new();
for (i, block) in blocks.iter().enumerate() {
if i > 0 {
out.push('\n');
}
out.push_str(&block_content_via_store(block, store));
}
out
}
pub fn matched_texts(full_text: &str, matches: &[(usize, usize)]) -> Vec<String> {
if matches.is_empty() {
return Vec::new();
}
let chars: Vec<char> = full_text.chars().collect();
matches
.iter()
.map(|&(position, length)| {
let start = position.min(chars.len());
let end = (position + length).min(chars.len());
chars[start..end].iter().collect()
})
.collect()
}
macro_rules! match_options_from {
($dto:ty) => {
impl From<&$dto> for MatchOptions {
fn from(dto: &$dto) -> Self {
MatchOptions {
case_sensitive: dto.case_sensitive,
diacritic_sensitive: dto.diacritic_sensitive,
whole_word: dto.whole_word,
locale: FoldLocale::from_tag(&dto.language),
}
}
}
};
}
match_options_from!(crate::FindTextDto);
match_options_from!(crate::FindAllDto);
match_options_from!(crate::ReplaceTextDto);
thread_local! {
static REGEX_CACHE: RefCell<HashMap<(String, bool), Regex>> = RefCell::new(HashMap::new());
}
const REGEX_CACHE_CAP: usize = 32;
fn compiled_regex(pattern: &str, case_sensitive: bool) -> Result<Regex> {
let key = (pattern.to_string(), case_sensitive);
REGEX_CACHE.with(|cache| {
if let Some(re) = cache.borrow().get(&key) {
return Ok(re.clone());
}
let re = RegexBuilder::new(pattern)
.case_insensitive(!case_sensitive)
.size_limit(1 << 20) .dfa_size_limit(1 << 20)
.build()
.map_err(|e| anyhow!("Invalid regex pattern: {}", e))?;
let mut cache = cache.borrow_mut();
if cache.len() >= REGEX_CACHE_CAP {
cache.clear();
}
cache.insert(key, re.clone());
Ok(re)
})
}
pub fn find_all_matches(
full_text: &str,
query: &str,
options: &MatchOptions,
use_regex: bool,
) -> Result<Vec<(usize, usize)>> {
if query.is_empty() {
return Ok(Vec::new());
}
if !use_regex {
return Ok(matching::find_all(full_text, query, options)
.into_iter()
.map(|m| (m.char_start, m.char_len))
.collect());
}
let re = compiled_regex(query, options.case_sensitive)?;
let folded = matching::Folded::new_for(
full_text,
&MatchOptions {
case_sensitive: true,
..*options
},
);
let boundaries = options
.whole_word
.then(|| matching::word_boundaries(full_text));
let mut results = Vec::new();
for mat in re.find_iter(folded.text()) {
if mat.start() == mat.end() {
continue;
}
let (Some(folded_start), Some(folded_end)) = (
folded.char_of_byte(mat.start()),
folded.char_of_byte(mat.end()),
) else {
continue;
};
let Some(m) = folded.to_source_match(folded_start, folded_end) else {
continue;
};
if let Some(boundaries) = &boundaries
&& !(matching::is_boundary(boundaries, m.char_start)
&& matching::is_boundary(boundaries, m.char_start + m.char_len))
{
continue;
}
results.push((m.char_start, m.char_len));
}
Ok(results)
}