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