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