1use crate::{
2 history::SearchHistory,
3 mode::{next_mode, SearchMode, Side},
4 search_bar::{render_nav_button, render_search_mode_button},
5 CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
6 SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace,
7 ToggleWholeWord,
8};
9use collections::HashMap;
10use editor::Editor;
11use futures::channel::oneshot;
12use gpui::{
13 actions, elements::*, impl_actions, Action, AnyViewHandle, AppContext, Entity, Subscription,
14 Task, View, ViewContext, ViewHandle, WindowContext,
15};
16use project::search::SearchQuery;
17use serde::Deserialize;
18use std::{any::Any, sync::Arc};
19
20use util::ResultExt;
21use workspace::{
22 item::ItemHandle,
23 searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
24 Pane, ToolbarItemLocation, ToolbarItemView,
25};
26
27#[derive(Clone, Deserialize, PartialEq)]
28pub struct Deploy {
29 pub focus: bool,
30}
31
32actions!(buffer_search, [Dismiss, FocusEditor]);
33impl_actions!(buffer_search, [Deploy]);
34
35pub enum Event {
36 UpdateLocation,
37}
38
39pub fn init(cx: &mut AppContext) {
40 cx.add_action(BufferSearchBar::deploy_bar);
41 cx.add_action(BufferSearchBar::dismiss);
42 cx.add_action(BufferSearchBar::focus_editor);
43 cx.add_action(BufferSearchBar::select_next_match);
44 cx.add_action(BufferSearchBar::select_prev_match);
45 cx.add_action(BufferSearchBar::select_all_matches);
46 cx.add_action(BufferSearchBar::select_next_match_on_pane);
47 cx.add_action(BufferSearchBar::select_prev_match_on_pane);
48 cx.add_action(BufferSearchBar::select_all_matches_on_pane);
49 cx.add_action(BufferSearchBar::handle_editor_cancel);
50 cx.add_action(BufferSearchBar::next_history_query);
51 cx.add_action(BufferSearchBar::previous_history_query);
52 cx.add_action(BufferSearchBar::cycle_mode);
53 cx.add_action(BufferSearchBar::cycle_mode_on_pane);
54 cx.add_action(BufferSearchBar::replace_all);
55 cx.add_action(BufferSearchBar::replace_next);
56 cx.add_action(BufferSearchBar::replace_all_on_pane);
57 cx.add_action(BufferSearchBar::replace_next_on_pane);
58 cx.add_action(BufferSearchBar::toggle_replace);
59 cx.add_action(BufferSearchBar::toggle_replace_on_a_pane);
60 add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
61 add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
62}
63
64fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
65 cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
66 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
67 search_bar.update(cx, |search_bar, cx| {
68 if search_bar.show(cx) {
69 search_bar.toggle_search_option(option, cx);
70 }
71 });
72 }
73 cx.propagate_action();
74 });
75}
76
77pub struct BufferSearchBar {
78 query_editor: ViewHandle<Editor>,
79 replacement_editor: ViewHandle<Editor>,
80 active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
81 active_match_index: Option<usize>,
82 active_searchable_item_subscription: Option<Subscription>,
83 active_search: Option<Arc<SearchQuery>>,
84 searchable_items_with_matches:
85 HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
86 pending_search: Option<Task<()>>,
87 search_options: SearchOptions,
88 default_options: SearchOptions,
89 query_contains_error: bool,
90 dismissed: bool,
91 search_history: SearchHistory,
92 current_mode: SearchMode,
93 replace_is_active: bool,
94}
95
96impl Entity for BufferSearchBar {
97 type Event = Event;
98}
99
100impl View for BufferSearchBar {
101 fn ui_name() -> &'static str {
102 "BufferSearchBar"
103 }
104
105 fn update_keymap_context(
106 &self,
107 keymap: &mut gpui::keymap_matcher::KeymapContext,
108 cx: &AppContext,
109 ) {
110 Self::reset_to_default_keymap_context(keymap);
111 let in_replace = self
112 .replacement_editor
113 .read_with(cx, |_, cx| cx.is_self_focused())
114 .unwrap_or(false);
115 if in_replace {
116 keymap.add_identifier("in_replace");
117 }
118 }
119
120 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
121 if cx.is_self_focused() {
122 cx.focus(&self.query_editor);
123 }
124 }
125
126 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
127 let theme = theme::current(cx).clone();
128 let query_container_style = if self.query_contains_error {
129 theme.search.invalid_editor
130 } else {
131 theme.search.editor.input.container
132 };
133 let supported_options = self
134 .active_searchable_item
135 .as_ref()
136 .map(|active_searchable_item| active_searchable_item.supported_options())
137 .unwrap_or_default();
138
139 let previous_query_keystrokes =
140 cx.binding_for_action(&PreviousHistoryQuery {})
141 .map(|binding| {
142 binding
143 .keystrokes()
144 .iter()
145 .map(|k| k.to_string())
146 .collect::<Vec<_>>()
147 });
148 let next_query_keystrokes = cx.binding_for_action(&NextHistoryQuery {}).map(|binding| {
149 binding
150 .keystrokes()
151 .iter()
152 .map(|k| k.to_string())
153 .collect::<Vec<_>>()
154 });
155 let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
156 (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => {
157 format!(
158 "Search ({}/{} for previous/next query)",
159 previous_query_keystrokes.join(" "),
160 next_query_keystrokes.join(" ")
161 )
162 }
163 (None, Some(next_query_keystrokes)) => {
164 format!(
165 "Search ({} for next query)",
166 next_query_keystrokes.join(" ")
167 )
168 }
169 (Some(previous_query_keystrokes), None) => {
170 format!(
171 "Search ({} for previous query)",
172 previous_query_keystrokes.join(" ")
173 )
174 }
175 (None, None) => String::new(),
176 };
177 self.query_editor.update(cx, |editor, cx| {
178 editor.set_placeholder_text(new_placeholder_text, cx);
179 });
180 self.replacement_editor.update(cx, |editor, cx| {
181 editor.set_placeholder_text("Replace with...", cx);
182 });
183 let search_button_for_mode = |mode, side, cx: &mut ViewContext<BufferSearchBar>| {
184 let is_active = self.current_mode == mode;
185
186 render_search_mode_button(
187 mode,
188 side,
189 is_active,
190 move |_, this, cx| {
191 this.activate_search_mode(mode, cx);
192 },
193 cx,
194 )
195 };
196 let search_option_button = |option| {
197 let is_active = self.search_options.contains(option);
198 option.as_button(
199 is_active,
200 theme.tooltip.clone(),
201 theme.search.option_button_component.clone(),
202 )
203 };
204 let match_count = self
205 .active_searchable_item
206 .as_ref()
207 .and_then(|searchable_item| {
208 if self.query(cx).is_empty() {
209 return None;
210 }
211 let matches = self
212 .searchable_items_with_matches
213 .get(&searchable_item.downgrade())?;
214 let message = if let Some(match_ix) = self.active_match_index {
215 format!("{}/{}", match_ix + 1, matches.len())
216 } else {
217 "No matches".to_string()
218 };
219
220 Some(
221 Label::new(message, theme.search.match_index.text.clone())
222 .contained()
223 .with_style(theme.search.match_index.container)
224 .aligned(),
225 )
226 });
227 let nav_button_for_direction = |label, direction, cx: &mut ViewContext<Self>| {
228 render_nav_button(
229 label,
230 direction,
231 self.active_match_index.is_some(),
232 move |_, this, cx| match direction {
233 Direction::Prev => this.select_prev_match(&Default::default(), cx),
234 Direction::Next => this.select_next_match(&Default::default(), cx),
235 },
236 cx,
237 )
238 };
239 let query_column = Flex::row()
240 .with_child(
241 Svg::for_style(theme.search.editor_icon.clone().icon)
242 .contained()
243 .with_style(theme.search.editor_icon.clone().container),
244 )
245 .with_child(ChildView::new(&self.query_editor, cx).flex(1., true))
246 .with_child(
247 Flex::row()
248 .with_children(
249 supported_options
250 .case
251 .then(|| search_option_button(SearchOptions::CASE_SENSITIVE)),
252 )
253 .with_children(
254 supported_options
255 .word
256 .then(|| search_option_button(SearchOptions::WHOLE_WORD)),
257 )
258 .flex_float()
259 .contained(),
260 )
261 .align_children_center()
262 .contained()
263 .with_style(query_container_style)
264 .constrained()
265 .with_min_width(theme.search.editor.min_width)
266 .with_max_width(theme.search.editor.max_width)
267 .with_height(theme.search.search_bar_row_height)
268 .flex(1., false);
269 let should_show_replace_input = self.replace_is_active && supported_options.replacement;
270
271 let replacement = should_show_replace_input.then(|| {
272 Flex::row()
273 .with_child(
274 Svg::for_style(theme.search.replace_icon.clone().icon)
275 .contained()
276 .with_style(theme.search.replace_icon.clone().container),
277 )
278 .with_child(ChildView::new(&self.replacement_editor, cx).flex(1., true))
279 .align_children_center()
280 .flex(1., true)
281 .contained()
282 .with_style(query_container_style)
283 .constrained()
284 .with_min_width(theme.search.editor.min_width)
285 .with_max_width(theme.search.editor.max_width)
286 .with_height(theme.search.search_bar_row_height)
287 .flex(1., false)
288 });
289 let replace_all = should_show_replace_input.then(|| {
290 super::replace_action(
291 ReplaceAll,
292 "Replace all",
293 "icons/replace_all.svg",
294 theme.tooltip.clone(),
295 theme.search.action_button.clone(),
296 )
297 });
298 let replace_next = should_show_replace_input.then(|| {
299 super::replace_action(
300 ReplaceNext,
301 "Replace next",
302 "icons/replace_next.svg",
303 theme.tooltip.clone(),
304 theme.search.action_button.clone(),
305 )
306 });
307 let switches_column = supported_options.replacement.then(|| {
308 Flex::row()
309 .align_children_center()
310 .with_child(super::toggle_replace_button(
311 self.replace_is_active,
312 theme.tooltip.clone(),
313 theme.search.option_button_component.clone(),
314 ))
315 .constrained()
316 .with_height(theme.search.search_bar_row_height)
317 .contained()
318 .with_style(theme.search.option_button_group)
319 });
320 let mode_column = Flex::row()
321 .with_child(search_button_for_mode(
322 SearchMode::Text,
323 Some(Side::Left),
324 cx,
325 ))
326 .with_child(search_button_for_mode(
327 SearchMode::Regex,
328 Some(Side::Right),
329 cx,
330 ))
331 .contained()
332 .with_style(theme.search.modes_container)
333 .constrained()
334 .with_height(theme.search.search_bar_row_height);
335
336 let nav_column = Flex::row()
337 .align_children_center()
338 .with_children(replace_next)
339 .with_children(replace_all)
340 .with_child(self.render_action_button("icons/select-all.svg", cx))
341 .with_child(Flex::row().with_children(match_count))
342 .with_child(nav_button_for_direction("<", Direction::Prev, cx))
343 .with_child(nav_button_for_direction(">", Direction::Next, cx))
344 .constrained()
345 .with_height(theme.search.search_bar_row_height)
346 .flex_float();
347
348 Flex::row()
349 .with_child(query_column)
350 .with_children(switches_column)
351 .with_children(replacement)
352 .with_child(mode_column)
353 .with_child(nav_column)
354 .contained()
355 .with_style(theme.search.container)
356 .into_any_named("search bar")
357 }
358}
359
360impl ToolbarItemView for BufferSearchBar {
361 fn set_active_pane_item(
362 &mut self,
363 item: Option<&dyn ItemHandle>,
364 cx: &mut ViewContext<Self>,
365 ) -> ToolbarItemLocation {
366 cx.notify();
367 self.active_searchable_item_subscription.take();
368 self.active_searchable_item.take();
369 self.pending_search.take();
370
371 if let Some(searchable_item_handle) =
372 item.and_then(|item| item.to_searchable_item_handle(cx))
373 {
374 let this = cx.weak_handle();
375 self.active_searchable_item_subscription =
376 Some(searchable_item_handle.subscribe_to_search_events(
377 cx,
378 Box::new(move |search_event, cx| {
379 if let Some(this) = this.upgrade(cx) {
380 this.update(cx, |this, cx| {
381 this.on_active_searchable_item_event(search_event, cx)
382 });
383 }
384 }),
385 ));
386
387 self.active_searchable_item = Some(searchable_item_handle);
388 let _ = self.update_matches(cx);
389 if !self.dismissed {
390 return ToolbarItemLocation::Secondary;
391 }
392 }
393
394 ToolbarItemLocation::Hidden
395 }
396
397 fn location_for_event(
398 &self,
399 _: &Self::Event,
400 _: ToolbarItemLocation,
401 _: &AppContext,
402 ) -> ToolbarItemLocation {
403 if self.active_searchable_item.is_some() && !self.dismissed {
404 ToolbarItemLocation::Secondary
405 } else {
406 ToolbarItemLocation::Hidden
407 }
408 }
409
410 fn row_count(&self, _: &ViewContext<Self>) -> usize {
411 1
412 }
413}
414
415impl BufferSearchBar {
416 pub fn new(cx: &mut ViewContext<Self>) -> Self {
417 let query_editor = cx.add_view(|cx| {
418 Editor::auto_height(
419 2,
420 Some(Arc::new(|theme| theme.search.editor.input.clone())),
421 cx,
422 )
423 });
424 cx.subscribe(&query_editor, Self::on_query_editor_event)
425 .detach();
426 let replacement_editor = cx.add_view(|cx| {
427 Editor::auto_height(
428 2,
429 Some(Arc::new(|theme| theme.search.editor.input.clone())),
430 cx,
431 )
432 });
433 // cx.subscribe(&replacement_editor, Self::on_query_editor_event)
434 // .detach();
435 Self {
436 query_editor,
437 replacement_editor,
438 active_searchable_item: None,
439 active_searchable_item_subscription: None,
440 active_match_index: None,
441 searchable_items_with_matches: Default::default(),
442 default_options: SearchOptions::NONE,
443 search_options: SearchOptions::NONE,
444 pending_search: None,
445 query_contains_error: false,
446 dismissed: true,
447 search_history: SearchHistory::default(),
448 current_mode: SearchMode::default(),
449 active_search: None,
450 replace_is_active: false,
451 }
452 }
453
454 pub fn is_dismissed(&self) -> bool {
455 self.dismissed
456 }
457
458 pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
459 self.dismissed = true;
460 for searchable_item in self.searchable_items_with_matches.keys() {
461 if let Some(searchable_item) =
462 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
463 {
464 searchable_item.clear_matches(cx);
465 }
466 }
467 if let Some(active_editor) = self.active_searchable_item.as_ref() {
468 cx.focus(active_editor.as_any());
469 }
470 cx.emit(Event::UpdateLocation);
471 cx.notify();
472 }
473
474 pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
475 if self.show(cx) {
476 self.search_suggested(cx);
477 if deploy.focus {
478 self.select_query(cx);
479 cx.focus_self();
480 }
481 return true;
482 }
483
484 false
485 }
486
487 pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
488 if self.active_searchable_item.is_none() {
489 return false;
490 }
491 self.dismissed = false;
492 cx.notify();
493 cx.emit(Event::UpdateLocation);
494 true
495 }
496
497 pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
498 let search = self
499 .query_suggestion(cx)
500 .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
501
502 if let Some(search) = search {
503 cx.spawn(|this, mut cx| async move {
504 search.await?;
505 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
506 })
507 .detach_and_log_err(cx);
508 }
509 }
510
511 pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
512 if let Some(match_ix) = self.active_match_index {
513 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
514 if let Some(matches) = self
515 .searchable_items_with_matches
516 .get(&active_searchable_item.downgrade())
517 {
518 active_searchable_item.activate_match(match_ix, matches, cx)
519 }
520 }
521 }
522 }
523
524 pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
525 self.query_editor.update(cx, |query_editor, cx| {
526 query_editor.select_all(&Default::default(), cx);
527 });
528 }
529
530 pub fn query(&self, cx: &WindowContext) -> String {
531 self.query_editor.read(cx).text(cx)
532 }
533 pub fn replacement(&self, cx: &WindowContext) -> String {
534 self.replacement_editor.read(cx).text(cx)
535 }
536 pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
537 self.active_searchable_item
538 .as_ref()
539 .map(|searchable_item| searchable_item.query_suggestion(cx))
540 }
541
542 pub fn search(
543 &mut self,
544 query: &str,
545 options: Option<SearchOptions>,
546 cx: &mut ViewContext<Self>,
547 ) -> oneshot::Receiver<()> {
548 let options = options.unwrap_or(self.default_options);
549 if query != self.query(cx) || self.search_options != options {
550 self.query_editor.update(cx, |query_editor, cx| {
551 query_editor.buffer().update(cx, |query_buffer, cx| {
552 let len = query_buffer.len(cx);
553 query_buffer.edit([(0..len, query)], None, cx);
554 });
555 });
556 self.search_options = options;
557 self.query_contains_error = false;
558 self.clear_matches(cx);
559 cx.notify();
560 }
561 self.update_matches(cx)
562 }
563
564 fn render_action_button(
565 &self,
566 icon: &'static str,
567 cx: &mut ViewContext<Self>,
568 ) -> AnyElement<Self> {
569 let tooltip = "Select All Matches";
570 let tooltip_style = theme::current(cx).tooltip.clone();
571
572 let theme = theme::current(cx);
573 let style = theme.search.action_button.clone();
574
575 gpui::elements::Component::element(SafeStylable::with_style(
576 theme::components::action_button::Button::action(SelectAllMatches)
577 .with_tooltip(tooltip, tooltip_style)
578 .with_contents(theme::components::svg::Svg::new(icon)),
579 style,
580 ))
581 .into_any()
582 }
583
584 pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
585 assert_ne!(
586 mode,
587 SearchMode::Semantic,
588 "Semantic search is not supported in buffer search"
589 );
590 if mode == self.current_mode {
591 return;
592 }
593 self.current_mode = mode;
594 let _ = self.update_matches(cx);
595 cx.notify();
596 }
597
598 fn deploy_bar(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
599 let mut propagate_action = true;
600 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
601 search_bar.update(cx, |search_bar, cx| {
602 if search_bar.deploy(action, cx) {
603 propagate_action = false;
604 }
605 });
606 }
607 if propagate_action {
608 cx.propagate_action();
609 }
610 }
611
612 fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
613 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
614 if !search_bar.read(cx).dismissed {
615 search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
616 return;
617 }
618 }
619 cx.propagate_action();
620 }
621
622 pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
623 if let Some(active_editor) = self.active_searchable_item.as_ref() {
624 cx.focus(active_editor.as_any());
625 }
626 }
627
628 fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
629 self.search_options.toggle(search_option);
630 self.default_options = self.search_options;
631 let _ = self.update_matches(cx);
632 cx.notify();
633 }
634
635 pub fn set_search_options(
636 &mut self,
637 search_options: SearchOptions,
638 cx: &mut ViewContext<Self>,
639 ) {
640 self.search_options = search_options;
641 cx.notify();
642 }
643
644 fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
645 self.select_match(Direction::Next, 1, cx);
646 }
647
648 fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
649 self.select_match(Direction::Prev, 1, cx);
650 }
651
652 fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
653 if !self.dismissed && self.active_match_index.is_some() {
654 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
655 if let Some(matches) = self
656 .searchable_items_with_matches
657 .get(&searchable_item.downgrade())
658 {
659 searchable_item.select_matches(matches, cx);
660 self.focus_editor(&FocusEditor, cx);
661 }
662 }
663 }
664 }
665
666 pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
667 if let Some(index) = self.active_match_index {
668 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
669 if let Some(matches) = self
670 .searchable_items_with_matches
671 .get(&searchable_item.downgrade())
672 {
673 let new_match_index = searchable_item
674 .match_index_for_direction(matches, index, direction, count, cx);
675 searchable_item.update_matches(matches, cx);
676 searchable_item.activate_match(new_match_index, matches, cx);
677 }
678 }
679 }
680 }
681
682 fn select_next_match_on_pane(
683 pane: &mut Pane,
684 action: &SelectNextMatch,
685 cx: &mut ViewContext<Pane>,
686 ) {
687 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
688 search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
689 }
690 }
691
692 fn select_prev_match_on_pane(
693 pane: &mut Pane,
694 action: &SelectPrevMatch,
695 cx: &mut ViewContext<Pane>,
696 ) {
697 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
698 search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
699 }
700 }
701
702 fn select_all_matches_on_pane(
703 pane: &mut Pane,
704 action: &SelectAllMatches,
705 cx: &mut ViewContext<Pane>,
706 ) {
707 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
708 search_bar.update(cx, |bar, cx| bar.select_all_matches(action, cx));
709 }
710 }
711
712 fn on_query_editor_event(
713 &mut self,
714 _: ViewHandle<Editor>,
715 event: &editor::Event,
716 cx: &mut ViewContext<Self>,
717 ) {
718 if let editor::Event::Edited { .. } = event {
719 self.query_contains_error = false;
720 self.clear_matches(cx);
721 let search = self.update_matches(cx);
722 cx.spawn(|this, mut cx| async move {
723 search.await?;
724 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
725 })
726 .detach_and_log_err(cx);
727 }
728 }
729
730 fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
731 match event {
732 SearchEvent::MatchesInvalidated => {
733 let _ = self.update_matches(cx);
734 }
735 SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
736 }
737 }
738
739 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
740 let mut active_item_matches = None;
741 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
742 if let Some(searchable_item) =
743 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
744 {
745 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
746 active_item_matches = Some((searchable_item.downgrade(), matches));
747 } else {
748 searchable_item.clear_matches(cx);
749 }
750 }
751 }
752
753 self.searchable_items_with_matches
754 .extend(active_item_matches);
755 }
756
757 fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
758 let (done_tx, done_rx) = oneshot::channel();
759 let query = self.query(cx);
760 self.pending_search.take();
761
762 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
763 if query.is_empty() {
764 self.active_match_index.take();
765 active_searchable_item.clear_matches(cx);
766 let _ = done_tx.send(());
767 cx.notify();
768 } else {
769 let query: Arc<_> = if self.current_mode == SearchMode::Regex {
770 match SearchQuery::regex(
771 query,
772 self.search_options.contains(SearchOptions::WHOLE_WORD),
773 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
774 Vec::new(),
775 Vec::new(),
776 ) {
777 Ok(query) => query
778 .with_replacement(Some(self.replacement(cx)).filter(|s| !s.is_empty())),
779 Err(_) => {
780 self.query_contains_error = true;
781 cx.notify();
782 return done_rx;
783 }
784 }
785 } else {
786 match SearchQuery::text(
787 query,
788 self.search_options.contains(SearchOptions::WHOLE_WORD),
789 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
790 Vec::new(),
791 Vec::new(),
792 ) {
793 Ok(query) => query
794 .with_replacement(Some(self.replacement(cx)).filter(|s| !s.is_empty())),
795 Err(_) => {
796 self.query_contains_error = true;
797 cx.notify();
798 return done_rx;
799 }
800 }
801 }
802 .into();
803 self.active_search = Some(query.clone());
804 let query_text = query.as_str().to_string();
805 let matches = active_searchable_item.find_matches(query, cx);
806
807 let active_searchable_item = active_searchable_item.downgrade();
808 self.pending_search = Some(cx.spawn(|this, mut cx| async move {
809 let matches = matches.await;
810 this.update(&mut cx, |this, cx| {
811 if let Some(active_searchable_item) =
812 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
813 {
814 this.searchable_items_with_matches
815 .insert(active_searchable_item.downgrade(), matches);
816
817 this.update_match_index(cx);
818 this.search_history.add(query_text);
819 if !this.dismissed {
820 let matches = this
821 .searchable_items_with_matches
822 .get(&active_searchable_item.downgrade())
823 .unwrap();
824 active_searchable_item.update_matches(matches, cx);
825 let _ = done_tx.send(());
826 }
827 cx.notify();
828 }
829 })
830 .log_err();
831 }));
832 }
833 }
834 done_rx
835 }
836
837 fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
838 let new_index = self
839 .active_searchable_item
840 .as_ref()
841 .and_then(|searchable_item| {
842 let matches = self
843 .searchable_items_with_matches
844 .get(&searchable_item.downgrade())?;
845 searchable_item.active_match_index(matches, cx)
846 });
847 if new_index != self.active_match_index {
848 self.active_match_index = new_index;
849 cx.notify();
850 }
851 }
852
853 fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
854 if let Some(new_query) = self.search_history.next().map(str::to_string) {
855 let _ = self.search(&new_query, Some(self.search_options), cx);
856 } else {
857 self.search_history.reset_selection();
858 let _ = self.search("", Some(self.search_options), cx);
859 }
860 }
861
862 fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
863 if self.query(cx).is_empty() {
864 if let Some(new_query) = self.search_history.current().map(str::to_string) {
865 let _ = self.search(&new_query, Some(self.search_options), cx);
866 return;
867 }
868 }
869
870 if let Some(new_query) = self.search_history.previous().map(str::to_string) {
871 let _ = self.search(&new_query, Some(self.search_options), cx);
872 }
873 }
874 fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
875 self.activate_search_mode(next_mode(&self.current_mode, false), cx);
876 }
877 fn cycle_mode_on_pane(pane: &mut Pane, action: &CycleMode, cx: &mut ViewContext<Pane>) {
878 let mut should_propagate = true;
879 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
880 search_bar.update(cx, |bar, cx| {
881 if bar.show(cx) {
882 should_propagate = false;
883 bar.cycle_mode(action, cx);
884 false
885 } else {
886 true
887 }
888 });
889 }
890 if should_propagate {
891 cx.propagate_action();
892 }
893 }
894 fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
895 if let Some(_) = &self.active_searchable_item {
896 self.replace_is_active = !self.replace_is_active;
897 cx.notify();
898 }
899 }
900 fn toggle_replace_on_a_pane(pane: &mut Pane, _: &ToggleReplace, cx: &mut ViewContext<Pane>) {
901 let mut should_propagate = true;
902 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
903 search_bar.update(cx, |bar, cx| {
904 if let Some(_) = &bar.active_searchable_item {
905 should_propagate = false;
906 bar.replace_is_active = !bar.replace_is_active;
907 cx.notify();
908 }
909 });
910 }
911 if should_propagate {
912 cx.propagate_action();
913 }
914 }
915 fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
916 if !self.dismissed && self.active_search.is_some() {
917 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
918 if let Some(query) = self.active_search.as_ref() {
919 if let Some(matches) = self
920 .searchable_items_with_matches
921 .get(&searchable_item.downgrade())
922 {
923 if let Some(active_index) = self.active_match_index {
924 let query = query.as_ref().clone().with_replacement(
925 Some(self.replacement(cx)).filter(|rep| !rep.is_empty()),
926 );
927 searchable_item.replace(&matches[active_index], &query, cx);
928 self.select_next_match(&SelectNextMatch, cx);
929 }
930
931 self.focus_editor(&FocusEditor, cx);
932 }
933 }
934 }
935 }
936 }
937 fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
938 if !self.dismissed && self.active_search.is_some() {
939 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
940 if let Some(query) = self.active_search.as_ref() {
941 if let Some(matches) = self
942 .searchable_items_with_matches
943 .get(&searchable_item.downgrade())
944 {
945 let query = query.as_ref().clone().with_replacement(
946 Some(self.replacement(cx)).filter(|rep| !rep.is_empty()),
947 );
948 for m in matches {
949 searchable_item.replace(m, &query, cx);
950 }
951
952 self.focus_editor(&FocusEditor, cx);
953 }
954 }
955 }
956 }
957 }
958 fn replace_next_on_pane(pane: &mut Pane, action: &ReplaceNext, cx: &mut ViewContext<Pane>) {
959 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
960 search_bar.update(cx, |bar, cx| bar.replace_next(action, cx));
961 return;
962 }
963 cx.propagate_action();
964 }
965 fn replace_all_on_pane(pane: &mut Pane, action: &ReplaceAll, cx: &mut ViewContext<Pane>) {
966 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
967 search_bar.update(cx, |bar, cx| bar.replace_all(action, cx));
968 return;
969 }
970 cx.propagate_action();
971 }
972}
973
974#[cfg(test)]
975mod tests {
976 use super::*;
977 use editor::{DisplayPoint, Editor};
978 use gpui::{color::Color, test::EmptyView, TestAppContext};
979 use language::Buffer;
980 use unindent::Unindent as _;
981
982 fn init_test(cx: &mut TestAppContext) -> (ViewHandle<Editor>, ViewHandle<BufferSearchBar>) {
983 crate::project_search::tests::init_test(cx);
984
985 let buffer = cx.add_model(|cx| {
986 Buffer::new(
987 0,
988 cx.model_id() as u64,
989 r#"
990 A regular expression (shortened as regex or regexp;[1] also referred to as
991 rational expression[2][3]) is a sequence of characters that specifies a search
992 pattern in text. Usually such patterns are used by string-searching algorithms
993 for "find" or "find and replace" operations on strings, or for input validation.
994 "#
995 .unindent(),
996 )
997 });
998 let window = cx.add_window(|_| EmptyView);
999 let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1000
1001 let search_bar = window.add_view(cx, |cx| {
1002 let mut search_bar = BufferSearchBar::new(cx);
1003 search_bar.set_active_pane_item(Some(&editor), cx);
1004 search_bar.show(cx);
1005 search_bar
1006 });
1007
1008 (editor, search_bar)
1009 }
1010
1011 #[gpui::test]
1012 async fn test_search_simple(cx: &mut TestAppContext) {
1013 let (editor, search_bar) = init_test(cx);
1014
1015 // Search for a string that appears with different casing.
1016 // By default, search is case-insensitive.
1017 search_bar
1018 .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
1019 .await
1020 .unwrap();
1021 editor.update(cx, |editor, cx| {
1022 assert_eq!(
1023 editor.all_text_background_highlights(cx),
1024 &[
1025 (
1026 DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
1027 Color::red(),
1028 ),
1029 (
1030 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1031 Color::red(),
1032 ),
1033 ]
1034 );
1035 });
1036
1037 // Switch to a case sensitive search.
1038 search_bar.update(cx, |search_bar, cx| {
1039 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1040 });
1041 editor.next_notification(cx).await;
1042 editor.update(cx, |editor, cx| {
1043 assert_eq!(
1044 editor.all_text_background_highlights(cx),
1045 &[(
1046 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1047 Color::red(),
1048 )]
1049 );
1050 });
1051
1052 // Search for a string that appears both as a whole word and
1053 // within other words. By default, all results are found.
1054 search_bar
1055 .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1056 .await
1057 .unwrap();
1058 editor.update(cx, |editor, cx| {
1059 assert_eq!(
1060 editor.all_text_background_highlights(cx),
1061 &[
1062 (
1063 DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
1064 Color::red(),
1065 ),
1066 (
1067 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
1068 Color::red(),
1069 ),
1070 (
1071 DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
1072 Color::red(),
1073 ),
1074 (
1075 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
1076 Color::red(),
1077 ),
1078 (
1079 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
1080 Color::red(),
1081 ),
1082 (
1083 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
1084 Color::red(),
1085 ),
1086 (
1087 DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
1088 Color::red(),
1089 ),
1090 ]
1091 );
1092 });
1093
1094 // Switch to a whole word search.
1095 search_bar.update(cx, |search_bar, cx| {
1096 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1097 });
1098 editor.next_notification(cx).await;
1099 editor.update(cx, |editor, cx| {
1100 assert_eq!(
1101 editor.all_text_background_highlights(cx),
1102 &[
1103 (
1104 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
1105 Color::red(),
1106 ),
1107 (
1108 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
1109 Color::red(),
1110 ),
1111 (
1112 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
1113 Color::red(),
1114 ),
1115 ]
1116 );
1117 });
1118
1119 editor.update(cx, |editor, cx| {
1120 editor.change_selections(None, cx, |s| {
1121 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1122 });
1123 });
1124 search_bar.update(cx, |search_bar, cx| {
1125 assert_eq!(search_bar.active_match_index, Some(0));
1126 search_bar.select_next_match(&SelectNextMatch, cx);
1127 assert_eq!(
1128 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1129 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1130 );
1131 });
1132 search_bar.read_with(cx, |search_bar, _| {
1133 assert_eq!(search_bar.active_match_index, Some(0));
1134 });
1135
1136 search_bar.update(cx, |search_bar, cx| {
1137 search_bar.select_next_match(&SelectNextMatch, cx);
1138 assert_eq!(
1139 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1140 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1141 );
1142 });
1143 search_bar.read_with(cx, |search_bar, _| {
1144 assert_eq!(search_bar.active_match_index, Some(1));
1145 });
1146
1147 search_bar.update(cx, |search_bar, cx| {
1148 search_bar.select_next_match(&SelectNextMatch, cx);
1149 assert_eq!(
1150 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1151 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1152 );
1153 });
1154 search_bar.read_with(cx, |search_bar, _| {
1155 assert_eq!(search_bar.active_match_index, Some(2));
1156 });
1157
1158 search_bar.update(cx, |search_bar, cx| {
1159 search_bar.select_next_match(&SelectNextMatch, cx);
1160 assert_eq!(
1161 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1162 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1163 );
1164 });
1165 search_bar.read_with(cx, |search_bar, _| {
1166 assert_eq!(search_bar.active_match_index, Some(0));
1167 });
1168
1169 search_bar.update(cx, |search_bar, cx| {
1170 search_bar.select_prev_match(&SelectPrevMatch, cx);
1171 assert_eq!(
1172 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1173 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1174 );
1175 });
1176 search_bar.read_with(cx, |search_bar, _| {
1177 assert_eq!(search_bar.active_match_index, Some(2));
1178 });
1179
1180 search_bar.update(cx, |search_bar, cx| {
1181 search_bar.select_prev_match(&SelectPrevMatch, cx);
1182 assert_eq!(
1183 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1184 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1185 );
1186 });
1187 search_bar.read_with(cx, |search_bar, _| {
1188 assert_eq!(search_bar.active_match_index, Some(1));
1189 });
1190
1191 search_bar.update(cx, |search_bar, cx| {
1192 search_bar.select_prev_match(&SelectPrevMatch, cx);
1193 assert_eq!(
1194 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1195 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1196 );
1197 });
1198 search_bar.read_with(cx, |search_bar, _| {
1199 assert_eq!(search_bar.active_match_index, Some(0));
1200 });
1201
1202 // Park the cursor in between matches and ensure that going to the previous match selects
1203 // the closest match to the left.
1204 editor.update(cx, |editor, cx| {
1205 editor.change_selections(None, cx, |s| {
1206 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1207 });
1208 });
1209 search_bar.update(cx, |search_bar, cx| {
1210 assert_eq!(search_bar.active_match_index, Some(1));
1211 search_bar.select_prev_match(&SelectPrevMatch, cx);
1212 assert_eq!(
1213 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1214 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1215 );
1216 });
1217 search_bar.read_with(cx, |search_bar, _| {
1218 assert_eq!(search_bar.active_match_index, Some(0));
1219 });
1220
1221 // Park the cursor in between matches and ensure that going to the next match selects the
1222 // closest match to the right.
1223 editor.update(cx, |editor, cx| {
1224 editor.change_selections(None, cx, |s| {
1225 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1226 });
1227 });
1228 search_bar.update(cx, |search_bar, cx| {
1229 assert_eq!(search_bar.active_match_index, Some(1));
1230 search_bar.select_next_match(&SelectNextMatch, cx);
1231 assert_eq!(
1232 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1233 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1234 );
1235 });
1236 search_bar.read_with(cx, |search_bar, _| {
1237 assert_eq!(search_bar.active_match_index, Some(1));
1238 });
1239
1240 // Park the cursor after the last match and ensure that going to the previous match selects
1241 // the last match.
1242 editor.update(cx, |editor, cx| {
1243 editor.change_selections(None, cx, |s| {
1244 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1245 });
1246 });
1247 search_bar.update(cx, |search_bar, cx| {
1248 assert_eq!(search_bar.active_match_index, Some(2));
1249 search_bar.select_prev_match(&SelectPrevMatch, cx);
1250 assert_eq!(
1251 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1252 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1253 );
1254 });
1255 search_bar.read_with(cx, |search_bar, _| {
1256 assert_eq!(search_bar.active_match_index, Some(2));
1257 });
1258
1259 // Park the cursor after the last match and ensure that going to the next match selects the
1260 // first match.
1261 editor.update(cx, |editor, cx| {
1262 editor.change_selections(None, cx, |s| {
1263 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1264 });
1265 });
1266 search_bar.update(cx, |search_bar, cx| {
1267 assert_eq!(search_bar.active_match_index, Some(2));
1268 search_bar.select_next_match(&SelectNextMatch, cx);
1269 assert_eq!(
1270 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1271 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1272 );
1273 });
1274 search_bar.read_with(cx, |search_bar, _| {
1275 assert_eq!(search_bar.active_match_index, Some(0));
1276 });
1277
1278 // Park the cursor before the first match and ensure that going to the previous match
1279 // selects the last match.
1280 editor.update(cx, |editor, cx| {
1281 editor.change_selections(None, cx, |s| {
1282 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1283 });
1284 });
1285 search_bar.update(cx, |search_bar, cx| {
1286 assert_eq!(search_bar.active_match_index, Some(0));
1287 search_bar.select_prev_match(&SelectPrevMatch, cx);
1288 assert_eq!(
1289 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1290 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1291 );
1292 });
1293 search_bar.read_with(cx, |search_bar, _| {
1294 assert_eq!(search_bar.active_match_index, Some(2));
1295 });
1296 }
1297
1298 #[gpui::test]
1299 async fn test_search_option_handling(cx: &mut TestAppContext) {
1300 let (editor, search_bar) = init_test(cx);
1301
1302 // show with options should make current search case sensitive
1303 search_bar
1304 .update(cx, |search_bar, cx| {
1305 search_bar.show(cx);
1306 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1307 })
1308 .await
1309 .unwrap();
1310 editor.update(cx, |editor, cx| {
1311 assert_eq!(
1312 editor.all_text_background_highlights(cx),
1313 &[(
1314 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1315 Color::red(),
1316 )]
1317 );
1318 });
1319
1320 // search_suggested should restore default options
1321 search_bar.update(cx, |search_bar, cx| {
1322 search_bar.search_suggested(cx);
1323 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1324 });
1325
1326 // toggling a search option should update the defaults
1327 search_bar
1328 .update(cx, |search_bar, cx| {
1329 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1330 })
1331 .await
1332 .unwrap();
1333 search_bar.update(cx, |search_bar, cx| {
1334 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1335 });
1336 editor.next_notification(cx).await;
1337 editor.update(cx, |editor, cx| {
1338 assert_eq!(
1339 editor.all_text_background_highlights(cx),
1340 &[(
1341 DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
1342 Color::red(),
1343 ),]
1344 );
1345 });
1346
1347 // defaults should still include whole word
1348 search_bar.update(cx, |search_bar, cx| {
1349 search_bar.search_suggested(cx);
1350 assert_eq!(
1351 search_bar.search_options,
1352 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1353 )
1354 });
1355 }
1356
1357 #[gpui::test]
1358 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1359 crate::project_search::tests::init_test(cx);
1360
1361 let buffer_text = r#"
1362 A regular expression (shortened as regex or regexp;[1] also referred to as
1363 rational expression[2][3]) is a sequence of characters that specifies a search
1364 pattern in text. Usually such patterns are used by string-searching algorithms
1365 for "find" or "find and replace" operations on strings, or for input validation.
1366 "#
1367 .unindent();
1368 let expected_query_matches_count = buffer_text
1369 .chars()
1370 .filter(|c| c.to_ascii_lowercase() == 'a')
1371 .count();
1372 assert!(
1373 expected_query_matches_count > 1,
1374 "Should pick a query with multiple results"
1375 );
1376 let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text));
1377 let window = cx.add_window(|_| EmptyView);
1378 let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1379
1380 let search_bar = window.add_view(cx, |cx| {
1381 let mut search_bar = BufferSearchBar::new(cx);
1382 search_bar.set_active_pane_item(Some(&editor), cx);
1383 search_bar.show(cx);
1384 search_bar
1385 });
1386
1387 search_bar
1388 .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1389 .await
1390 .unwrap();
1391 search_bar.update(cx, |search_bar, cx| {
1392 cx.focus(search_bar.query_editor.as_any());
1393 search_bar.activate_current_match(cx);
1394 });
1395
1396 window.read_with(cx, |cx| {
1397 assert!(
1398 !editor.is_focused(cx),
1399 "Initially, the editor should not be focused"
1400 );
1401 });
1402
1403 let initial_selections = editor.update(cx, |editor, cx| {
1404 let initial_selections = editor.selections.display_ranges(cx);
1405 assert_eq!(
1406 initial_selections.len(), 1,
1407 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1408 );
1409 initial_selections
1410 });
1411 search_bar.update(cx, |search_bar, _| {
1412 assert_eq!(search_bar.active_match_index, Some(0));
1413 });
1414
1415 search_bar.update(cx, |search_bar, cx| {
1416 cx.focus(search_bar.query_editor.as_any());
1417 search_bar.select_all_matches(&SelectAllMatches, cx);
1418 });
1419 window.read_with(cx, |cx| {
1420 assert!(
1421 editor.is_focused(cx),
1422 "Should focus editor after successful SelectAllMatches"
1423 );
1424 });
1425 search_bar.update(cx, |search_bar, cx| {
1426 let all_selections =
1427 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1428 assert_eq!(
1429 all_selections.len(),
1430 expected_query_matches_count,
1431 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1432 );
1433 assert_eq!(
1434 search_bar.active_match_index,
1435 Some(0),
1436 "Match index should not change after selecting all matches"
1437 );
1438 });
1439
1440 search_bar.update(cx, |search_bar, cx| {
1441 search_bar.select_next_match(&SelectNextMatch, cx);
1442 });
1443 window.read_with(cx, |cx| {
1444 assert!(
1445 editor.is_focused(cx),
1446 "Should still have editor focused after SelectNextMatch"
1447 );
1448 });
1449 search_bar.update(cx, |search_bar, cx| {
1450 let all_selections =
1451 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1452 assert_eq!(
1453 all_selections.len(),
1454 1,
1455 "On next match, should deselect items and select the next match"
1456 );
1457 assert_ne!(
1458 all_selections, initial_selections,
1459 "Next match should be different from the first selection"
1460 );
1461 assert_eq!(
1462 search_bar.active_match_index,
1463 Some(1),
1464 "Match index should be updated to the next one"
1465 );
1466 });
1467
1468 search_bar.update(cx, |search_bar, cx| {
1469 cx.focus(search_bar.query_editor.as_any());
1470 search_bar.select_all_matches(&SelectAllMatches, cx);
1471 });
1472 window.read_with(cx, |cx| {
1473 assert!(
1474 editor.is_focused(cx),
1475 "Should focus editor after successful SelectAllMatches"
1476 );
1477 });
1478 search_bar.update(cx, |search_bar, cx| {
1479 let all_selections =
1480 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1481 assert_eq!(
1482 all_selections.len(),
1483 expected_query_matches_count,
1484 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1485 );
1486 assert_eq!(
1487 search_bar.active_match_index,
1488 Some(1),
1489 "Match index should not change after selecting all matches"
1490 );
1491 });
1492
1493 search_bar.update(cx, |search_bar, cx| {
1494 search_bar.select_prev_match(&SelectPrevMatch, cx);
1495 });
1496 window.read_with(cx, |cx| {
1497 assert!(
1498 editor.is_focused(cx),
1499 "Should still have editor focused after SelectPrevMatch"
1500 );
1501 });
1502 let last_match_selections = search_bar.update(cx, |search_bar, cx| {
1503 let all_selections =
1504 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1505 assert_eq!(
1506 all_selections.len(),
1507 1,
1508 "On previous match, should deselect items and select the previous item"
1509 );
1510 assert_eq!(
1511 all_selections, initial_selections,
1512 "Previous match should be the same as the first selection"
1513 );
1514 assert_eq!(
1515 search_bar.active_match_index,
1516 Some(0),
1517 "Match index should be updated to the previous one"
1518 );
1519 all_selections
1520 });
1521
1522 search_bar
1523 .update(cx, |search_bar, cx| {
1524 cx.focus(search_bar.query_editor.as_any());
1525 search_bar.search("abas_nonexistent_match", None, cx)
1526 })
1527 .await
1528 .unwrap();
1529 search_bar.update(cx, |search_bar, cx| {
1530 search_bar.select_all_matches(&SelectAllMatches, cx);
1531 });
1532 window.read_with(cx, |cx| {
1533 assert!(
1534 !editor.is_focused(cx),
1535 "Should not switch focus to editor if SelectAllMatches does not find any matches"
1536 );
1537 });
1538 search_bar.update(cx, |search_bar, cx| {
1539 let all_selections =
1540 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1541 assert_eq!(
1542 all_selections, last_match_selections,
1543 "Should not select anything new if there are no matches"
1544 );
1545 assert!(
1546 search_bar.active_match_index.is_none(),
1547 "For no matches, there should be no active match index"
1548 );
1549 });
1550 }
1551
1552 #[gpui::test]
1553 async fn test_search_query_history(cx: &mut TestAppContext) {
1554 crate::project_search::tests::init_test(cx);
1555
1556 let buffer_text = r#"
1557 A regular expression (shortened as regex or regexp;[1] also referred to as
1558 rational expression[2][3]) is a sequence of characters that specifies a search
1559 pattern in text. Usually such patterns are used by string-searching algorithms
1560 for "find" or "find and replace" operations on strings, or for input validation.
1561 "#
1562 .unindent();
1563 let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text));
1564 let window = cx.add_window(|_| EmptyView);
1565
1566 let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1567
1568 let search_bar = window.add_view(cx, |cx| {
1569 let mut search_bar = BufferSearchBar::new(cx);
1570 search_bar.set_active_pane_item(Some(&editor), cx);
1571 search_bar.show(cx);
1572 search_bar
1573 });
1574
1575 // Add 3 search items into the history.
1576 search_bar
1577 .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1578 .await
1579 .unwrap();
1580 search_bar
1581 .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1582 .await
1583 .unwrap();
1584 search_bar
1585 .update(cx, |search_bar, cx| {
1586 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1587 })
1588 .await
1589 .unwrap();
1590 // Ensure that the latest search is active.
1591 search_bar.read_with(cx, |search_bar, cx| {
1592 assert_eq!(search_bar.query(cx), "c");
1593 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1594 });
1595
1596 // Next history query after the latest should set the query to the empty string.
1597 search_bar.update(cx, |search_bar, cx| {
1598 search_bar.next_history_query(&NextHistoryQuery, cx);
1599 });
1600 search_bar.read_with(cx, |search_bar, cx| {
1601 assert_eq!(search_bar.query(cx), "");
1602 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1603 });
1604 search_bar.update(cx, |search_bar, cx| {
1605 search_bar.next_history_query(&NextHistoryQuery, cx);
1606 });
1607 search_bar.read_with(cx, |search_bar, cx| {
1608 assert_eq!(search_bar.query(cx), "");
1609 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1610 });
1611
1612 // First previous query for empty current query should set the query to the latest.
1613 search_bar.update(cx, |search_bar, cx| {
1614 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1615 });
1616 search_bar.read_with(cx, |search_bar, cx| {
1617 assert_eq!(search_bar.query(cx), "c");
1618 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1619 });
1620
1621 // Further previous items should go over the history in reverse order.
1622 search_bar.update(cx, |search_bar, cx| {
1623 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1624 });
1625 search_bar.read_with(cx, |search_bar, cx| {
1626 assert_eq!(search_bar.query(cx), "b");
1627 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1628 });
1629
1630 // Previous items should never go behind the first history item.
1631 search_bar.update(cx, |search_bar, cx| {
1632 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1633 });
1634 search_bar.read_with(cx, |search_bar, cx| {
1635 assert_eq!(search_bar.query(cx), "a");
1636 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1637 });
1638 search_bar.update(cx, |search_bar, cx| {
1639 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1640 });
1641 search_bar.read_with(cx, |search_bar, cx| {
1642 assert_eq!(search_bar.query(cx), "a");
1643 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1644 });
1645
1646 // Next items should go over the history in the original order.
1647 search_bar.update(cx, |search_bar, cx| {
1648 search_bar.next_history_query(&NextHistoryQuery, cx);
1649 });
1650 search_bar.read_with(cx, |search_bar, cx| {
1651 assert_eq!(search_bar.query(cx), "b");
1652 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1653 });
1654
1655 search_bar
1656 .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1657 .await
1658 .unwrap();
1659 search_bar.read_with(cx, |search_bar, cx| {
1660 assert_eq!(search_bar.query(cx), "ba");
1661 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1662 });
1663
1664 // New search input should add another entry to history and move the selection to the end of the history.
1665 search_bar.update(cx, |search_bar, cx| {
1666 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1667 });
1668 search_bar.read_with(cx, |search_bar, cx| {
1669 assert_eq!(search_bar.query(cx), "c");
1670 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1671 });
1672 search_bar.update(cx, |search_bar, cx| {
1673 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1674 });
1675 search_bar.read_with(cx, |search_bar, cx| {
1676 assert_eq!(search_bar.query(cx), "b");
1677 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1678 });
1679 search_bar.update(cx, |search_bar, cx| {
1680 search_bar.next_history_query(&NextHistoryQuery, cx);
1681 });
1682 search_bar.read_with(cx, |search_bar, cx| {
1683 assert_eq!(search_bar.query(cx), "c");
1684 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1685 });
1686 search_bar.update(cx, |search_bar, cx| {
1687 search_bar.next_history_query(&NextHistoryQuery, cx);
1688 });
1689 search_bar.read_with(cx, |search_bar, cx| {
1690 assert_eq!(search_bar.query(cx), "ba");
1691 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1692 });
1693 search_bar.update(cx, |search_bar, cx| {
1694 search_bar.next_history_query(&NextHistoryQuery, cx);
1695 });
1696 search_bar.read_with(cx, |search_bar, cx| {
1697 assert_eq!(search_bar.query(cx), "");
1698 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1699 });
1700 }
1701 #[gpui::test]
1702 async fn test_replace_simple(cx: &mut TestAppContext) {
1703 let (editor, search_bar) = init_test(cx);
1704
1705 search_bar
1706 .update(cx, |search_bar, cx| {
1707 search_bar.search("expression", None, cx)
1708 })
1709 .await
1710 .unwrap();
1711
1712 search_bar.update(cx, |search_bar, cx| {
1713 search_bar.replacement_editor.update(cx, |editor, cx| {
1714 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
1715 editor.set_text("expr$1", cx);
1716 });
1717 search_bar.replace_all(&ReplaceAll, cx)
1718 });
1719 assert_eq!(
1720 editor.read_with(cx, |this, cx| { this.text(cx) }),
1721 r#"
1722 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
1723 rational expr$1[2][3]) is a sequence of characters that specifies a search
1724 pattern in text. Usually such patterns are used by string-searching algorithms
1725 for "find" or "find and replace" operations on strings, or for input validation.
1726 "#
1727 .unindent()
1728 );
1729
1730 // Search for word boundaries and replace just a single one.
1731 search_bar
1732 .update(cx, |search_bar, cx| {
1733 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
1734 })
1735 .await
1736 .unwrap();
1737
1738 search_bar.update(cx, |search_bar, cx| {
1739 search_bar.replacement_editor.update(cx, |editor, cx| {
1740 editor.set_text("banana", cx);
1741 });
1742 search_bar.replace_next(&ReplaceNext, cx)
1743 });
1744 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
1745 assert_eq!(
1746 editor.read_with(cx, |this, cx| { this.text(cx) }),
1747 r#"
1748 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
1749 rational expr$1[2][3]) is a sequence of characters that specifies a search
1750 pattern in text. Usually such patterns are used by string-searching algorithms
1751 for "find" or "find and replace" operations on strings, or for input validation.
1752 "#
1753 .unindent()
1754 );
1755 // Let's turn on regex mode.
1756 search_bar
1757 .update(cx, |search_bar, cx| {
1758 search_bar.activate_search_mode(SearchMode::Regex, cx);
1759 search_bar.search("\\[([^\\]]+)\\]", None, cx)
1760 })
1761 .await
1762 .unwrap();
1763 search_bar.update(cx, |search_bar, cx| {
1764 search_bar.replacement_editor.update(cx, |editor, cx| {
1765 editor.set_text("${1}number", cx);
1766 });
1767 search_bar.replace_all(&ReplaceAll, cx)
1768 });
1769 assert_eq!(
1770 editor.read_with(cx, |this, cx| { this.text(cx) }),
1771 r#"
1772 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1773 rational expr$12number3number) is a sequence of characters that specifies a search
1774 pattern in text. Usually such patterns are used by string-searching algorithms
1775 for "find" or "find and replace" operations on strings, or for input validation.
1776 "#
1777 .unindent()
1778 );
1779 // Now with a whole-word twist.
1780 search_bar
1781 .update(cx, |search_bar, cx| {
1782 search_bar.activate_search_mode(SearchMode::Regex, cx);
1783 search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
1784 })
1785 .await
1786 .unwrap();
1787 search_bar.update(cx, |search_bar, cx| {
1788 search_bar.replacement_editor.update(cx, |editor, cx| {
1789 editor.set_text("things", cx);
1790 });
1791 search_bar.replace_all(&ReplaceAll, cx)
1792 });
1793 // The only word affected by this edit should be `algorithms`, even though there's a bunch
1794 // of words in this text that would match this regex if not for WHOLE_WORD.
1795 assert_eq!(
1796 editor.read_with(cx, |this, cx| { this.text(cx) }),
1797 r#"
1798 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1799 rational expr$12number3number) is a sequence of characters that specifies a search
1800 pattern in text. Usually such patterns are used by string-searching things
1801 for "find" or "find and replace" operations on strings, or for input validation.
1802 "#
1803 .unindent()
1804 );
1805 }
1806}