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 matches<'a>(
302 &'a self,
303 doc: &'a Document,
304 range: Range<ByteOffset>,
305 ) -> impl Iterator<Item = Range<ByteOffset>> + 'a {
306 self.matcher
307 .find_iter(doc, range.start.0..range.end.0)
308 .map(to_offsets)
309 }
310
311 pub fn find_all(&self, doc: &Document, range: Range<ByteOffset>) -> Vec<Range<ByteOffset>> {
313 self.matcher
314 .find_iter(doc, range.start.0..range.end.0)
315 .map(to_offsets)
316 .collect()
317 }
318
319 pub fn expand(&self, doc: &Document, m: Range<ByteOffset>, replacement: &str) -> String {
321 self.matcher.expand(doc, m.start.0..m.end.0, replacement)
322 }
323
324 pub fn ranges(&self, doc: &Document, sels: &SelectionSet) -> Vec<Range<ByteOffset>> {
327 let selected: Vec<_> = sels
328 .ranges()
329 .iter()
330 .filter(|s| !s.is_empty())
331 .map(|s| s.range())
332 .collect();
333 if self.options.in_selection && !selected.is_empty() {
334 selected
335 } else {
336 vec![ByteOffset(0)..doc.len()]
337 }
338 }
339
340 pub fn find_in(&self, doc: &Document, sels: &SelectionSet) -> Vec<Range<ByteOffset>> {
342 self.ranges(doc, sels)
343 .into_iter()
344 .flat_map(|r| self.find_all(doc, r))
345 .collect()
346 }
347
348 pub fn next_from(
351 &self,
352 doc: &Document,
353 sels: &SelectionSet,
354 backward: bool,
355 ) -> Option<Range<ByteOffset>> {
356 let cur = sels.primary().range();
357 if backward {
358 self.find_prev(doc, cur.start)
359 .filter(|m| *m != cur)
360 .or_else(|| {
361 self.options
362 .wrap
363 .then(|| self.find_prev(doc, ByteOffset(doc.len().0 + 1)))
364 .flatten()
365 })
366 } else {
367 let mut found = self.find_next(doc, cur.end);
368 if found.as_ref() == Some(&cur) && cur.end < doc.len() {
370 found = self.find_next(doc, next_grapheme_boundary(doc, cur.end));
371 } else if found.as_ref() == Some(&cur) {
372 found = None;
373 }
374 found.or_else(|| {
375 self.options
376 .wrap
377 .then(|| self.find_next(doc, ByteOffset(0)))
378 .flatten()
379 })
380 }
381 }
382
383 pub fn replacement_for_selection(
386 &self,
387 doc: &Document,
388 sels: &SelectionSet,
389 replacement: &str,
390 ) -> Option<(Range<ByteOffset>, String)> {
391 let cur = sels.primary().range();
392 let m = self.find_next(doc, cur.start)?;
393 (m == cur).then(|| {
394 let text = self.expand(doc, m.clone(), replacement);
395 (m, text)
396 })
397 }
398}
399
400fn to_offsets(r: Range<usize>) -> Range<ByteOffset> {
401 ByteOffset(r.start)..ByteOffset(r.end)
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 fn q(pattern: &str, f: impl FnOnce(&mut SearchOptions)) -> Searcher {
409 let mut o = SearchOptions::default();
410 f(&mut o);
411 Searcher::new(&SearchQuery::new(pattern, o)).expect("valid pattern")
412 }
413
414 fn all(s: &Searcher, text: &str) -> Vec<(usize, usize)> {
415 let doc = Document::new(text);
416 s.find_all(&doc, ByteOffset(0)..doc.len())
417 .into_iter()
418 .map(|r| (r.start.0, r.end.0))
419 .collect()
420 }
421
422 #[test]
423 fn literal_case_and_whole_word() {
424 let text = "Foo foo food (foo) foo(";
425 assert_eq!(all(&q("foo", |_| {}), text).len(), 5);
426 assert_eq!(all(&q("foo", |o| o.match_case = true), text).len(), 4);
427 assert_eq!(
428 all(&q("foo", |o| o.whole_word = true), text),
429 vec![(0, 3), (4, 7), (14, 17), (19, 22)]
430 );
431 assert_eq!(
433 all(&q("foo(", |o| o.whole_word = true), text),
434 vec![(19, 23)]
435 );
436 assert_eq!(all(&q("(foo)", |_| {}), text), vec![(13, 18)]);
438 }
439
440 #[test]
441 fn regex_lines_and_invalid_patterns() {
442 let s = q(r"^\w+", |o| o.regex = true);
443 assert_eq!(all(&s, "ab cd\nef"), vec![(0, 2), (6, 8)]);
444 let err = Searcher::new(&SearchQuery::new(
445 "(unclosed",
446 SearchOptions {
447 regex: true,
448 ..Default::default()
449 },
450 ));
451 assert!(err.is_err());
452 assert!(
454 Searcher::new(&SearchQuery::new(
455 "a(?=b)",
456 SearchOptions {
457 regex: true,
458 ..Default::default()
459 }
460 ))
461 .is_err()
462 );
463 }
464
465 #[test]
466 fn matches_across_rope_chunks() {
467 let text = format!("{}needle{}", "x".repeat(5000), "y".repeat(5000)).repeat(20);
468 let s = q("needle", |_| {});
469 let found = all(&s, &text);
470 assert_eq!(found.len(), 20);
471 let doc = Document::new(&text);
472 for (a, b) in found {
473 assert_eq!(doc.slice(ByteOffset(a)..ByteOffset(b)), "needle");
474 }
475 }
476
477 #[test]
478 fn previous_and_next() {
479 let doc = Document::new(&format!("ab{}ab", "-".repeat(200_000)));
480 let s = q("ab", |_| {});
481 let end = doc.len();
482 assert_eq!(s.find_prev(&doc, end).map(|r| r.start.0), Some(200_002));
483 assert_eq!(
484 s.find_prev(&doc, ByteOffset(200_002)).map(|r| r.start.0),
485 Some(0)
486 );
487 assert_eq!(s.find_prev(&doc, ByteOffset(0)), None);
488 assert_eq!(
489 s.find_next(&doc, ByteOffset(1)).map(|r| r.start.0),
490 Some(200_002)
491 );
492 }
493
494 #[test]
495 fn expansion() {
496 let doc = Document::new("John Smith");
497 let s = q(r"(?<first>\w+) (\w+)", |o| o.regex = true);
498 let m = s.find_next(&doc, ByteOffset(0)).unwrap();
499 assert_eq!(s.expand(&doc, m.clone(), "$2, ${first}"), "Smith, John");
500 assert_eq!(s.expand(&doc, m.clone(), r"$0\n\t$$"), "John Smith\n\t$");
501 let lit = q("John", |_| {});
503 let m = lit.find_next(&doc, ByteOffset(0)).unwrap();
504 assert_eq!(lit.expand(&doc, m, "$1 \\n"), "$1 \\n");
505 }
506}