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