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 MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
11 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 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)], 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(move |_, _, cx| cx.dispatch_any_action(option.to_toggle_action()))
289 .with_cursor_style(CursorStyle::PointingHand)
290 .with_tooltip::<Self, _>(
291 option as usize,
292 format!("Toggle {}", option.label()),
293 Some(option.to_toggle_action()),
294 tooltip_style,
295 cx,
296 )
297 .boxed()
298 }
299
300 fn render_nav_button(
301 &self,
302 icon: &str,
303 direction: Direction,
304 cx: &mut RenderContext<Self>,
305 ) -> ElementBox {
306 let action: Box<dyn Action>;
307 let tooltip;
308 match direction {
309 Direction::Prev => {
310 action = Box::new(SelectPrevMatch);
311 tooltip = "Select Previous Match";
312 }
313 Direction::Next => {
314 action = Box::new(SelectNextMatch);
315 tooltip = "Select Next Match";
316 }
317 };
318 let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
319
320 enum NavButton {}
321 MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
322 let style = &cx
323 .global::<Settings>()
324 .theme
325 .search
326 .option_button
327 .style_for(state, false);
328 Label::new(icon.to_string(), style.text.clone())
329 .contained()
330 .with_style(style.container)
331 .boxed()
332 })
333 .on_click({
334 let action = action.boxed_clone();
335 move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())
336 })
337 .with_cursor_style(CursorStyle::PointingHand)
338 .with_tooltip::<NavButton, _>(
339 direction as usize,
340 tooltip.to_string(),
341 Some(action),
342 tooltip_style,
343 cx,
344 )
345 .boxed()
346 }
347
348 fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
349 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
350 if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
351 return;
352 }
353 }
354 cx.propagate_action();
355 }
356
357 fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
358 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
359 if !search_bar.read(cx).dismissed {
360 search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
361 return;
362 }
363 }
364 cx.propagate_action();
365 }
366
367 fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
368 if let Some(active_editor) = self.active_editor.as_ref() {
369 cx.focus(active_editor);
370 }
371 }
372
373 fn is_search_option_enabled(&self, search_option: SearchOption) -> bool {
374 match search_option {
375 SearchOption::WholeWord => self.whole_word,
376 SearchOption::CaseSensitive => self.case_sensitive,
377 SearchOption::Regex => self.regex,
378 }
379 }
380
381 fn toggle_search_option(&mut self, search_option: SearchOption, cx: &mut ViewContext<Self>) {
382 let value = match search_option {
383 SearchOption::WholeWord => &mut self.whole_word,
384 SearchOption::CaseSensitive => &mut self.case_sensitive,
385 SearchOption::Regex => &mut self.regex,
386 };
387 *value = !*value;
388 self.update_matches(false, cx);
389 cx.notify();
390 }
391
392 fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
393 self.select_match(Direction::Next, cx);
394 }
395
396 fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
397 self.select_match(Direction::Prev, cx);
398 }
399
400 fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
401 if let Some(index) = self.active_match_index {
402 if let Some(editor) = self.active_editor.as_ref() {
403 editor.update(cx, |editor, cx| {
404 if let Some(ranges) = self.editors_with_matches.get(&cx.weak_handle()) {
405 let new_index = match_index_for_direction(
406 ranges,
407 &editor.selections.newest_anchor().head(),
408 index,
409 direction,
410 &editor.buffer().read(cx).snapshot(cx),
411 );
412 let range_to_select = ranges[new_index].clone();
413 editor.unfold_ranges([range_to_select.clone()], false, cx);
414 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
415 s.select_ranges([range_to_select])
416 });
417 }
418 });
419 }
420 }
421 }
422
423 fn select_next_match_on_pane(
424 pane: &mut Pane,
425 action: &SelectNextMatch,
426 cx: &mut ViewContext<Pane>,
427 ) {
428 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
429 search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
430 }
431 }
432
433 fn select_prev_match_on_pane(
434 pane: &mut Pane,
435 action: &SelectPrevMatch,
436 cx: &mut ViewContext<Pane>,
437 ) {
438 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
439 search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
440 }
441 }
442
443 fn on_query_editor_event(
444 &mut self,
445 _: ViewHandle<Editor>,
446 event: &editor::Event,
447 cx: &mut ViewContext<Self>,
448 ) {
449 match event {
450 editor::Event::BufferEdited { .. } => {
451 self.query_contains_error = false;
452 self.clear_matches(cx);
453 self.update_matches(true, cx);
454 cx.notify();
455 }
456 _ => {}
457 }
458 }
459
460 fn on_active_editor_event(
461 &mut self,
462 _: ViewHandle<Editor>,
463 event: &editor::Event,
464 cx: &mut ViewContext<Self>,
465 ) {
466 match event {
467 editor::Event::BufferEdited { .. } => self.update_matches(false, cx),
468 editor::Event::SelectionsChanged { .. } => self.update_match_index(cx),
469 _ => {}
470 }
471 }
472
473 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
474 let mut active_editor_matches = None;
475 for (editor, ranges) in self.editors_with_matches.drain() {
476 if let Some(editor) = editor.upgrade(cx) {
477 if Some(&editor) == self.active_editor.as_ref() {
478 active_editor_matches = Some((editor.downgrade(), ranges));
479 } else {
480 editor.update(cx, |editor, cx| {
481 editor.clear_background_highlights::<Self>(cx)
482 });
483 }
484 }
485 }
486 self.editors_with_matches.extend(active_editor_matches);
487 }
488
489 fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
490 let query = self.query_editor.read(cx).text(cx);
491 self.pending_search.take();
492 if let Some(editor) = self.active_editor.as_ref() {
493 if query.is_empty() {
494 self.active_match_index.take();
495 editor.update(cx, |editor, cx| {
496 editor.clear_background_highlights::<Self>(cx)
497 });
498 } else {
499 let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
500 let query = if self.regex {
501 match SearchQuery::regex(query, self.whole_word, self.case_sensitive) {
502 Ok(query) => query,
503 Err(_) => {
504 self.query_contains_error = true;
505 cx.notify();
506 return;
507 }
508 }
509 } else {
510 SearchQuery::text(query, self.whole_word, self.case_sensitive)
511 };
512
513 let ranges = cx.background().spawn(async move {
514 let mut ranges = Vec::new();
515 if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
516 ranges.extend(
517 query
518 .search(excerpt_buffer.as_rope())
519 .await
520 .into_iter()
521 .map(|range| {
522 buffer.anchor_after(range.start)
523 ..buffer.anchor_before(range.end)
524 }),
525 );
526 } else {
527 for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
528 let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);
529 let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone());
530 ranges.extend(query.search(&rope).await.into_iter().map(|range| {
531 let start = excerpt
532 .buffer
533 .anchor_after(excerpt_range.start + range.start);
534 let end = excerpt
535 .buffer
536 .anchor_before(excerpt_range.start + range.end);
537 buffer.anchor_in_excerpt(excerpt.id.clone(), start)
538 ..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
539 }));
540 }
541 }
542 ranges
543 });
544
545 let editor = editor.downgrade();
546 self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
547 let ranges = ranges.await;
548 if let Some((this, editor)) = this.upgrade(&cx).zip(editor.upgrade(&cx)) {
549 this.update(&mut cx, |this, cx| {
550 this.editors_with_matches
551 .insert(editor.downgrade(), ranges.clone());
552 this.update_match_index(cx);
553 if !this.dismissed {
554 editor.update(cx, |editor, cx| {
555 if select_closest_match {
556 if let Some(match_ix) = this.active_match_index {
557 editor.change_selections(
558 Some(Autoscroll::Fit),
559 cx,
560 |s| s.select_ranges([ranges[match_ix].clone()]),
561 );
562 }
563 }
564
565 editor.highlight_background::<Self>(
566 ranges,
567 |theme| theme.search.match_background,
568 cx,
569 );
570 });
571 }
572 cx.notify();
573 });
574 }
575 }));
576 }
577 }
578 }
579
580 fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
581 let new_index = self.active_editor.as_ref().and_then(|editor| {
582 let ranges = self.editors_with_matches.get(&editor.downgrade())?;
583 let editor = editor.read(cx);
584 active_match_index(
585 &ranges,
586 &editor.selections.newest_anchor().head(),
587 &editor.buffer().read(cx).snapshot(cx),
588 )
589 });
590 if new_index != self.active_match_index {
591 self.active_match_index = new_index;
592 cx.notify();
593 }
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600 use editor::{DisplayPoint, Editor};
601 use gpui::{color::Color, TestAppContext};
602 use language::Buffer;
603 use std::sync::Arc;
604 use unindent::Unindent as _;
605
606 #[gpui::test]
607 async fn test_search_simple(cx: &mut TestAppContext) {
608 let fonts = cx.font_cache();
609 let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
610 theme.search.match_background = Color::red();
611 let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
612 cx.update(|cx| cx.set_global(settings));
613
614 let buffer = cx.add_model(|cx| {
615 Buffer::new(
616 0,
617 r#"
618 A regular expression (shortened as regex or regexp;[1] also referred to as
619 rational expression[2][3]) is a sequence of characters that specifies a search
620 pattern in text. Usually such patterns are used by string-searching algorithms
621 for "find" or "find and replace" operations on strings, or for input validation.
622 "#
623 .unindent(),
624 cx,
625 )
626 });
627 let editor = cx.add_view(Default::default(), |cx| {
628 Editor::for_buffer(buffer.clone(), None, cx)
629 });
630
631 let search_bar = cx.add_view(Default::default(), |cx| {
632 let mut search_bar = BufferSearchBar::new(cx);
633 search_bar.set_active_pane_item(Some(&editor), cx);
634 search_bar.show(false, true, cx);
635 search_bar
636 });
637
638 // Search for a string that appears with different casing.
639 // By default, search is case-insensitive.
640 search_bar.update(cx, |search_bar, cx| {
641 search_bar.set_query("us", cx);
642 });
643 editor.next_notification(&cx).await;
644 editor.update(cx, |editor, cx| {
645 assert_eq!(
646 editor.all_background_highlights(cx),
647 &[
648 (
649 DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
650 Color::red(),
651 ),
652 (
653 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
654 Color::red(),
655 ),
656 ]
657 );
658 });
659
660 // Switch to a case sensitive search.
661 search_bar.update(cx, |search_bar, cx| {
662 search_bar.toggle_search_option(SearchOption::CaseSensitive, cx);
663 });
664 editor.next_notification(&cx).await;
665 editor.update(cx, |editor, cx| {
666 assert_eq!(
667 editor.all_background_highlights(cx),
668 &[(
669 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
670 Color::red(),
671 )]
672 );
673 });
674
675 // Search for a string that appears both as a whole word and
676 // within other words. By default, all results are found.
677 search_bar.update(cx, |search_bar, cx| {
678 search_bar.set_query("or", cx);
679 });
680 editor.next_notification(&cx).await;
681 editor.update(cx, |editor, cx| {
682 assert_eq!(
683 editor.all_background_highlights(cx),
684 &[
685 (
686 DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
687 Color::red(),
688 ),
689 (
690 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
691 Color::red(),
692 ),
693 (
694 DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
695 Color::red(),
696 ),
697 (
698 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
699 Color::red(),
700 ),
701 (
702 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
703 Color::red(),
704 ),
705 (
706 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
707 Color::red(),
708 ),
709 (
710 DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
711 Color::red(),
712 ),
713 ]
714 );
715 });
716
717 // Switch to a whole word search.
718 search_bar.update(cx, |search_bar, cx| {
719 search_bar.toggle_search_option(SearchOption::WholeWord, cx);
720 });
721 editor.next_notification(&cx).await;
722 editor.update(cx, |editor, cx| {
723 assert_eq!(
724 editor.all_background_highlights(cx),
725 &[
726 (
727 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
728 Color::red(),
729 ),
730 (
731 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
732 Color::red(),
733 ),
734 (
735 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
736 Color::red(),
737 ),
738 ]
739 );
740 });
741
742 editor.update(cx, |editor, cx| {
743 editor.change_selections(None, cx, |s| {
744 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
745 });
746 });
747 search_bar.update(cx, |search_bar, cx| {
748 assert_eq!(search_bar.active_match_index, Some(0));
749 search_bar.select_next_match(&SelectNextMatch, cx);
750 assert_eq!(
751 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
752 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
753 );
754 });
755 search_bar.read_with(cx, |search_bar, _| {
756 assert_eq!(search_bar.active_match_index, Some(0));
757 });
758
759 search_bar.update(cx, |search_bar, cx| {
760 search_bar.select_next_match(&SelectNextMatch, cx);
761 assert_eq!(
762 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
763 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
764 );
765 });
766 search_bar.read_with(cx, |search_bar, _| {
767 assert_eq!(search_bar.active_match_index, Some(1));
768 });
769
770 search_bar.update(cx, |search_bar, cx| {
771 search_bar.select_next_match(&SelectNextMatch, cx);
772 assert_eq!(
773 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
774 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
775 );
776 });
777 search_bar.read_with(cx, |search_bar, _| {
778 assert_eq!(search_bar.active_match_index, Some(2));
779 });
780
781 search_bar.update(cx, |search_bar, cx| {
782 search_bar.select_next_match(&SelectNextMatch, cx);
783 assert_eq!(
784 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
785 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
786 );
787 });
788 search_bar.read_with(cx, |search_bar, _| {
789 assert_eq!(search_bar.active_match_index, Some(0));
790 });
791
792 search_bar.update(cx, |search_bar, cx| {
793 search_bar.select_prev_match(&SelectPrevMatch, cx);
794 assert_eq!(
795 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
796 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
797 );
798 });
799 search_bar.read_with(cx, |search_bar, _| {
800 assert_eq!(search_bar.active_match_index, Some(2));
801 });
802
803 search_bar.update(cx, |search_bar, cx| {
804 search_bar.select_prev_match(&SelectPrevMatch, cx);
805 assert_eq!(
806 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
807 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
808 );
809 });
810 search_bar.read_with(cx, |search_bar, _| {
811 assert_eq!(search_bar.active_match_index, Some(1));
812 });
813
814 search_bar.update(cx, |search_bar, cx| {
815 search_bar.select_prev_match(&SelectPrevMatch, cx);
816 assert_eq!(
817 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
818 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
819 );
820 });
821 search_bar.read_with(cx, |search_bar, _| {
822 assert_eq!(search_bar.active_match_index, Some(0));
823 });
824
825 // Park the cursor in between matches and ensure that going to the previous match selects
826 // the closest match to the left.
827 editor.update(cx, |editor, cx| {
828 editor.change_selections(None, cx, |s| {
829 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
830 });
831 });
832 search_bar.update(cx, |search_bar, cx| {
833 assert_eq!(search_bar.active_match_index, Some(1));
834 search_bar.select_prev_match(&SelectPrevMatch, cx);
835 assert_eq!(
836 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
837 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
838 );
839 });
840 search_bar.read_with(cx, |search_bar, _| {
841 assert_eq!(search_bar.active_match_index, Some(0));
842 });
843
844 // Park the cursor in between matches and ensure that going to the next match selects the
845 // closest match to the right.
846 editor.update(cx, |editor, cx| {
847 editor.change_selections(None, cx, |s| {
848 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
849 });
850 });
851 search_bar.update(cx, |search_bar, cx| {
852 assert_eq!(search_bar.active_match_index, Some(1));
853 search_bar.select_next_match(&SelectNextMatch, cx);
854 assert_eq!(
855 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
856 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
857 );
858 });
859 search_bar.read_with(cx, |search_bar, _| {
860 assert_eq!(search_bar.active_match_index, Some(1));
861 });
862
863 // Park the cursor after the last match and ensure that going to the previous match selects
864 // the last match.
865 editor.update(cx, |editor, cx| {
866 editor.change_selections(None, cx, |s| {
867 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
868 });
869 });
870 search_bar.update(cx, |search_bar, cx| {
871 assert_eq!(search_bar.active_match_index, Some(2));
872 search_bar.select_prev_match(&SelectPrevMatch, cx);
873 assert_eq!(
874 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
875 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
876 );
877 });
878 search_bar.read_with(cx, |search_bar, _| {
879 assert_eq!(search_bar.active_match_index, Some(2));
880 });
881
882 // Park the cursor after the last match and ensure that going to the next match selects the
883 // first match.
884 editor.update(cx, |editor, cx| {
885 editor.change_selections(None, cx, |s| {
886 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
887 });
888 });
889 search_bar.update(cx, |search_bar, cx| {
890 assert_eq!(search_bar.active_match_index, Some(2));
891 search_bar.select_next_match(&SelectNextMatch, cx);
892 assert_eq!(
893 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
894 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
895 );
896 });
897 search_bar.read_with(cx, |search_bar, _| {
898 assert_eq!(search_bar.active_match_index, Some(0));
899 });
900
901 // Park the cursor before the first match and ensure that going to the previous match
902 // selects the last match.
903 editor.update(cx, |editor, cx| {
904 editor.change_selections(None, cx, |s| {
905 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
906 });
907 });
908 search_bar.update(cx, |search_bar, cx| {
909 assert_eq!(search_bar.active_match_index, Some(0));
910 search_bar.select_prev_match(&SelectPrevMatch, cx);
911 assert_eq!(
912 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
913 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
914 );
915 });
916 search_bar.read_with(cx, |search_bar, _| {
917 assert_eq!(search_bar.active_match_index, Some(2));
918 });
919 }
920}