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