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