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