1use crate::input::InputModeKind;
2use aho_corasick::AhoCorasick;
3use gpui::{Context, Window};
4use ropey::Rope;
5use std::{ops::Range, rc::Rc};
6
7use super::{
8 InputBaseState, Replace, RopeExt as _, Search, movement::MoveDirection, state::ScrollPadding,
9};
10
11#[derive(Debug, Clone)]
13pub struct SearchMatcher {
14 text: Rope,
15 pub query: Option<AhoCorasick>,
16 matched_ranges: Rc<Vec<Range<usize>>>,
17 current_match_ix: usize,
18 replacing: bool,
19}
20
21#[derive(Debug, Clone)]
26#[non_exhaustive]
27pub struct SearchSession {
28 pub open: bool,
30 pub replace_mode: bool,
31 pub case_insensitive: bool,
32 pub query: String,
33 pub replacement: String,
34 pub anchor_offset: Option<usize>,
35 pub matcher: SearchMatcher,
36 active: bool,
39}
40
41impl Default for SearchSession {
42 fn default() -> Self {
43 Self {
44 open: false,
45 active: false,
46 replace_mode: false,
47 case_insensitive: true,
48 query: String::new(),
49 replacement: String::new(),
50 anchor_offset: None,
51 matcher: SearchMatcher::new(),
52 }
53 }
54}
55
56impl SearchSession {
57 pub(crate) fn open(&mut self, replace_mode: bool, replaceable: bool) {
58 self.open = true;
59 self.active = true;
60 self.replace_mode = replace_mode && replaceable;
61 }
62
63 pub(crate) fn activate(&mut self) {
67 self.active = true;
68 }
69
70 pub(crate) fn close(&mut self) {
71 self.open = false;
72 self.active = false;
73 }
74
75 pub fn is_active(&self) -> bool {
79 self.active
80 }
81
82 pub(crate) fn update_query(&mut self, query: impl Into<String>, case_insensitive: bool) {
83 let query = query.into();
84 if self.query == query && self.case_insensitive == case_insensitive {
85 return;
86 }
87
88 self.query = query;
89 self.case_insensitive = case_insensitive;
90 self.matcher.update_query(&self.query, case_insensitive);
91 }
92}
93
94impl<M: InputModeKind> InputBaseState<M> {
95 pub fn open_search(&mut self, replace_mode: bool, cx: &mut Context<Self>) {
105 if !self.searchable {
106 return;
107 }
108 self.search_activation_revision = self.search_activation_revision.wrapping_add(1);
109 self.search_session
110 .open(replace_mode, self.is_replaceable());
111 let selected = self.selected_text().to_string();
112 let query = if selected.is_empty() {
113 self.search_session.query.clone()
114 } else {
115 selected
116 };
117 let query_changed = query != self.search_session.query;
118 self.search_session.anchor_offset = if query_changed {
121 self.last_layout
122 .as_ref()
123 .map(|layout| layout.visible_range_offset.start)
124 } else {
125 None
126 };
127 let case_insensitive = self.search_session.case_insensitive;
128 self.search_session.update_query(query, case_insensitive);
129 self.search_session.matcher.update(&self.text);
130 if query_changed && let Some(anchor) = self.search_session.anchor_offset {
131 self.search_session.matcher.update_cursor_by_offset(anchor);
132 }
133 cx.notify();
134 }
135
136 pub fn search_session(&self) -> &SearchSession {
137 &self.search_session
138 }
139
140 pub fn search_activation_revision(&self) -> u64 {
147 self.search_activation_revision
148 }
149
150 #[doc(hidden)]
151 pub fn set_search_replace_mode(&mut self, replace_mode: bool, cx: &mut Context<Self>) {
152 self.search_session.replace_mode = replace_mode && self.is_replaceable();
153 cx.notify();
154 }
155
156 pub fn is_replaceable(&self) -> bool {
161 self.replaceable && self.is_editable()
162 }
163
164 pub fn set_search_query(
176 &mut self,
177 query: impl Into<String>,
178 case_insensitive: bool,
179 cx: &mut Context<Self>,
180 ) {
181 let query = query.into();
182 let query_changed = query != self.search_session.query
183 || case_insensitive != self.search_session.case_insensitive;
184 self.search_session.anchor_offset = if query_changed {
185 self.last_layout
186 .as_ref()
187 .map(|layout| layout.visible_range_offset.start)
188 } else {
189 None
190 };
191 self.search_session.activate();
192 self.search_session.update_query(query, case_insensitive);
193 self.search_session.matcher.update(&self.text);
194 if query_changed && let Some(anchor) = self.search_session.anchor_offset {
195 self.search_session.matcher.update_cursor_by_offset(anchor);
196 }
197 cx.notify();
198 }
199
200 pub fn close_search(&mut self, cx: &mut Context<Self>) {
203 self.search_session.close();
204 cx.notify();
205 }
206
207 pub fn next_search_match(&mut self, cx: &mut Context<Self>) -> Option<Range<usize>> {
208 self.sync_search_matcher();
209 let range = self.search_session.matcher.next()?;
210 self.scroll_to_with_padding(range.end, None, ScrollPadding::SurroundingLines, cx);
213 Some(range)
214 }
215
216 pub fn previous_search_match(&mut self, cx: &mut Context<Self>) -> Option<Range<usize>> {
217 self.sync_search_matcher();
218 let range = self.search_session.matcher.next_back()?;
219 self.scroll_to_with_padding(range.start, None, ScrollPadding::SurroundingLines, cx);
222 Some(range)
223 }
224
225 pub fn replace_current_search_match(
228 &mut self,
229 replacement: &str,
230 window: &mut Window,
231 cx: &mut Context<Self>,
232 ) -> bool {
233 if !self.is_replaceable() {
234 return false;
235 }
236 self.sync_search_matcher();
237 let matcher = &mut self.search_session.matcher;
238 let Some(range) = matcher
239 .matched_ranges()
240 .get(matcher.current_match_index())
241 .cloned()
242 else {
243 return false;
244 };
245 let next = matcher.peek().unwrap_or_else(|| range.clone());
246 let direction = matcher
247 .has_next_without_wrap()
248 .then_some(MoveDirection::Down);
249 if direction.is_none() {
250 matcher.set_current_match_index(0);
251 }
252 matcher.begin_replacement();
253 let range_utf16 = self.range_to_utf16(&range);
254 self.scroll_to(next.end, direction, cx);
255 self.replace_text_in_range_silent(Some(range_utf16), replacement, window, cx);
256 true
257 }
258
259 pub fn replace_all_search_matches(
261 &mut self,
262 replacement: &str,
263 window: &mut Window,
264 cx: &mut Context<Self>,
265 ) -> usize {
266 if !self.is_replaceable() {
267 return 0;
268 }
269 self.sync_search_matcher();
270 let ranges = self.search_session.matcher.matched_ranges();
271 if ranges.is_empty() {
272 return 0;
273 }
274 let mut text = self.text.clone();
275 for range in ranges.iter().rev() {
276 text.replace(range.clone(), replacement);
277 }
278 self.search_session.matcher.begin_replacement();
279 let count = ranges.len();
280 self.replace_text_in_range_silent(Some(0..self.text.len()), &text.to_string(), window, cx);
281 self.scroll_to(0, Some(MoveDirection::Down), cx);
282 count
283 }
284
285 pub(super) fn update_search(&mut self, _cx: &mut gpui::App) {
289 if !self.search_session.is_active() {
290 return;
291 }
292 self.sync_search_matcher();
293 }
294
295 fn sync_search_matcher(&mut self) {
297 self.search_session.matcher.update(&self.text);
298 }
299
300 pub(super) fn on_action_search(&mut self, _: &Search, _: &mut Window, cx: &mut Context<Self>) {
303 if !self.searchable {
304 cx.propagate();
305 return;
306 }
307 self.open_search(false, cx);
308 }
309
310 pub(super) fn on_action_replace(
311 &mut self,
312 _: &Replace,
313 _: &mut Window,
314 cx: &mut Context<Self>,
315 ) {
316 if !self.searchable {
317 cx.propagate();
318 return;
319 }
320 self.open_search(true, cx);
321 }
322}
323
324impl Default for SearchMatcher {
325 fn default() -> Self {
326 Self::new()
327 }
328}
329
330impl SearchMatcher {
331 pub fn new() -> Self {
332 Self {
333 text: "".into(),
334 query: None,
335 matched_ranges: Rc::new(Vec::new()),
336 current_match_ix: 0,
337 replacing: false,
338 }
339 }
340
341 pub fn update(&mut self, text: &Rope) {
343 if self.text.eq(text) {
344 self.replacing = false;
345 return;
346 }
347 self.text = text.clone();
348 self.update_matches();
349 }
350
351 pub fn update_query(&mut self, query: &str, case_insensitive: bool) {
352 self.query = (!query.is_empty()).then(|| {
353 AhoCorasick::builder()
354 .ascii_case_insensitive(case_insensitive)
355 .build([query])
356 .expect("failed to build input search query")
357 });
358 self.update_matches();
359 }
360
361 pub fn matched_ranges(&self) -> Rc<Vec<Range<usize>>> {
362 self.matched_ranges.clone()
363 }
364
365 pub fn current_match_index(&self) -> usize {
366 self.current_match_ix
367 }
368
369 pub fn current(&self) -> Option<usize> {
372 (!self.is_empty()).then_some(self.current_match_ix)
373 }
374
375 pub fn len(&self) -> usize {
376 self.matched_ranges.len()
377 }
378
379 pub fn is_empty(&self) -> bool {
380 self.matched_ranges.is_empty()
381 }
382
383 pub fn label(&self) -> String {
385 match self.current() {
386 Some(ix) => format!("{}/{}", ix + 1, self.len()),
387 None => "0/0".into(),
388 }
389 }
390
391 fn peek(&self) -> Option<Range<usize>> {
392 self.next_index()
393 .and_then(|ix| self.matched_ranges.get(ix).cloned())
394 }
395
396 fn has_next_without_wrap(&self) -> bool {
397 self.current_match_ix < self.matched_ranges.len().saturating_sub(1)
398 }
399
400 pub fn update_cursor_by_offset(&mut self, offset: usize) {
401 for (ix, range) in self.matched_ranges.iter().enumerate() {
402 self.current_match_ix = ix;
403 if range.contains(&offset) || range.end >= offset {
404 return;
405 }
406 }
407 }
408
409 fn begin_replacement(&mut self) {
411 self.replacing = true;
412 }
413
414 fn set_current_match_index(&mut self, index: usize) {
415 self.current_match_ix = index.min(self.matched_ranges.len().saturating_sub(1));
416 }
417
418 fn next_index(&self) -> Option<usize> {
419 if self.is_empty() {
420 None
421 } else if self.has_next_without_wrap() {
422 Some(self.current_match_ix + 1)
423 } else {
424 Some(0)
425 }
426 }
427
428 fn update_matches(&mut self) {
429 let mut ranges = Vec::new();
430 if let Some(query) = &self.query {
431 let text = self.text.to_string();
432 ranges.extend(
433 query
434 .stream_find_iter(text.as_bytes())
435 .map(|result| result.expect("input search match").range()),
436 );
437 }
438 self.matched_ranges = Rc::new(ranges);
439 if !self.replacing || self.is_empty() {
440 self.current_match_ix = 0;
441 } else {
442 self.current_match_ix = self.current_match_ix.min(self.len() - 1);
443 }
444 self.replacing = false;
445 }
446}
447
448impl Iterator for SearchMatcher {
449 type Item = Range<usize>;
450
451 fn next(&mut self) -> Option<Self::Item> {
452 let ix = self.next_index()?;
453 self.current_match_ix = ix;
454 self.matched_ranges.get(ix).cloned()
455 }
456}
457
458impl DoubleEndedIterator for SearchMatcher {
459 fn next_back(&mut self) -> Option<Self::Item> {
460 if self.is_empty() {
461 return None;
462 }
463 if self.current_match_ix == 0 {
464 self.current_match_ix = self.len();
465 }
466 self.current_match_ix -= 1;
467 self.matched_ranges.get(self.current_match_ix).cloned()
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474
475 #[test]
476 fn finds_navigates_and_preserves_replacement_position() {
477 let mut matcher = SearchMatcher::new();
478 matcher.update(&Rope::from("foo FOO foo"));
479 matcher.update_query("foo", true);
480 assert_eq!(&*matcher.matched_ranges(), &[0..3, 4..7, 8..11]);
481 assert_eq!(matcher.next(), Some(4..7));
482 assert_eq!(matcher.next_back(), Some(0..3));
483
484 matcher.set_current_match_index(2);
485 matcher.begin_replacement();
486 matcher.update(&Rope::from("foo FOO bar"));
487 assert_eq!(matcher.current_match_index(), 1);
488 }
489
490 #[test]
491 fn next_wraps_to_start() {
492 let mut matcher = SearchMatcher::new();
493 matcher.update(&Rope::from(".....aaaaa.....aaaaa.....aaaaa"));
494 matcher.update_query("aaaaa", false);
495 matcher.set_current_match_index(2);
496 assert_eq!(matcher.next(), Some(5..10));
497 }
498
499 #[test]
500 fn a_query_set_without_the_panel_keeps_the_session_active_until_closed() {
501 let mut session = SearchSession::default();
502 assert!(!session.is_active());
503
504 session.open(false, true);
505 assert!(session.is_active());
506 session.close();
507 assert!(!session.is_active());
508
509 session.activate();
512 assert!(session.is_active());
513 assert!(!session.open);
514 session.close();
515 assert!(!session.is_active());
516 }
517
518 #[test]
519 fn identical_query_keeps_the_current_match() {
520 let mut session = SearchSession::default();
521 session.update_query("foo", true);
522 session.matcher.update(&Rope::from("foo bar foo baz foo"));
523 session.matcher.update_cursor_by_offset(12);
524 assert_eq!(session.matcher.current_match_index(), 2);
525
526 session.update_query("foo", true);
530
531 assert_eq!(session.matcher.current_match_index(), 2);
532 assert_eq!(session.matcher.label(), "3/3");
533 }
534
535 #[test]
536 fn replacement_keeps_current_match_index_on_next_match() {
537 let mut matcher = SearchMatcher::new();
538 matcher.update(&Rope::from("foo foo foo"));
539 matcher.update_query("foo", true);
540 assert_eq!(matcher.label(), "1/3");
541
542 assert!(matcher.has_next_without_wrap());
543 matcher.begin_replacement();
544 matcher.update(&Rope::from("bar foo foo"));
545 assert_eq!(matcher.current_match_index(), 0);
546 assert_eq!(matcher.matched_ranges()[0], 4..7);
547 assert_eq!(matcher.label(), "1/2");
548
549 matcher.set_current_match_index(1);
550 assert!(!matcher.has_next_without_wrap());
551 matcher.set_current_match_index(0);
552 matcher.begin_replacement();
553 matcher.update(&Rope::from("bar foo bar"));
554 assert_eq!(matcher.current_match_index(), 0);
555 assert_eq!(matcher.matched_ranges()[0], 4..7);
556 assert_eq!(matcher.label(), "1/1");
557 }
558
559 #[test]
560 fn update_matches_clamps_current_match_index_while_replacing() {
561 let mut matcher = SearchMatcher::new();
562 matcher.update(&Rope::from("foo foo foo"));
563 matcher.update_query("foo", true);
564 matcher.set_current_match_index(2);
565 matcher.begin_replacement();
566
567 matcher.update(&Rope::from("foo xoo foo"));
568
569 assert_eq!(matcher.len(), 2);
570 assert_eq!(matcher.current_match_index(), 1);
571 assert_eq!(matcher.label(), "2/2");
572 }
573}