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