1use 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 pub in_selection: bool,
35}
36
37#[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
62pub trait Matcher: Send + Sync {
64 fn find(&self, doc: &Document, span: Range<usize>) -> Option<Range<usize>>;
67
68 fn find_iter<'a>(
70 &'a self,
71 doc: &'a Document,
72 span: Range<usize>,
73 ) -> Box<dyn Iterator<Item = Range<usize>> + 'a>;
74
75 fn expand(&self, doc: &Document, m: Range<usize>, replacement: &str) -> String;
77}
78
79struct DocCursor<'a> {
81 doc: &'a Document,
82 chunk: &'a [u8],
83 start: usize,
84}
85
86impl<'a> DocCursor<'a> {
87 fn at(doc: &'a Document, offset: usize) -> Self {
88 let (chunk, start) = doc.chunk_at(ByteOffset(offset.min(doc.len().0)));
89 Self {
90 doc,
91 chunk: chunk.as_bytes(),
92 start: start.0,
93 }
94 }
95}
96
97impl Cursor for DocCursor<'_> {
98 fn chunk(&self) -> &[u8] {
99 self.chunk
100 }
101
102 fn advance(&mut self) -> bool {
103 let next = self.start + self.chunk.len();
104 if next >= self.doc.len().0 {
105 return false;
106 }
107 let (chunk, start) = self.doc.chunk_at(ByteOffset(next));
108 self.chunk = chunk.as_bytes();
109 self.start = start.0;
110 true
111 }
112
113 fn backtrack(&mut self) -> bool {
114 if self.start == 0 {
115 return false;
116 }
117 let (chunk, start) = self.doc.chunk_at(ByteOffset(self.start - 1));
118 self.chunk = chunk.as_bytes();
119 self.start = start.0;
120 true
121 }
122
123 fn total_bytes(&self) -> Option<usize> {
124 Some(self.doc.len().0)
125 }
126
127 fn offset(&self) -> usize {
128 self.start
129 }
130}
131
132struct CursorRegex {
133 re: Regex,
134 expand_captures: bool,
136}
137
138fn input(doc: &Document, span: Range<usize>) -> Input<DocCursor<'_>> {
139 Input::new(DocCursor::at(doc, span.start)).range(span)
140}
141
142impl Matcher for CursorRegex {
143 fn find(&self, doc: &Document, span: Range<usize>) -> Option<Range<usize>> {
144 self.re.find(input(doc, span)).map(|m| m.range())
145 }
146
147 fn find_iter<'a>(
148 &'a self,
149 doc: &'a Document,
150 span: Range<usize>,
151 ) -> Box<dyn Iterator<Item = Range<usize>> + 'a> {
152 Box::new(self.re.find_iter(input(doc, span)).map(|m| m.range()))
153 }
154
155 fn expand(&self, doc: &Document, m: Range<usize>, replacement: &str) -> String {
156 if !self.expand_captures {
157 return replacement.to_owned();
158 }
159 let mut caps: Captures = self.re.create_captures();
160 let mut inp = input(doc, m.start..doc.len().0);
161 inp.anchored(Anchored::Yes);
162 self.re.captures(inp, &mut caps);
163 let replacement = unescape(replacement);
164 let mut out = String::new();
165 interpolate::string(
166 &replacement,
167 |i, dst| {
168 if let Some(span) = caps.get_group(i) {
169 dst.push_str(&doc.slice(ByteOffset(span.start)..ByteOffset(span.end)));
170 }
171 },
172 |name| caps.group_info().to_index(PatternID::ZERO, name),
173 &mut out,
174 );
175 out
176 }
177}
178
179fn unescape(s: &str) -> String {
181 let mut out = String::with_capacity(s.len());
182 let mut chars = s.chars();
183 while let Some(c) = chars.next() {
184 if c != '\\' {
185 out.push(c);
186 continue;
187 }
188 match chars.next() {
189 Some('n') => out.push('\n'),
190 Some('t') => out.push('\t'),
191 Some('\\') => out.push('\\'),
192 Some(other) => {
193 out.push('\\');
194 out.push(other);
195 }
196 None => out.push('\\'),
197 }
198 }
199 out
200}
201
202pub fn escape(s: &str) -> String {
204 let mut out = String::with_capacity(s.len() * 2);
205 for c in s.chars() {
206 if r"\.+*?()|[]{}^$#&-~".contains(c) {
207 out.push('\\');
208 }
209 out.push(c);
210 }
211 out
212}
213
214fn is_word_char(c: char) -> bool {
215 c.is_alphanumeric() || c == '_'
216}
217
218pub struct Searcher {
220 matcher: Box<dyn Matcher>,
221 options: SearchOptions,
222}
223
224impl Searcher {
225 pub fn new(query: &SearchQuery) -> Result<Self, SearchError> {
226 let o = &query.options;
227 if query.pattern.is_empty() {
228 return Err(SearchError("nothing to find".to_owned()));
229 }
230 let pattern = if o.regex {
231 if o.whole_word {
232 format!(r"\b(?:{})\b", query.pattern)
233 } else {
234 query.pattern.clone()
235 }
236 } else {
237 let mut p = escape(&query.pattern);
238 if o.whole_word {
241 if query.pattern.chars().next().is_some_and(is_word_char) {
242 p.insert_str(0, r"\b");
243 }
244 if query.pattern.chars().last().is_some_and(is_word_char) {
245 p.push_str(r"\b");
246 }
247 }
248 p
249 };
250 let re = Regex::builder()
251 .syntax(
252 syntax::Config::new()
253 .case_insensitive(!o.match_case)
254 .multi_line(true),
255 )
256 .build(&pattern)
257 .map_err(|e| SearchError(e.to_string()))?;
258 Ok(Self {
259 matcher: Box::new(CursorRegex {
260 re,
261 expand_captures: o.regex,
262 }),
263 options: o.clone(),
264 })
265 }
266
267 pub fn options(&self) -> &SearchOptions {
268 &self.options
269 }
270
271 pub fn find_next(&self, doc: &Document, from: ByteOffset) -> Option<Range<ByteOffset>> {
273 let len = doc.len().0;
274 (from.0 <= len)
275 .then(|| self.matcher.find(doc, from.0..len))
276 .flatten()
277 .map(to_offsets)
278 }
279
280 pub fn find_prev(&self, doc: &Document, before: ByteOffset) -> Option<Range<ByteOffset>> {
283 let len = doc.len().0;
284 let mut window = 64 * 1024;
285 loop {
286 let start = before.0.saturating_sub(window);
287 let last = self
288 .matcher
289 .find_iter(doc, start..len)
290 .take_while(|m| m.start < before.0)
291 .last();
292 if last.is_some() || start == 0 {
293 return last.map(to_offsets);
294 }
295 window *= 4;
296 }
297 }
298
299 pub fn find_all(&self, doc: &Document, range: Range<ByteOffset>) -> Vec<Range<ByteOffset>> {
301 self.matcher
302 .find_iter(doc, range.start.0..range.end.0)
303 .map(to_offsets)
304 .collect()
305 }
306
307 pub fn expand(&self, doc: &Document, m: Range<ByteOffset>, replacement: &str) -> String {
309 self.matcher.expand(doc, m.start.0..m.end.0, replacement)
310 }
311
312 pub fn ranges(&self, doc: &Document, sels: &SelectionSet) -> Vec<Range<ByteOffset>> {
315 let selected: Vec<_> = sels
316 .ranges()
317 .iter()
318 .filter(|s| !s.is_empty())
319 .map(|s| s.range())
320 .collect();
321 if self.options.in_selection && !selected.is_empty() {
322 selected
323 } else {
324 vec![ByteOffset(0)..doc.len()]
325 }
326 }
327
328 pub fn find_in(&self, doc: &Document, sels: &SelectionSet) -> Vec<Range<ByteOffset>> {
330 self.ranges(doc, sels)
331 .into_iter()
332 .flat_map(|r| self.find_all(doc, r))
333 .collect()
334 }
335
336 pub fn next_from(
339 &self,
340 doc: &Document,
341 sels: &SelectionSet,
342 backward: bool,
343 ) -> Option<Range<ByteOffset>> {
344 let cur = sels.primary().range();
345 if backward {
346 self.find_prev(doc, cur.start)
347 .filter(|m| *m != cur)
348 .or_else(|| {
349 self.options
350 .wrap
351 .then(|| self.find_prev(doc, ByteOffset(doc.len().0 + 1)))
352 .flatten()
353 })
354 } else {
355 let mut found = self.find_next(doc, cur.end);
356 if found.as_ref() == Some(&cur) && cur.end < doc.len() {
358 found = self.find_next(doc, next_grapheme_boundary(doc, cur.end));
359 } else if found.as_ref() == Some(&cur) {
360 found = None;
361 }
362 found.or_else(|| {
363 self.options
364 .wrap
365 .then(|| self.find_next(doc, ByteOffset(0)))
366 .flatten()
367 })
368 }
369 }
370
371 pub fn replacement_for_selection(
374 &self,
375 doc: &Document,
376 sels: &SelectionSet,
377 replacement: &str,
378 ) -> Option<(Range<ByteOffset>, String)> {
379 let cur = sels.primary().range();
380 let m = self.find_next(doc, cur.start)?;
381 (m == cur).then(|| {
382 let text = self.expand(doc, m.clone(), replacement);
383 (m, text)
384 })
385 }
386}
387
388fn to_offsets(r: Range<usize>) -> Range<ByteOffset> {
389 ByteOffset(r.start)..ByteOffset(r.end)
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 fn q(pattern: &str, f: impl FnOnce(&mut SearchOptions)) -> Searcher {
397 let mut o = SearchOptions::default();
398 f(&mut o);
399 Searcher::new(&SearchQuery::new(pattern, o)).expect("valid pattern")
400 }
401
402 fn all(s: &Searcher, text: &str) -> Vec<(usize, usize)> {
403 let doc = Document::new(text);
404 s.find_all(&doc, ByteOffset(0)..doc.len())
405 .into_iter()
406 .map(|r| (r.start.0, r.end.0))
407 .collect()
408 }
409
410 #[test]
411 fn literal_case_and_whole_word() {
412 let text = "Foo foo food (foo) foo(";
413 assert_eq!(all(&q("foo", |_| {}), text).len(), 5);
414 assert_eq!(all(&q("foo", |o| o.match_case = true), text).len(), 4);
415 assert_eq!(
416 all(&q("foo", |o| o.whole_word = true), text),
417 vec![(0, 3), (4, 7), (14, 17), (19, 22)]
418 );
419 assert_eq!(
421 all(&q("foo(", |o| o.whole_word = true), text),
422 vec![(19, 23)]
423 );
424 assert_eq!(all(&q("(foo)", |_| {}), text), vec![(13, 18)]);
426 }
427
428 #[test]
429 fn regex_lines_and_invalid_patterns() {
430 let s = q(r"^\w+", |o| o.regex = true);
431 assert_eq!(all(&s, "ab cd\nef"), vec![(0, 2), (6, 8)]);
432 let err = Searcher::new(&SearchQuery::new(
433 "(unclosed",
434 SearchOptions {
435 regex: true,
436 ..Default::default()
437 },
438 ));
439 assert!(err.is_err());
440 assert!(
442 Searcher::new(&SearchQuery::new(
443 "a(?=b)",
444 SearchOptions {
445 regex: true,
446 ..Default::default()
447 }
448 ))
449 .is_err()
450 );
451 }
452
453 #[test]
454 fn matches_across_rope_chunks() {
455 let text = format!("{}needle{}", "x".repeat(5000), "y".repeat(5000)).repeat(20);
456 let s = q("needle", |_| {});
457 let found = all(&s, &text);
458 assert_eq!(found.len(), 20);
459 let doc = Document::new(&text);
460 for (a, b) in found {
461 assert_eq!(doc.slice(ByteOffset(a)..ByteOffset(b)), "needle");
462 }
463 }
464
465 #[test]
466 fn previous_and_next() {
467 let doc = Document::new(&format!("ab{}ab", "-".repeat(200_000)));
468 let s = q("ab", |_| {});
469 let end = doc.len();
470 assert_eq!(s.find_prev(&doc, end).map(|r| r.start.0), Some(200_002));
471 assert_eq!(
472 s.find_prev(&doc, ByteOffset(200_002)).map(|r| r.start.0),
473 Some(0)
474 );
475 assert_eq!(s.find_prev(&doc, ByteOffset(0)), None);
476 assert_eq!(
477 s.find_next(&doc, ByteOffset(1)).map(|r| r.start.0),
478 Some(200_002)
479 );
480 }
481
482 #[test]
483 fn expansion() {
484 let doc = Document::new("John Smith");
485 let s = q(r"(?<first>\w+) (\w+)", |o| o.regex = true);
486 let m = s.find_next(&doc, ByteOffset(0)).unwrap();
487 assert_eq!(s.expand(&doc, m.clone(), "$2, ${first}"), "Smith, John");
488 assert_eq!(s.expand(&doc, m.clone(), r"$0\n\t$$"), "John Smith\n\t$");
489 let lit = q("John", |_| {});
491 let m = lit.find_next(&doc, ByteOffset(0)).unwrap();
492 assert_eq!(lit.expand(&doc, m, "$1 \\n"), "$1 \\n");
493 }
494}