1mod registrar;
2
3use crate::{
4 search_bar::render_nav_button, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll,
5 ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch,
6 ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
7};
8use any_vec::AnyVec;
9use collections::HashMap;
10use editor::{
11 actions::{Tab, TabPrev},
12 DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle,
13};
14use futures::channel::oneshot;
15use gpui::{
16 actions, div, impl_actions, Action, AppContext, ClickEvent, EventEmitter, FocusHandle,
17 FocusableView, Hsla, InteractiveElement as _, IntoElement, KeyContext, ParentElement as _,
18 Render, ScrollHandle, Styled, Subscription, Task, TextStyle, View, ViewContext,
19 VisualContext as _, WindowContext,
20};
21use project::{
22 search::SearchQuery,
23 search_history::{SearchHistory, SearchHistoryCursor},
24};
25use serde::Deserialize;
26use settings::Settings;
27use std::sync::Arc;
28use theme::ThemeSettings;
29
30use ui::{h_flex, prelude::*, IconButton, IconButtonShape, IconName, Tooltip, BASE_REM_SIZE_IN_PX};
31use util::ResultExt;
32use workspace::{
33 item::ItemHandle,
34 searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
35 ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
36};
37
38pub use registrar::DivRegistrar;
39use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults};
40
41const MIN_INPUT_WIDTH_REMS: f32 = 10.;
42const MAX_INPUT_WIDTH_REMS: f32 = 30.;
43const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
44
45#[derive(PartialEq, Clone, Deserialize)]
46pub struct Deploy {
47 #[serde(default = "util::serde::default_true")]
48 pub focus: bool,
49 #[serde(default)]
50 pub replace_enabled: bool,
51 #[serde(default)]
52 pub selection_search_enabled: bool,
53}
54
55impl_actions!(buffer_search, [Deploy]);
56
57actions!(buffer_search, [Dismiss, FocusEditor]);
58
59impl Deploy {
60 pub fn find() -> Self {
61 Self {
62 focus: true,
63 replace_enabled: false,
64 selection_search_enabled: false,
65 }
66 }
67}
68
69pub enum Event {
70 UpdateLocation,
71}
72
73pub fn init(cx: &mut AppContext) {
74 cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace))
75 .detach();
76}
77
78pub struct BufferSearchBar {
79 query_editor: View<Editor>,
80 query_editor_focused: bool,
81 replacement_editor: View<Editor>,
82 replacement_editor_focused: bool,
83 active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
84 active_match_index: Option<usize>,
85 active_searchable_item_subscription: Option<Subscription>,
86 active_search: Option<Arc<SearchQuery>>,
87 searchable_items_with_matches: HashMap<Box<dyn WeakSearchableItemHandle>, AnyVec<dyn Send>>,
88 pending_search: Option<Task<()>>,
89 search_options: SearchOptions,
90 default_options: SearchOptions,
91 configured_options: SearchOptions,
92 query_contains_error: bool,
93 dismissed: bool,
94 search_history: SearchHistory,
95 search_history_cursor: SearchHistoryCursor,
96 replace_enabled: bool,
97 selection_search_enabled: bool,
98 scroll_handle: ScrollHandle,
99 editor_scroll_handle: ScrollHandle,
100 editor_needed_width: Pixels,
101}
102
103impl BufferSearchBar {
104 fn render_text_input(
105 &self,
106 editor: &View<Editor>,
107 color: Hsla,
108 cx: &ViewContext<Self>,
109 ) -> impl IntoElement {
110 let settings = ThemeSettings::get_global(cx);
111 let text_style = TextStyle {
112 color: if editor.read(cx).read_only(cx) {
113 cx.theme().colors().text_disabled
114 } else {
115 color
116 },
117 font_family: settings.buffer_font.family.clone(),
118 font_features: settings.buffer_font.features.clone(),
119 font_fallbacks: settings.buffer_font.fallbacks.clone(),
120 font_size: rems(0.875).into(),
121 font_weight: settings.buffer_font.weight,
122 line_height: relative(1.3),
123 ..Default::default()
124 };
125
126 EditorElement::new(
127 editor,
128 EditorStyle {
129 background: cx.theme().colors().editor_background,
130 local_player: cx.theme().players().local(),
131 text: text_style,
132 ..Default::default()
133 },
134 )
135 }
136
137 pub fn query_editor_focused(&self) -> bool {
138 self.query_editor_focused
139 }
140}
141
142impl EventEmitter<Event> for BufferSearchBar {}
143impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
144impl Render for BufferSearchBar {
145 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
146 if self.dismissed {
147 return div().id("search_bar");
148 }
149
150 let focus_handle = self.focus_handle(cx);
151
152 let narrow_mode =
153 self.scroll_handle.bounds().size.width / cx.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
154 let hide_inline_icons = self.editor_needed_width
155 > self.editor_scroll_handle.bounds().size.width - cx.rem_size() * 6.;
156
157 let supported_options = self.supported_options();
158
159 if self.query_editor.update(cx, |query_editor, cx| {
160 query_editor.placeholder_text(cx).is_none()
161 }) {
162 self.query_editor.update(cx, |editor, cx| {
163 editor.set_placeholder_text("Search", cx);
164 });
165 }
166
167 self.replacement_editor.update(cx, |editor, cx| {
168 editor.set_placeholder_text("Replace with...", cx);
169 });
170
171 let mut text_color = Color::Default;
172 let match_text = self
173 .active_searchable_item
174 .as_ref()
175 .and_then(|searchable_item| {
176 if self.query(cx).is_empty() {
177 return None;
178 }
179 let matches_count = self
180 .searchable_items_with_matches
181 .get(&searchable_item.downgrade())
182 .map(AnyVec::len)
183 .unwrap_or(0);
184 if let Some(match_ix) = self.active_match_index {
185 Some(format!("{}/{}", match_ix + 1, matches_count))
186 } else {
187 text_color = Color::Error; // No matches found
188 None
189 }
190 })
191 .unwrap_or_else(|| "0/0".to_string());
192 let should_show_replace_input = self.replace_enabled && supported_options.replacement;
193 let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx);
194
195 let mut key_context = KeyContext::new_with_defaults();
196 key_context.add("BufferSearchBar");
197 if in_replace {
198 key_context.add("in_replace");
199 }
200 let editor_border = if self.query_contains_error {
201 Color::Error.color(cx)
202 } else {
203 cx.theme().colors().border
204 };
205
206 let search_line = h_flex()
207 .gap_2()
208 .child(
209 h_flex()
210 .id("editor-scroll")
211 .track_scroll(&self.editor_scroll_handle)
212 .flex_1()
213 .h_8()
214 .px_2()
215 .py_1()
216 .border_1()
217 .border_color(editor_border)
218 .min_w(rems(MIN_INPUT_WIDTH_REMS))
219 .max_w(rems(MAX_INPUT_WIDTH_REMS))
220 .rounded_lg()
221 .child(self.render_text_input(&self.query_editor, text_color.color(cx), cx))
222 .when(!hide_inline_icons, |div| {
223 div.children(supported_options.case.then(|| {
224 self.render_search_option_button(
225 SearchOptions::CASE_SENSITIVE,
226 focus_handle.clone(),
227 cx.listener(|this, _, cx| {
228 this.toggle_case_sensitive(&ToggleCaseSensitive, cx)
229 }),
230 )
231 }))
232 .children(supported_options.word.then(|| {
233 self.render_search_option_button(
234 SearchOptions::WHOLE_WORD,
235 focus_handle.clone(),
236 cx.listener(|this, _, cx| {
237 this.toggle_whole_word(&ToggleWholeWord, cx)
238 }),
239 )
240 }))
241 .children(supported_options.regex.then(|| {
242 self.render_search_option_button(
243 SearchOptions::REGEX,
244 focus_handle.clone(),
245 cx.listener(|this, _, cx| this.toggle_regex(&ToggleRegex, cx)),
246 )
247 }))
248 }),
249 )
250 .child(
251 h_flex()
252 .flex_none()
253 .gap_0p5()
254 .when(supported_options.replacement, |this| {
255 this.child(
256 IconButton::new(
257 "buffer-search-bar-toggle-replace-button",
258 IconName::Replace,
259 )
260 .style(ButtonStyle::Subtle)
261 .shape(IconButtonShape::Square)
262 .when(self.replace_enabled, |button| {
263 button.style(ButtonStyle::Filled)
264 })
265 .on_click(cx.listener(|this, _: &ClickEvent, cx| {
266 this.toggle_replace(&ToggleReplace, cx);
267 }))
268 .selected(self.replace_enabled)
269 .tooltip({
270 let focus_handle = focus_handle.clone();
271 move |cx| {
272 Tooltip::for_action_in(
273 "Toggle Replace",
274 &ToggleReplace,
275 &focus_handle,
276 cx,
277 )
278 }
279 }),
280 )
281 })
282 .when(supported_options.selection, |this| {
283 this.child(
284 IconButton::new(
285 "buffer-search-bar-toggle-search-selection-button",
286 IconName::SearchSelection,
287 )
288 .style(ButtonStyle::Subtle)
289 .shape(IconButtonShape::Square)
290 .when(self.selection_search_enabled, |button| {
291 button.style(ButtonStyle::Filled)
292 })
293 .on_click(cx.listener(|this, _: &ClickEvent, cx| {
294 this.toggle_selection(&ToggleSelection, cx);
295 }))
296 .selected(self.selection_search_enabled)
297 .tooltip({
298 let focus_handle = focus_handle.clone();
299 move |cx| {
300 Tooltip::for_action_in(
301 "Toggle Search Selection",
302 &ToggleSelection,
303 &focus_handle,
304 cx,
305 )
306 }
307 }),
308 )
309 })
310 .child(
311 IconButton::new("select-all", ui::IconName::SelectAll)
312 .on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone()))
313 .shape(IconButtonShape::Square)
314 .tooltip({
315 let focus_handle = focus_handle.clone();
316 move |cx| {
317 Tooltip::for_action_in(
318 "Select All Matches",
319 &SelectAllMatches,
320 &focus_handle,
321 cx,
322 )
323 }
324 }),
325 )
326 .child(render_nav_button(
327 ui::IconName::ChevronLeft,
328 self.active_match_index.is_some(),
329 "Select Previous Match",
330 &SelectPrevMatch,
331 focus_handle.clone(),
332 ))
333 .child(render_nav_button(
334 ui::IconName::ChevronRight,
335 self.active_match_index.is_some(),
336 "Select Next Match",
337 &SelectNextMatch,
338 focus_handle.clone(),
339 ))
340 .when(!narrow_mode, |this| {
341 this.child(h_flex().ml_2().min_w(rems_from_px(40.)).child(
342 Label::new(match_text).size(LabelSize::Small).color(
343 if self.active_match_index.is_some() {
344 Color::Default
345 } else {
346 Color::Disabled
347 },
348 ),
349 ))
350 }),
351 );
352
353 let replace_line = should_show_replace_input.then(|| {
354 h_flex()
355 .gap_2()
356 .flex_1()
357 .child(
358 h_flex()
359 .flex_1()
360 // We're giving this a fixed height to match the height of the search input,
361 // which has an icon inside that is increasing its height.
362 .h_8()
363 .px_2()
364 .py_1()
365 .border_1()
366 .border_color(cx.theme().colors().border)
367 .rounded_lg()
368 .min_w(rems(MIN_INPUT_WIDTH_REMS))
369 .max_w(rems(MAX_INPUT_WIDTH_REMS))
370 .child(self.render_text_input(
371 &self.replacement_editor,
372 cx.theme().colors().text,
373 cx,
374 )),
375 )
376 .child(
377 h_flex()
378 .flex_none()
379 .gap_0p5()
380 .child(
381 IconButton::new("search-replace-next", ui::IconName::ReplaceNext)
382 .shape(IconButtonShape::Square)
383 .tooltip({
384 let focus_handle = focus_handle.clone();
385 move |cx| {
386 Tooltip::for_action_in(
387 "Replace Next Match",
388 &ReplaceNext,
389 &focus_handle,
390 cx,
391 )
392 }
393 })
394 .on_click(
395 cx.listener(|this, _, cx| this.replace_next(&ReplaceNext, cx)),
396 ),
397 )
398 .child(
399 IconButton::new("search-replace-all", ui::IconName::ReplaceAll)
400 .shape(IconButtonShape::Square)
401 .tooltip({
402 let focus_handle = focus_handle.clone();
403 move |cx| {
404 Tooltip::for_action_in(
405 "Replace All Matches",
406 &ReplaceAll,
407 &focus_handle,
408 cx,
409 )
410 }
411 })
412 .on_click(
413 cx.listener(|this, _, cx| this.replace_all(&ReplaceAll, cx)),
414 ),
415 ),
416 )
417 });
418
419 v_flex()
420 .id("buffer_search")
421 .track_scroll(&self.scroll_handle)
422 .key_context(key_context)
423 .capture_action(cx.listener(Self::tab))
424 .capture_action(cx.listener(Self::tab_prev))
425 .on_action(cx.listener(Self::previous_history_query))
426 .on_action(cx.listener(Self::next_history_query))
427 .on_action(cx.listener(Self::dismiss))
428 .on_action(cx.listener(Self::select_next_match))
429 .on_action(cx.listener(Self::select_prev_match))
430 .when(self.supported_options().replacement, |this| {
431 this.on_action(cx.listener(Self::toggle_replace))
432 .when(in_replace, |this| {
433 this.on_action(cx.listener(Self::replace_next))
434 .on_action(cx.listener(Self::replace_all))
435 })
436 })
437 .when(self.supported_options().case, |this| {
438 this.on_action(cx.listener(Self::toggle_case_sensitive))
439 })
440 .when(self.supported_options().word, |this| {
441 this.on_action(cx.listener(Self::toggle_whole_word))
442 })
443 .when(self.supported_options().regex, |this| {
444 this.on_action(cx.listener(Self::toggle_regex))
445 })
446 .when(self.supported_options().selection, |this| {
447 this.on_action(cx.listener(Self::toggle_selection))
448 })
449 .gap_2()
450 .child(
451 h_flex()
452 .child(search_line.w_full())
453 .when(!narrow_mode, |div| {
454 div.child(
455 IconButton::new(SharedString::from("Close"), IconName::Close)
456 .shape(IconButtonShape::Square)
457 .tooltip(move |cx| {
458 Tooltip::for_action("Close Search Bar", &Dismiss, cx)
459 })
460 .on_click(cx.listener(|this, _: &ClickEvent, cx| {
461 this.dismiss(&Dismiss, cx)
462 })),
463 )
464 }),
465 )
466 .children(replace_line)
467 }
468}
469
470impl FocusableView for BufferSearchBar {
471 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
472 self.query_editor.focus_handle(cx)
473 }
474}
475
476impl ToolbarItemView for BufferSearchBar {
477 fn set_active_pane_item(
478 &mut self,
479 item: Option<&dyn ItemHandle>,
480 cx: &mut ViewContext<Self>,
481 ) -> ToolbarItemLocation {
482 cx.notify();
483 self.active_searchable_item_subscription.take();
484 self.active_searchable_item.take();
485
486 self.pending_search.take();
487
488 if let Some(searchable_item_handle) =
489 item.and_then(|item| item.to_searchable_item_handle(cx))
490 {
491 let this = cx.view().downgrade();
492
493 self.active_searchable_item_subscription =
494 Some(searchable_item_handle.subscribe_to_search_events(
495 cx,
496 Box::new(move |search_event, cx| {
497 if let Some(this) = this.upgrade() {
498 this.update(cx, |this, cx| {
499 this.on_active_searchable_item_event(search_event, cx)
500 });
501 }
502 }),
503 ));
504
505 self.active_searchable_item = Some(searchable_item_handle);
506 drop(self.update_matches(true, cx));
507 if !self.dismissed {
508 return ToolbarItemLocation::Secondary;
509 }
510 }
511 ToolbarItemLocation::Hidden
512 }
513}
514
515impl BufferSearchBar {
516 pub fn register(registrar: &mut impl SearchActionsRegistrar) {
517 registrar.register_handler(ForDeployed(|this, _: &FocusSearch, cx| {
518 this.query_editor.focus_handle(cx).focus(cx);
519 this.select_query(cx);
520 }));
521 registrar.register_handler(ForDeployed(|this, action: &ToggleCaseSensitive, cx| {
522 if this.supported_options().case {
523 this.toggle_case_sensitive(action, cx);
524 }
525 }));
526 registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, cx| {
527 if this.supported_options().word {
528 this.toggle_whole_word(action, cx);
529 }
530 }));
531 registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, cx| {
532 if this.supported_options().selection {
533 this.toggle_selection(action, cx);
534 }
535 }));
536 registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, cx| {
537 if this.supported_options().replacement {
538 this.toggle_replace(action, cx);
539 }
540 }));
541 registrar.register_handler(WithResults(|this, action: &SelectNextMatch, cx| {
542 this.select_next_match(action, cx);
543 }));
544 registrar.register_handler(WithResults(|this, action: &SelectPrevMatch, cx| {
545 this.select_prev_match(action, cx);
546 }));
547 registrar.register_handler(WithResults(|this, action: &SelectAllMatches, cx| {
548 this.select_all_matches(action, cx);
549 }));
550 registrar.register_handler(ForDeployed(|this, _: &editor::actions::Cancel, cx| {
551 this.dismiss(&Dismiss, cx);
552 }));
553 registrar.register_handler(ForDeployed(|this, _: &Dismiss, cx| {
554 this.dismiss(&Dismiss, cx);
555 }));
556
557 // register deploy buffer search for both search bar states, since we want to focus into the search bar
558 // when the deploy action is triggered in the buffer.
559 registrar.register_handler(ForDeployed(|this, deploy, cx| {
560 this.deploy(deploy, cx);
561 }));
562 registrar.register_handler(ForDismissed(|this, deploy, cx| {
563 this.deploy(deploy, cx);
564 }))
565 }
566
567 pub fn new(cx: &mut ViewContext<Self>) -> Self {
568 let query_editor = cx.new_view(Editor::single_line);
569 cx.subscribe(&query_editor, Self::on_query_editor_event)
570 .detach();
571 let replacement_editor = cx.new_view(Editor::single_line);
572 cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
573 .detach();
574
575 let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
576
577 Self {
578 query_editor,
579 query_editor_focused: false,
580 replacement_editor,
581 replacement_editor_focused: false,
582 active_searchable_item: None,
583 active_searchable_item_subscription: None,
584 active_match_index: None,
585 searchable_items_with_matches: Default::default(),
586 default_options: search_options,
587 configured_options: search_options,
588 search_options,
589 pending_search: None,
590 query_contains_error: false,
591 dismissed: true,
592 search_history: SearchHistory::new(
593 Some(MAX_BUFFER_SEARCH_HISTORY_SIZE),
594 project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
595 ),
596 search_history_cursor: Default::default(),
597 active_search: None,
598 replace_enabled: false,
599 selection_search_enabled: false,
600 scroll_handle: ScrollHandle::new(),
601 editor_scroll_handle: ScrollHandle::new(),
602 editor_needed_width: px(0.),
603 }
604 }
605
606 pub fn is_dismissed(&self) -> bool {
607 self.dismissed
608 }
609
610 pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
611 self.dismissed = true;
612 for searchable_item in self.searchable_items_with_matches.keys() {
613 if let Some(searchable_item) =
614 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
615 {
616 searchable_item.clear_matches(cx);
617 }
618 }
619 if let Some(active_editor) = self.active_searchable_item.as_mut() {
620 self.selection_search_enabled = false;
621 self.replace_enabled = false;
622 active_editor.search_bar_visibility_changed(false, cx);
623 active_editor.toggle_filtered_search_ranges(false, cx);
624 let handle = active_editor.focus_handle(cx);
625 self.focus(&handle, cx);
626 }
627 cx.emit(Event::UpdateLocation);
628 cx.emit(ToolbarItemEvent::ChangeLocation(
629 ToolbarItemLocation::Hidden,
630 ));
631 cx.notify();
632 }
633
634 pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
635 if self.show(cx) {
636 if let Some(active_item) = self.active_searchable_item.as_mut() {
637 active_item.toggle_filtered_search_ranges(deploy.selection_search_enabled, cx);
638 }
639 self.search_suggested(cx);
640 self.smartcase(cx);
641 self.replace_enabled = deploy.replace_enabled;
642 self.selection_search_enabled = deploy.selection_search_enabled;
643 if deploy.focus {
644 let mut handle = self.query_editor.focus_handle(cx).clone();
645 let mut select_query = true;
646 if deploy.replace_enabled && handle.is_focused(cx) {
647 handle = self.replacement_editor.focus_handle(cx).clone();
648 select_query = false;
649 };
650
651 if select_query {
652 self.select_query(cx);
653 }
654
655 cx.focus(&handle);
656 }
657 return true;
658 }
659
660 cx.propagate();
661 false
662 }
663
664 pub fn toggle(&mut self, action: &Deploy, cx: &mut ViewContext<Self>) {
665 if self.is_dismissed() {
666 self.deploy(action, cx);
667 } else {
668 self.dismiss(&Dismiss, cx);
669 }
670 }
671
672 pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
673 let Some(handle) = self.active_searchable_item.as_ref() else {
674 return false;
675 };
676
677 self.configured_options =
678 SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
679 if self.dismissed && self.configured_options != self.default_options {
680 self.search_options = self.configured_options;
681 self.default_options = self.configured_options;
682 }
683
684 self.dismissed = false;
685 handle.search_bar_visibility_changed(true, cx);
686 cx.notify();
687 cx.emit(Event::UpdateLocation);
688 cx.emit(ToolbarItemEvent::ChangeLocation(
689 ToolbarItemLocation::Secondary,
690 ));
691 true
692 }
693
694 fn supported_options(&self) -> workspace::searchable::SearchOptions {
695 self.active_searchable_item
696 .as_deref()
697 .map(SearchableItemHandle::supported_options)
698 .unwrap_or_default()
699 }
700
701 pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
702 let search = self
703 .query_suggestion(cx)
704 .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
705
706 if let Some(search) = search {
707 cx.spawn(|this, mut cx| async move {
708 search.await?;
709 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
710 })
711 .detach_and_log_err(cx);
712 }
713 }
714
715 pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
716 if let Some(match_ix) = self.active_match_index {
717 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
718 if let Some(matches) = self
719 .searchable_items_with_matches
720 .get(&active_searchable_item.downgrade())
721 {
722 active_searchable_item.activate_match(match_ix, matches, cx)
723 }
724 }
725 }
726 }
727
728 pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
729 self.query_editor.update(cx, |query_editor, cx| {
730 query_editor.select_all(&Default::default(), cx);
731 });
732 }
733
734 pub fn query(&self, cx: &WindowContext) -> String {
735 self.query_editor.read(cx).text(cx)
736 }
737 pub fn replacement(&self, cx: &WindowContext) -> String {
738 self.replacement_editor.read(cx).text(cx)
739 }
740 pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
741 self.active_searchable_item
742 .as_ref()
743 .map(|searchable_item| searchable_item.query_suggestion(cx))
744 .filter(|suggestion| !suggestion.is_empty())
745 }
746
747 pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
748 if replacement.is_none() {
749 self.replace_enabled = false;
750 return;
751 }
752 self.replace_enabled = true;
753 self.replacement_editor
754 .update(cx, |replacement_editor, cx| {
755 replacement_editor
756 .buffer()
757 .update(cx, |replacement_buffer, cx| {
758 let len = replacement_buffer.len(cx);
759 replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
760 });
761 });
762 }
763
764 pub fn search(
765 &mut self,
766 query: &str,
767 options: Option<SearchOptions>,
768 cx: &mut ViewContext<Self>,
769 ) -> oneshot::Receiver<()> {
770 let options = options.unwrap_or(self.default_options);
771 let updated = query != self.query(cx) || self.search_options != options;
772 if updated {
773 self.query_editor.update(cx, |query_editor, cx| {
774 query_editor.buffer().update(cx, |query_buffer, cx| {
775 let len = query_buffer.len(cx);
776 query_buffer.edit([(0..len, query)], None, cx);
777 });
778 });
779 self.search_options = options;
780 self.clear_matches(cx);
781 cx.notify();
782 }
783 self.update_matches(!updated, cx)
784 }
785
786 fn render_search_option_button(
787 &self,
788 option: SearchOptions,
789 focus_handle: FocusHandle,
790 action: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
791 ) -> impl IntoElement {
792 let is_active = self.search_options.contains(option);
793 option.as_button(is_active, focus_handle, action)
794 }
795
796 pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
797 if let Some(active_editor) = self.active_searchable_item.as_ref() {
798 let handle = active_editor.focus_handle(cx);
799 cx.focus(&handle);
800 }
801 }
802
803 pub fn toggle_search_option(
804 &mut self,
805 search_option: SearchOptions,
806 cx: &mut ViewContext<Self>,
807 ) {
808 self.search_options.toggle(search_option);
809 self.default_options = self.search_options;
810 drop(self.update_matches(false, cx));
811 cx.notify();
812 }
813
814 pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
815 self.search_options.contains(search_option)
816 }
817
818 pub fn enable_search_option(
819 &mut self,
820 search_option: SearchOptions,
821 cx: &mut ViewContext<Self>,
822 ) {
823 if !self.search_options.contains(search_option) {
824 self.toggle_search_option(search_option, cx)
825 }
826 }
827
828 pub fn set_search_options(
829 &mut self,
830 search_options: SearchOptions,
831 cx: &mut ViewContext<Self>,
832 ) {
833 self.search_options = search_options;
834 cx.notify();
835 }
836
837 fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
838 self.select_match(Direction::Next, 1, cx);
839 }
840
841 fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
842 self.select_match(Direction::Prev, 1, cx);
843 }
844
845 fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
846 if !self.dismissed && self.active_match_index.is_some() {
847 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
848 if let Some(matches) = self
849 .searchable_items_with_matches
850 .get(&searchable_item.downgrade())
851 {
852 searchable_item.select_matches(matches, cx);
853 self.focus_editor(&FocusEditor, cx);
854 }
855 }
856 }
857 }
858
859 pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
860 if let Some(index) = self.active_match_index {
861 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
862 if let Some(matches) = self
863 .searchable_items_with_matches
864 .get(&searchable_item.downgrade())
865 .filter(|matches| !matches.is_empty())
866 {
867 // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
868 if !EditorSettings::get_global(cx).search_wrap
869 && ((direction == Direction::Next && index + count >= matches.len())
870 || (direction == Direction::Prev && index < count))
871 {
872 crate::show_no_more_matches(cx);
873 return;
874 }
875 let new_match_index = searchable_item
876 .match_index_for_direction(matches, index, direction, count, cx);
877
878 searchable_item.update_matches(matches, cx);
879 searchable_item.activate_match(new_match_index, matches, cx);
880 }
881 }
882 }
883 }
884
885 pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
886 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
887 if let Some(matches) = self
888 .searchable_items_with_matches
889 .get(&searchable_item.downgrade())
890 {
891 if matches.is_empty() {
892 return;
893 }
894 let new_match_index = matches.len() - 1;
895 searchable_item.update_matches(matches, cx);
896 searchable_item.activate_match(new_match_index, matches, cx);
897 }
898 }
899 }
900
901 fn on_query_editor_event(
902 &mut self,
903 editor: View<Editor>,
904 event: &editor::EditorEvent,
905 cx: &mut ViewContext<Self>,
906 ) {
907 match event {
908 editor::EditorEvent::Focused => self.query_editor_focused = true,
909 editor::EditorEvent::Blurred => self.query_editor_focused = false,
910 editor::EditorEvent::Edited { .. } => {
911 self.smartcase(cx);
912 self.clear_matches(cx);
913 let search = self.update_matches(false, cx);
914
915 let width = editor.update(cx, |editor, cx| {
916 let text_layout_details = editor.text_layout_details(cx);
917 let snapshot = editor.snapshot(cx).display_snapshot;
918
919 snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
920 - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
921 });
922 self.editor_needed_width = width;
923 cx.notify();
924
925 cx.spawn(|this, mut cx| async move {
926 search.await?;
927 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
928 })
929 .detach_and_log_err(cx);
930 }
931 _ => {}
932 }
933 }
934
935 fn on_replacement_editor_event(
936 &mut self,
937 _: View<Editor>,
938 event: &editor::EditorEvent,
939 _: &mut ViewContext<Self>,
940 ) {
941 match event {
942 editor::EditorEvent::Focused => self.replacement_editor_focused = true,
943 editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
944 _ => {}
945 }
946 }
947
948 fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext<Self>) {
949 match event {
950 SearchEvent::MatchesInvalidated => {
951 drop(self.update_matches(false, cx));
952 }
953 SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
954 }
955 }
956
957 fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext<Self>) {
958 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
959 }
960
961 fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext<Self>) {
962 self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
963 }
964
965 fn toggle_selection(&mut self, _: &ToggleSelection, cx: &mut ViewContext<Self>) {
966 if let Some(active_item) = self.active_searchable_item.as_mut() {
967 self.selection_search_enabled = !self.selection_search_enabled;
968 active_item.toggle_filtered_search_ranges(self.selection_search_enabled, cx);
969 drop(self.update_matches(false, cx));
970 cx.notify();
971 }
972 }
973
974 fn toggle_regex(&mut self, _: &ToggleRegex, cx: &mut ViewContext<Self>) {
975 self.toggle_search_option(SearchOptions::REGEX, cx)
976 }
977
978 fn clear_active_searchable_item_matches(&mut self, cx: &mut WindowContext) {
979 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
980 self.active_match_index = None;
981 self.searchable_items_with_matches
982 .remove(&active_searchable_item.downgrade());
983 active_searchable_item.clear_matches(cx);
984 }
985 }
986
987 pub fn has_active_match(&self) -> bool {
988 self.active_match_index.is_some()
989 }
990
991 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
992 let mut active_item_matches = None;
993 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
994 if let Some(searchable_item) =
995 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
996 {
997 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
998 active_item_matches = Some((searchable_item.downgrade(), matches));
999 } else {
1000 searchable_item.clear_matches(cx);
1001 }
1002 }
1003 }
1004
1005 self.searchable_items_with_matches
1006 .extend(active_item_matches);
1007 }
1008
1009 fn update_matches(
1010 &mut self,
1011 reuse_existing_query: bool,
1012 cx: &mut ViewContext<Self>,
1013 ) -> oneshot::Receiver<()> {
1014 let (done_tx, done_rx) = oneshot::channel();
1015 let query = self.query(cx);
1016 self.pending_search.take();
1017
1018 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1019 self.query_contains_error = false;
1020 if query.is_empty() {
1021 self.clear_active_searchable_item_matches(cx);
1022 let _ = done_tx.send(());
1023 cx.notify();
1024 } else {
1025 let query: Arc<_> = if let Some(search) =
1026 self.active_search.take().filter(|_| reuse_existing_query)
1027 {
1028 search
1029 } else {
1030 if self.search_options.contains(SearchOptions::REGEX) {
1031 match SearchQuery::regex(
1032 query,
1033 self.search_options.contains(SearchOptions::WHOLE_WORD),
1034 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1035 false,
1036 Default::default(),
1037 Default::default(),
1038 None,
1039 ) {
1040 Ok(query) => query.with_replacement(self.replacement(cx)),
1041 Err(_) => {
1042 self.query_contains_error = true;
1043 self.clear_active_searchable_item_matches(cx);
1044 cx.notify();
1045 return done_rx;
1046 }
1047 }
1048 } else {
1049 match SearchQuery::text(
1050 query,
1051 self.search_options.contains(SearchOptions::WHOLE_WORD),
1052 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1053 false,
1054 Default::default(),
1055 Default::default(),
1056 None,
1057 ) {
1058 Ok(query) => query.with_replacement(self.replacement(cx)),
1059 Err(_) => {
1060 self.query_contains_error = true;
1061 self.clear_active_searchable_item_matches(cx);
1062 cx.notify();
1063 return done_rx;
1064 }
1065 }
1066 }
1067 .into()
1068 };
1069
1070 self.active_search = Some(query.clone());
1071 let query_text = query.as_str().to_string();
1072
1073 let matches = active_searchable_item.find_matches(query, cx);
1074
1075 let active_searchable_item = active_searchable_item.downgrade();
1076 self.pending_search = Some(cx.spawn(|this, mut cx| async move {
1077 let matches = matches.await;
1078
1079 this.update(&mut cx, |this, cx| {
1080 if let Some(active_searchable_item) =
1081 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1082 {
1083 this.searchable_items_with_matches
1084 .insert(active_searchable_item.downgrade(), matches);
1085
1086 this.update_match_index(cx);
1087 this.search_history
1088 .add(&mut this.search_history_cursor, query_text);
1089 if !this.dismissed {
1090 let matches = this
1091 .searchable_items_with_matches
1092 .get(&active_searchable_item.downgrade())
1093 .unwrap();
1094 if matches.is_empty() {
1095 active_searchable_item.clear_matches(cx);
1096 } else {
1097 active_searchable_item.update_matches(matches, cx);
1098 }
1099 let _ = done_tx.send(());
1100 }
1101 cx.notify();
1102 }
1103 })
1104 .log_err();
1105 }));
1106 }
1107 }
1108 done_rx
1109 }
1110
1111 pub fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
1112 let new_index = self
1113 .active_searchable_item
1114 .as_ref()
1115 .and_then(|searchable_item| {
1116 let matches = self
1117 .searchable_items_with_matches
1118 .get(&searchable_item.downgrade())?;
1119 searchable_item.active_match_index(matches, cx)
1120 });
1121 if new_index != self.active_match_index {
1122 self.active_match_index = new_index;
1123 cx.notify();
1124 }
1125 }
1126
1127 fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
1128 // Search -> Replace -> Editor
1129 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1130 self.replacement_editor.focus_handle(cx)
1131 } else if let Some(item) = self.active_searchable_item.as_ref() {
1132 item.focus_handle(cx)
1133 } else {
1134 return;
1135 };
1136 self.focus(&focus_handle, cx);
1137 cx.stop_propagation();
1138 }
1139
1140 fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext<Self>) {
1141 // Search -> Replace -> Search
1142 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1143 self.replacement_editor.focus_handle(cx)
1144 } else if self.replacement_editor_focused {
1145 self.query_editor.focus_handle(cx)
1146 } else {
1147 return;
1148 };
1149 self.focus(&focus_handle, cx);
1150 cx.stop_propagation();
1151 }
1152
1153 fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
1154 if let Some(new_query) = self
1155 .search_history
1156 .next(&mut self.search_history_cursor)
1157 .map(str::to_string)
1158 {
1159 drop(self.search(&new_query, Some(self.search_options), cx));
1160 } else {
1161 self.search_history_cursor.reset();
1162 drop(self.search("", Some(self.search_options), cx));
1163 }
1164 }
1165
1166 fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
1167 if self.query(cx).is_empty() {
1168 if let Some(new_query) = self
1169 .search_history
1170 .current(&mut self.search_history_cursor)
1171 .map(str::to_string)
1172 {
1173 drop(self.search(&new_query, Some(self.search_options), cx));
1174 return;
1175 }
1176 }
1177
1178 if let Some(new_query) = self
1179 .search_history
1180 .previous(&mut self.search_history_cursor)
1181 .map(str::to_string)
1182 {
1183 drop(self.search(&new_query, Some(self.search_options), cx));
1184 }
1185 }
1186
1187 fn focus(&self, handle: &gpui::FocusHandle, cx: &mut ViewContext<Self>) {
1188 cx.on_next_frame(|_, cx| {
1189 cx.invalidate_character_coordinates();
1190 });
1191 cx.focus(handle);
1192 }
1193
1194 fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
1195 if self.active_searchable_item.is_some() {
1196 self.replace_enabled = !self.replace_enabled;
1197 let handle = if self.replace_enabled {
1198 self.replacement_editor.focus_handle(cx)
1199 } else {
1200 self.query_editor.focus_handle(cx)
1201 };
1202 self.focus(&handle, cx);
1203 cx.notify();
1204 }
1205 }
1206
1207 fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
1208 let mut should_propagate = true;
1209 if !self.dismissed && self.active_search.is_some() {
1210 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1211 if let Some(query) = self.active_search.as_ref() {
1212 if let Some(matches) = self
1213 .searchable_items_with_matches
1214 .get(&searchable_item.downgrade())
1215 {
1216 if let Some(active_index) = self.active_match_index {
1217 let query = query
1218 .as_ref()
1219 .clone()
1220 .with_replacement(self.replacement(cx));
1221 searchable_item.replace(matches.at(active_index), &query, cx);
1222 self.select_next_match(&SelectNextMatch, cx);
1223 }
1224 should_propagate = false;
1225 self.focus_editor(&FocusEditor, cx);
1226 }
1227 }
1228 }
1229 }
1230 if !should_propagate {
1231 cx.stop_propagation();
1232 }
1233 }
1234
1235 pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
1236 if !self.dismissed && self.active_search.is_some() {
1237 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1238 if let Some(query) = self.active_search.as_ref() {
1239 if let Some(matches) = self
1240 .searchable_items_with_matches
1241 .get(&searchable_item.downgrade())
1242 {
1243 let query = query
1244 .as_ref()
1245 .clone()
1246 .with_replacement(self.replacement(cx));
1247 searchable_item.replace_all(&mut matches.iter(), &query, cx);
1248 }
1249 }
1250 }
1251 }
1252 }
1253
1254 pub fn match_exists(&mut self, cx: &mut ViewContext<Self>) -> bool {
1255 self.update_match_index(cx);
1256 self.active_match_index.is_some()
1257 }
1258
1259 pub fn should_use_smartcase_search(&mut self, cx: &mut ViewContext<Self>) -> bool {
1260 EditorSettings::get_global(cx).use_smartcase_search
1261 }
1262
1263 pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1264 str.chars().any(|c| c.is_uppercase())
1265 }
1266
1267 fn smartcase(&mut self, cx: &mut ViewContext<Self>) {
1268 if self.should_use_smartcase_search(cx) {
1269 let query = self.query(cx);
1270 if !query.is_empty() {
1271 let is_case = self.is_contains_uppercase(&query);
1272 if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1273 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1274 }
1275 }
1276 }
1277 }
1278}
1279
1280#[cfg(test)]
1281mod tests {
1282 use std::ops::Range;
1283
1284 use super::*;
1285 use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer, SearchSettings};
1286 use gpui::{Context, Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1287 use language::{Buffer, Point};
1288 use project::Project;
1289 use settings::SettingsStore;
1290 use smol::stream::StreamExt as _;
1291 use unindent::Unindent as _;
1292
1293 fn init_globals(cx: &mut TestAppContext) {
1294 cx.update(|cx| {
1295 let store = settings::SettingsStore::test(cx);
1296 cx.set_global(store);
1297 editor::init(cx);
1298
1299 language::init(cx);
1300 Project::init_settings(cx);
1301 theme::init(theme::LoadThemes::JustBase, cx);
1302 crate::init(cx);
1303 });
1304 }
1305
1306 fn init_test(
1307 cx: &mut TestAppContext,
1308 ) -> (View<Editor>, View<BufferSearchBar>, &mut VisualTestContext) {
1309 init_globals(cx);
1310 let buffer = cx.new_model(|cx| {
1311 Buffer::local(
1312 r#"
1313 A regular expression (shortened as regex or regexp;[1] also referred to as
1314 rational expression[2][3]) is a sequence of characters that specifies a search
1315 pattern in text. Usually such patterns are used by string-searching algorithms
1316 for "find" or "find and replace" operations on strings, or for input validation.
1317 "#
1318 .unindent(),
1319 cx,
1320 )
1321 });
1322 let cx = cx.add_empty_window();
1323 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1324
1325 let search_bar = cx.new_view(|cx| {
1326 let mut search_bar = BufferSearchBar::new(cx);
1327 search_bar.set_active_pane_item(Some(&editor), cx);
1328 search_bar.show(cx);
1329 search_bar
1330 });
1331
1332 (editor, search_bar, cx)
1333 }
1334
1335 #[gpui::test]
1336 async fn test_search_simple(cx: &mut TestAppContext) {
1337 let (editor, search_bar, cx) = init_test(cx);
1338 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1339 background_highlights
1340 .into_iter()
1341 .map(|(range, _)| range)
1342 .collect::<Vec<_>>()
1343 };
1344 // Search for a string that appears with different casing.
1345 // By default, search is case-insensitive.
1346 search_bar
1347 .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
1348 .await
1349 .unwrap();
1350 editor.update(cx, |editor, cx| {
1351 assert_eq!(
1352 display_points_of(editor.all_text_background_highlights(cx)),
1353 &[
1354 DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1355 DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1356 ]
1357 );
1358 });
1359
1360 // Switch to a case sensitive search.
1361 search_bar.update(cx, |search_bar, cx| {
1362 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1363 });
1364 let mut editor_notifications = cx.notifications(&editor);
1365 editor_notifications.next().await;
1366 editor.update(cx, |editor, cx| {
1367 assert_eq!(
1368 display_points_of(editor.all_text_background_highlights(cx)),
1369 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1370 );
1371 });
1372
1373 // Search for a string that appears both as a whole word and
1374 // within other words. By default, all results are found.
1375 search_bar
1376 .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1377 .await
1378 .unwrap();
1379 editor.update(cx, |editor, cx| {
1380 assert_eq!(
1381 display_points_of(editor.all_text_background_highlights(cx)),
1382 &[
1383 DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1384 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1385 DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1386 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1387 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1388 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1389 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1390 ]
1391 );
1392 });
1393
1394 // Switch to a whole word search.
1395 search_bar.update(cx, |search_bar, cx| {
1396 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1397 });
1398 let mut editor_notifications = cx.notifications(&editor);
1399 editor_notifications.next().await;
1400 editor.update(cx, |editor, cx| {
1401 assert_eq!(
1402 display_points_of(editor.all_text_background_highlights(cx)),
1403 &[
1404 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1405 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1406 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1407 ]
1408 );
1409 });
1410
1411 editor.update(cx, |editor, cx| {
1412 editor.change_selections(None, cx, |s| {
1413 s.select_display_ranges([
1414 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1415 ])
1416 });
1417 });
1418 search_bar.update(cx, |search_bar, cx| {
1419 assert_eq!(search_bar.active_match_index, Some(0));
1420 search_bar.select_next_match(&SelectNextMatch, cx);
1421 assert_eq!(
1422 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1423 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1424 );
1425 });
1426 search_bar.update(cx, |search_bar, _| {
1427 assert_eq!(search_bar.active_match_index, Some(0));
1428 });
1429
1430 search_bar.update(cx, |search_bar, cx| {
1431 search_bar.select_next_match(&SelectNextMatch, cx);
1432 assert_eq!(
1433 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1434 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1435 );
1436 });
1437 search_bar.update(cx, |search_bar, _| {
1438 assert_eq!(search_bar.active_match_index, Some(1));
1439 });
1440
1441 search_bar.update(cx, |search_bar, cx| {
1442 search_bar.select_next_match(&SelectNextMatch, cx);
1443 assert_eq!(
1444 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1445 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1446 );
1447 });
1448 search_bar.update(cx, |search_bar, _| {
1449 assert_eq!(search_bar.active_match_index, Some(2));
1450 });
1451
1452 search_bar.update(cx, |search_bar, cx| {
1453 search_bar.select_next_match(&SelectNextMatch, cx);
1454 assert_eq!(
1455 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1456 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1457 );
1458 });
1459 search_bar.update(cx, |search_bar, _| {
1460 assert_eq!(search_bar.active_match_index, Some(0));
1461 });
1462
1463 search_bar.update(cx, |search_bar, cx| {
1464 search_bar.select_prev_match(&SelectPrevMatch, cx);
1465 assert_eq!(
1466 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1467 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1468 );
1469 });
1470 search_bar.update(cx, |search_bar, _| {
1471 assert_eq!(search_bar.active_match_index, Some(2));
1472 });
1473
1474 search_bar.update(cx, |search_bar, cx| {
1475 search_bar.select_prev_match(&SelectPrevMatch, cx);
1476 assert_eq!(
1477 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1478 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1479 );
1480 });
1481 search_bar.update(cx, |search_bar, _| {
1482 assert_eq!(search_bar.active_match_index, Some(1));
1483 });
1484
1485 search_bar.update(cx, |search_bar, cx| {
1486 search_bar.select_prev_match(&SelectPrevMatch, cx);
1487 assert_eq!(
1488 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1489 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1490 );
1491 });
1492 search_bar.update(cx, |search_bar, _| {
1493 assert_eq!(search_bar.active_match_index, Some(0));
1494 });
1495
1496 // Park the cursor in between matches and ensure that going to the previous match selects
1497 // the closest match to the left.
1498 editor.update(cx, |editor, cx| {
1499 editor.change_selections(None, cx, |s| {
1500 s.select_display_ranges([
1501 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1502 ])
1503 });
1504 });
1505 search_bar.update(cx, |search_bar, cx| {
1506 assert_eq!(search_bar.active_match_index, Some(1));
1507 search_bar.select_prev_match(&SelectPrevMatch, cx);
1508 assert_eq!(
1509 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1510 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1511 );
1512 });
1513 search_bar.update(cx, |search_bar, _| {
1514 assert_eq!(search_bar.active_match_index, Some(0));
1515 });
1516
1517 // Park the cursor in between matches and ensure that going to the next match selects the
1518 // closest match to the right.
1519 editor.update(cx, |editor, cx| {
1520 editor.change_selections(None, cx, |s| {
1521 s.select_display_ranges([
1522 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1523 ])
1524 });
1525 });
1526 search_bar.update(cx, |search_bar, cx| {
1527 assert_eq!(search_bar.active_match_index, Some(1));
1528 search_bar.select_next_match(&SelectNextMatch, cx);
1529 assert_eq!(
1530 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1531 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1532 );
1533 });
1534 search_bar.update(cx, |search_bar, _| {
1535 assert_eq!(search_bar.active_match_index, Some(1));
1536 });
1537
1538 // Park the cursor after the last match and ensure that going to the previous match selects
1539 // the last match.
1540 editor.update(cx, |editor, cx| {
1541 editor.change_selections(None, cx, |s| {
1542 s.select_display_ranges([
1543 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1544 ])
1545 });
1546 });
1547 search_bar.update(cx, |search_bar, cx| {
1548 assert_eq!(search_bar.active_match_index, Some(2));
1549 search_bar.select_prev_match(&SelectPrevMatch, cx);
1550 assert_eq!(
1551 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1552 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1553 );
1554 });
1555 search_bar.update(cx, |search_bar, _| {
1556 assert_eq!(search_bar.active_match_index, Some(2));
1557 });
1558
1559 // Park the cursor after the last match and ensure that going to the next match selects the
1560 // first match.
1561 editor.update(cx, |editor, cx| {
1562 editor.change_selections(None, cx, |s| {
1563 s.select_display_ranges([
1564 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1565 ])
1566 });
1567 });
1568 search_bar.update(cx, |search_bar, cx| {
1569 assert_eq!(search_bar.active_match_index, Some(2));
1570 search_bar.select_next_match(&SelectNextMatch, cx);
1571 assert_eq!(
1572 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1573 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1574 );
1575 });
1576 search_bar.update(cx, |search_bar, _| {
1577 assert_eq!(search_bar.active_match_index, Some(0));
1578 });
1579
1580 // Park the cursor before the first match and ensure that going to the previous match
1581 // selects the last match.
1582 editor.update(cx, |editor, cx| {
1583 editor.change_selections(None, cx, |s| {
1584 s.select_display_ranges([
1585 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1586 ])
1587 });
1588 });
1589 search_bar.update(cx, |search_bar, cx| {
1590 assert_eq!(search_bar.active_match_index, Some(0));
1591 search_bar.select_prev_match(&SelectPrevMatch, cx);
1592 assert_eq!(
1593 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1594 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1595 );
1596 });
1597 search_bar.update(cx, |search_bar, _| {
1598 assert_eq!(search_bar.active_match_index, Some(2));
1599 });
1600 }
1601
1602 fn display_points_of(
1603 background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1604 ) -> Vec<Range<DisplayPoint>> {
1605 background_highlights
1606 .into_iter()
1607 .map(|(range, _)| range)
1608 .collect::<Vec<_>>()
1609 }
1610
1611 #[gpui::test]
1612 async fn test_search_option_handling(cx: &mut TestAppContext) {
1613 let (editor, search_bar, cx) = init_test(cx);
1614
1615 // show with options should make current search case sensitive
1616 search_bar
1617 .update(cx, |search_bar, cx| {
1618 search_bar.show(cx);
1619 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1620 })
1621 .await
1622 .unwrap();
1623 editor.update(cx, |editor, cx| {
1624 assert_eq!(
1625 display_points_of(editor.all_text_background_highlights(cx)),
1626 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1627 );
1628 });
1629
1630 // search_suggested should restore default options
1631 search_bar.update(cx, |search_bar, cx| {
1632 search_bar.search_suggested(cx);
1633 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1634 });
1635
1636 // toggling a search option should update the defaults
1637 search_bar
1638 .update(cx, |search_bar, cx| {
1639 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1640 })
1641 .await
1642 .unwrap();
1643 search_bar.update(cx, |search_bar, cx| {
1644 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1645 });
1646 let mut editor_notifications = cx.notifications(&editor);
1647 editor_notifications.next().await;
1648 editor.update(cx, |editor, cx| {
1649 assert_eq!(
1650 display_points_of(editor.all_text_background_highlights(cx)),
1651 &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1652 );
1653 });
1654
1655 // defaults should still include whole word
1656 search_bar.update(cx, |search_bar, cx| {
1657 search_bar.search_suggested(cx);
1658 assert_eq!(
1659 search_bar.search_options,
1660 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1661 )
1662 });
1663 }
1664
1665 #[gpui::test]
1666 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1667 init_globals(cx);
1668 let buffer_text = r#"
1669 A regular expression (shortened as regex or regexp;[1] also referred to as
1670 rational expression[2][3]) is a sequence of characters that specifies a search
1671 pattern in text. Usually such patterns are used by string-searching algorithms
1672 for "find" or "find and replace" operations on strings, or for input validation.
1673 "#
1674 .unindent();
1675 let expected_query_matches_count = buffer_text
1676 .chars()
1677 .filter(|c| c.to_ascii_lowercase() == 'a')
1678 .count();
1679 assert!(
1680 expected_query_matches_count > 1,
1681 "Should pick a query with multiple results"
1682 );
1683 let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1684 let window = cx.add_window(|_| gpui::Empty);
1685
1686 let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1687
1688 let search_bar = window.build_view(cx, |cx| {
1689 let mut search_bar = BufferSearchBar::new(cx);
1690 search_bar.set_active_pane_item(Some(&editor), cx);
1691 search_bar.show(cx);
1692 search_bar
1693 });
1694
1695 window
1696 .update(cx, |_, cx| {
1697 search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1698 })
1699 .unwrap()
1700 .await
1701 .unwrap();
1702 let initial_selections = window
1703 .update(cx, |_, cx| {
1704 search_bar.update(cx, |search_bar, cx| {
1705 let handle = search_bar.query_editor.focus_handle(cx);
1706 cx.focus(&handle);
1707 search_bar.activate_current_match(cx);
1708 });
1709 assert!(
1710 !editor.read(cx).is_focused(cx),
1711 "Initially, the editor should not be focused"
1712 );
1713 let initial_selections = editor.update(cx, |editor, cx| {
1714 let initial_selections = editor.selections.display_ranges(cx);
1715 assert_eq!(
1716 initial_selections.len(), 1,
1717 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1718 );
1719 initial_selections
1720 });
1721 search_bar.update(cx, |search_bar, cx| {
1722 assert_eq!(search_bar.active_match_index, Some(0));
1723 let handle = search_bar.query_editor.focus_handle(cx);
1724 cx.focus(&handle);
1725 search_bar.select_all_matches(&SelectAllMatches, cx);
1726 });
1727 assert!(
1728 editor.read(cx).is_focused(cx),
1729 "Should focus editor after successful SelectAllMatches"
1730 );
1731 search_bar.update(cx, |search_bar, cx| {
1732 let all_selections =
1733 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1734 assert_eq!(
1735 all_selections.len(),
1736 expected_query_matches_count,
1737 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1738 );
1739 assert_eq!(
1740 search_bar.active_match_index,
1741 Some(0),
1742 "Match index should not change after selecting all matches"
1743 );
1744 });
1745
1746 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
1747 initial_selections
1748 }).unwrap();
1749
1750 window
1751 .update(cx, |_, cx| {
1752 assert!(
1753 editor.read(cx).is_focused(cx),
1754 "Should still have editor focused after SelectNextMatch"
1755 );
1756 search_bar.update(cx, |search_bar, cx| {
1757 let all_selections =
1758 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1759 assert_eq!(
1760 all_selections.len(),
1761 1,
1762 "On next match, should deselect items and select the next match"
1763 );
1764 assert_ne!(
1765 all_selections, initial_selections,
1766 "Next match should be different from the first selection"
1767 );
1768 assert_eq!(
1769 search_bar.active_match_index,
1770 Some(1),
1771 "Match index should be updated to the next one"
1772 );
1773 let handle = search_bar.query_editor.focus_handle(cx);
1774 cx.focus(&handle);
1775 search_bar.select_all_matches(&SelectAllMatches, cx);
1776 });
1777 })
1778 .unwrap();
1779 window
1780 .update(cx, |_, cx| {
1781 assert!(
1782 editor.read(cx).is_focused(cx),
1783 "Should focus editor after successful SelectAllMatches"
1784 );
1785 search_bar.update(cx, |search_bar, cx| {
1786 let all_selections =
1787 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1788 assert_eq!(
1789 all_selections.len(),
1790 expected_query_matches_count,
1791 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1792 );
1793 assert_eq!(
1794 search_bar.active_match_index,
1795 Some(1),
1796 "Match index should not change after selecting all matches"
1797 );
1798 });
1799 search_bar.update(cx, |search_bar, cx| {
1800 search_bar.select_prev_match(&SelectPrevMatch, cx);
1801 });
1802 })
1803 .unwrap();
1804 let last_match_selections = window
1805 .update(cx, |_, cx| {
1806 assert!(
1807 editor.read(cx).is_focused(cx),
1808 "Should still have editor focused after SelectPrevMatch"
1809 );
1810
1811 search_bar.update(cx, |search_bar, cx| {
1812 let all_selections =
1813 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1814 assert_eq!(
1815 all_selections.len(),
1816 1,
1817 "On previous match, should deselect items and select the previous item"
1818 );
1819 assert_eq!(
1820 all_selections, initial_selections,
1821 "Previous match should be the same as the first selection"
1822 );
1823 assert_eq!(
1824 search_bar.active_match_index,
1825 Some(0),
1826 "Match index should be updated to the previous one"
1827 );
1828 all_selections
1829 })
1830 })
1831 .unwrap();
1832
1833 window
1834 .update(cx, |_, cx| {
1835 search_bar.update(cx, |search_bar, cx| {
1836 let handle = search_bar.query_editor.focus_handle(cx);
1837 cx.focus(&handle);
1838 search_bar.search("abas_nonexistent_match", None, cx)
1839 })
1840 })
1841 .unwrap()
1842 .await
1843 .unwrap();
1844 window
1845 .update(cx, |_, cx| {
1846 search_bar.update(cx, |search_bar, cx| {
1847 search_bar.select_all_matches(&SelectAllMatches, cx);
1848 });
1849 assert!(
1850 editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
1851 "Should not switch focus to editor if SelectAllMatches does not find any matches"
1852 );
1853 search_bar.update(cx, |search_bar, cx| {
1854 let all_selections =
1855 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1856 assert_eq!(
1857 all_selections, last_match_selections,
1858 "Should not select anything new if there are no matches"
1859 );
1860 assert!(
1861 search_bar.active_match_index.is_none(),
1862 "For no matches, there should be no active match index"
1863 );
1864 });
1865 })
1866 .unwrap();
1867 }
1868
1869 #[gpui::test]
1870 async fn test_search_query_history(cx: &mut TestAppContext) {
1871 init_globals(cx);
1872 let buffer_text = r#"
1873 A regular expression (shortened as regex or regexp;[1] also referred to as
1874 rational expression[2][3]) is a sequence of characters that specifies a search
1875 pattern in text. Usually such patterns are used by string-searching algorithms
1876 for "find" or "find and replace" operations on strings, or for input validation.
1877 "#
1878 .unindent();
1879 let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1880 let cx = cx.add_empty_window();
1881
1882 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1883
1884 let search_bar = cx.new_view(|cx| {
1885 let mut search_bar = BufferSearchBar::new(cx);
1886 search_bar.set_active_pane_item(Some(&editor), cx);
1887 search_bar.show(cx);
1888 search_bar
1889 });
1890
1891 // Add 3 search items into the history.
1892 search_bar
1893 .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1894 .await
1895 .unwrap();
1896 search_bar
1897 .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1898 .await
1899 .unwrap();
1900 search_bar
1901 .update(cx, |search_bar, cx| {
1902 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1903 })
1904 .await
1905 .unwrap();
1906 // Ensure that the latest search is active.
1907 search_bar.update(cx, |search_bar, cx| {
1908 assert_eq!(search_bar.query(cx), "c");
1909 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1910 });
1911
1912 // Next history query after the latest should set the query to the empty string.
1913 search_bar.update(cx, |search_bar, cx| {
1914 search_bar.next_history_query(&NextHistoryQuery, cx);
1915 });
1916 search_bar.update(cx, |search_bar, cx| {
1917 assert_eq!(search_bar.query(cx), "");
1918 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1919 });
1920 search_bar.update(cx, |search_bar, cx| {
1921 search_bar.next_history_query(&NextHistoryQuery, cx);
1922 });
1923 search_bar.update(cx, |search_bar, cx| {
1924 assert_eq!(search_bar.query(cx), "");
1925 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1926 });
1927
1928 // First previous query for empty current query should set the query to the latest.
1929 search_bar.update(cx, |search_bar, cx| {
1930 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1931 });
1932 search_bar.update(cx, |search_bar, cx| {
1933 assert_eq!(search_bar.query(cx), "c");
1934 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1935 });
1936
1937 // Further previous items should go over the history in reverse order.
1938 search_bar.update(cx, |search_bar, cx| {
1939 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1940 });
1941 search_bar.update(cx, |search_bar, cx| {
1942 assert_eq!(search_bar.query(cx), "b");
1943 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1944 });
1945
1946 // Previous items should never go behind the first history item.
1947 search_bar.update(cx, |search_bar, cx| {
1948 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1949 });
1950 search_bar.update(cx, |search_bar, cx| {
1951 assert_eq!(search_bar.query(cx), "a");
1952 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1953 });
1954 search_bar.update(cx, |search_bar, cx| {
1955 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1956 });
1957 search_bar.update(cx, |search_bar, cx| {
1958 assert_eq!(search_bar.query(cx), "a");
1959 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1960 });
1961
1962 // Next items should go over the history in the original order.
1963 search_bar.update(cx, |search_bar, cx| {
1964 search_bar.next_history_query(&NextHistoryQuery, cx);
1965 });
1966 search_bar.update(cx, |search_bar, cx| {
1967 assert_eq!(search_bar.query(cx), "b");
1968 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1969 });
1970
1971 search_bar
1972 .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1973 .await
1974 .unwrap();
1975 search_bar.update(cx, |search_bar, cx| {
1976 assert_eq!(search_bar.query(cx), "ba");
1977 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1978 });
1979
1980 // New search input should add another entry to history and move the selection to the end of the history.
1981 search_bar.update(cx, |search_bar, cx| {
1982 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1983 });
1984 search_bar.update(cx, |search_bar, cx| {
1985 assert_eq!(search_bar.query(cx), "c");
1986 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1987 });
1988 search_bar.update(cx, |search_bar, cx| {
1989 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1990 });
1991 search_bar.update(cx, |search_bar, cx| {
1992 assert_eq!(search_bar.query(cx), "b");
1993 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1994 });
1995 search_bar.update(cx, |search_bar, cx| {
1996 search_bar.next_history_query(&NextHistoryQuery, cx);
1997 });
1998 search_bar.update(cx, |search_bar, cx| {
1999 assert_eq!(search_bar.query(cx), "c");
2000 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2001 });
2002 search_bar.update(cx, |search_bar, cx| {
2003 search_bar.next_history_query(&NextHistoryQuery, cx);
2004 });
2005 search_bar.update(cx, |search_bar, cx| {
2006 assert_eq!(search_bar.query(cx), "ba");
2007 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2008 });
2009 search_bar.update(cx, |search_bar, cx| {
2010 search_bar.next_history_query(&NextHistoryQuery, cx);
2011 });
2012 search_bar.update(cx, |search_bar, cx| {
2013 assert_eq!(search_bar.query(cx), "");
2014 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2015 });
2016 }
2017
2018 #[gpui::test]
2019 async fn test_replace_simple(cx: &mut TestAppContext) {
2020 let (editor, search_bar, cx) = init_test(cx);
2021
2022 search_bar
2023 .update(cx, |search_bar, cx| {
2024 search_bar.search("expression", None, cx)
2025 })
2026 .await
2027 .unwrap();
2028
2029 search_bar.update(cx, |search_bar, cx| {
2030 search_bar.replacement_editor.update(cx, |editor, cx| {
2031 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2032 editor.set_text("expr$1", cx);
2033 });
2034 search_bar.replace_all(&ReplaceAll, cx)
2035 });
2036 assert_eq!(
2037 editor.update(cx, |this, cx| { this.text(cx) }),
2038 r#"
2039 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2040 rational expr$1[2][3]) is a sequence of characters that specifies a search
2041 pattern in text. Usually such patterns are used by string-searching algorithms
2042 for "find" or "find and replace" operations on strings, or for input validation.
2043 "#
2044 .unindent()
2045 );
2046
2047 // Search for word boundaries and replace just a single one.
2048 search_bar
2049 .update(cx, |search_bar, cx| {
2050 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
2051 })
2052 .await
2053 .unwrap();
2054
2055 search_bar.update(cx, |search_bar, cx| {
2056 search_bar.replacement_editor.update(cx, |editor, cx| {
2057 editor.set_text("banana", cx);
2058 });
2059 search_bar.replace_next(&ReplaceNext, cx)
2060 });
2061 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2062 assert_eq!(
2063 editor.update(cx, |this, cx| { this.text(cx) }),
2064 r#"
2065 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2066 rational expr$1[2][3]) is a sequence of characters that specifies a search
2067 pattern in text. Usually such patterns are used by string-searching algorithms
2068 for "find" or "find and replace" operations on strings, or for input validation.
2069 "#
2070 .unindent()
2071 );
2072 // Let's turn on regex mode.
2073 search_bar
2074 .update(cx, |search_bar, cx| {
2075 search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), cx)
2076 })
2077 .await
2078 .unwrap();
2079 search_bar.update(cx, |search_bar, cx| {
2080 search_bar.replacement_editor.update(cx, |editor, cx| {
2081 editor.set_text("${1}number", cx);
2082 });
2083 search_bar.replace_all(&ReplaceAll, cx)
2084 });
2085 assert_eq!(
2086 editor.update(cx, |this, cx| { this.text(cx) }),
2087 r#"
2088 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2089 rational expr$12number3number) is a sequence of characters that specifies a search
2090 pattern in text. Usually such patterns are used by string-searching algorithms
2091 for "find" or "find and replace" operations on strings, or for input validation.
2092 "#
2093 .unindent()
2094 );
2095 // Now with a whole-word twist.
2096 search_bar
2097 .update(cx, |search_bar, cx| {
2098 search_bar.search(
2099 "a\\w+s",
2100 Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2101 cx,
2102 )
2103 })
2104 .await
2105 .unwrap();
2106 search_bar.update(cx, |search_bar, cx| {
2107 search_bar.replacement_editor.update(cx, |editor, cx| {
2108 editor.set_text("things", cx);
2109 });
2110 search_bar.replace_all(&ReplaceAll, cx)
2111 });
2112 // The only word affected by this edit should be `algorithms`, even though there's a bunch
2113 // of words in this text that would match this regex if not for WHOLE_WORD.
2114 assert_eq!(
2115 editor.update(cx, |this, cx| { this.text(cx) }),
2116 r#"
2117 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2118 rational expr$12number3number) is a sequence of characters that specifies a search
2119 pattern in text. Usually such patterns are used by string-searching things
2120 for "find" or "find and replace" operations on strings, or for input validation.
2121 "#
2122 .unindent()
2123 );
2124 }
2125
2126 struct ReplacementTestParams<'a> {
2127 editor: &'a View<Editor>,
2128 search_bar: &'a View<BufferSearchBar>,
2129 cx: &'a mut VisualTestContext,
2130 search_text: &'static str,
2131 search_options: Option<SearchOptions>,
2132 replacement_text: &'static str,
2133 replace_all: bool,
2134 expected_text: String,
2135 }
2136
2137 async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2138 options
2139 .search_bar
2140 .update(options.cx, |search_bar, cx| {
2141 if let Some(options) = options.search_options {
2142 search_bar.set_search_options(options, cx);
2143 }
2144 search_bar.search(options.search_text, options.search_options, cx)
2145 })
2146 .await
2147 .unwrap();
2148
2149 options.search_bar.update(options.cx, |search_bar, cx| {
2150 search_bar.replacement_editor.update(cx, |editor, cx| {
2151 editor.set_text(options.replacement_text, cx);
2152 });
2153
2154 if options.replace_all {
2155 search_bar.replace_all(&ReplaceAll, cx)
2156 } else {
2157 search_bar.replace_next(&ReplaceNext, cx)
2158 }
2159 });
2160
2161 assert_eq!(
2162 options
2163 .editor
2164 .update(options.cx, |this, cx| { this.text(cx) }),
2165 options.expected_text
2166 );
2167 }
2168
2169 #[gpui::test]
2170 async fn test_replace_special_characters(cx: &mut TestAppContext) {
2171 let (editor, search_bar, cx) = init_test(cx);
2172
2173 run_replacement_test(ReplacementTestParams {
2174 editor: &editor,
2175 search_bar: &search_bar,
2176 cx,
2177 search_text: "expression",
2178 search_options: None,
2179 replacement_text: r"\n",
2180 replace_all: true,
2181 expected_text: r#"
2182 A regular \n (shortened as regex or regexp;[1] also referred to as
2183 rational \n[2][3]) is a sequence of characters that specifies a search
2184 pattern in text. Usually such patterns are used by string-searching algorithms
2185 for "find" or "find and replace" operations on strings, or for input validation.
2186 "#
2187 .unindent(),
2188 })
2189 .await;
2190
2191 run_replacement_test(ReplacementTestParams {
2192 editor: &editor,
2193 search_bar: &search_bar,
2194 cx,
2195 search_text: "or",
2196 search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2197 replacement_text: r"\\\n\\\\",
2198 replace_all: false,
2199 expected_text: r#"
2200 A regular \n (shortened as regex \
2201 \\ regexp;[1] also referred to as
2202 rational \n[2][3]) is a sequence of characters that specifies a search
2203 pattern in text. Usually such patterns are used by string-searching algorithms
2204 for "find" or "find and replace" operations on strings, or for input validation.
2205 "#
2206 .unindent(),
2207 })
2208 .await;
2209
2210 run_replacement_test(ReplacementTestParams {
2211 editor: &editor,
2212 search_bar: &search_bar,
2213 cx,
2214 search_text: r"(that|used) ",
2215 search_options: Some(SearchOptions::REGEX),
2216 replacement_text: r"$1\n",
2217 replace_all: true,
2218 expected_text: r#"
2219 A regular \n (shortened as regex \
2220 \\ regexp;[1] also referred to as
2221 rational \n[2][3]) is a sequence of characters that
2222 specifies a search
2223 pattern in text. Usually such patterns are used
2224 by string-searching algorithms
2225 for "find" or "find and replace" operations on strings, or for input validation.
2226 "#
2227 .unindent(),
2228 })
2229 .await;
2230 }
2231
2232 #[gpui::test]
2233 async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2234 cx: &mut TestAppContext,
2235 ) {
2236 init_globals(cx);
2237 let buffer = cx.new_model(|cx| {
2238 Buffer::local(
2239 r#"
2240 aaa bbb aaa ccc
2241 aaa bbb aaa ccc
2242 aaa bbb aaa ccc
2243 aaa bbb aaa ccc
2244 aaa bbb aaa ccc
2245 aaa bbb aaa ccc
2246 "#
2247 .unindent(),
2248 cx,
2249 )
2250 });
2251 let cx = cx.add_empty_window();
2252 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
2253
2254 let search_bar = cx.new_view(|cx| {
2255 let mut search_bar = BufferSearchBar::new(cx);
2256 search_bar.set_active_pane_item(Some(&editor), cx);
2257 search_bar.show(cx);
2258 search_bar
2259 });
2260
2261 editor.update(cx, |editor, cx| {
2262 editor.change_selections(None, cx, |s| {
2263 s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2264 })
2265 });
2266
2267 search_bar.update(cx, |search_bar, cx| {
2268 let deploy = Deploy {
2269 focus: true,
2270 replace_enabled: false,
2271 selection_search_enabled: true,
2272 };
2273 search_bar.deploy(&deploy, cx);
2274 });
2275
2276 cx.run_until_parked();
2277
2278 search_bar
2279 .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2280 .await
2281 .unwrap();
2282
2283 editor.update(cx, |editor, cx| {
2284 assert_eq!(
2285 editor.search_background_highlights(cx),
2286 &[
2287 Point::new(1, 0)..Point::new(1, 3),
2288 Point::new(1, 8)..Point::new(1, 11),
2289 Point::new(2, 0)..Point::new(2, 3),
2290 ]
2291 );
2292 });
2293 }
2294
2295 #[gpui::test]
2296 async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2297 cx: &mut TestAppContext,
2298 ) {
2299 init_globals(cx);
2300 let text = r#"
2301 aaa bbb aaa ccc
2302 aaa bbb aaa ccc
2303 aaa bbb aaa ccc
2304 aaa bbb aaa ccc
2305 aaa bbb aaa ccc
2306 aaa bbb aaa ccc
2307
2308 aaa bbb aaa ccc
2309 aaa bbb aaa ccc
2310 aaa bbb aaa ccc
2311 aaa bbb aaa ccc
2312 aaa bbb aaa ccc
2313 aaa bbb aaa ccc
2314 "#
2315 .unindent();
2316
2317 let cx = cx.add_empty_window();
2318 let editor = cx.new_view(|cx| {
2319 let multibuffer = MultiBuffer::build_multi(
2320 [
2321 (
2322 &text,
2323 vec![
2324 Point::new(0, 0)..Point::new(2, 0),
2325 Point::new(4, 0)..Point::new(5, 0),
2326 ],
2327 ),
2328 (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2329 ],
2330 cx,
2331 );
2332 Editor::for_multibuffer(multibuffer, None, false, cx)
2333 });
2334
2335 let search_bar = cx.new_view(|cx| {
2336 let mut search_bar = BufferSearchBar::new(cx);
2337 search_bar.set_active_pane_item(Some(&editor), cx);
2338 search_bar.show(cx);
2339 search_bar
2340 });
2341
2342 editor.update(cx, |editor, cx| {
2343 editor.change_selections(None, cx, |s| {
2344 s.select_ranges(vec![
2345 Point::new(1, 0)..Point::new(1, 4),
2346 Point::new(5, 3)..Point::new(6, 4),
2347 ])
2348 })
2349 });
2350
2351 search_bar.update(cx, |search_bar, cx| {
2352 let deploy = Deploy {
2353 focus: true,
2354 replace_enabled: false,
2355 selection_search_enabled: true,
2356 };
2357 search_bar.deploy(&deploy, cx);
2358 });
2359
2360 cx.run_until_parked();
2361
2362 search_bar
2363 .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2364 .await
2365 .unwrap();
2366
2367 editor.update(cx, |editor, cx| {
2368 assert_eq!(
2369 editor.search_background_highlights(cx),
2370 &[
2371 Point::new(1, 0)..Point::new(1, 3),
2372 Point::new(5, 8)..Point::new(5, 11),
2373 Point::new(6, 0)..Point::new(6, 3),
2374 ]
2375 );
2376 });
2377 }
2378
2379 #[gpui::test]
2380 async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2381 let (editor, search_bar, cx) = init_test(cx);
2382 // Search using valid regexp
2383 search_bar
2384 .update(cx, |search_bar, cx| {
2385 search_bar.enable_search_option(SearchOptions::REGEX, cx);
2386 search_bar.search("expression", None, cx)
2387 })
2388 .await
2389 .unwrap();
2390 editor.update(cx, |editor, cx| {
2391 assert_eq!(
2392 display_points_of(editor.all_text_background_highlights(cx)),
2393 &[
2394 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2395 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2396 ],
2397 );
2398 });
2399
2400 // Now, the expression is invalid
2401 search_bar
2402 .update(cx, |search_bar, cx| {
2403 search_bar.search("expression (", None, cx)
2404 })
2405 .await
2406 .unwrap_err();
2407 editor.update(cx, |editor, cx| {
2408 assert!(display_points_of(editor.all_text_background_highlights(cx)).is_empty(),);
2409 });
2410 }
2411
2412 #[gpui::test]
2413 async fn test_search_options_changes(cx: &mut TestAppContext) {
2414 let (_editor, search_bar, cx) = init_test(cx);
2415 update_search_settings(
2416 SearchSettings {
2417 whole_word: false,
2418 case_sensitive: false,
2419 include_ignored: false,
2420 regex: false,
2421 },
2422 cx,
2423 );
2424
2425 let deploy = Deploy {
2426 focus: true,
2427 replace_enabled: false,
2428 selection_search_enabled: true,
2429 };
2430
2431 search_bar.update(cx, |search_bar, cx| {
2432 assert_eq!(
2433 search_bar.search_options,
2434 SearchOptions::NONE,
2435 "Should have no search options enabled by default"
2436 );
2437 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
2438 assert_eq!(
2439 search_bar.search_options,
2440 SearchOptions::WHOLE_WORD,
2441 "Should enable the option toggled"
2442 );
2443 assert!(
2444 !search_bar.dismissed,
2445 "Search bar should be present and visible"
2446 );
2447 search_bar.deploy(&deploy, cx);
2448 assert_eq!(
2449 search_bar.configured_options,
2450 SearchOptions::NONE,
2451 "Should have configured search options matching the settings"
2452 );
2453 assert_eq!(
2454 search_bar.search_options,
2455 SearchOptions::WHOLE_WORD,
2456 "After (re)deploying, the option should still be enabled"
2457 );
2458
2459 search_bar.dismiss(&Dismiss, cx);
2460 search_bar.deploy(&deploy, cx);
2461 assert_eq!(
2462 search_bar.search_options,
2463 SearchOptions::NONE,
2464 "After hiding and showing the search bar, default options should be used"
2465 );
2466
2467 search_bar.toggle_search_option(SearchOptions::REGEX, cx);
2468 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
2469 assert_eq!(
2470 search_bar.search_options,
2471 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2472 "Should enable the options toggled"
2473 );
2474 assert!(
2475 !search_bar.dismissed,
2476 "Search bar should be present and visible"
2477 );
2478 });
2479
2480 update_search_settings(
2481 SearchSettings {
2482 whole_word: false,
2483 case_sensitive: true,
2484 include_ignored: false,
2485 regex: false,
2486 },
2487 cx,
2488 );
2489 search_bar.update(cx, |search_bar, cx| {
2490 assert_eq!(
2491 search_bar.search_options,
2492 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2493 "Should have no search options enabled by default"
2494 );
2495
2496 search_bar.deploy(&deploy, cx);
2497 assert_eq!(
2498 search_bar.configured_options,
2499 SearchOptions::CASE_SENSITIVE,
2500 "Should have configured search options matching the settings"
2501 );
2502 assert_eq!(
2503 search_bar.search_options,
2504 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2505 "Toggling a non-dismissed search bar with custom options should not change the default options"
2506 );
2507 search_bar.dismiss(&Dismiss, cx);
2508 search_bar.deploy(&deploy, cx);
2509 assert_eq!(
2510 search_bar.search_options,
2511 SearchOptions::CASE_SENSITIVE,
2512 "After hiding and showing the search bar, default options should be used"
2513 );
2514 });
2515 }
2516
2517 fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2518 cx.update(|cx| {
2519 SettingsStore::update_global(cx, |store, cx| {
2520 store.update_user_settings::<EditorSettings>(cx, |settings| {
2521 settings.search = Some(search_settings);
2522 });
2523 });
2524 });
2525 }
2526}