use core::ops::Range;
use crate::buffer::Buffer;
use crate::coords::{Bias, Point};
use crate::decorations::{
DecorationId, DecorationKind, DecorationStore, Stickiness, TrackedRange,
};
use crate::patch::Patch;
#[derive(Clone, Default, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub struct FindQuery {
pub text: String,
pub case_sensitive: bool,
pub whole_word: bool,
pub regex: bool,
}
impl FindQuery {
#[must_use]
pub fn new(text: impl Into<String>) -> Self {
Self { text: text.into(), ..Self::default() }
}
}
#[derive(Debug)]
enum Matcher {
Literal,
Lines(regex::Regex),
}
impl Matcher {
fn compile(query: &FindQuery) -> Result<Self, String> {
if !query.whole_word && !query.regex {
return Ok(Self::Literal);
}
let body =
if query.regex { format!("(?:{})", query.text) } else { regex::escape(&query.text) };
let pattern = if query.whole_word { format!(r"\b{body}\b") } else { body };
regex::RegexBuilder::new(&pattern)
.case_insensitive(!query.case_sensitive)
.multi_line(true)
.build()
.map(Self::Lines)
.map_err(|e| e.to_string())
}
fn is_line_scoped(&self) -> bool {
matches!(self, Self::Lines(_))
}
}
#[must_use]
pub fn default_find_debounce() -> u64 {
100
}
pub const FIND_MATCH_CAP: usize = 10_000;
#[must_use]
pub fn scan(text: &str, query: &FindQuery) -> (Vec<Range<u32>>, bool) {
scan_capped(text, query, FIND_MATCH_CAP)
}
fn scan_capped(text: &str, query: &FindQuery, cap: usize) -> (Vec<Range<u32>>, bool) {
match Matcher::compile(query) {
Ok(m) => scan_with(text, query, &m, cap),
Err(_) => (Vec::new(), false),
}
}
fn scan_with(text: &str, query: &FindQuery, m: &Matcher, cap: usize) -> (Vec<Range<u32>>, bool) {
match m {
Matcher::Literal => scan_literal(text, query, cap),
Matcher::Lines(re) => scan_lines(text, re, cap),
}
}
fn scan_lines(text: &str, re: ®ex::Regex, cap: usize) -> (Vec<Range<u32>>, bool) {
let mut spans: Vec<Range<u32>> = Vec::new();
let mut base = 0usize;
for line in text.split_inclusive('\n') {
let body = line.strip_suffix('\n').unwrap_or(line);
for m in re.find_iter(body) {
if m.start() >= m.end() {
continue;
}
if spans.len() == cap {
return (spans, true); }
spans.push((base + m.start()) as u32..(base + m.end()) as u32);
}
base += line.len();
}
(spans, false)
}
fn scan_literal(text: &str, query: &FindQuery, cap: usize) -> (Vec<Range<u32>>, bool) {
let needle = query.text.as_bytes();
if needle.is_empty() {
return (Vec::new(), false); }
let hay = text.as_bytes();
let mut spans = Vec::new();
if query.case_sensitive {
for i in memchr::memmem::find_iter(hay, needle) {
if spans.len() == cap {
return (spans, true); }
spans.push(i as u32..(i + needle.len()) as u32);
}
} else {
let (lo, up) = (needle[0].to_ascii_lowercase(), needle[0].to_ascii_uppercase());
let mut i = 0usize;
while i + needle.len() <= hay.len() {
let Some(j) = memchr::memchr2(lo, up, &hay[i..]) else {
break;
};
let c = i + j;
if c + needle.len() > hay.len() {
break;
}
if hay[c..c + needle.len()].eq_ignore_ascii_case(needle) {
if spans.len() == cap {
return (spans, true); }
spans.push(c as u32..(c + needle.len()) as u32);
i = c + needle.len(); } else {
i = c + 1;
}
}
}
(spans, false)
}
fn scan_buffer(
buffer: &Buffer,
query: &FindQuery,
window: u32,
within: Range<u32>,
) -> (Vec<Range<u32>>, bool) {
scan_buffer_capped(buffer, query, window, FIND_MATCH_CAP, within)
}
pub(crate) fn scan_all(buffer: &Buffer, query: &FindQuery, within: Range<u32>) -> Vec<Range<u32>> {
scan_buffer_capped(buffer, query, crate::buffer::SCAN_WINDOW, usize::MAX, within).0
}
fn scan_buffer_capped(
buffer: &Buffer,
query: &FindQuery,
window: u32,
cap: usize,
within: Range<u32>,
) -> (Vec<Range<u32>>, bool) {
let Ok(m) = Matcher::compile(query) else {
return (Vec::new(), false); };
let hi = buffer.clip_offset(within.end.min(buffer.len()), Bias::Left);
let lo = buffer.clip_offset(within.start.min(hi), Bias::Right);
if let Matcher::Lines(re) = &m {
return scan_buffer_lines(buffer, re, cap, lo..hi);
}
let k = query.text.len() as u64;
if k == 0 {
return (Vec::new(), false); }
let window = u64::from(window).max(k * 2);
let mut spans: Vec<Range<u32>> = Vec::new();
let mut pos: u32 = lo; while u64::from(pos) + k <= u64::from(hi) {
let win_end = buffer
.clip_offset((u64::from(pos) + window).min(u64::from(hi)) as u32, Bias::Right);
let (local, local_capped) = scan_with(&buffer.slice(pos..win_end), query, &m, cap);
let found = !local.is_empty();
for r in local {
if spans.len() == cap {
return (spans, true); }
spans.push(pos + r.start..pos + r.end);
}
if win_end == hi && !local_capped {
break;
}
let last_end = found.then(|| spans.last().expect("found ⇒ spans non-empty").end);
pos = buffer.scan_resume(pos, win_end, k as u32, last_end);
}
(spans, false)
}
fn scan_buffer_lines(
buffer: &Buffer,
re: ®ex::Regex,
cap: usize,
within: Range<u32>,
) -> (Vec<Range<u32>>, bool) {
let mut spans: Vec<Range<u32>> = Vec::new();
if within.start >= within.end {
return (spans, false);
}
let first = buffer.offset_to_point(within.start).row;
let last = buffer.offset_to_point(within.end).row;
for row in first..=last {
let base = buffer.point_to_offset(Point::new(row, 0));
let line = buffer.line(row); for m in re.find_iter(&line) {
if m.start() >= m.end() {
continue; }
let (s, e) = (base + m.start() as u32, base + m.end() as u32);
if s < within.start || e > within.end {
continue; }
if spans.len() == cap {
return (spans, true); }
spans.push(s..e);
}
}
(spans, false)
}
fn whole_lines(buffer: &Buffer, r: &Range<u32>) -> Range<u32> {
let first = buffer.offset_to_point(r.start).row;
let last = buffer.offset_to_point(r.end).row;
let start = buffer.point_to_offset(Point::new(first, 0));
let base = buffer.point_to_offset(Point::new(last, 0));
start..base + buffer.line(last).len() as u32
}
pub(crate) fn replacements(
buffer: &Buffer,
query: &FindQuery,
within: Range<u32>,
replacement: &str,
preserve_case: bool,
) -> Vec<(Range<u32>, String)> {
if !query.regex {
return scan_all(buffer, query, within)
.into_iter()
.map(|r| {
let text = if preserve_case {
preserve_case_of(&buffer.slice(r.clone()), replacement)
} else {
replacement.to_string()
};
(r, text)
})
.collect();
}
let Ok(Matcher::Lines(re)) = Matcher::compile(query) else {
return Vec::new(); };
let mut out = Vec::new();
if within.start >= within.end {
return out;
}
let first = buffer.offset_to_point(within.start).row;
let last = buffer.offset_to_point(within.end).row;
for row in first..=last {
let base = buffer.point_to_offset(Point::new(row, 0));
let line = buffer.line(row); for c in re.captures_iter(&line) {
let m = c.get(0).expect("capture group 0 is the whole match");
if m.start() >= m.end() {
continue; }
let (s, e) = (base + m.start() as u32, base + m.end() as u32);
if s < within.start || e > within.end {
continue; }
let mut dst = String::new();
c.expand(replacement, &mut dst);
let text = if preserve_case { preserve_case_of(m.as_str(), &dst) } else { dst };
out.push((s..e, text));
}
}
out
}
fn preserve_case_of(matched: &str, replacement: &str) -> String {
let mut has_upper = false;
let mut has_lower = false;
let mut first_is_upper = None; for ch in matched.chars() {
if ch.is_uppercase() {
has_upper = true;
first_is_upper.get_or_insert(true);
} else if ch.is_lowercase() {
has_lower = true;
first_is_upper.get_or_insert(false);
}
}
match (has_upper, has_lower) {
(true, false) => replacement.to_uppercase(),
(false, true) => replacement.to_lowercase(),
(true, true) if first_is_upper == Some(true) => {
let mut chars = replacement.chars();
match chars.next() {
Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
_ => replacement.to_string(),
}
}
fn is_find(r: &TrackedRange) -> bool {
matches!(r.kind, DecorationKind::FindMatch)
}
#[derive(Debug, Default)]
pub struct FindState {
query: Option<FindQuery>,
active: Option<DecorationId>,
active_start: Option<u32>,
capped: bool,
coverage_end: u32,
capped_stale: bool,
last_scan_ms: u64,
scope: Option<Range<u32>>,
matcher: Option<Matcher>,
pattern_error: Option<String>,
}
impl FindState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn query(&self) -> Option<&FindQuery> {
self.query.as_ref()
}
#[must_use]
pub fn match_count(&self, store: &DecorationStore) -> usize {
store.find_count()
}
#[must_use]
pub fn active_id(&self) -> Option<DecorationId> {
self.active
}
#[must_use]
pub(crate) fn active_start(&self) -> Option<u32> {
self.active_start
}
#[must_use]
pub(crate) fn active_range(&self, store: &DecorationStore) -> Option<Range<u32>> {
let (id, start) = (self.active?, self.active_start?);
let (found, range) = store.nth_find(store.find_rank_before(start))?;
(found == id).then_some(range)
}
#[must_use]
pub fn capped(&self) -> bool {
self.capped
}
#[must_use]
pub fn scope(&self) -> Option<Range<u32>> {
self.scope.clone()
}
pub fn set_scope(
&mut self,
scope: Option<Range<u32>>,
buffer: &Buffer,
store: &mut DecorationStore,
now_ms: u64,
) {
let scope = scope.filter(|s| s.start < s.end);
if scope == self.scope {
return;
}
self.scope = scope;
self.rescan(buffer, store, now_ms);
}
pub fn set_query(
&mut self,
query: Option<FindQuery>,
buffer: &Buffer,
store: &mut DecorationStore,
now_ms: u64,
) {
if query == self.query {
return;
}
self.query = query;
(self.matcher, self.pattern_error) = match &self.query {
None => (None, None),
Some(q) => match Matcher::compile(q) {
Ok(m) => (Some(m), None),
Err(e) => (None, Some(e)),
},
};
self.active = None; self.active_start = None; self.rescan(buffer, store, now_ms);
}
#[must_use]
pub fn pattern_error(&self) -> Option<&str> {
self.pattern_error.as_deref()
}
pub fn maybe_rescan(&mut self, buffer: &Buffer, store: &mut DecorationStore, now_ms: u64) -> bool {
if self.query.is_none()
|| !(self.capped && self.capped_stale)
|| now_ms.saturating_sub(self.last_scan_ms) < default_find_debounce()
{
return false;
}
self.rescan(buffer, store, now_ms);
true
}
pub fn on_commit(&mut self, patch: &Patch, buffer: &Buffer, store: &mut DecorationStore) {
let Some(query) = &self.query else { return };
let Some(matcher) = &self.matcher else { return };
let line_scoped = matcher.is_line_scoped();
let k = query.text.len() as u32;
if patch.is_empty() || (!line_scoped && k == 0) {
return; }
self.coverage_end = if self.capped {
patch.map_offset(self.coverage_end, Bias::Left)
} else {
buffer.len()
};
self.active_start = self.active_start.map(|s| patch.map_offset(s, Bias::Right));
self.scope = self.scope.take().and_then(|s| {
let lo = patch.map_offset(s.start, Bias::Left);
let hi = patch.map_offset(s.end, Bias::Right);
(lo < hi).then_some(lo..hi)
});
let (lo_bound, hi_bound) = match &self.scope {
Some(s) => (s.start, s.end.min(self.coverage_end)),
None => (0, self.coverage_end),
};
let mut removed_active_start: Option<u32> = None;
for e in patch.edits() {
let (w_start, w_end) = if line_scoped {
let lines = whole_lines(buffer, &e.new);
(lines.start, lines.end)
} else {
let s = buffer.clip_offset(e.new.start.saturating_sub(k - 1), Bias::Left);
let t_raw = e.new.end.saturating_add(k - 1).min(hi_bound);
(s, buffer.clip_offset(t_raw, Bias::Right))
};
let (w_start, w_end) = (w_start.max(lo_bound), w_end.min(hi_bound));
if w_start > w_end {
continue; }
let removed = store.take_matching_in(w_start..w_end, is_find);
if let Some(active) = self.active {
if let Some(r) = removed.iter().find(|r| r.id == active) {
removed_active_start = Some(r.range.start);
self.active = None;
self.active_start = None; }
}
let (scan_lo, scan_hi, guard) = if line_scoped {
(w_start, w_end, None)
} else {
let ext_lo =
removed.iter().map(|r| r.range.start).min().map_or(w_start, |s| s.min(w_start));
let ext_hi =
removed.iter().map(|r| r.range.end).max().map_or(w_end, |e| e.max(w_end));
let scan_lo =
buffer.clip_offset(ext_lo.saturating_sub(k - 1), Bias::Left).max(lo_bound);
let scan_hi = buffer
.clip_offset(ext_hi.saturating_add(k - 1).min(hi_bound), Bias::Right)
.min(hi_bound);
let anchor = store
.decorations_in(scan_lo..ext_lo)
.filter(is_find)
.map(|r| r.range.end)
.max();
let scan_lo = anchor.map_or(scan_lo, |a| a.max(scan_lo));
let guard = store
.decorations_in(ext_hi..scan_hi)
.filter(|r| is_find(r) && r.range.start >= ext_hi)
.map(|r| r.range.start)
.min();
(scan_lo, scan_hi, guard)
};
let mut spans: Vec<Range<u32>> = match matcher {
Matcher::Lines(re) => {
scan_buffer_lines(buffer, re, FIND_MATCH_CAP, scan_lo..scan_hi).0
}
Matcher::Literal => scan_literal(
&buffer.slice(scan_lo..scan_hi),
query,
FIND_MATCH_CAP,
)
.0
.into_iter()
.map(|r| scan_lo + r.start..scan_lo + r.end)
.collect(),
};
if let Some(guard) = guard {
spans.retain(|s| s.end <= guard);
}
store.splice_sorted_batch(&spans, DecorationKind::FindMatch, Stickiness::NeverGrows);
}
if let Some(start) = removed_active_start {
let found = store
.decorations_in(start..start)
.find(|r| is_find(r) && r.range.start == start);
self.active = found.as_ref().map(|r| r.id);
self.active_start = found.map(|r| r.range.start);
}
let fc = store.find_count();
if fc > FIND_MATCH_CAP {
if let (Some((_, kept)), Some((_, first_trimmed))) =
(store.nth_find(FIND_MATCH_CAP - 1), store.nth_find(FIND_MATCH_CAP))
{
let (kept_end, trim_from) = (kept.end, first_trimmed.start);
store.take_matching_in(trim_from..u32::MAX, |r| {
is_find(r) && r.range.start >= trim_from
});
self.capped = true;
self.coverage_end = kept_end;
self.capped_stale = false; }
} else if self.capped {
self.capped_stale = fc < FIND_MATCH_CAP;
}
if let (Some(id), Some(s)) = (self.active, self.active_start) {
let present = store.decorations_in(s..s).any(|r| is_find(&r) && r.id == id);
if !present {
self.active = None;
self.active_start = None;
}
}
}
pub fn find_next(
&mut self,
head: u32,
selection: Range<u32>,
store: &DecorationStore,
) -> Option<Range<u32>> {
self.navigate(head, selection, store, true)
}
pub fn find_prev(
&mut self,
head: u32,
selection: Range<u32>,
store: &DecorationStore,
) -> Option<Range<u32>> {
self.navigate(head, selection, store, false)
}
fn rescan(&mut self, buffer: &Buffer, store: &mut DecorationStore, now_ms: u64) {
let old_active_start =
self.active.and_then(|id| store.decoration_range(id)).map(|r| r.start);
store.take_matching_in(0..u32::MAX, is_find);
self.active = None;
self.active_start = None;
if let Some(q) = &self.query {
let within = self.scope.clone().unwrap_or(0..buffer.len());
let (spans, capped) = scan_buffer(buffer, q, crate::buffer::SCAN_WINDOW, within);
self.capped = capped;
self.coverage_end =
if capped { spans.last().map_or(0, |s| s.end) } else { buffer.len() };
let ids = store.add_sorted_batch(
spans.iter().cloned(),
DecorationKind::FindMatch,
Stickiness::NeverGrows,
);
if let Some(start) = old_active_start {
if let Some(i) = spans.iter().position(|s| s.start == start) {
self.active = Some(ids[i]); self.active_start = Some(spans[i].start); }
}
} else {
self.capped = false;
self.coverage_end = buffer.len();
}
self.capped_stale = false;
self.last_scan_ms = now_ms;
}
fn navigate(
&mut self,
head: u32,
selection: Range<u32>,
store: &DecorationStore,
forward: bool,
) -> Option<Range<u32>> {
let count = store.find_count();
if count == 0 {
self.active = None;
self.active_start = None;
return None;
}
let on = {
let r = store.find_rank_before(selection.start);
store.nth_find(r).filter(|(_, rng)| *rng == selection).map(|_| r)
};
let pick = match (on, forward) {
(Some(r), true) => (r + 1) % count,
(Some(r), false) => (r + count - 1) % count,
(None, true) => store.find_rank_before(head) % count,
(None, false) => (store.find_rank_before(head) + count - 1) % count,
};
let (id, range) = store.nth_find(pick)?;
self.active = Some(id);
self.active_start = Some(range.start);
Some(range)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn q(text: &str, case_sensitive: bool) -> FindQuery {
FindQuery { text: text.into(), case_sensitive, ..Default::default() }
}
fn qw(text: &str, case_sensitive: bool) -> FindQuery {
FindQuery { text: text.into(), case_sensitive, whole_word: true, ..Default::default() }
}
fn qr(text: &str, case_sensitive: bool) -> FindQuery {
FindQuery { text: text.into(), case_sensitive, regex: true, ..Default::default() }
}
#[test]
fn matches_are_leftmost_and_non_overlapping() {
assert_eq!(scan("aaaa", &q("aa", true)), (vec![0..2, 2..4], false));
let (spans, capped) = scan("ababa", &q("aba", true));
assert_eq!((spans.len(), spans.first().cloned(), capped), (1, Some(0..3), false));
}
#[test]
fn case_fold_is_ascii_and_toggled() {
assert_eq!(scan("AbAb", &q("ab", false)), (vec![0..2, 2..4], false));
assert_eq!(scan("AbAb", &q("ab", true)).0, Vec::<Range<u32>>::new());
assert_eq!(scan("AbAb", &q("Ab", true)), (vec![0..2, 2..4], false));
}
#[test]
fn empty_query_matches_nothing() {
assert_eq!(scan("anything", &q("", false)), (Vec::new(), false));
assert_eq!(scan("anything", &q("", true)), (Vec::new(), false));
}
#[test]
fn a_query_longer_than_the_text_finds_nothing() {
assert_eq!(scan("hi", &q("hello", true)), (Vec::new(), false));
}
#[test]
fn scan_equals_the_naive_oracle() {
fn naive(text: &str, query: &FindQuery) -> (Vec<Range<u32>>, bool) {
let needle = query.text.as_bytes();
if needle.is_empty() {
return (Vec::new(), false);
}
let hay = text.as_bytes();
let hit = |window: &[u8]| {
if query.case_sensitive {
window == needle
} else {
window.iter().zip(needle).all(|(a, b)| a.eq_ignore_ascii_case(b))
}
};
let mut spans = Vec::new();
let mut i = 0;
while i + needle.len() <= hay.len() {
if hit(&hay[i..i + needle.len()]) {
if spans.len() == FIND_MATCH_CAP {
return (spans, true);
}
spans.push(i as u32..(i + needle.len()) as u32);
i += needle.len();
} else {
i += 1;
}
}
(spans, false)
}
let mut rng = 0x2545F4914F6CDD1Du64;
let mut next = move || {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
rng
};
let alphabet = ['a', 'A', 'b', 'B', 'c', ' '];
for round in 0..200 {
let text: String = (0..(next() % 120)).map(|_| alphabet[(next() % 6) as usize]).collect();
let needle: String = (0..1 + (next() % 4)).map(|_| alphabet[(next() % 6) as usize]).collect();
for cs in [true, false] {
let query = q(&needle, cs);
assert_eq!(
scan(&text, &query),
naive(&text, &query),
"round {round}: {needle:?} (cs={cs}) in {text:?}"
);
}
}
}
#[test]
fn scan_caps_and_reports() {
let text = "ab".repeat(FIND_MATCH_CAP + 5);
let (spans, capped) = scan(&text, &q("ab", true));
assert_eq!((spans.len(), capped), (FIND_MATCH_CAP, true));
let text = "aB".repeat(FIND_MATCH_CAP + 1);
let (spans, capped) = scan(&text, &q("ab", false));
assert_eq!((spans.len(), capped), (FIND_MATCH_CAP, true));
let text = "ab".repeat(FIND_MATCH_CAP);
let (spans, capped) = scan(&text, &q("ab", true));
assert_eq!((spans.len(), capped), (FIND_MATCH_CAP, false));
}
#[test]
fn windowed_scan_equals_the_whole_text_scan() {
let corpora = [
String::new(),
"aaaa".into(),
"abababab".into(),
"aa aa aaa aaaa a".into(),
"the fox\nreturns the fox to the FOX den\nfoxfoxfox\n".repeat(40),
"ä🦀ab🦀äab日本語ab".repeat(30),
"🦀🦀🦀ab🦀🦀🦀".repeat(50),
];
let needles = ["a", "ab", "aa", "aaa", "fox", "FOX", "🦀ä", "ab🦀", "abababababab", "語ab"];
for text in &corpora {
let b = Buffer::new(text).unwrap();
let full_text = b.text();
for needle in needles {
for cs in [true, false] {
let query = q(needle, cs);
let expect = scan(&full_text, &query);
for window in [2, 3, 5, 7, 16, 64, 4096] {
assert_eq!(
scan_buffer(&b, &query, window, 0..b.len()),
expect,
"window {window}, needle {needle:?} (cs={cs}) in {:?}…",
&text[..text.len().min(24)]
);
}
}
}
}
}
#[test]
fn whole_word_is_bounded_by_line_edges() {
let text = "foo bar\nfoo\nbarfoo foo";
assert_eq!(
scan(text, &qw("foo", false)).0,
vec![0..3, 8..11, 19..22],
"line-start, whole-line, and line-end words are whole; `barfoo` is not"
);
assert_eq!(scan(text, &q("foo", false)).0, vec![0..3, 8..11, 15..18, 19..22]);
}
#[test]
fn a_regex_can_never_match_across_a_line() {
let text = "foo\nbar";
for pattern in [r"foo\nbar", r"foo.bar", r"foo\s+bar", r"foo[^x]bar"] {
assert!(
scan(text, &qr(pattern, false)).0.is_empty(),
"{pattern:?} must not span the newline"
);
}
assert_eq!(scan(text, &qr("^foo$", false)).0, vec![0..3]);
assert_eq!(scan(text, &qr("^bar$", false)).0, vec![4..7]);
assert_eq!(scan(text, &qr("[a-z]+", false)).0, vec![0..3, 4..7]);
}
#[test]
fn an_unfinished_pattern_matches_nothing() {
for pattern in ["(", "[a-", "a{2", "*"] {
assert!(scan("aaa (bbb)", &qr(pattern, false)).0.is_empty(), "{pattern:?}");
}
assert_eq!(scan("aaa (bbb)", &qr(r"\(b+\)", false)).0, vec![4..9]);
}
#[test]
fn zero_width_regex_hits_are_dropped() {
assert!(scan("bbb\n\nbbb", &qr("a*", false)).0.is_empty());
assert_eq!(scan("bab", &qr("a*", false)).0, vec![1..2], "only the non-empty hit");
}
#[test]
fn whole_word_escapes_its_literal() {
assert_eq!(scan("a.c abc", &qw("a.c", false)).0, vec![0..3], "`.` is literal here");
let q = FindQuery {
text: "foo|bar".into(),
whole_word: true,
regex: true,
..Default::default()
};
assert_eq!(scan("foo bar foobar", &q).0, vec![0..3, 4..7], "not `\\bfoo|bar\\b`");
}
#[test]
fn scoped_scan_equals_the_whole_text_scan_within_the_scope() {
let corpora = [
"fox fox fox fox fox".to_string(),
"the fox\nreturns the fox to the FOX den\nfoxfoxfox\n".repeat(8),
"ä🦀ab🦀äab日本語ab".repeat(6),
];
for text in &corpora {
let b = Buffer::new(text).unwrap();
let full = b.text();
for needle in ["a", "ab", "fox", "🦀ä"] {
for cs in [true, false] {
let query = q(needle, cs);
let all = scan(&full, &query).0;
for lo in 0..=b.len() {
for hi in lo..=b.len() {
let (slo, shi) =
(b.clip_offset(lo, Bias::Right), b.clip_offset(hi, Bias::Left));
if slo > shi {
continue;
}
let expect: Vec<_> = all
.iter()
.filter(|m| m.start >= slo && m.end <= shi)
.cloned()
.collect();
let got = scan_buffer(&b, &query, 4096, lo..hi).0;
if expect.first().is_none_or(|m| m.start == slo)
&& all.iter().all(|m| m.end <= slo || m.start >= slo)
{
assert_eq!(got, expect, "scope {lo}..{hi}, needle {needle:?}");
}
assert!(
got.iter().all(|m| m.start >= slo && m.end <= shi),
"scope {lo}..{hi} leaked a match {got:?}"
);
}
}
}
}
}
}
#[test]
fn windowed_scan_caps_like_the_whole_text_scan() {
for extra in [0usize, 3] {
let text = "ab".repeat(FIND_MATCH_CAP + extra);
let b = Buffer::new(&text).unwrap();
let query = q("ab", true);
assert_eq!(
scan_buffer(&b, &query, 4096, 0..b.len()),
scan(&b.text(), &query),
"extra {extra}"
);
}
}
#[test]
fn navigate_equals_the_full_list_walk() {
fn navigate_ref(
store: &DecorationStore,
head: u32,
selection: Range<u32>,
forward: bool,
) -> Option<(DecorationId, Range<u32>)> {
let live: Vec<(DecorationId, Range<u32>)> = store
.decorations_in(0..u32::MAX)
.filter(|r| is_find(r) && r.range.start < r.range.end)
.map(|r| (r.id, r.range.clone()))
.collect();
if live.is_empty() {
return None;
}
let on = live.iter().position(|(_, r)| *r == selection);
let pick = match (on, forward) {
(Some(p), true) => (p + 1) % live.len(),
(Some(p), false) => (p + live.len() - 1) % live.len(),
(None, true) => live.iter().position(|(_, r)| r.start >= head).unwrap_or(0),
(None, false) => {
live.iter().rposition(|(_, r)| r.start < head).unwrap_or(live.len() - 1)
}
};
Some(live[pick].clone())
}
let mut rng = 0x1234_5678_9ABC_DEF0u64;
let mut next = move || {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
rng
};
for trial in 0..3000u32 {
let mut store = DecorationStore::new();
let n = next() % 12;
let mut pos = 0u32;
let mut find_spans: Vec<Range<u32>> = Vec::new();
for _ in 0..n {
pos += (next() % 5) as u32;
let len = 1 + (next() % 4) as u32;
find_spans.push(pos..pos + len);
pos += len;
}
store.add_sorted_batch(
find_spans.iter().cloned(),
DecorationKind::FindMatch,
Stickiness::NeverGrows,
);
for _ in 0..(next() % 5) {
let s = (next() % u64::from(pos + 1)) as u32;
let l = (next() % 4) as u32;
store.add_decoration(s..s + l, DecorationKind::AutoClosePair, Stickiness::AlwaysGrows);
}
debug_assert!(
store.decorations_in(0..u32::MAX).filter(is_find).all(|r| r.range.start < r.range.end),
"trial {trial}: an empty FindMatch persisted"
);
let head = (next() % u64::from(pos + 6)) as u32;
let selection = if !find_spans.is_empty() && next() % 2 == 0 {
find_spans[(next() as usize) % find_spans.len()].clone()
} else {
let a = (next() % u64::from(pos + 6)) as u32;
let b = a + (next() % 5) as u32;
a..b
};
for forward in [true, false] {
let want = navigate_ref(&store, head, selection.clone(), forward);
let mut state = FindState::new();
let got = state.navigate(head, selection.clone(), &store, forward);
match (&want, &got) {
(Some((id, range)), Some(got_range)) => {
assert_eq!(got_range, range, "trial {trial} fwd={forward}: range");
assert_eq!(state.active, Some(*id), "trial {trial} fwd={forward}: active id");
assert_eq!(
state.active_start,
Some(range.start),
"trial {trial} fwd={forward}: active_start tracks the pick"
);
}
(None, None) => {
assert_eq!(state.active, None, "trial {trial} fwd={forward}");
assert_eq!(state.active_start, None, "trial {trial} fwd={forward}");
}
_ => panic!("trial {trial} fwd={forward}: {want:?} vs {got:?}"),
}
}
}
}
}