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