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