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