1use aho_corasick::AhoCorasickBuilder;
2use anyhow::Result;
3use collections::HashMap;
4use editor::{
5 char_kind, display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor, MultiBufferSnapshot,
6};
7use gpui::{
8 action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext,
9 RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
10};
11use postage::watch;
12use regex::RegexBuilder;
13use smol::future::yield_now;
14use std::{
15 cmp::{self, Ordering},
16 ops::Range,
17};
18use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace};
19
20action!(Deploy, bool);
21action!(Dismiss);
22action!(FocusEditor);
23action!(ToggleMode, SearchMode);
24action!(GoToMatch, Direction);
25
26#[derive(Clone, Copy, PartialEq, Eq)]
27pub enum Direction {
28 Prev,
29 Next,
30}
31
32#[derive(Clone, Copy)]
33pub enum SearchMode {
34 WholeWord,
35 CaseSensitive,
36 Regex,
37}
38
39pub fn init(cx: &mut MutableAppContext) {
40 cx.add_bindings([
41 Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")),
42 Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")),
43 Binding::new("escape", Dismiss, Some("FindBar")),
44 Binding::new("cmd-f", FocusEditor, Some("FindBar")),
45 Binding::new("enter", GoToMatch(Direction::Next), Some("FindBar")),
46 Binding::new("shift-enter", GoToMatch(Direction::Prev), Some("FindBar")),
47 Binding::new("cmd-g", GoToMatch(Direction::Next), Some("Pane")),
48 Binding::new("cmd-shift-G", GoToMatch(Direction::Prev), Some("Pane")),
49 ]);
50 cx.add_action(FindBar::deploy);
51 cx.add_action(FindBar::dismiss);
52 cx.add_action(FindBar::focus_editor);
53 cx.add_action(FindBar::toggle_mode);
54 cx.add_action(FindBar::go_to_match);
55 cx.add_action(FindBar::go_to_match_on_pane);
56}
57
58struct FindBar {
59 settings: watch::Receiver<Settings>,
60 query_editor: ViewHandle<Editor>,
61 active_editor: Option<ViewHandle<Editor>>,
62 active_match_index: Option<usize>,
63 active_editor_subscription: Option<Subscription>,
64 editors_with_matches: HashMap<WeakViewHandle<Editor>, Vec<Range<Anchor>>>,
65 pending_search: Option<Task<()>>,
66 case_sensitive_mode: bool,
67 whole_word_mode: bool,
68 regex_mode: bool,
69 query_contains_error: bool,
70 dismissed: bool,
71}
72
73impl Entity for FindBar {
74 type Event = ();
75}
76
77impl View for FindBar {
78 fn ui_name() -> &'static str {
79 "FindBar"
80 }
81
82 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
83 cx.focus(&self.query_editor);
84 }
85
86 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
87 let theme = &self.settings.borrow().theme;
88 let editor_container = if self.query_contains_error {
89 theme.find.invalid_editor
90 } else {
91 theme.find.editor.input.container
92 };
93 Flex::row()
94 .with_child(
95 ChildView::new(&self.query_editor)
96 .contained()
97 .with_style(editor_container)
98 .aligned()
99 .constrained()
100 .with_max_width(theme.find.editor.max_width)
101 .boxed(),
102 )
103 .with_child(
104 Flex::row()
105 .with_child(self.render_mode_button("Case", SearchMode::CaseSensitive, cx))
106 .with_child(self.render_mode_button("Word", SearchMode::WholeWord, cx))
107 .with_child(self.render_mode_button("Regex", SearchMode::Regex, cx))
108 .contained()
109 .with_style(theme.find.mode_button_group)
110 .aligned()
111 .boxed(),
112 )
113 .with_child(
114 Flex::row()
115 .with_child(self.render_nav_button("<", Direction::Prev, cx))
116 .with_child(self.render_nav_button(">", Direction::Next, cx))
117 .aligned()
118 .boxed(),
119 )
120 .with_children(self.active_editor.as_ref().and_then(|editor| {
121 let matches = self.editors_with_matches.get(&editor.downgrade())?;
122 let message = if let Some(match_ix) = self.active_match_index {
123 format!("{}/{}", match_ix + 1, matches.len())
124 } else {
125 "No matches".to_string()
126 };
127
128 Some(
129 Label::new(message, theme.find.match_index.text.clone())
130 .contained()
131 .with_style(theme.find.match_index.container)
132 .aligned()
133 .boxed(),
134 )
135 }))
136 .contained()
137 .with_style(theme.find.container)
138 .constrained()
139 .with_height(theme.workspace.toolbar.height)
140 .named("find bar")
141 }
142}
143
144impl Toolbar for FindBar {
145 fn active_item_changed(
146 &mut self,
147 item: Option<Box<dyn ItemViewHandle>>,
148 cx: &mut ViewContext<Self>,
149 ) -> bool {
150 self.active_editor_subscription.take();
151 self.active_editor.take();
152 self.pending_search.take();
153
154 if let Some(editor) = item.and_then(|item| item.act_as::<Editor>(cx)) {
155 self.active_editor_subscription =
156 Some(cx.subscribe(&editor, Self::on_active_editor_event));
157 self.active_editor = Some(editor);
158 self.update_matches(false, cx);
159 true
160 } else {
161 false
162 }
163 }
164
165 fn on_dismiss(&mut self, cx: &mut ViewContext<Self>) {
166 self.dismissed = true;
167 for (editor, _) in &self.editors_with_matches {
168 if let Some(editor) = editor.upgrade(cx) {
169 editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::<Self>(cx));
170 }
171 }
172 }
173}
174
175impl FindBar {
176 fn new(settings: watch::Receiver<Settings>, cx: &mut ViewContext<Self>) -> Self {
177 let query_editor = cx.add_view(|cx| {
178 Editor::auto_height(
179 2,
180 settings.clone(),
181 Some(|theme| theme.find.editor.input.clone()),
182 cx,
183 )
184 });
185 cx.subscribe(&query_editor, Self::on_query_editor_event)
186 .detach();
187
188 Self {
189 query_editor,
190 active_editor: None,
191 active_editor_subscription: None,
192 active_match_index: None,
193 editors_with_matches: Default::default(),
194 case_sensitive_mode: false,
195 whole_word_mode: false,
196 regex_mode: false,
197 settings,
198 pending_search: None,
199 query_contains_error: false,
200 dismissed: false,
201 }
202 }
203
204 fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
205 self.query_editor.update(cx, |query_editor, cx| {
206 query_editor.buffer().update(cx, |query_buffer, cx| {
207 let len = query_buffer.read(cx).len();
208 query_buffer.edit([0..len], query, cx);
209 });
210 });
211 }
212
213 fn render_mode_button(
214 &self,
215 icon: &str,
216 mode: SearchMode,
217 cx: &mut RenderContext<Self>,
218 ) -> ElementBox {
219 let theme = &self.settings.borrow().theme.find;
220 let is_active = self.is_mode_enabled(mode);
221 MouseEventHandler::new::<Self, _, _>(mode as usize, cx, |state, _| {
222 let style = match (is_active, state.hovered) {
223 (false, false) => &theme.mode_button,
224 (false, true) => &theme.hovered_mode_button,
225 (true, false) => &theme.active_mode_button,
226 (true, true) => &theme.active_hovered_mode_button,
227 };
228 Label::new(icon.to_string(), style.text.clone())
229 .contained()
230 .with_style(style.container)
231 .boxed()
232 })
233 .on_click(move |cx| cx.dispatch_action(ToggleMode(mode)))
234 .with_cursor_style(CursorStyle::PointingHand)
235 .boxed()
236 }
237
238 fn render_nav_button(
239 &self,
240 icon: &str,
241 direction: Direction,
242 cx: &mut RenderContext<Self>,
243 ) -> ElementBox {
244 let theme = &self.settings.borrow().theme.find;
245 enum NavButton {}
246 MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, _| {
247 let style = if state.hovered {
248 &theme.hovered_mode_button
249 } else {
250 &theme.mode_button
251 };
252 Label::new(icon.to_string(), style.text.clone())
253 .contained()
254 .with_style(style.container)
255 .boxed()
256 })
257 .on_click(move |cx| cx.dispatch_action(GoToMatch(direction)))
258 .with_cursor_style(CursorStyle::PointingHand)
259 .boxed()
260 }
261
262 fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext<Workspace>) {
263 let settings = workspace.settings();
264 workspace.active_pane().update(cx, |pane, cx| {
265 pane.show_toolbar(cx, |cx| FindBar::new(settings, cx));
266
267 if let Some(find_bar) = pane
268 .active_toolbar()
269 .and_then(|toolbar| toolbar.downcast::<Self>())
270 {
271 find_bar.update(cx, |find_bar, _| find_bar.dismissed = false);
272 let editor = pane.active_item().unwrap().act_as::<Editor>(cx).unwrap();
273 let display_map = editor
274 .update(cx, |editor, cx| editor.snapshot(cx))
275 .display_snapshot;
276 let selection = editor
277 .read(cx)
278 .newest_selection::<usize>(&display_map.buffer_snapshot);
279
280 let mut text: String;
281 if selection.start == selection.end {
282 let point = selection.start.to_display_point(&display_map);
283 let range = editor::movement::surrounding_word(&display_map, point);
284 let range = range.start.to_offset(&display_map, Bias::Left)
285 ..range.end.to_offset(&display_map, Bias::Right);
286 text = display_map.buffer_snapshot.text_for_range(range).collect();
287 if text.trim().is_empty() {
288 text = String::new();
289 }
290 } else {
291 text = display_map
292 .buffer_snapshot
293 .text_for_range(selection.start..selection.end)
294 .collect();
295 }
296
297 if !text.is_empty() {
298 find_bar.update(cx, |find_bar, cx| find_bar.set_query(&text, cx));
299 }
300
301 if *focus {
302 let query_editor = find_bar.read(cx).query_editor.clone();
303 query_editor.update(cx, |query_editor, cx| {
304 query_editor.select_all(&editor::SelectAll, cx);
305 });
306 cx.focus(&find_bar);
307 }
308 }
309 });
310 }
311
312 fn dismiss(pane: &mut Pane, _: &Dismiss, cx: &mut ViewContext<Pane>) {
313 if pane.toolbar::<FindBar>().is_some() {
314 pane.dismiss_toolbar(cx);
315 }
316 }
317
318 fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
319 if let Some(active_editor) = self.active_editor.as_ref() {
320 cx.focus(active_editor);
321 }
322 }
323
324 fn is_mode_enabled(&self, mode: SearchMode) -> bool {
325 match mode {
326 SearchMode::WholeWord => self.whole_word_mode,
327 SearchMode::CaseSensitive => self.case_sensitive_mode,
328 SearchMode::Regex => self.regex_mode,
329 }
330 }
331
332 fn toggle_mode(&mut self, ToggleMode(mode): &ToggleMode, cx: &mut ViewContext<Self>) {
333 let value = match mode {
334 SearchMode::WholeWord => &mut self.whole_word_mode,
335 SearchMode::CaseSensitive => &mut self.case_sensitive_mode,
336 SearchMode::Regex => &mut self.regex_mode,
337 };
338 *value = !*value;
339 self.update_matches(true, cx);
340 cx.notify();
341 }
342
343 fn go_to_match(&mut self, GoToMatch(direction): &GoToMatch, cx: &mut ViewContext<Self>) {
344 if let Some(mut index) = self.active_match_index {
345 if let Some(editor) = self.active_editor.as_ref() {
346 editor.update(cx, |editor, cx| {
347 let newest_selection = editor.newest_anchor_selection().clone();
348 if let Some(ranges) = self.editors_with_matches.get(&cx.weak_handle()) {
349 let position = newest_selection.head();
350 let buffer = editor.buffer().read(cx).read(cx);
351 if ranges[index].start.cmp(&position, &buffer).unwrap().is_gt() {
352 if *direction == Direction::Prev {
353 if index == 0 {
354 index = ranges.len() - 1;
355 } else {
356 index -= 1;
357 }
358 }
359 } else if ranges[index].end.cmp(&position, &buffer).unwrap().is_lt() {
360 if *direction == Direction::Next {
361 index = 0;
362 }
363 } else if *direction == Direction::Prev {
364 if index == 0 {
365 index = ranges.len() - 1;
366 } else {
367 index -= 1;
368 }
369 } else if *direction == Direction::Next {
370 if index == ranges.len() - 1 {
371 index = 0
372 } else {
373 index += 1;
374 }
375 }
376
377 let range_to_select = ranges[index].clone();
378 drop(buffer);
379 editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
380 }
381 });
382 }
383 }
384 }
385
386 fn go_to_match_on_pane(pane: &mut Pane, action: &GoToMatch, cx: &mut ViewContext<Pane>) {
387 if let Some(find_bar) = pane.toolbar::<FindBar>() {
388 find_bar.update(cx, |find_bar, cx| find_bar.go_to_match(action, cx));
389 }
390 }
391
392 fn on_query_editor_event(
393 &mut self,
394 _: ViewHandle<Editor>,
395 event: &editor::Event,
396 cx: &mut ViewContext<Self>,
397 ) {
398 match event {
399 editor::Event::Edited => {
400 self.query_contains_error = false;
401 self.clear_matches(cx);
402 self.update_matches(true, cx);
403 cx.notify();
404 }
405 _ => {}
406 }
407 }
408
409 fn on_active_editor_event(
410 &mut self,
411 _: ViewHandle<Editor>,
412 event: &editor::Event,
413 cx: &mut ViewContext<Self>,
414 ) {
415 match event {
416 editor::Event::Edited => self.update_matches(false, cx),
417 editor::Event::SelectionsChanged => self.update_match_index(cx),
418 _ => {}
419 }
420 }
421
422 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
423 let mut active_editor_matches = None;
424 for (editor, ranges) in self.editors_with_matches.drain() {
425 if let Some(editor) = editor.upgrade(cx) {
426 if Some(&editor) == self.active_editor.as_ref() {
427 active_editor_matches = Some((editor.downgrade(), ranges));
428 } else {
429 editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::<Self>(cx));
430 }
431 }
432 }
433 self.editors_with_matches.extend(active_editor_matches);
434 }
435
436 fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
437 let query = self.query_editor.read(cx).text(cx);
438 self.pending_search.take();
439 if let Some(editor) = self.active_editor.as_ref() {
440 if query.is_empty() {
441 self.active_match_index.take();
442 editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::<Self>(cx));
443 } else {
444 let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
445 let case_sensitive = self.case_sensitive_mode;
446 let whole_word = self.whole_word_mode;
447 let ranges = if self.regex_mode {
448 cx.background()
449 .spawn(regex_search(buffer, query, case_sensitive, whole_word))
450 } else {
451 cx.background().spawn(async move {
452 Ok(search(buffer, query, case_sensitive, whole_word).await)
453 })
454 };
455
456 let editor = editor.downgrade();
457 self.pending_search = Some(cx.spawn(|this, mut cx| async move {
458 match ranges.await {
459 Ok(ranges) => {
460 if let Some(editor) = editor.upgrade(&cx) {
461 this.update(&mut cx, |this, cx| {
462 this.editors_with_matches
463 .insert(editor.downgrade(), ranges.clone());
464 this.update_match_index(cx);
465 if !this.dismissed {
466 editor.update(cx, |editor, cx| {
467 let theme = &this.settings.borrow().theme.find;
468
469 if select_closest_match {
470 if let Some(match_ix) = this.active_match_index {
471 editor.select_ranges(
472 [ranges[match_ix].clone()],
473 Some(Autoscroll::Fit),
474 cx,
475 );
476 }
477 }
478
479 editor.highlight_ranges::<Self>(
480 ranges,
481 theme.match_background,
482 cx,
483 );
484 });
485 }
486 });
487 }
488 }
489 Err(_) => {
490 this.update(&mut cx, |this, cx| {
491 this.query_contains_error = true;
492 cx.notify();
493 });
494 }
495 }
496 }));
497 }
498 }
499 }
500
501 fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
502 self.active_match_index = self.active_match_index(cx);
503 cx.notify();
504 }
505
506 fn active_match_index(&mut self, cx: &mut ViewContext<Self>) -> Option<usize> {
507 let editor = self.active_editor.as_ref()?;
508 let ranges = self.editors_with_matches.get(&editor.downgrade())?;
509 let editor = editor.read(cx);
510 let position = editor.newest_anchor_selection().head();
511 if ranges.is_empty() {
512 None
513 } else {
514 let buffer = editor.buffer().read(cx).read(cx);
515 match ranges.binary_search_by(|probe| {
516 if probe.end.cmp(&position, &*buffer).unwrap().is_lt() {
517 Ordering::Less
518 } else if probe.start.cmp(&position, &*buffer).unwrap().is_gt() {
519 Ordering::Greater
520 } else {
521 Ordering::Equal
522 }
523 }) {
524 Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
525 }
526 }
527 }
528}
529
530const YIELD_INTERVAL: usize = 20000;
531
532async fn search(
533 buffer: MultiBufferSnapshot,
534 query: String,
535 case_sensitive: bool,
536 whole_word: bool,
537) -> Vec<Range<Anchor>> {
538 let mut ranges = Vec::new();
539
540 let search = AhoCorasickBuilder::new()
541 .auto_configure(&[&query])
542 .ascii_case_insensitive(!case_sensitive)
543 .build(&[&query]);
544 for (ix, mat) in search
545 .stream_find_iter(buffer.bytes_in_range(0..buffer.len()))
546 .enumerate()
547 {
548 if (ix + 1) % YIELD_INTERVAL == 0 {
549 yield_now().await;
550 }
551
552 let mat = mat.unwrap();
553
554 if whole_word {
555 let prev_kind = buffer.reversed_chars_at(mat.start()).next().map(char_kind);
556 let start_kind = char_kind(buffer.chars_at(mat.start()).next().unwrap());
557 let end_kind = char_kind(buffer.reversed_chars_at(mat.end()).next().unwrap());
558 let next_kind = buffer.chars_at(mat.end()).next().map(char_kind);
559 if Some(start_kind) == prev_kind || Some(end_kind) == next_kind {
560 continue;
561 }
562 }
563
564 ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end()));
565 }
566
567 ranges
568}
569
570async fn regex_search(
571 buffer: MultiBufferSnapshot,
572 mut query: String,
573 case_sensitive: bool,
574 whole_word: bool,
575) -> Result<Vec<Range<Anchor>>> {
576 if whole_word {
577 let mut word_query = String::new();
578 word_query.push_str("\\b");
579 word_query.push_str(&query);
580 word_query.push_str("\\b");
581 query = word_query;
582 }
583
584 let mut ranges = Vec::new();
585
586 if query.contains("\n") || query.contains("\\n") {
587 let regex = RegexBuilder::new(&query)
588 .case_insensitive(!case_sensitive)
589 .multi_line(true)
590 .build()?;
591 for (ix, mat) in regex.find_iter(&buffer.text()).enumerate() {
592 if (ix + 1) % YIELD_INTERVAL == 0 {
593 yield_now().await;
594 }
595
596 ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end()));
597 }
598 } else {
599 let regex = RegexBuilder::new(&query)
600 .case_insensitive(!case_sensitive)
601 .build()?;
602
603 let mut line = String::new();
604 let mut line_offset = 0;
605 for (chunk_ix, chunk) in buffer
606 .chunks(0..buffer.len(), false)
607 .map(|c| c.text)
608 .chain(["\n"])
609 .enumerate()
610 {
611 if (chunk_ix + 1) % YIELD_INTERVAL == 0 {
612 yield_now().await;
613 }
614
615 for (newline_ix, text) in chunk.split('\n').enumerate() {
616 if newline_ix > 0 {
617 for mat in regex.find_iter(&line) {
618 let start = line_offset + mat.start();
619 let end = line_offset + mat.end();
620 ranges.push(buffer.anchor_after(start)..buffer.anchor_before(end));
621 }
622
623 line_offset += line.len() + 1;
624 line.clear();
625 }
626 line.push_str(text);
627 }
628 }
629 }
630
631 Ok(ranges)
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637 use editor::{DisplayPoint, Editor, MultiBuffer};
638 use gpui::{color::Color, TestAppContext};
639 use std::sync::Arc;
640 use unindent::Unindent as _;
641
642 #[gpui::test]
643 async fn test_find_simple(mut cx: TestAppContext) {
644 let fonts = cx.font_cache();
645 let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
646 theme.find.match_background = Color::red();
647 let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
648 let settings = watch::channel_with(settings).1;
649
650 let buffer = cx.update(|cx| {
651 MultiBuffer::build_simple(
652 &r#"
653 A regular expression (shortened as regex or regexp;[1] also referred to as
654 rational expression[2][3]) is a sequence of characters that specifies a search
655 pattern in text. Usually such patterns are used by string-searching algorithms
656 for "find" or "find and replace" operations on strings, or for input validation.
657 "#
658 .unindent(),
659 cx,
660 )
661 });
662 let editor = cx.add_view(Default::default(), |cx| {
663 Editor::for_buffer(buffer.clone(), None, settings.clone(), cx)
664 });
665
666 let find_bar = cx.add_view(Default::default(), |cx| {
667 let mut find_bar = FindBar::new(settings, cx);
668 find_bar.active_item_changed(Some(Box::new(editor.clone())), cx);
669 find_bar
670 });
671
672 // Search for a string that appears with different casing.
673 // By default, search is case-insensitive.
674 find_bar.update(&mut cx, |find_bar, cx| {
675 find_bar.set_query("us", cx);
676 });
677 editor.next_notification(&cx).await;
678 editor.update(&mut cx, |editor, cx| {
679 assert_eq!(
680 editor.all_highlighted_ranges(cx),
681 &[
682 (
683 DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
684 Color::red(),
685 ),
686 (
687 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
688 Color::red(),
689 ),
690 ]
691 );
692 });
693
694 // Switch to a case sensitive search.
695 find_bar.update(&mut cx, |find_bar, cx| {
696 find_bar.toggle_mode(&ToggleMode(SearchMode::CaseSensitive), cx);
697 });
698 editor.next_notification(&cx).await;
699 editor.update(&mut cx, |editor, cx| {
700 assert_eq!(
701 editor.all_highlighted_ranges(cx),
702 &[(
703 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
704 Color::red(),
705 )]
706 );
707 });
708
709 // Search for a string that appears both as a whole word and
710 // within other words. By default, all results are found.
711 find_bar.update(&mut cx, |find_bar, cx| {
712 find_bar.set_query("or", cx);
713 });
714 editor.next_notification(&cx).await;
715 editor.update(&mut cx, |editor, cx| {
716 assert_eq!(
717 editor.all_highlighted_ranges(cx),
718 &[
719 (
720 DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
721 Color::red(),
722 ),
723 (
724 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
725 Color::red(),
726 ),
727 (
728 DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
729 Color::red(),
730 ),
731 (
732 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
733 Color::red(),
734 ),
735 (
736 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
737 Color::red(),
738 ),
739 (
740 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
741 Color::red(),
742 ),
743 (
744 DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
745 Color::red(),
746 ),
747 ]
748 );
749 });
750
751 // Switch to a whole word search.
752 find_bar.update(&mut cx, |find_bar, cx| {
753 find_bar.toggle_mode(&ToggleMode(SearchMode::WholeWord), cx);
754 });
755 editor.next_notification(&cx).await;
756 editor.update(&mut cx, |editor, cx| {
757 assert_eq!(
758 editor.all_highlighted_ranges(cx),
759 &[
760 (
761 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
762 Color::red(),
763 ),
764 (
765 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
766 Color::red(),
767 ),
768 (
769 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
770 Color::red(),
771 ),
772 ]
773 );
774 });
775
776 editor.update(&mut cx, |editor, cx| {
777 editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx);
778 });
779 find_bar.update(&mut cx, |find_bar, cx| {
780 assert_eq!(find_bar.active_match_index, Some(0));
781 find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
782 assert_eq!(
783 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
784 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
785 );
786 });
787 find_bar.read_with(&cx, |find_bar, _| {
788 assert_eq!(find_bar.active_match_index, Some(0));
789 });
790
791 find_bar.update(&mut cx, |find_bar, cx| {
792 find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
793 assert_eq!(
794 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
795 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
796 );
797 });
798 find_bar.read_with(&cx, |find_bar, _| {
799 assert_eq!(find_bar.active_match_index, Some(1));
800 });
801
802 find_bar.update(&mut cx, |find_bar, cx| {
803 find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
804 assert_eq!(
805 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
806 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
807 );
808 });
809 find_bar.read_with(&cx, |find_bar, _| {
810 assert_eq!(find_bar.active_match_index, Some(2));
811 });
812
813 find_bar.update(&mut cx, |find_bar, cx| {
814 find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
815 assert_eq!(
816 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
817 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
818 );
819 });
820 find_bar.read_with(&cx, |find_bar, _| {
821 assert_eq!(find_bar.active_match_index, Some(0));
822 });
823
824 find_bar.update(&mut cx, |find_bar, cx| {
825 find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
826 assert_eq!(
827 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
828 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
829 );
830 });
831 find_bar.read_with(&cx, |find_bar, _| {
832 assert_eq!(find_bar.active_match_index, Some(2));
833 });
834
835 find_bar.update(&mut cx, |find_bar, cx| {
836 find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
837 assert_eq!(
838 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
839 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
840 );
841 });
842 find_bar.read_with(&cx, |find_bar, _| {
843 assert_eq!(find_bar.active_match_index, Some(1));
844 });
845
846 find_bar.update(&mut cx, |find_bar, cx| {
847 find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
848 assert_eq!(
849 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
850 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
851 );
852 });
853 find_bar.read_with(&cx, |find_bar, _| {
854 assert_eq!(find_bar.active_match_index, Some(0));
855 });
856
857 // Park the cursor in between matches and ensure that going to the previous match selects
858 // the closest match to the left.
859 editor.update(&mut cx, |editor, cx| {
860 editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx);
861 });
862 find_bar.update(&mut cx, |find_bar, cx| {
863 assert_eq!(find_bar.active_match_index, Some(1));
864 find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
865 assert_eq!(
866 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
867 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
868 );
869 });
870 find_bar.read_with(&cx, |find_bar, _| {
871 assert_eq!(find_bar.active_match_index, Some(0));
872 });
873
874 // Park the cursor in between matches and ensure that going to the next match selects the
875 // closest match to the right.
876 editor.update(&mut cx, |editor, cx| {
877 editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx);
878 });
879 find_bar.update(&mut cx, |find_bar, cx| {
880 assert_eq!(find_bar.active_match_index, Some(1));
881 find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
882 assert_eq!(
883 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
884 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
885 );
886 });
887 find_bar.read_with(&cx, |find_bar, _| {
888 assert_eq!(find_bar.active_match_index, Some(1));
889 });
890
891 // Park the cursor after the last match and ensure that going to the previous match selects
892 // the last match.
893 editor.update(&mut cx, |editor, cx| {
894 editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx);
895 });
896 find_bar.update(&mut cx, |find_bar, cx| {
897 assert_eq!(find_bar.active_match_index, Some(2));
898 find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
899 assert_eq!(
900 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
901 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
902 );
903 });
904 find_bar.read_with(&cx, |find_bar, _| {
905 assert_eq!(find_bar.active_match_index, Some(2));
906 });
907
908 // Park the cursor after the last match and ensure that going to the next match selects the
909 // first match.
910 editor.update(&mut cx, |editor, cx| {
911 editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx);
912 });
913 find_bar.update(&mut cx, |find_bar, cx| {
914 assert_eq!(find_bar.active_match_index, Some(2));
915 find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
916 assert_eq!(
917 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
918 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
919 );
920 });
921 find_bar.read_with(&cx, |find_bar, _| {
922 assert_eq!(find_bar.active_match_index, Some(0));
923 });
924
925 // Park the cursor before the first match and ensure that going to the previous match
926 // selects the last match.
927 editor.update(&mut cx, |editor, cx| {
928 editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx);
929 });
930 find_bar.update(&mut cx, |find_bar, cx| {
931 assert_eq!(find_bar.active_match_index, Some(0));
932 find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
933 assert_eq!(
934 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
935 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
936 );
937 });
938 find_bar.read_with(&cx, |find_bar, _| {
939 assert_eq!(find_bar.active_match_index, Some(2));
940 });
941 }
942}