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