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