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