Skip to main content

ochrepad_core/
search.rs

1//! Find and replace over the document without copying it into a `String`.
2//!
3//! Regexes run on `regex-cursor` (a prototype-stage fork of
4//! `regex-automata` that searches discontiguous text) through a cursor over
5//! the document's chunks. It is wrapped behind [`Matcher`] so another engine
6//! — for example a `fancy-regex` fallback that materializes the text, for
7//! lookaround and backreferences — can be added later.
8//!
9//! Dialect: Rust `regex` syntax, not Notepad++'s Boost regex. There is no
10//! lookaround and no backreferences; `^`/`$` match at line boundaries.
11
12use std::fmt;
13use std::ops::Range;
14
15use regex_cursor::engines::meta::Regex;
16use regex_cursor::regex_automata::util::captures::Captures;
17use regex_cursor::regex_automata::util::interpolate;
18use regex_cursor::regex_automata::util::syntax;
19use regex_cursor::regex_automata::{Anchored, PatternID};
20use regex_cursor::{Cursor, Input};
21use serde::{Deserialize, Serialize};
22
23use crate::position::{ByteOffset, next_grapheme_boundary};
24use crate::selection::SelectionSet;
25use crate::text::Document;
26
27#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
28pub struct SearchOptions {
29    pub regex: bool,
30    pub match_case: bool,
31    pub whole_word: bool,
32    pub wrap: bool,
33    /// Count, select all and replace all only look inside the selections.
34    pub in_selection: bool,
35}
36
37/// What the find panel searches for; part of the recorded find actions.
38#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
39pub struct SearchQuery {
40    pub pattern: String,
41    pub options: SearchOptions,
42}
43
44impl SearchQuery {
45    pub fn new(pattern: impl Into<String>, options: SearchOptions) -> Self {
46        Self {
47            pattern: pattern.into(),
48            options,
49        }
50    }
51}
52
53#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct SearchError(pub String);
55
56impl fmt::Display for SearchError {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        f.write_str(&self.0)
59    }
60}
61
62/// Bytes read on demand from somewhere too large to hold in memory, such as
63/// a huge file the app reads in pieces (see [`Searcher::find_next_in`]).
64pub trait ByteSource: Sync {
65    /// The length in bytes, fixed for the duration of a search.
66    fn len(&self) -> u64;
67
68    fn is_empty(&self) -> bool {
69        self.len() == 0
70    }
71
72    /// Reads from `at` into `buf`; returns how many bytes were read: fewer
73    /// at the end of the data, and 0 past it, on an error, or to stop a
74    /// search early.
75    fn read_at(&self, at: u64, buf: &mut [u8]) -> usize;
76}
77
78/// A compiled pattern that can search a document.
79pub trait Matcher: Send + Sync {
80    /// The leftmost match inside `span`, using the text around `span` as
81    /// context for anchors and word boundaries.
82    fn find(&self, doc: &Document, span: Range<usize>) -> Option<Range<usize>>;
83
84    /// All non-overlapping matches inside `span`, in order.
85    fn find_iter<'a>(
86        &'a self,
87        doc: &'a Document,
88        span: Range<usize>,
89    ) -> Box<dyn Iterator<Item = Range<usize>> + 'a>;
90
91    /// All non-overlapping matches inside `span` of bytes read from `src`.
92    fn find_iter_source<'a>(
93        &'a self,
94        src: &'a dyn ByteSource,
95        span: Range<usize>,
96    ) -> Box<dyn Iterator<Item = Range<usize>> + 'a>;
97
98    /// All non-overlapping matches inside `span` of `hay`.
99    fn find_iter_bytes<'a>(
100        &'a self,
101        hay: &'a [u8],
102        span: Range<usize>,
103    ) -> Box<dyn Iterator<Item = Range<usize>> + 'a>;
104
105    /// Expands `$1`/`${name}` in `replacement` for the match at `m`.
106    fn expand(&self, doc: &Document, m: Range<usize>, replacement: &str) -> String;
107}
108
109/// Bytes a [`SourceCursor`] reads at a time.
110const SOURCE_CHUNK: usize = 1 << 20;
111
112fn is_continuation(b: u8) -> bool {
113    b & 0xC0 == 0x80
114}
115
116/// A [`regex_cursor::Cursor`] over a [`ByteSource`], one chunk in memory at
117/// a time. Chunks start and end only where a byte is not a UTF-8
118/// continuation byte (as the cursor contract asks: no character is split),
119/// unless the data there is not UTF-8 anyway (four or more continuation
120/// bytes in a row).
121struct SourceCursor<'a> {
122    src: &'a dyn ByteSource,
123    len: usize,
124    buf: Vec<u8>,
125    /// Where the next chunk is read, swapped in when the read works.
126    spare: Vec<u8>,
127    /// The current chunk is `buf[..used]`, at `start` in the data.
128    used: usize,
129    start: usize,
130}
131
132impl<'a> SourceCursor<'a> {
133    /// A cursor whose first chunk holds `offset`.
134    fn at(src: &'a dyn ByteSource, offset: usize) -> Self {
135        let len = usize::try_from(src.len()).unwrap_or(usize::MAX);
136        let mut cursor = Self {
137            src,
138            len,
139            buf: Vec::new(),
140            spare: Vec::new(),
141            used: 0,
142            start: 0,
143        };
144        // A boundary at or before `offset`.
145        let mut start = (offset.min(len) / SOURCE_CHUNK) * SOURCE_CHUNK;
146        let mut probe = [0u8; 1];
147        for _ in 0..3 {
148            if start == 0 || src.read_at(start as u64, &mut probe) == 0 {
149                break;
150            }
151            if !is_continuation(probe[0]) {
152                break;
153            }
154            start -= 1;
155        }
156        if !cursor.load_from(start) {
157            // Nothing to read: an empty haystack (a chunk may be empty only
158            // then).
159            cursor.len = 0;
160        }
161        cursor
162    }
163
164    /// Makes the chunk starting at `from` current; false (and nothing
165    /// changed, as the cursor contract asks) at the end of the data.
166    fn load_from(&mut self, from: usize) -> bool {
167        let want = (SOURCE_CHUNK + 3).min(self.len.saturating_sub(from));
168        if want == 0 {
169            return false;
170        }
171        self.spare.resize(want, 0);
172        let n = self.src.read_at(from as u64, &mut self.spare);
173        if n == 0 {
174            return false;
175        }
176        let mut end = n.min(SOURCE_CHUNK);
177        if n > SOURCE_CHUNK {
178            // `spare[end]` starts the next chunk: not inside a character.
179            for _ in 0..3 {
180                if !is_continuation(self.spare[end]) {
181                    break;
182                }
183                end -= 1;
184            }
185        }
186        std::mem::swap(&mut self.buf, &mut self.spare);
187        self.used = end;
188        self.start = from;
189        true
190    }
191
192    /// Makes the chunk ending at `to` current; false (and nothing changed)
193    /// at the start.
194    fn load_to(&mut self, to: usize) -> bool {
195        if to == 0 {
196            return false;
197        }
198        let from = to.saturating_sub(SOURCE_CHUNK);
199        self.spare.resize(to - from, 0);
200        let n = self.src.read_at(from as u64, &mut self.spare);
201        if n < to - from {
202            // The data shrank under us. regex-cursor keeps backtracking
203            // until it can, so this must not fail: the missing bytes read
204            // as zeros (the results are no longer exact anyway).
205            self.spare[n..].fill(0);
206        }
207        let mut skip = 0;
208        if from > 0 {
209            while skip < 3 && is_continuation(self.spare[skip]) {
210                skip += 1;
211            }
212        }
213        self.spare.copy_within(skip.., 0);
214        self.spare.truncate(to - from - skip);
215        std::mem::swap(&mut self.buf, &mut self.spare);
216        self.used = self.buf.len();
217        self.start = from + skip;
218        true
219    }
220}
221
222impl Cursor for SourceCursor<'_> {
223    fn chunk(&self) -> &[u8] {
224        &self.buf[..self.used]
225    }
226
227    fn advance(&mut self) -> bool {
228        let next = self.start + self.used;
229        next < self.len && self.load_from(next)
230    }
231
232    fn backtrack(&mut self) -> bool {
233        self.start > 0 && self.load_to(self.start)
234    }
235
236    fn total_bytes(&self) -> Option<usize> {
237        Some(self.len)
238    }
239
240    fn offset(&self) -> usize {
241        self.start
242    }
243}
244
245/// A [`regex_cursor::Cursor`] over the document's chunks.
246struct DocCursor<'a> {
247    doc: &'a Document,
248    chunk: &'a [u8],
249    start: usize,
250}
251
252impl<'a> DocCursor<'a> {
253    fn at(doc: &'a Document, offset: usize) -> Self {
254        let (chunk, start) = doc.chunk_at(ByteOffset(offset.min(doc.len().0)));
255        Self {
256            doc,
257            chunk: chunk.as_bytes(),
258            start: start.0,
259        }
260    }
261}
262
263impl Cursor for DocCursor<'_> {
264    fn chunk(&self) -> &[u8] {
265        self.chunk
266    }
267
268    fn advance(&mut self) -> bool {
269        let next = self.start + self.chunk.len();
270        if next >= self.doc.len().0 {
271            return false;
272        }
273        let (chunk, start) = self.doc.chunk_at(ByteOffset(next));
274        self.chunk = chunk.as_bytes();
275        self.start = start.0;
276        true
277    }
278
279    fn backtrack(&mut self) -> bool {
280        if self.start == 0 {
281            return false;
282        }
283        let (chunk, start) = self.doc.chunk_at(ByteOffset(self.start - 1));
284        self.chunk = chunk.as_bytes();
285        self.start = start.0;
286        true
287    }
288
289    fn total_bytes(&self) -> Option<usize> {
290        Some(self.doc.len().0)
291    }
292
293    fn offset(&self) -> usize {
294        self.start
295    }
296}
297
298struct CursorRegex {
299    re: Regex,
300    /// Literal mode: `$` in the replacement is not special.
301    expand_captures: bool,
302}
303
304fn input(doc: &Document, span: Range<usize>) -> Input<DocCursor<'_>> {
305    Input::new(DocCursor::at(doc, span.start)).range(span)
306}
307
308impl Matcher for CursorRegex {
309    fn find(&self, doc: &Document, span: Range<usize>) -> Option<Range<usize>> {
310        self.re.find(input(doc, span)).map(|m| m.range())
311    }
312
313    fn find_iter<'a>(
314        &'a self,
315        doc: &'a Document,
316        span: Range<usize>,
317    ) -> Box<dyn Iterator<Item = Range<usize>> + 'a> {
318        Box::new(self.re.find_iter(input(doc, span)).map(|m| m.range()))
319    }
320
321    fn find_iter_source<'a>(
322        &'a self,
323        src: &'a dyn ByteSource,
324        span: Range<usize>,
325    ) -> Box<dyn Iterator<Item = Range<usize>> + 'a> {
326        let cursor = SourceCursor::at(src, span.start);
327        let len = cursor.len;
328        let span = span.start.min(len)..span.end.min(len);
329        Box::new(
330            self.re
331                .find_iter(Input::new(cursor).range(span))
332                .map(|m| m.range()),
333        )
334    }
335
336    fn find_iter_bytes<'a>(
337        &'a self,
338        hay: &'a [u8],
339        span: Range<usize>,
340    ) -> Box<dyn Iterator<Item = Range<usize>> + 'a> {
341        let span = span.start.min(hay.len())..span.end.min(hay.len());
342        Box::new(
343            self.re
344                .find_iter(Input::new(hay).range(span))
345                .map(|m| m.range()),
346        )
347    }
348
349    fn expand(&self, doc: &Document, m: Range<usize>, replacement: &str) -> String {
350        if !self.expand_captures {
351            return replacement.to_owned();
352        }
353        let mut caps: Captures = self.re.create_captures();
354        let mut inp = input(doc, m.start..doc.len().0);
355        inp.anchored(Anchored::Yes);
356        self.re.captures(inp, &mut caps);
357        let replacement = unescape(replacement);
358        let mut out = String::new();
359        interpolate::string(
360            &replacement,
361            |i, dst| {
362                if let Some(span) = caps.get_group(i) {
363                    dst.push_str(&doc.slice(ByteOffset(span.start)..ByteOffset(span.end)));
364                }
365            },
366            |name| caps.group_info().to_index(PatternID::ZERO, name),
367            &mut out,
368        );
369        out
370    }
371}
372
373/// `\n`, `\t` and `\\` in a regex-mode replacement, as in Notepad++.
374fn unescape(s: &str) -> String {
375    let mut out = String::with_capacity(s.len());
376    let mut chars = s.chars();
377    while let Some(c) = chars.next() {
378        if c != '\\' {
379            out.push(c);
380            continue;
381        }
382        match chars.next() {
383            Some('n') => out.push('\n'),
384            Some('t') => out.push('\t'),
385            Some('\\') => out.push('\\'),
386            Some(other) => {
387                out.push('\\');
388                out.push(other);
389            }
390            None => out.push('\\'),
391        }
392    }
393    out
394}
395
396/// Escapes regex metacharacters so `s` matches literally.
397pub fn escape(s: &str) -> String {
398    let mut out = String::with_capacity(s.len() * 2);
399    for c in s.chars() {
400        if r"\.+*?()|[]{}^$#&-~".contains(c) {
401            out.push('\\');
402        }
403        out.push(c);
404    }
405    out
406}
407
408fn is_word_char(c: char) -> bool {
409    c.is_alphanumeric() || c == '_'
410}
411
412/// A query compiled for searching.
413pub struct Searcher {
414    matcher: Box<dyn Matcher>,
415    options: SearchOptions,
416}
417
418impl Searcher {
419    pub fn new(query: &SearchQuery) -> Result<Self, SearchError> {
420        Self::build(query, false)
421    }
422
423    /// A searcher for text that keeps its CR LF and CR line endings (a file
424    /// read as it is on disk): `^`, `$` and `.` treat `\r` as part of the
425    /// line break, as they treat `\n`.
426    pub fn new_raw(query: &SearchQuery) -> Result<Self, SearchError> {
427        Self::build(query, true)
428    }
429
430    fn build(query: &SearchQuery, crlf: bool) -> Result<Self, SearchError> {
431        let o = &query.options;
432        if query.pattern.is_empty() {
433            return Err(SearchError("nothing to find".to_owned()));
434        }
435        let pattern = if o.regex {
436            if o.whole_word {
437                format!(r"\b(?:{})\b", query.pattern)
438            } else {
439                query.pattern.clone()
440            }
441        } else {
442            let mut p = escape(&query.pattern);
443            // Word boundaries only where the text starts or ends with a word
444            // character, so "foo(" can still match as a whole word.
445            if o.whole_word {
446                if query.pattern.chars().next().is_some_and(is_word_char) {
447                    p.insert_str(0, r"\b");
448                }
449                if query.pattern.chars().last().is_some_and(is_word_char) {
450                    p.push_str(r"\b");
451                }
452            }
453            p
454        };
455        let re = Regex::builder()
456            .syntax(
457                syntax::Config::new()
458                    .case_insensitive(!o.match_case)
459                    .multi_line(true)
460                    .crlf(crlf),
461            )
462            .build(&pattern)
463            .map_err(|e| SearchError(e.to_string()))?;
464        Ok(Self {
465            matcher: Box::new(CursorRegex {
466                re,
467                expand_captures: o.regex,
468            }),
469            options: o.clone(),
470        })
471    }
472
473    pub fn options(&self) -> &SearchOptions {
474        &self.options
475    }
476
477    /// The first match starting at or after `from`.
478    pub fn find_next(&self, doc: &Document, from: ByteOffset) -> Option<Range<ByteOffset>> {
479        let len = doc.len().0;
480        (from.0 <= len)
481            .then(|| self.matcher.find(doc, from.0..len))
482            .flatten()
483            .map(to_offsets)
484    }
485
486    /// The last match starting before `before`. Searches backwards in
487    /// growing windows, so a match near the caret is found quickly.
488    pub fn find_prev(&self, doc: &Document, before: ByteOffset) -> Option<Range<ByteOffset>> {
489        let len = doc.len().0;
490        let mut window = 64 * 1024;
491        loop {
492            let start = before.0.saturating_sub(window);
493            let last = self
494                .matcher
495                .find_iter(doc, start..len)
496                .take_while(|m| m.start < before.0)
497                .last();
498            if last.is_some() || start == 0 {
499                return last.map(to_offsets);
500            }
501            window *= 4;
502        }
503    }
504
505    /// The matches inside `range`, found as the iterator is advanced (so a
506    /// caller can stop after enough of them).
507    pub fn matches<'a>(
508        &'a self,
509        doc: &'a Document,
510        range: Range<ByteOffset>,
511    ) -> impl Iterator<Item = Range<ByteOffset>> + 'a {
512        self.matcher
513            .find_iter(doc, range.start.0..range.end.0)
514            .map(to_offsets)
515    }
516
517    /// All matches inside `range`.
518    pub fn find_all(&self, doc: &Document, range: Range<ByteOffset>) -> Vec<Range<ByteOffset>> {
519        self.matcher
520            .find_iter(doc, range.start.0..range.end.0)
521            .map(to_offsets)
522            .collect()
523    }
524
525    /// Every match in `hay`: text already in memory, such as a line on
526    /// screen.
527    pub fn find_all_bytes(&self, hay: &[u8]) -> Vec<Range<usize>> {
528        self.matcher.find_iter_bytes(hay, 0..hay.len()).collect()
529    }
530
531    /// The matches inside `range` of `src`, found as the iterator is
532    /// advanced (reading `src` a chunk at a time).
533    pub fn matches_in<'a>(
534        &'a self,
535        src: &'a dyn ByteSource,
536        range: Range<u64>,
537    ) -> impl Iterator<Item = Range<u64>> + 'a {
538        self.matcher
539            .find_iter_source(src, range.start as usize..range.end as usize)
540            .map(|m| m.start as u64..m.end as u64)
541    }
542
543    /// The first match in `src` starting at or after `from`.
544    pub fn find_next_in(&self, src: &dyn ByteSource, from: u64) -> Option<Range<u64>> {
545        let len = src.len();
546        (from <= len)
547            .then(|| self.matches_in(src, from..len).next())
548            .flatten()
549    }
550
551    /// The last match in `src` starting before `before`, looked for in
552    /// growing windows before it. A match may run at most 64 KB past
553    /// `before` (the search stops there, so a document without matches
554    /// after the caret is not read to its end).
555    pub fn find_prev_in(&self, src: &dyn ByteSource, before: u64) -> Option<Range<u64>> {
556        let len = src.len();
557        let before = before.min(len);
558        let end = before.saturating_add(64 * 1024).min(len);
559        let mut window: u64 = 64 * 1024;
560        loop {
561            let start = before.saturating_sub(window);
562            let last = self
563                .matches_in(src, start..end)
564                .take_while(|m| m.start < before)
565                .last();
566            if last.is_some() || start == 0 {
567                return last;
568            }
569            window = window.saturating_mul(4);
570        }
571    }
572
573    /// The replacement text for the match at `m`.
574    pub fn expand(&self, doc: &Document, m: Range<ByteOffset>, replacement: &str) -> String {
575        self.matcher.expand(doc, m.start.0..m.end.0, replacement)
576    }
577
578    /// The ranges to search: the non-empty selections with "in selection"
579    /// on, otherwise the whole document.
580    pub fn ranges(&self, doc: &Document, sels: &SelectionSet) -> Vec<Range<ByteOffset>> {
581        let selected: Vec<_> = sels
582            .ranges()
583            .iter()
584            .filter(|s| !s.is_empty())
585            .map(|s| s.range())
586            .collect();
587        if self.options.in_selection && !selected.is_empty() {
588            selected
589        } else {
590            vec![ByteOffset(0)..doc.len()]
591        }
592    }
593
594    /// Matches in the ranges `ranges` selects.
595    pub fn find_in(&self, doc: &Document, sels: &SelectionSet) -> Vec<Range<ByteOffset>> {
596        self.ranges(doc, sels)
597            .into_iter()
598            .flat_map(|r| self.find_all(doc, r))
599            .collect()
600    }
601
602    /// The match to select for Find Next/Previous from the current primary
603    /// selection, wrapping if enabled.
604    pub fn next_from(
605        &self,
606        doc: &Document,
607        sels: &SelectionSet,
608        backward: bool,
609    ) -> Option<Range<ByteOffset>> {
610        let cur = sels.primary().range();
611        if backward {
612            self.find_prev(doc, cur.start)
613                .filter(|m| *m != cur)
614                .or_else(|| {
615                    self.options
616                        .wrap
617                        .then(|| self.find_prev(doc, ByteOffset(doc.len().0 + 1)))
618                        .flatten()
619                })
620        } else {
621            let mut found = self.find_next(doc, cur.end);
622            // An empty match at the caret is where we already are: step on.
623            if found.as_ref() == Some(&cur) && cur.end < doc.len() {
624                found = self.find_next(doc, next_grapheme_boundary(doc, cur.end));
625            } else if found.as_ref() == Some(&cur) {
626                found = None;
627            }
628            found.or_else(|| {
629                self.options
630                    .wrap
631                    .then(|| self.find_next(doc, ByteOffset(0)))
632                    .flatten()
633            })
634        }
635    }
636
637    /// If the primary selection is exactly a match, the range and its
638    /// replacement text (for Replace).
639    pub fn replacement_for_selection(
640        &self,
641        doc: &Document,
642        sels: &SelectionSet,
643        replacement: &str,
644    ) -> Option<(Range<ByteOffset>, String)> {
645        let cur = sels.primary().range();
646        let m = self.find_next(doc, cur.start)?;
647        (m == cur).then(|| {
648            let text = self.expand(doc, m.clone(), replacement);
649            (m, text)
650        })
651    }
652}
653
654fn to_offsets(r: Range<usize>) -> Range<ByteOffset> {
655    ByteOffset(r.start)..ByteOffset(r.end)
656}
657
658#[cfg(test)]
659mod tests {
660    use super::*;
661
662    fn q(pattern: &str, f: impl FnOnce(&mut SearchOptions)) -> Searcher {
663        let mut o = SearchOptions::default();
664        f(&mut o);
665        Searcher::new(&SearchQuery::new(pattern, o)).expect("valid pattern")
666    }
667
668    fn all(s: &Searcher, text: &str) -> Vec<(usize, usize)> {
669        let doc = Document::new(text);
670        s.find_all(&doc, ByteOffset(0)..doc.len())
671            .into_iter()
672            .map(|r| (r.start.0, r.end.0))
673            .collect()
674    }
675
676    #[test]
677    fn literal_case_and_whole_word() {
678        let text = "Foo foo food (foo) foo(";
679        assert_eq!(all(&q("foo", |_| {}), text).len(), 5);
680        assert_eq!(all(&q("foo", |o| o.match_case = true), text).len(), 4);
681        assert_eq!(
682            all(&q("foo", |o| o.whole_word = true), text),
683            vec![(0, 3), (4, 7), (14, 17), (19, 22)]
684        );
685        // A literal ending in a non-word char still matches as a word.
686        assert_eq!(
687            all(&q("foo(", |o| o.whole_word = true), text),
688            vec![(19, 23)]
689        );
690        // Metacharacters are literal unless regex is on.
691        assert_eq!(all(&q("(foo)", |_| {}), text), vec![(13, 18)]);
692    }
693
694    #[test]
695    fn regex_lines_and_invalid_patterns() {
696        let s = q(r"^\w+", |o| o.regex = true);
697        assert_eq!(all(&s, "ab cd\nef"), vec![(0, 2), (6, 8)]);
698        let err = Searcher::new(&SearchQuery::new(
699            "(unclosed",
700            SearchOptions {
701                regex: true,
702                ..Default::default()
703            },
704        ));
705        assert!(err.is_err());
706        // No lookaround in this dialect.
707        assert!(
708            Searcher::new(&SearchQuery::new(
709                "a(?=b)",
710                SearchOptions {
711                    regex: true,
712                    ..Default::default()
713                }
714            ))
715            .is_err()
716        );
717    }
718
719    #[test]
720    fn matches_across_rope_chunks() {
721        let text = format!("{}needle{}", "x".repeat(5000), "y".repeat(5000)).repeat(20);
722        let s = q("needle", |_| {});
723        let found = all(&s, &text);
724        assert_eq!(found.len(), 20);
725        let doc = Document::new(&text);
726        for (a, b) in found {
727            assert_eq!(doc.slice(ByteOffset(a)..ByteOffset(b)), "needle");
728        }
729    }
730
731    #[test]
732    fn previous_and_next() {
733        let doc = Document::new(&format!("ab{}ab", "-".repeat(200_000)));
734        let s = q("ab", |_| {});
735        let end = doc.len();
736        assert_eq!(s.find_prev(&doc, end).map(|r| r.start.0), Some(200_002));
737        assert_eq!(
738            s.find_prev(&doc, ByteOffset(200_002)).map(|r| r.start.0),
739            Some(0)
740        );
741        assert_eq!(s.find_prev(&doc, ByteOffset(0)), None);
742        assert_eq!(
743            s.find_next(&doc, ByteOffset(1)).map(|r| r.start.0),
744            Some(200_002)
745        );
746    }
747
748    /// Bytes in memory, read like a file; with `shrink_to`, everything from
749    /// there on reads as gone (a file truncated during the search).
750    struct Bytes {
751        data: Vec<u8>,
752        shrink_to: Option<usize>,
753    }
754
755    impl ByteSource for Bytes {
756        fn len(&self) -> u64 {
757            self.data.len() as u64
758        }
759
760        fn read_at(&self, at: u64, buf: &mut [u8]) -> usize {
761            let end = self.shrink_to.unwrap_or(self.data.len());
762            let at = (at as usize).min(end);
763            let n = buf.len().min(end - at);
764            buf[..n].copy_from_slice(&self.data[at..at + n]);
765            n
766        }
767    }
768
769    fn source(text: &str) -> Bytes {
770        Bytes {
771            data: text.as_bytes().to_vec(),
772            shrink_to: None,
773        }
774    }
775
776    fn found(s: &Searcher, src: &Bytes) -> Vec<(usize, usize)> {
777        s.matches_in(src, 0..src.len())
778            .map(|m| (m.start as usize, m.end as usize))
779            .collect()
780    }
781
782    #[test]
783    fn sources_are_searched_across_chunk_boundaries() {
784        // Matches straddling every alignment of the 1 MB chunk boundary,
785        // with multibyte characters around it.
786        for shift in 0..8 {
787            let pad = SOURCE_CHUNK - 3 - shift;
788            let text = format!("{}é needle éé needle", "x".repeat(pad));
789            let src = source(&text);
790            let s = q("needle", |_| {});
791            let expected = all(&s, &text);
792            assert_eq!(expected.len(), 2);
793            assert_eq!(found(&s, &src), expected, "shift {shift}");
794            // Whole words need the characters on both sides of the boundary.
795            let w = q("é", |o| o.whole_word = true);
796            assert_eq!(found(&w, &src), all(&w, &text), "shift {shift}");
797        }
798    }
799
800    #[test]
801    fn next_and_previous_in_a_source() {
802        let text = format!("ab{}ab", "-".repeat(3 * SOURCE_CHUNK));
803        let src = source(&text);
804        let s = q("ab", |_| {});
805        let last = (text.len() - 2) as u64;
806        assert_eq!(s.find_next_in(&src, 1).map(|m| m.start), Some(last));
807        assert_eq!(s.find_prev_in(&src, src.len()).map(|m| m.start), Some(last));
808        assert_eq!(s.find_prev_in(&src, last).map(|m| m.start), Some(0));
809        assert_eq!(s.find_prev_in(&src, 0), None);
810        assert_eq!(s.find_next_in(&src, last + 1), None);
811    }
812
813    #[test]
814    fn raw_searchers_treat_cr_as_part_of_the_line_break() {
815        let text = "one\r\ntwo\rthree\n";
816        let raw = Searcher::new_raw(&SearchQuery::new(
817            r"\w+$",
818            SearchOptions {
819                regex: true,
820                ..Default::default()
821            },
822        ))
823        .unwrap();
824        let src = source(text);
825        assert_eq!(found(&raw, &src), vec![(0, 3), (5, 8), (9, 14)]);
826        assert_eq!(raw.find_all_bytes(text.as_bytes()).len(), 3);
827    }
828
829    #[test]
830    fn a_source_that_shrinks_mid_search_ends_it() {
831        let text = format!(
832            "{}needle{}needle",
833            "x".repeat(SOURCE_CHUNK),
834            "y".repeat(2 * SOURCE_CHUNK)
835        );
836        let mut src = source(&text);
837        src.shrink_to = Some(SOURCE_CHUNK + 100);
838        let s = q("needle", |_| {});
839        assert_eq!(found(&s, &src), vec![(SOURCE_CHUNK, SOURCE_CHUNK + 6)]);
840        // Backwards too (regex-cursor backtracks until it can).
841        assert!(s.find_prev_in(&src, src.len()).is_some());
842        let empty = source("");
843        assert_eq!(found(&s, &empty), vec![]);
844    }
845
846    #[test]
847    fn expansion() {
848        let doc = Document::new("John Smith");
849        let s = q(r"(?<first>\w+) (\w+)", |o| o.regex = true);
850        let m = s.find_next(&doc, ByteOffset(0)).unwrap();
851        assert_eq!(s.expand(&doc, m.clone(), "$2, ${first}"), "Smith, John");
852        assert_eq!(s.expand(&doc, m.clone(), r"$0\n\t$$"), "John Smith\n\t$");
853        // Literal mode leaves the replacement alone.
854        let lit = q("John", |_| {});
855        let m = lit.find_next(&doc, ByteOffset(0)).unwrap();
856        assert_eq!(lit.expand(&doc, m, "$1 \\n"), "$1 \\n");
857    }
858}