use std::fmt;
use std::sync::OnceLock;
use regex::bytes::Regex as ByteRegex;
use regex::Regex as TextRegex;
use regex_automata::dfa::dense;
use regex_automata::dfa::Automaton;
use regex_automata::nfa::thompson;
use regex_automata::{Input, MatchKind};
use ropey::Rope;
use super::alignment::AlignDirection;
use super::encoding_engine::SingleByteEngine;
use super::search::{advance_position_by_bytes, search_text_units};
use super::{Document, SearchMatch, TextPosition, TextRange};
use crate::DocumentEncoding;
const REGEX_CHUNK_BYTES: usize = 8 * 1024 * 1024;
const REGEX_CHUNK_OVERLAP_BYTES: usize = 1024 * 1024;
const REVERSE_DFA_SIZE_LIMIT_BYTES: usize = 32 * 1024 * 1024;
#[derive(Debug)]
pub struct RegexSearchQuery {
pattern: String,
bytes: ByteRegex,
text: OnceLock<TextRegex>,
reverse: OnceLock<Result<dense::DFA<Vec<u32>>, RegexCompileError>>,
}
impl Clone for RegexSearchQuery {
fn clone(&self) -> Self {
Self {
pattern: self.pattern.clone(),
bytes: self.bytes.clone(),
text: OnceLock::new(),
reverse: OnceLock::new(),
}
}
}
impl RegexSearchQuery {
pub fn new(pattern: impl Into<String>) -> Result<Self, RegexCompileError> {
let pattern = pattern.into();
if pattern.is_empty() {
return Err(RegexCompileError {
message: "regex pattern must not be empty".to_owned(),
});
}
let bytes = ByteRegex::new(&pattern).map_err(RegexCompileError::from_regex)?;
Ok(Self {
pattern,
bytes,
text: OnceLock::new(),
reverse: OnceLock::new(),
})
}
pub fn pattern(&self) -> &str {
&self.pattern
}
pub(super) fn bytes_regex(&self) -> &ByteRegex {
&self.bytes
}
pub(super) fn text_regex(&self) -> &TextRegex {
self.text.get_or_init(|| {
TextRegex::new(&self.pattern)
.expect("byte-engine compilation already validated pattern syntax")
})
}
pub(crate) fn ensure_reverse(&self) -> Result<&dense::DFA<Vec<u32>>, RegexCompileError> {
let cached = self
.reverse
.get_or_init(|| build_reverse_dfa(&self.pattern));
match cached {
Ok(dfa) => Ok(dfa),
Err(err) => Err(err.clone()),
}
}
}
fn build_reverse_dfa(pattern: &str) -> Result<dense::DFA<Vec<u32>>, RegexCompileError> {
build_reverse_dfa_with_limit(pattern, REVERSE_DFA_SIZE_LIMIT_BYTES)
}
pub(crate) fn build_reverse_dfa_with_limit(
pattern: &str,
limit_bytes: usize,
) -> Result<dense::DFA<Vec<u32>>, RegexCompileError> {
let config = dense::Config::new()
.match_kind(MatchKind::LeftmostFirst)
.dfa_size_limit(Some(limit_bytes))
.determinize_size_limit(Some(limit_bytes));
dense::Builder::new()
.configure(config)
.thompson(thompson::Config::new().reverse(true))
.build(pattern)
.map_err(RegexCompileError::from_dense_build)
}
#[derive(Clone, Debug)]
pub struct RegexCompileError {
message: String,
}
impl RegexCompileError {
fn from_regex(error: regex::Error) -> Self {
Self {
message: error.to_string(),
}
}
fn from_dense_build(error: dense::BuildError) -> Self {
Self {
message: format!("reverse DFA compilation failed: {error}"),
}
}
pub fn message(&self) -> &str {
&self.message
}
}
impl fmt::Display for RegexCompileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for RegexCompileError {}
#[derive(Debug)]
pub struct RegexSearchIter<'a> {
doc: &'a Document,
query: RegexSearchQuery,
next_from: TextPosition,
end: Option<TextPosition>,
finished: bool,
}
impl<'a> RegexSearchIter<'a> {
fn new(
doc: &'a Document,
query: RegexSearchQuery,
next_from: TextPosition,
end: Option<TextPosition>,
) -> Self {
let next_from = doc.clamp_position(next_from);
let end = end.map(|position| doc.clamp_position(position));
Self {
doc,
query,
next_from,
end,
finished: false,
}
}
}
impl Iterator for RegexSearchIter<'_> {
type Item = SearchMatch;
fn next(&mut self) -> Option<Self::Item> {
if self.finished {
return None;
}
let from = self.next_from;
let found = match self.end {
Some(end) => {
if from >= end {
self.finished = true;
return None;
}
self.doc.find_next_regex_bounded(&self.query, from, end)
}
None => self.doc.find_next_regex_query(&self.query, from),
};
let Some(found) = found else {
self.finished = true;
return None;
};
let advanced = if found.end() <= from {
advance_one_text_unit(self.doc, from)
} else {
found.end()
};
self.next_from = advanced;
if let Some(end) = self.end {
if self.next_from >= end {
self.finished = true;
}
}
Some(found)
}
}
impl std::iter::FusedIterator for RegexSearchIter<'_> {}
fn advance_one_text_unit(doc: &Document, position: TextPosition) -> TextPosition {
let line_len = doc.line_len_chars(position.line0());
if position.col0() < line_len {
return TextPosition::new(position.line0(), position.col0().saturating_add(1));
}
TextPosition::new(position.line0().saturating_add(1), 0)
}
impl Document {
pub fn find_next_regex(
&self,
pattern: &str,
from: TextPosition,
) -> Result<Option<SearchMatch>, RegexCompileError> {
let query = RegexSearchQuery::new(pattern)?;
Ok(self.find_next_regex_query(&query, from))
}
pub fn find_prev_regex(
&self,
pattern: &str,
before: TextPosition,
) -> Result<Option<SearchMatch>, RegexCompileError> {
let query = RegexSearchQuery::new(pattern)?;
Ok(self.find_prev_regex_query(&query, before))
}
pub fn find_next_regex_query(
&self,
query: &RegexSearchQuery,
from: TextPosition,
) -> Option<SearchMatch> {
let from = self.clamp_position(from);
if let Some(rope) = &self.rope {
return find_next_regex_in_rope(self, rope, query, from);
}
find_next_regex_in_bytes(self, query, from, None)
}
pub fn find_prev_regex_query(
&self,
query: &RegexSearchQuery,
before: TextPosition,
) -> Option<SearchMatch> {
let before = self.clamp_position(before);
if before == TextPosition::new(0, 0) {
return None;
}
find_prev_regex_via_reverse_dfa(self, query, TextPosition::new(0, 0), before)
}
pub fn find_next_regex_in_range(
&self,
pattern: &str,
range: TextRange,
) -> Result<Option<SearchMatch>, RegexCompileError> {
let query = RegexSearchQuery::new(pattern)?;
Ok(self.find_next_regex_query_in_range(&query, range))
}
pub fn find_next_regex_between(
&self,
pattern: &str,
start: TextPosition,
end: TextPosition,
) -> Result<Option<SearchMatch>, RegexCompileError> {
let query = RegexSearchQuery::new(pattern)?;
Ok(self.find_next_regex_query_between(&query, start, end))
}
pub fn find_next_regex_query_in_range(
&self,
query: &RegexSearchQuery,
range: TextRange,
) -> Option<SearchMatch> {
let (start, end) = self.search_range_bounds_public(range);
self.find_next_regex_bounded(query, start, end)
}
pub fn find_next_regex_query_between(
&self,
query: &RegexSearchQuery,
start: TextPosition,
end: TextPosition,
) -> Option<SearchMatch> {
let (start, end) = self.ordered_positions(start, end);
self.find_next_regex_bounded(query, start, end)
}
pub fn find_prev_regex_query_in_range(
&self,
query: &RegexSearchQuery,
range: TextRange,
) -> Option<SearchMatch> {
let (start, end) = self.search_range_bounds_public(range);
self.find_prev_regex_bounded(query, start, end)
}
pub fn find_prev_regex_query_between(
&self,
query: &RegexSearchQuery,
start: TextPosition,
end: TextPosition,
) -> Option<SearchMatch> {
let (start, end) = self.ordered_positions(start, end);
self.find_prev_regex_bounded(query, start, end)
}
pub fn find_all_regex(&self, pattern: &str) -> Result<RegexSearchIter<'_>, RegexCompileError> {
let query = RegexSearchQuery::new(pattern)?;
Ok(self.find_all_regex_query(&query))
}
pub fn find_all_regex_from(
&self,
pattern: &str,
from: TextPosition,
) -> Result<RegexSearchIter<'_>, RegexCompileError> {
let query = RegexSearchQuery::new(pattern)?;
Ok(self.find_all_regex_query_from(&query, from))
}
pub fn find_all_regex_query(&self, query: &RegexSearchQuery) -> RegexSearchIter<'_> {
self.find_all_regex_query_from(query, TextPosition::new(0, 0))
}
pub fn find_all_regex_query_from(
&self,
query: &RegexSearchQuery,
from: TextPosition,
) -> RegexSearchIter<'_> {
RegexSearchIter::new(self, query.clone(), from, None)
}
pub fn find_all_regex_in_range(
&self,
pattern: &str,
range: TextRange,
) -> Result<RegexSearchIter<'_>, RegexCompileError> {
let query = RegexSearchQuery::new(pattern)?;
Ok(self.find_all_regex_query_in_range(&query, range))
}
pub fn find_all_regex_between(
&self,
pattern: &str,
start: TextPosition,
end: TextPosition,
) -> Result<RegexSearchIter<'_>, RegexCompileError> {
let query = RegexSearchQuery::new(pattern)?;
Ok(self.find_all_regex_query_between(&query, start, end))
}
pub fn find_all_regex_query_in_range(
&self,
query: &RegexSearchQuery,
range: TextRange,
) -> RegexSearchIter<'_> {
let (start, end) = self.search_range_bounds_public(range);
RegexSearchIter::new(self, query.clone(), start, Some(end))
}
pub fn find_all_regex_query_between(
&self,
query: &RegexSearchQuery,
start: TextPosition,
end: TextPosition,
) -> RegexSearchIter<'_> {
let (start, end) = self.ordered_positions(start, end);
RegexSearchIter::new(self, query.clone(), start, Some(end))
}
pub(super) fn find_next_regex_bounded(
&self,
query: &RegexSearchQuery,
start: TextPosition,
end: TextPosition,
) -> Option<SearchMatch> {
if start >= end {
return None;
}
if let Some(rope) = &self.rope {
return find_next_regex_in_rope_bounded(self, rope, query, start, end);
}
find_next_regex_in_bytes(self, query, start, Some(end))
}
pub(super) fn find_prev_regex_bounded(
&self,
query: &RegexSearchQuery,
start: TextPosition,
end: TextPosition,
) -> Option<SearchMatch> {
if start >= end {
return None;
}
find_prev_regex_via_reverse_dfa(self, query, start, end)
}
fn search_range_bounds_public(&self, range: TextRange) -> (TextPosition, TextPosition) {
let start = self.clamp_position(range.start());
if range.is_empty() {
return (start, start);
}
let end_offset = self
.char_index_for_position(start)
.saturating_add(range.len_chars());
let end = self.position_for_char_index(end_offset);
(start, end)
}
}
fn find_next_regex_in_rope(
doc: &Document,
rope: &Rope,
query: &RegexSearchQuery,
from: TextPosition,
) -> Option<SearchMatch> {
let start_char = doc.char_index_for_position(from);
let start_byte = rope.char_to_byte(start_char);
if start_byte > rope.len_bytes() {
return None;
}
find_next_regex_in_rope_chunks(doc, rope, query, from, start_byte, rope.len_bytes())
}
fn find_next_regex_in_rope_bounded(
doc: &Document,
rope: &Rope,
query: &RegexSearchQuery,
start: TextPosition,
end: TextPosition,
) -> Option<SearchMatch> {
let start_char = doc.char_index_for_position(start);
let end_char = doc.char_index_for_position(end).max(start_char);
if start_char >= rope.len_chars() {
return None;
}
let start_byte = rope.char_to_byte(start_char);
let end_byte = rope.char_to_byte(end_char.min(rope.len_chars()));
let m = find_next_regex_in_rope_chunks(doc, rope, query, start, start_byte, end_byte)?;
if m.end() > end {
return None;
}
Some(m)
}
fn find_next_regex_in_rope_chunks(
doc: &Document,
rope: &Rope,
query: &RegexSearchQuery,
from: TextPosition,
start_byte: usize,
end_byte: usize,
) -> Option<SearchMatch> {
if start_byte >= end_byte {
return None;
}
let mut window = String::new();
let mut window_start_byte = start_byte;
let mut chunk_base = 0usize;
let target_chunk = REGEX_CHUNK_BYTES;
for chunk in rope.chunks() {
let chunk_bytes = chunk.as_bytes();
let chunk_end = chunk_base.saturating_add(chunk_bytes.len());
if chunk_end <= start_byte {
chunk_base = chunk_end;
continue;
}
if chunk_base >= end_byte {
break;
}
let chunk_window_start = start_byte.saturating_sub(chunk_base);
let chunk_window_end = end_byte.saturating_sub(chunk_base).min(chunk_bytes.len());
if chunk_window_end <= chunk_window_start {
chunk_base = chunk_end;
continue;
}
let segment = &chunk[chunk_window_start..chunk_window_end];
window.push_str(segment);
if window.len() >= target_chunk || chunk_end >= end_byte {
if let Some(m) = query.text_regex().find(&window) {
return finalize_rope_window_match(doc, from, &window, m.start(), m.end());
}
if window.len() > REGEX_CHUNK_OVERLAP_BYTES {
let trim_at = window.len().saturating_sub(REGEX_CHUNK_OVERLAP_BYTES);
let trim_at = utf8_floor_boundary(&window, trim_at);
let kept_bytes = window.len().saturating_sub(trim_at);
window_start_byte = window_start_byte
.saturating_add(window.len())
.saturating_sub(kept_bytes);
let kept = window.split_off(trim_at);
window = kept;
}
}
chunk_base = chunk_end;
}
if !window.is_empty() {
if let Some(m) = query.text_regex().find(&window) {
return finalize_rope_window_match(doc, from, &window, m.start(), m.end());
}
}
let _ = window_start_byte;
None
}
fn finalize_rope_window_match(
doc: &Document,
from: TextPosition,
window: &str,
match_start: usize,
match_end: usize,
) -> Option<SearchMatch> {
let prefix_units = window[..match_start].chars().count();
let match_units = window[match_start..match_end].chars().count();
let from_char = doc.char_index_for_position(from);
let match_start_char = from_char.saturating_add(prefix_units);
let start_pos = doc.position_for_char_index(match_start_char);
let end_pos = doc.position_for_char_index(match_start_char.saturating_add(match_units));
Some(SearchMatch::new(
TextRange::new(start_pos, match_units),
end_pos,
))
}
fn utf8_floor_boundary(text: &str, byte: usize) -> usize {
let byte = byte.min(text.len());
let mut i = byte;
while i > 0 && !text.is_char_boundary(i) {
i -= 1;
}
i
}
fn find_next_regex_in_bytes(
doc: &Document,
query: &RegexSearchQuery,
from: TextPosition,
end: Option<TextPosition>,
) -> Option<SearchMatch> {
let start_offset = doc.search_byte_offset_for_position(from);
let end_offset = match end {
Some(end_pos) => doc.search_byte_offset_for_position(end_pos),
None => doc.file_len(),
};
if start_offset >= end_offset {
return None;
}
let encoding = doc.encoding();
if !encoding.is_utf8() && !SingleByteEngine::supports(encoding) {
return find_next_regex_in_class_b_chunked(doc, query, start_offset, end_offset).and_then(
|(match_start, match_end, match_bytes)| {
build_class_b_search_match(doc, match_start, match_end, &match_bytes, end)
},
);
}
if let Some(slice) = doc.mmap_search_slice(start_offset, end_offset) {
let m = query.bytes_regex().find(slice)?;
return finalize_byte_match(from, &slice[..m.start()], &slice[m.start()..m.end()], end);
}
let mut chunk_start = start_offset;
let mut first_chunk = true;
while chunk_start < end_offset {
let chunk_end = chunk_start
.saturating_add(REGEX_CHUNK_BYTES)
.min(end_offset);
let chunk = doc.piece_table_uncapped_range(chunk_start, chunk_end)?;
let overlap_already_matched = if first_chunk {
0
} else {
REGEX_CHUNK_OVERLAP_BYTES.min(chunk.len())
};
if let Some(m) = query.bytes_regex().find(&chunk) {
let chunk_match_end = m.end();
if first_chunk || chunk_match_end > overlap_already_matched {
let from_for_chunk = if first_chunk {
from
} else {
let prefix_into_chunk = &chunk[..0];
advance_position_by_bytes(from, prefix_into_chunk)
};
let absolute_match_start = chunk_start.saturating_add(m.start());
let absolute_match_end = chunk_start.saturating_add(m.end());
let prefix_bytes =
doc.piece_table_uncapped_range(start_offset, absolute_match_start)?;
let match_bytes =
doc.piece_table_uncapped_range(absolute_match_start, absolute_match_end)?;
return finalize_byte_match(from_for_chunk, &prefix_bytes, &match_bytes, end);
}
}
if chunk_end >= end_offset {
break;
}
chunk_start = chunk_end.saturating_sub(REGEX_CHUNK_OVERLAP_BYTES);
first_chunk = false;
}
None
}
fn finalize_byte_match(
from: TextPosition,
prefix_bytes: &[u8],
match_bytes: &[u8],
end: Option<TextPosition>,
) -> Option<SearchMatch> {
let start_pos = advance_position_by_bytes(from, prefix_bytes);
let match_units = search_text_units(std::str::from_utf8(match_bytes).unwrap_or(""));
let end_pos = advance_position_by_bytes(start_pos, match_bytes);
if let Some(end_bound) = end {
if end_pos > end_bound {
return None;
}
}
Some(SearchMatch::new(
TextRange::new(start_pos, match_units),
end_pos,
))
}
fn find_prev_regex_via_reverse_dfa(
doc: &Document,
query: &RegexSearchQuery,
bound_start: TextPosition,
bound_end: TextPosition,
) -> Option<SearchMatch> {
if let Some(rope) = &doc.rope {
return reverse_dfa_search_in_rope(doc, rope, query, bound_start, bound_end);
}
let start_off = doc.search_byte_offset_for_position(bound_start);
let end_off = doc.search_byte_offset_for_position(bound_end);
if start_off >= end_off {
return None;
}
if let Some(slice) = doc.mmap_search_slice(start_off, end_off) {
return reverse_dfa_search_in_slice(doc, query, bound_start, start_off, slice);
}
reverse_dfa_search_in_piece_tree(doc, query, bound_start, bound_end)
}
fn collect_rope_chunks_with_offsets(rope: &Rope) -> Vec<(usize, &str)> {
let mut out = Vec::new();
let mut offset = 0usize;
for chunk in rope.chunks() {
out.push((offset, chunk));
offset = offset.saturating_add(chunk.len());
}
out
}
fn utf8_floor_boundary_bytes(bytes: &[u8], byte: usize) -> usize {
let byte = byte.min(bytes.len());
let mut i = byte;
while i > 0 && (bytes[i] & 0b1100_0000) == 0b1000_0000 {
i -= 1;
}
i
}
fn reverse_dfa_locate_in_slice(
dfa: &dense::DFA<Vec<u32>>,
forward_regex: &ByteRegex,
slice: &[u8],
) -> Option<(usize, usize)> {
if slice.is_empty() {
return None;
}
let input = Input::new(slice);
let half = match dfa.try_search_rev(&input) {
Ok(Some(hm)) => hm,
_ => return None,
};
let match_start = half.offset();
let m = forward_regex.find_at(slice, match_start)?;
if m.start() != match_start {
return None;
}
Some((m.start(), m.end()))
}
fn reverse_dfa_search_in_slice(
doc: &Document,
query: &RegexSearchQuery,
bound_start: TextPosition,
slice_start_off: usize,
slice: &[u8],
) -> Option<SearchMatch> {
let dfa = query.ensure_reverse().ok()?;
let (rel_start, rel_end) = reverse_dfa_locate_in_slice(dfa, query.bytes_regex(), slice)?;
let absolute_start = slice_start_off.saturating_add(rel_start);
let absolute_end = slice_start_off.saturating_add(rel_end);
let aligned_start = doc.align_byte_offset(absolute_start, AlignDirection::Backward);
let aligned_end = doc.align_byte_offset(absolute_end, AlignDirection::Forward);
if aligned_end <= aligned_start {
return None;
}
let rebased_start = aligned_start
.saturating_sub(slice_start_off)
.min(slice.len());
let rebased_end = aligned_end.saturating_sub(slice_start_off).min(slice.len());
if rebased_end <= rebased_start {
return None;
}
finalize_byte_match(
bound_start,
&slice[..rebased_start],
&slice[rebased_start..rebased_end],
None,
)
}
fn reverse_dfa_search_in_piece_tree(
doc: &Document,
query: &RegexSearchQuery,
bound_start: TextPosition,
bound_end: TextPosition,
) -> Option<SearchMatch> {
let start_off = doc.search_byte_offset_for_position(bound_start);
let end_off = doc.search_byte_offset_for_position(bound_end);
if start_off >= end_off {
return None;
}
let dfa = query.ensure_reverse().ok()?;
let forward_regex = query.bytes_regex();
let total_span = end_off.saturating_sub(start_off);
let mut window_end = end_off;
loop {
let window_size = REGEX_CHUNK_BYTES.min(total_span);
let window_start = window_end.saturating_sub(window_size).max(start_off);
let chunk = doc.piece_table_uncapped_range(window_start, window_end)?;
if let Some((rel_start, rel_end)) = reverse_dfa_locate_in_slice(dfa, forward_regex, &chunk)
{
let absolute_start = window_start.saturating_add(rel_start);
let absolute_end = window_start.saturating_add(rel_end);
let aligned_start = doc.align_byte_offset(absolute_start, AlignDirection::Backward);
let aligned_end = doc.align_byte_offset(absolute_end, AlignDirection::Forward);
if aligned_end > aligned_start {
let prefix_bytes = doc.piece_table_uncapped_range(start_off, aligned_start)?;
let match_bytes = doc.piece_table_uncapped_range(aligned_start, aligned_end)?;
if let Some(found) =
finalize_byte_match(bound_start, &prefix_bytes, &match_bytes, Some(bound_end))
{
return Some(found);
}
}
}
if window_start == start_off {
break;
}
window_end = window_start.saturating_add(REGEX_CHUNK_OVERLAP_BYTES);
if window_end <= start_off {
break;
}
}
None
}
fn reverse_dfa_search_in_rope(
doc: &Document,
rope: &Rope,
query: &RegexSearchQuery,
bound_start: TextPosition,
bound_end: TextPosition,
) -> Option<SearchMatch> {
let start_char = doc.char_index_for_position(bound_start);
let end_char = doc.char_index_for_position(bound_end).max(start_char);
if start_char >= rope.len_chars() {
return None;
}
let start_byte = rope.char_to_byte(start_char);
let end_byte = rope.char_to_byte(end_char.min(rope.len_chars()));
if start_byte >= end_byte {
return None;
}
let dfa = query.ensure_reverse().ok()?;
let forward_regex = query.bytes_regex();
let window_capacity = REGEX_CHUNK_BYTES.saturating_add(REGEX_CHUNK_OVERLAP_BYTES);
let mut window: Vec<u8> = Vec::with_capacity(window_capacity.min(end_byte - start_byte));
let mut window_start_byte = end_byte;
let chunks: Vec<(usize, &str)> = collect_rope_chunks_with_offsets(rope);
for (chunk_base, chunk) in chunks.iter().rev() {
let chunk_base = *chunk_base;
let chunk_bytes = chunk.as_bytes();
let chunk_end_byte = chunk_base.saturating_add(chunk_bytes.len());
if chunk_base >= end_byte {
continue;
}
if chunk_end_byte <= start_byte {
break;
}
let take_start = start_byte.saturating_sub(chunk_base).min(chunk_bytes.len());
let take_end = end_byte.saturating_sub(chunk_base).min(chunk_bytes.len());
if take_end <= take_start {
continue;
}
let segment = &chunk_bytes[take_start..take_end];
let mut new_window = Vec::with_capacity(segment.len() + window.len());
new_window.extend_from_slice(segment);
new_window.extend_from_slice(&window);
window = new_window;
window_start_byte = chunk_base.saturating_add(take_start);
if window.len() >= REGEX_CHUNK_BYTES {
if let Some(found) = reverse_dfa_finalize_rope_window(
doc,
rope,
dfa,
forward_regex,
window_start_byte,
&window,
bound_end,
) {
return Some(found);
}
if window.len() > REGEX_CHUNK_OVERLAP_BYTES {
let drop_at = window.len().saturating_sub(REGEX_CHUNK_OVERLAP_BYTES);
let drop_at = utf8_floor_boundary_bytes(&window, drop_at);
window.truncate(drop_at);
}
}
}
if !window.is_empty() {
if let Some(found) = reverse_dfa_finalize_rope_window(
doc,
rope,
dfa,
forward_regex,
window_start_byte,
&window,
bound_end,
) {
return Some(found);
}
}
None
}
fn reverse_dfa_finalize_rope_window(
doc: &Document,
rope: &Rope,
dfa: &dense::DFA<Vec<u32>>,
forward_regex: &ByteRegex,
window_start_byte: usize,
window: &[u8],
bound_end: TextPosition,
) -> Option<SearchMatch> {
let (rel_start, rel_end) = reverse_dfa_locate_in_slice(dfa, forward_regex, window)?;
let abs_start = window_start_byte.saturating_add(rel_start);
let abs_end = window_start_byte.saturating_add(rel_end);
let aligned_start = doc.align_byte_offset(abs_start, AlignDirection::Backward);
let aligned_end = doc.align_byte_offset(abs_end, AlignDirection::Forward);
if aligned_end <= aligned_start {
return None;
}
let start_char = rope.byte_to_char(aligned_start);
let end_char = rope.byte_to_char(aligned_end);
let match_units = end_char.saturating_sub(start_char);
let start_pos = doc.position_for_char_index(start_char);
let end_pos = doc.position_for_char_index(end_char);
if end_pos > bound_end {
return None;
}
Some(SearchMatch::new(
TextRange::new(start_pos, match_units),
end_pos,
))
}
fn class_b_source_bytes_for_decoded_prefix(
encoding: DocumentEncoding,
decoded: &str,
decoded_byte_count: usize,
) -> Option<usize> {
if decoded_byte_count > decoded.len() {
return None;
}
if !decoded.is_char_boundary(decoded_byte_count) {
return None;
}
let prefix = &decoded[..decoded_byte_count];
if encoding.name() == "UTF-16LE" || encoding.name() == "UTF-16BE" {
let mut bytes: usize = 0;
for c in prefix.chars() {
bytes = bytes.saturating_add(c.len_utf16().saturating_mul(2));
}
return Some(bytes);
}
let encoded = encoding.as_encoding().encode(prefix).0;
Some(encoded.len())
}
fn class_b_requires_even_alignment(encoding: DocumentEncoding) -> bool {
matches!(encoding.name(), "UTF-16LE" | "UTF-16BE")
}
fn find_next_regex_in_class_b_chunked(
doc: &Document,
query: &RegexSearchQuery,
start_offset: usize,
end_offset: usize,
) -> Option<(usize, usize, Vec<u8>)> {
if start_offset >= end_offset {
return None;
}
let doc_encoding = doc.encoding();
let encoding = doc_encoding.as_encoding();
let requires_even_alignment = class_b_requires_even_alignment(doc_encoding);
let mut chunk_start = start_offset;
let mut first_chunk = true;
while chunk_start < end_offset {
let chunk_end = chunk_start
.saturating_add(REGEX_CHUNK_BYTES)
.min(end_offset);
let chunk_end = if requires_even_alignment && chunk_end & 1 == 1 && chunk_end > chunk_start
{
chunk_end - 1
} else {
chunk_end
};
if chunk_end <= chunk_start {
break;
}
let window: Vec<u8> = if let Some(slice) = doc.mmap_search_slice(chunk_start, chunk_end) {
slice.to_vec()
} else if let Some(buf) = doc.piece_table_uncapped_range(chunk_start, chunk_end) {
buf
} else {
return None;
};
let (decoded, _had_errors) = encoding.decode_without_bom_handling(&window);
let overlap_already_matched_decoded = if first_chunk {
0
} else {
let overlap_source_bytes = REGEX_CHUNK_OVERLAP_BYTES.min(window.len());
decoded_prefix_for_source_byte_count(doc_encoding, &decoded, overlap_source_bytes)
};
if let Some(m) = query.text_regex().find(&decoded) {
if first_chunk || m.end() > overlap_already_matched_decoded {
let prefix_source_len =
class_b_source_bytes_for_decoded_prefix(doc_encoding, &decoded, m.start())?;
let match_source_len = class_b_source_bytes_for_decoded_prefix(
doc_encoding,
&decoded[m.start()..],
m.end() - m.start(),
)?;
let absolute_match_start = chunk_start.saturating_add(prefix_source_len);
let absolute_match_end = absolute_match_start.saturating_add(match_source_len);
let alignment_ok = if requires_even_alignment {
absolute_match_start & 1 == 0 && absolute_match_end & 1 == 0
} else {
true
};
if alignment_ok && prefix_source_len + match_source_len <= window.len() {
let match_bytes: Vec<u8> =
window[prefix_source_len..(prefix_source_len + match_source_len)].to_vec();
return Some((absolute_match_start, absolute_match_end, match_bytes));
}
}
}
if chunk_end >= end_offset {
break;
}
let next_chunk_start = chunk_end.saturating_sub(REGEX_CHUNK_OVERLAP_BYTES);
chunk_start = if next_chunk_start > chunk_start {
next_chunk_start
} else if requires_even_alignment {
chunk_start.saturating_add(2)
} else {
chunk_start.saturating_add(1)
};
first_chunk = false;
}
None
}
fn decoded_prefix_for_source_byte_count(
encoding: DocumentEncoding,
decoded: &str,
source_bytes: usize,
) -> usize {
let is_utf16 = matches!(encoding.name(), "UTF-16LE" | "UTF-16BE");
let mut acc_source = 0usize;
let mut acc_decoded = 0usize;
if is_utf16 {
for c in decoded.chars() {
let next_source = acc_source.saturating_add(c.len_utf16().saturating_mul(2));
if next_source > source_bytes {
break;
}
acc_source = next_source;
acc_decoded = acc_decoded.saturating_add(c.len_utf8());
}
} else {
let mut buf = [0u8; 4];
for c in decoded.chars() {
let s = c.encode_utf8(&mut buf);
let encoded = encoding.as_encoding().encode(s).0;
let next_source = acc_source.saturating_add(encoded.len());
if next_source > source_bytes {
break;
}
acc_source = next_source;
acc_decoded = acc_decoded.saturating_add(c.len_utf8());
}
}
acc_decoded
}
fn build_class_b_search_match(
doc: &Document,
match_start: usize,
match_end: usize,
match_bytes: &[u8],
end: Option<TextPosition>,
) -> Option<SearchMatch> {
let start_pos = doc.position_for_byte_offset_in_class_b(match_start);
let end_pos = doc.position_for_byte_offset_in_class_b(match_end);
if let Some(end_bound) = end {
if end_pos > end_bound {
return None;
}
}
let engine = doc.encoding_engine();
let mut units = 0usize;
let mut offset = 0usize;
let total = match_bytes.len();
loop {
let next = engine.advance_offset_by_text_units(match_bytes, total, offset, 1);
if next <= offset {
break;
}
offset = next;
units = units.saturating_add(1);
if offset >= total {
break;
}
}
Some(SearchMatch::new(TextRange::new(start_pos, units), end_pos))
}