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 false
661 }
662
663 pub fn toggle(&mut self, action: &Deploy, cx: &mut ViewContext<Self>) {
664 if self.is_dismissed() {
665 self.deploy(action, cx);
666 } else {
667 self.dismiss(&Dismiss, cx);
668 }
669 }
670
671 pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
672 let Some(handle) = self.active_searchable_item.as_ref() else {
673 return false;
674 };
675
676 self.configured_options =
677 SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
678 if self.dismissed && self.configured_options != self.default_options {
679 self.search_options = self.configured_options;
680 self.default_options = self.configured_options;
681 }
682
683 self.dismissed = false;
684 handle.search_bar_visibility_changed(true, cx);
685 cx.notify();
686 cx.emit(Event::UpdateLocation);
687 cx.emit(ToolbarItemEvent::ChangeLocation(
688 ToolbarItemLocation::Secondary,
689 ));
690 true
691 }
692
693 fn supported_options(&self) -> workspace::searchable::SearchOptions {
694 self.active_searchable_item
695 .as_deref()
696 .map(SearchableItemHandle::supported_options)
697 .unwrap_or_default()
698 }
699
700 pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
701 let search = self
702 .query_suggestion(cx)
703 .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
704
705 if let Some(search) = search {
706 cx.spawn(|this, mut cx| async move {
707 search.await?;
708 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
709 })
710 .detach_and_log_err(cx);
711 }
712 }
713
714 pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
715 if let Some(match_ix) = self.active_match_index {
716 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
717 if let Some(matches) = self
718 .searchable_items_with_matches
719 .get(&active_searchable_item.downgrade())
720 {
721 active_searchable_item.activate_match(match_ix, matches, cx)
722 }
723 }
724 }
725 }
726
727 pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
728 self.query_editor.update(cx, |query_editor, cx| {
729 query_editor.select_all(&Default::default(), cx);
730 });
731 }
732
733 pub fn query(&self, cx: &WindowContext) -> String {
734 self.query_editor.read(cx).text(cx)
735 }
736 pub fn replacement(&self, cx: &WindowContext) -> String {
737 self.replacement_editor.read(cx).text(cx)
738 }
739 pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
740 self.active_searchable_item
741 .as_ref()
742 .map(|searchable_item| searchable_item.query_suggestion(cx))
743 .filter(|suggestion| !suggestion.is_empty())
744 }
745
746 pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
747 if replacement.is_none() {
748 self.replace_enabled = false;
749 return;
750 }
751 self.replace_enabled = true;
752 self.replacement_editor
753 .update(cx, |replacement_editor, cx| {
754 replacement_editor
755 .buffer()
756 .update(cx, |replacement_buffer, cx| {
757 let len = replacement_buffer.len(cx);
758 replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
759 });
760 });
761 }
762
763 pub fn search(
764 &mut self,
765 query: &str,
766 options: Option<SearchOptions>,
767 cx: &mut ViewContext<Self>,
768 ) -> oneshot::Receiver<()> {
769 let options = options.unwrap_or(self.default_options);
770 let updated = query != self.query(cx) || self.search_options != options;
771 if updated {
772 self.query_editor.update(cx, |query_editor, cx| {
773 query_editor.buffer().update(cx, |query_buffer, cx| {
774 let len = query_buffer.len(cx);
775 query_buffer.edit([(0..len, query)], None, cx);
776 });
777 });
778 self.search_options = options;
779 self.clear_matches(cx);
780 cx.notify();
781 }
782 self.update_matches(!updated, cx)
783 }
784
785 fn render_search_option_button(
786 &self,
787 option: SearchOptions,
788 focus_handle: FocusHandle,
789 action: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
790 ) -> impl IntoElement {
791 let is_active = self.search_options.contains(option);
792 option.as_button(is_active, focus_handle, action)
793 }
794
795 pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
796 if let Some(active_editor) = self.active_searchable_item.as_ref() {
797 let handle = active_editor.focus_handle(cx);
798 cx.focus(&handle);
799 }
800 }
801
802 pub fn toggle_search_option(
803 &mut self,
804 search_option: SearchOptions,
805 cx: &mut ViewContext<Self>,
806 ) {
807 self.search_options.toggle(search_option);
808 self.default_options = self.search_options;
809 drop(self.update_matches(false, cx));
810 cx.notify();
811 }
812
813 pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
814 self.search_options.contains(search_option)
815 }
816
817 pub fn enable_search_option(
818 &mut self,
819 search_option: SearchOptions,
820 cx: &mut ViewContext<Self>,
821 ) {
822 if !self.search_options.contains(search_option) {
823 self.toggle_search_option(search_option, cx)
824 }
825 }
826
827 pub fn set_search_options(
828 &mut self,
829 search_options: SearchOptions,
830 cx: &mut ViewContext<Self>,
831 ) {
832 self.search_options = search_options;
833 cx.notify();
834 }
835
836 fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
837 self.select_match(Direction::Next, 1, cx);
838 }
839
840 fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
841 self.select_match(Direction::Prev, 1, cx);
842 }
843
844 fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
845 if !self.dismissed && self.active_match_index.is_some() {
846 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
847 if let Some(matches) = self
848 .searchable_items_with_matches
849 .get(&searchable_item.downgrade())
850 {
851 searchable_item.select_matches(matches, cx);
852 self.focus_editor(&FocusEditor, cx);
853 }
854 }
855 }
856 }
857
858 pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
859 if let Some(index) = self.active_match_index {
860 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
861 if let Some(matches) = self
862 .searchable_items_with_matches
863 .get(&searchable_item.downgrade())
864 .filter(|matches| !matches.is_empty())
865 {
866 // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
867 if !EditorSettings::get_global(cx).search_wrap
868 && ((direction == Direction::Next && index + count >= matches.len())
869 || (direction == Direction::Prev && index < count))
870 {
871 crate::show_no_more_matches(cx);
872 return;
873 }
874 let new_match_index = searchable_item
875 .match_index_for_direction(matches, index, direction, count, cx);
876
877 searchable_item.update_matches(matches, cx);
878 searchable_item.activate_match(new_match_index, matches, cx);
879 }
880 }
881 }
882 }
883
884 pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
885 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
886 if let Some(matches) = self
887 .searchable_items_with_matches
888 .get(&searchable_item.downgrade())
889 {
890 if matches.is_empty() {
891 return;
892 }
893 let new_match_index = matches.len() - 1;
894 searchable_item.update_matches(matches, cx);
895 searchable_item.activate_match(new_match_index, matches, cx);
896 }
897 }
898 }
899
900 fn on_query_editor_event(
901 &mut self,
902 editor: View<Editor>,
903 event: &editor::EditorEvent,
904 cx: &mut ViewContext<Self>,
905 ) {
906 match event {
907 editor::EditorEvent::Focused => self.query_editor_focused = true,
908 editor::EditorEvent::Blurred => self.query_editor_focused = false,
909 editor::EditorEvent::Edited { .. } => {
910 self.smartcase(cx);
911 self.clear_matches(cx);
912 let search = self.update_matches(false, cx);
913
914 let width = editor.update(cx, |editor, cx| {
915 let text_layout_details = editor.text_layout_details(cx);
916 let snapshot = editor.snapshot(cx).display_snapshot;
917
918 snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
919 - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
920 });
921 self.editor_needed_width = width;
922 cx.notify();
923
924 cx.spawn(|this, mut cx| async move {
925 search.await?;
926 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
927 })
928 .detach_and_log_err(cx);
929 }
930 _ => {}
931 }
932 }
933
934 fn on_replacement_editor_event(
935 &mut self,
936 _: View<Editor>,
937 event: &editor::EditorEvent,
938 _: &mut ViewContext<Self>,
939 ) {
940 match event {
941 editor::EditorEvent::Focused => self.replacement_editor_focused = true,
942 editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
943 _ => {}
944 }
945 }
946
947 fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext<Self>) {
948 match event {
949 SearchEvent::MatchesInvalidated => {
950 drop(self.update_matches(false, cx));
951 }
952 SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
953 }
954 }
955
956 fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext<Self>) {
957 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
958 }
959
960 fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext<Self>) {
961 self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
962 }
963
964 fn toggle_selection(&mut self, _: &ToggleSelection, cx: &mut ViewContext<Self>) {
965 if let Some(active_item) = self.active_searchable_item.as_mut() {
966 self.selection_search_enabled = !self.selection_search_enabled;
967 active_item.toggle_filtered_search_ranges(self.selection_search_enabled, cx);
968 drop(self.update_matches(false, cx));
969 cx.notify();
970 }
971 }
972
973 fn toggle_regex(&mut self, _: &ToggleRegex, cx: &mut ViewContext<Self>) {
974 self.toggle_search_option(SearchOptions::REGEX, cx)
975 }
976
977 fn clear_active_searchable_item_matches(&mut self, cx: &mut WindowContext) {
978 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
979 self.active_match_index = None;
980 self.searchable_items_with_matches
981 .remove(&active_searchable_item.downgrade());
982 active_searchable_item.clear_matches(cx);
983 }
984 }
985
986 pub fn has_active_match(&self) -> bool {
987 self.active_match_index.is_some()
988 }
989
990 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
991 let mut active_item_matches = None;
992 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
993 if let Some(searchable_item) =
994 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
995 {
996 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
997 active_item_matches = Some((searchable_item.downgrade(), matches));
998 } else {
999 searchable_item.clear_matches(cx);
1000 }
1001 }
1002 }
1003
1004 self.searchable_items_with_matches
1005 .extend(active_item_matches);
1006 }
1007
1008 fn update_matches(
1009 &mut self,
1010 reuse_existing_query: bool,
1011 cx: &mut ViewContext<Self>,
1012 ) -> oneshot::Receiver<()> {
1013 let (done_tx, done_rx) = oneshot::channel();
1014 let query = self.query(cx);
1015 self.pending_search.take();
1016
1017 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1018 self.query_contains_error = false;
1019 if query.is_empty() {
1020 self.clear_active_searchable_item_matches(cx);
1021 let _ = done_tx.send(());
1022 cx.notify();
1023 } else {
1024 let query: Arc<_> = if let Some(search) =
1025 self.active_search.take().filter(|_| reuse_existing_query)
1026 {
1027 search
1028 } else {
1029 if self.search_options.contains(SearchOptions::REGEX) {
1030 match SearchQuery::regex(
1031 query,
1032 self.search_options.contains(SearchOptions::WHOLE_WORD),
1033 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1034 false,
1035 Default::default(),
1036 Default::default(),
1037 None,
1038 ) {
1039 Ok(query) => query.with_replacement(self.replacement(cx)),
1040 Err(_) => {
1041 self.query_contains_error = true;
1042 self.clear_active_searchable_item_matches(cx);
1043 cx.notify();
1044 return done_rx;
1045 }
1046 }
1047 } else {
1048 match SearchQuery::text(
1049 query,
1050 self.search_options.contains(SearchOptions::WHOLE_WORD),
1051 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1052 false,
1053 Default::default(),
1054 Default::default(),
1055 None,
1056 ) {
1057 Ok(query) => query.with_replacement(self.replacement(cx)),
1058 Err(_) => {
1059 self.query_contains_error = true;
1060 self.clear_active_searchable_item_matches(cx);
1061 cx.notify();
1062 return done_rx;
1063 }
1064 }
1065 }
1066 .into()
1067 };
1068
1069 self.active_search = Some(query.clone());
1070 let query_text = query.as_str().to_string();
1071
1072 let matches = active_searchable_item.find_matches(query, cx);
1073
1074 let active_searchable_item = active_searchable_item.downgrade();
1075 self.pending_search = Some(cx.spawn(|this, mut cx| async move {
1076 let matches = matches.await;
1077
1078 this.update(&mut cx, |this, cx| {
1079 if let Some(active_searchable_item) =
1080 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1081 {
1082 this.searchable_items_with_matches
1083 .insert(active_searchable_item.downgrade(), matches);
1084
1085 this.update_match_index(cx);
1086 this.search_history
1087 .add(&mut this.search_history_cursor, query_text);
1088 if !this.dismissed {
1089 let matches = this
1090 .searchable_items_with_matches
1091 .get(&active_searchable_item.downgrade())
1092 .unwrap();
1093 if matches.is_empty() {
1094 active_searchable_item.clear_matches(cx);
1095 } else {
1096 active_searchable_item.update_matches(matches, cx);
1097 }
1098 let _ = done_tx.send(());
1099 }
1100 cx.notify();
1101 }
1102 })
1103 .log_err();
1104 }));
1105 }
1106 }
1107 done_rx
1108 }
1109
1110 pub fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
1111 let new_index = self
1112 .active_searchable_item
1113 .as_ref()
1114 .and_then(|searchable_item| {
1115 let matches = self
1116 .searchable_items_with_matches
1117 .get(&searchable_item.downgrade())?;
1118 searchable_item.active_match_index(matches, cx)
1119 });
1120 if new_index != self.active_match_index {
1121 self.active_match_index = new_index;
1122 cx.notify();
1123 }
1124 }
1125
1126 fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
1127 // Search -> Replace -> Editor
1128 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1129 self.replacement_editor.focus_handle(cx)
1130 } else if let Some(item) = self.active_searchable_item.as_ref() {
1131 item.focus_handle(cx)
1132 } else {
1133 return;
1134 };
1135 self.focus(&focus_handle, cx);
1136 cx.stop_propagation();
1137 }
1138
1139 fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext<Self>) {
1140 // Search -> Replace -> Search
1141 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1142 self.replacement_editor.focus_handle(cx)
1143 } else if self.replacement_editor_focused {
1144 self.query_editor.focus_handle(cx)
1145 } else {
1146 return;
1147 };
1148 self.focus(&focus_handle, cx);
1149 cx.stop_propagation();
1150 }
1151
1152 fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
1153 if let Some(new_query) = self
1154 .search_history
1155 .next(&mut self.search_history_cursor)
1156 .map(str::to_string)
1157 {
1158 drop(self.search(&new_query, Some(self.search_options), cx));
1159 } else {
1160 self.search_history_cursor.reset();
1161 drop(self.search("", Some(self.search_options), cx));
1162 }
1163 }
1164
1165 fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
1166 if self.query(cx).is_empty() {
1167 if let Some(new_query) = self
1168 .search_history
1169 .current(&mut self.search_history_cursor)
1170 .map(str::to_string)
1171 {
1172 drop(self.search(&new_query, Some(self.search_options), cx));
1173 return;
1174 }
1175 }
1176
1177 if let Some(new_query) = self
1178 .search_history
1179 .previous(&mut self.search_history_cursor)
1180 .map(str::to_string)
1181 {
1182 drop(self.search(&new_query, Some(self.search_options), cx));
1183 }
1184 }
1185
1186 fn focus(&self, handle: &gpui::FocusHandle, cx: &mut ViewContext<Self>) {
1187 cx.on_next_frame(|_, cx| {
1188 cx.invalidate_character_coordinates();
1189 });
1190 cx.focus(handle);
1191 }
1192
1193 fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
1194 if self.active_searchable_item.is_some() {
1195 self.replace_enabled = !self.replace_enabled;
1196 let handle = if self.replace_enabled {
1197 self.replacement_editor.focus_handle(cx)
1198 } else {
1199 self.query_editor.focus_handle(cx)
1200 };
1201 self.focus(&handle, cx);
1202 cx.notify();
1203 }
1204 }
1205
1206 fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
1207 let mut should_propagate = true;
1208 if !self.dismissed && self.active_search.is_some() {
1209 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1210 if let Some(query) = self.active_search.as_ref() {
1211 if let Some(matches) = self
1212 .searchable_items_with_matches
1213 .get(&searchable_item.downgrade())
1214 {
1215 if let Some(active_index) = self.active_match_index {
1216 let query = query
1217 .as_ref()
1218 .clone()
1219 .with_replacement(self.replacement(cx));
1220 searchable_item.replace(matches.at(active_index), &query, cx);
1221 self.select_next_match(&SelectNextMatch, cx);
1222 }
1223 should_propagate = false;
1224 self.focus_editor(&FocusEditor, cx);
1225 }
1226 }
1227 }
1228 }
1229 if !should_propagate {
1230 cx.stop_propagation();
1231 }
1232 }
1233
1234 pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
1235 if !self.dismissed && self.active_search.is_some() {
1236 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1237 if let Some(query) = self.active_search.as_ref() {
1238 if let Some(matches) = self
1239 .searchable_items_with_matches
1240 .get(&searchable_item.downgrade())
1241 {
1242 let query = query
1243 .as_ref()
1244 .clone()
1245 .with_replacement(self.replacement(cx));
1246 searchable_item.replace_all(&mut matches.iter(), &query, cx);
1247 }
1248 }
1249 }
1250 }
1251 }
1252
1253 pub fn match_exists(&mut self, cx: &mut ViewContext<Self>) -> bool {
1254 self.update_match_index(cx);
1255 self.active_match_index.is_some()
1256 }
1257
1258 pub fn should_use_smartcase_search(&mut self, cx: &mut ViewContext<Self>) -> bool {
1259 EditorSettings::get_global(cx).use_smartcase_search
1260 }
1261
1262 pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1263 str.chars().any(|c| c.is_uppercase())
1264 }
1265
1266 fn smartcase(&mut self, cx: &mut ViewContext<Self>) {
1267 if self.should_use_smartcase_search(cx) {
1268 let query = self.query(cx);
1269 if !query.is_empty() {
1270 let is_case = self.is_contains_uppercase(&query);
1271 if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1272 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1273 }
1274 }
1275 }
1276 }
1277}
1278
1279#[cfg(test)]
1280mod tests {
1281 use std::ops::Range;
1282
1283 use super::*;
1284 use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer, SearchSettings};
1285 use gpui::{Context, Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1286 use language::{Buffer, Point};
1287 use project::Project;
1288 use settings::SettingsStore;
1289 use smol::stream::StreamExt as _;
1290 use unindent::Unindent as _;
1291
1292 fn init_globals(cx: &mut TestAppContext) {
1293 cx.update(|cx| {
1294 let store = settings::SettingsStore::test(cx);
1295 cx.set_global(store);
1296 editor::init(cx);
1297
1298 language::init(cx);
1299 Project::init_settings(cx);
1300 theme::init(theme::LoadThemes::JustBase, cx);
1301 crate::init(cx);
1302 });
1303 }
1304
1305 fn init_test(
1306 cx: &mut TestAppContext,
1307 ) -> (View<Editor>, View<BufferSearchBar>, &mut VisualTestContext) {
1308 init_globals(cx);
1309 let buffer = cx.new_model(|cx| {
1310 Buffer::local(
1311 r#"
1312 A regular expression (shortened as regex or regexp;[1] also referred to as
1313 rational expression[2][3]) is a sequence of characters that specifies a search
1314 pattern in text. Usually such patterns are used by string-searching algorithms
1315 for "find" or "find and replace" operations on strings, or for input validation.
1316 "#
1317 .unindent(),
1318 cx,
1319 )
1320 });
1321 let cx = cx.add_empty_window();
1322 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1323
1324 let search_bar = cx.new_view(|cx| {
1325 let mut search_bar = BufferSearchBar::new(cx);
1326 search_bar.set_active_pane_item(Some(&editor), cx);
1327 search_bar.show(cx);
1328 search_bar
1329 });
1330
1331 (editor, search_bar, cx)
1332 }
1333
1334 #[gpui::test]
1335 async fn test_search_simple(cx: &mut TestAppContext) {
1336 let (editor, search_bar, cx) = init_test(cx);
1337 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1338 background_highlights
1339 .into_iter()
1340 .map(|(range, _)| range)
1341 .collect::<Vec<_>>()
1342 };
1343 // Search for a string that appears with different casing.
1344 // By default, search is case-insensitive.
1345 search_bar
1346 .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
1347 .await
1348 .unwrap();
1349 editor.update(cx, |editor, cx| {
1350 assert_eq!(
1351 display_points_of(editor.all_text_background_highlights(cx)),
1352 &[
1353 DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1354 DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1355 ]
1356 );
1357 });
1358
1359 // Switch to a case sensitive search.
1360 search_bar.update(cx, |search_bar, cx| {
1361 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1362 });
1363 let mut editor_notifications = cx.notifications(&editor);
1364 editor_notifications.next().await;
1365 editor.update(cx, |editor, cx| {
1366 assert_eq!(
1367 display_points_of(editor.all_text_background_highlights(cx)),
1368 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1369 );
1370 });
1371
1372 // Search for a string that appears both as a whole word and
1373 // within other words. By default, all results are found.
1374 search_bar
1375 .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1376 .await
1377 .unwrap();
1378 editor.update(cx, |editor, cx| {
1379 assert_eq!(
1380 display_points_of(editor.all_text_background_highlights(cx)),
1381 &[
1382 DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1383 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1384 DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1385 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1386 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1387 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1388 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1389 ]
1390 );
1391 });
1392
1393 // Switch to a whole word search.
1394 search_bar.update(cx, |search_bar, cx| {
1395 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1396 });
1397 let mut editor_notifications = cx.notifications(&editor);
1398 editor_notifications.next().await;
1399 editor.update(cx, |editor, cx| {
1400 assert_eq!(
1401 display_points_of(editor.all_text_background_highlights(cx)),
1402 &[
1403 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1404 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1405 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1406 ]
1407 );
1408 });
1409
1410 editor.update(cx, |editor, cx| {
1411 editor.change_selections(None, cx, |s| {
1412 s.select_display_ranges([
1413 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1414 ])
1415 });
1416 });
1417 search_bar.update(cx, |search_bar, cx| {
1418 assert_eq!(search_bar.active_match_index, Some(0));
1419 search_bar.select_next_match(&SelectNextMatch, cx);
1420 assert_eq!(
1421 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1422 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1423 );
1424 });
1425 search_bar.update(cx, |search_bar, _| {
1426 assert_eq!(search_bar.active_match_index, Some(0));
1427 });
1428
1429 search_bar.update(cx, |search_bar, cx| {
1430 search_bar.select_next_match(&SelectNextMatch, cx);
1431 assert_eq!(
1432 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1433 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1434 );
1435 });
1436 search_bar.update(cx, |search_bar, _| {
1437 assert_eq!(search_bar.active_match_index, Some(1));
1438 });
1439
1440 search_bar.update(cx, |search_bar, cx| {
1441 search_bar.select_next_match(&SelectNextMatch, cx);
1442 assert_eq!(
1443 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1444 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1445 );
1446 });
1447 search_bar.update(cx, |search_bar, _| {
1448 assert_eq!(search_bar.active_match_index, Some(2));
1449 });
1450
1451 search_bar.update(cx, |search_bar, cx| {
1452 search_bar.select_next_match(&SelectNextMatch, cx);
1453 assert_eq!(
1454 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1455 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1456 );
1457 });
1458 search_bar.update(cx, |search_bar, _| {
1459 assert_eq!(search_bar.active_match_index, Some(0));
1460 });
1461
1462 search_bar.update(cx, |search_bar, cx| {
1463 search_bar.select_prev_match(&SelectPrevMatch, cx);
1464 assert_eq!(
1465 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1466 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1467 );
1468 });
1469 search_bar.update(cx, |search_bar, _| {
1470 assert_eq!(search_bar.active_match_index, Some(2));
1471 });
1472
1473 search_bar.update(cx, |search_bar, cx| {
1474 search_bar.select_prev_match(&SelectPrevMatch, cx);
1475 assert_eq!(
1476 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1477 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1478 );
1479 });
1480 search_bar.update(cx, |search_bar, _| {
1481 assert_eq!(search_bar.active_match_index, Some(1));
1482 });
1483
1484 search_bar.update(cx, |search_bar, cx| {
1485 search_bar.select_prev_match(&SelectPrevMatch, cx);
1486 assert_eq!(
1487 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1488 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1489 );
1490 });
1491 search_bar.update(cx, |search_bar, _| {
1492 assert_eq!(search_bar.active_match_index, Some(0));
1493 });
1494
1495 // Park the cursor in between matches and ensure that going to the previous match selects
1496 // the closest match to the left.
1497 editor.update(cx, |editor, cx| {
1498 editor.change_selections(None, cx, |s| {
1499 s.select_display_ranges([
1500 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1501 ])
1502 });
1503 });
1504 search_bar.update(cx, |search_bar, cx| {
1505 assert_eq!(search_bar.active_match_index, Some(1));
1506 search_bar.select_prev_match(&SelectPrevMatch, cx);
1507 assert_eq!(
1508 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1509 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1510 );
1511 });
1512 search_bar.update(cx, |search_bar, _| {
1513 assert_eq!(search_bar.active_match_index, Some(0));
1514 });
1515
1516 // Park the cursor in between matches and ensure that going to the next match selects the
1517 // closest match to the right.
1518 editor.update(cx, |editor, cx| {
1519 editor.change_selections(None, cx, |s| {
1520 s.select_display_ranges([
1521 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1522 ])
1523 });
1524 });
1525 search_bar.update(cx, |search_bar, cx| {
1526 assert_eq!(search_bar.active_match_index, Some(1));
1527 search_bar.select_next_match(&SelectNextMatch, cx);
1528 assert_eq!(
1529 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1530 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1531 );
1532 });
1533 search_bar.update(cx, |search_bar, _| {
1534 assert_eq!(search_bar.active_match_index, Some(1));
1535 });
1536
1537 // Park the cursor after the last match and ensure that going to the previous match selects
1538 // the last match.
1539 editor.update(cx, |editor, cx| {
1540 editor.change_selections(None, cx, |s| {
1541 s.select_display_ranges([
1542 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1543 ])
1544 });
1545 });
1546 search_bar.update(cx, |search_bar, cx| {
1547 assert_eq!(search_bar.active_match_index, Some(2));
1548 search_bar.select_prev_match(&SelectPrevMatch, cx);
1549 assert_eq!(
1550 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1551 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1552 );
1553 });
1554 search_bar.update(cx, |search_bar, _| {
1555 assert_eq!(search_bar.active_match_index, Some(2));
1556 });
1557
1558 // Park the cursor after the last match and ensure that going to the next match selects the
1559 // first match.
1560 editor.update(cx, |editor, cx| {
1561 editor.change_selections(None, cx, |s| {
1562 s.select_display_ranges([
1563 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1564 ])
1565 });
1566 });
1567 search_bar.update(cx, |search_bar, cx| {
1568 assert_eq!(search_bar.active_match_index, Some(2));
1569 search_bar.select_next_match(&SelectNextMatch, cx);
1570 assert_eq!(
1571 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1572 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1573 );
1574 });
1575 search_bar.update(cx, |search_bar, _| {
1576 assert_eq!(search_bar.active_match_index, Some(0));
1577 });
1578
1579 // Park the cursor before the first match and ensure that going to the previous match
1580 // selects the last match.
1581 editor.update(cx, |editor, cx| {
1582 editor.change_selections(None, cx, |s| {
1583 s.select_display_ranges([
1584 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1585 ])
1586 });
1587 });
1588 search_bar.update(cx, |search_bar, cx| {
1589 assert_eq!(search_bar.active_match_index, Some(0));
1590 search_bar.select_prev_match(&SelectPrevMatch, cx);
1591 assert_eq!(
1592 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1593 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1594 );
1595 });
1596 search_bar.update(cx, |search_bar, _| {
1597 assert_eq!(search_bar.active_match_index, Some(2));
1598 });
1599 }
1600
1601 fn display_points_of(
1602 background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1603 ) -> Vec<Range<DisplayPoint>> {
1604 background_highlights
1605 .into_iter()
1606 .map(|(range, _)| range)
1607 .collect::<Vec<_>>()
1608 }
1609
1610 #[gpui::test]
1611 async fn test_search_option_handling(cx: &mut TestAppContext) {
1612 let (editor, search_bar, cx) = init_test(cx);
1613
1614 // show with options should make current search case sensitive
1615 search_bar
1616 .update(cx, |search_bar, cx| {
1617 search_bar.show(cx);
1618 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1619 })
1620 .await
1621 .unwrap();
1622 editor.update(cx, |editor, cx| {
1623 assert_eq!(
1624 display_points_of(editor.all_text_background_highlights(cx)),
1625 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1626 );
1627 });
1628
1629 // search_suggested should restore default options
1630 search_bar.update(cx, |search_bar, cx| {
1631 search_bar.search_suggested(cx);
1632 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1633 });
1634
1635 // toggling a search option should update the defaults
1636 search_bar
1637 .update(cx, |search_bar, cx| {
1638 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1639 })
1640 .await
1641 .unwrap();
1642 search_bar.update(cx, |search_bar, cx| {
1643 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1644 });
1645 let mut editor_notifications = cx.notifications(&editor);
1646 editor_notifications.next().await;
1647 editor.update(cx, |editor, cx| {
1648 assert_eq!(
1649 display_points_of(editor.all_text_background_highlights(cx)),
1650 &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1651 );
1652 });
1653
1654 // defaults should still include whole word
1655 search_bar.update(cx, |search_bar, cx| {
1656 search_bar.search_suggested(cx);
1657 assert_eq!(
1658 search_bar.search_options,
1659 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1660 )
1661 });
1662 }
1663
1664 #[gpui::test]
1665 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1666 init_globals(cx);
1667 let buffer_text = r#"
1668 A regular expression (shortened as regex or regexp;[1] also referred to as
1669 rational expression[2][3]) is a sequence of characters that specifies a search
1670 pattern in text. Usually such patterns are used by string-searching algorithms
1671 for "find" or "find and replace" operations on strings, or for input validation.
1672 "#
1673 .unindent();
1674 let expected_query_matches_count = buffer_text
1675 .chars()
1676 .filter(|c| c.to_ascii_lowercase() == 'a')
1677 .count();
1678 assert!(
1679 expected_query_matches_count > 1,
1680 "Should pick a query with multiple results"
1681 );
1682 let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1683 let window = cx.add_window(|_| gpui::Empty);
1684
1685 let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1686
1687 let search_bar = window.build_view(cx, |cx| {
1688 let mut search_bar = BufferSearchBar::new(cx);
1689 search_bar.set_active_pane_item(Some(&editor), cx);
1690 search_bar.show(cx);
1691 search_bar
1692 });
1693
1694 window
1695 .update(cx, |_, cx| {
1696 search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1697 })
1698 .unwrap()
1699 .await
1700 .unwrap();
1701 let initial_selections = window
1702 .update(cx, |_, cx| {
1703 search_bar.update(cx, |search_bar, cx| {
1704 let handle = search_bar.query_editor.focus_handle(cx);
1705 cx.focus(&handle);
1706 search_bar.activate_current_match(cx);
1707 });
1708 assert!(
1709 !editor.read(cx).is_focused(cx),
1710 "Initially, the editor should not be focused"
1711 );
1712 let initial_selections = editor.update(cx, |editor, cx| {
1713 let initial_selections = editor.selections.display_ranges(cx);
1714 assert_eq!(
1715 initial_selections.len(), 1,
1716 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1717 );
1718 initial_selections
1719 });
1720 search_bar.update(cx, |search_bar, cx| {
1721 assert_eq!(search_bar.active_match_index, Some(0));
1722 let handle = search_bar.query_editor.focus_handle(cx);
1723 cx.focus(&handle);
1724 search_bar.select_all_matches(&SelectAllMatches, cx);
1725 });
1726 assert!(
1727 editor.read(cx).is_focused(cx),
1728 "Should focus editor after successful SelectAllMatches"
1729 );
1730 search_bar.update(cx, |search_bar, cx| {
1731 let all_selections =
1732 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1733 assert_eq!(
1734 all_selections.len(),
1735 expected_query_matches_count,
1736 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1737 );
1738 assert_eq!(
1739 search_bar.active_match_index,
1740 Some(0),
1741 "Match index should not change after selecting all matches"
1742 );
1743 });
1744
1745 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
1746 initial_selections
1747 }).unwrap();
1748
1749 window
1750 .update(cx, |_, cx| {
1751 assert!(
1752 editor.read(cx).is_focused(cx),
1753 "Should still have editor focused after SelectNextMatch"
1754 );
1755 search_bar.update(cx, |search_bar, cx| {
1756 let all_selections =
1757 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1758 assert_eq!(
1759 all_selections.len(),
1760 1,
1761 "On next match, should deselect items and select the next match"
1762 );
1763 assert_ne!(
1764 all_selections, initial_selections,
1765 "Next match should be different from the first selection"
1766 );
1767 assert_eq!(
1768 search_bar.active_match_index,
1769 Some(1),
1770 "Match index should be updated to the next one"
1771 );
1772 let handle = search_bar.query_editor.focus_handle(cx);
1773 cx.focus(&handle);
1774 search_bar.select_all_matches(&SelectAllMatches, cx);
1775 });
1776 })
1777 .unwrap();
1778 window
1779 .update(cx, |_, cx| {
1780 assert!(
1781 editor.read(cx).is_focused(cx),
1782 "Should focus editor after successful SelectAllMatches"
1783 );
1784 search_bar.update(cx, |search_bar, cx| {
1785 let all_selections =
1786 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1787 assert_eq!(
1788 all_selections.len(),
1789 expected_query_matches_count,
1790 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1791 );
1792 assert_eq!(
1793 search_bar.active_match_index,
1794 Some(1),
1795 "Match index should not change after selecting all matches"
1796 );
1797 });
1798 search_bar.update(cx, |search_bar, cx| {
1799 search_bar.select_prev_match(&SelectPrevMatch, cx);
1800 });
1801 })
1802 .unwrap();
1803 let last_match_selections = window
1804 .update(cx, |_, cx| {
1805 assert!(
1806 editor.read(cx).is_focused(cx),
1807 "Should still have editor focused after SelectPrevMatch"
1808 );
1809
1810 search_bar.update(cx, |search_bar, cx| {
1811 let all_selections =
1812 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1813 assert_eq!(
1814 all_selections.len(),
1815 1,
1816 "On previous match, should deselect items and select the previous item"
1817 );
1818 assert_eq!(
1819 all_selections, initial_selections,
1820 "Previous match should be the same as the first selection"
1821 );
1822 assert_eq!(
1823 search_bar.active_match_index,
1824 Some(0),
1825 "Match index should be updated to the previous one"
1826 );
1827 all_selections
1828 })
1829 })
1830 .unwrap();
1831
1832 window
1833 .update(cx, |_, cx| {
1834 search_bar.update(cx, |search_bar, cx| {
1835 let handle = search_bar.query_editor.focus_handle(cx);
1836 cx.focus(&handle);
1837 search_bar.search("abas_nonexistent_match", None, cx)
1838 })
1839 })
1840 .unwrap()
1841 .await
1842 .unwrap();
1843 window
1844 .update(cx, |_, cx| {
1845 search_bar.update(cx, |search_bar, cx| {
1846 search_bar.select_all_matches(&SelectAllMatches, cx);
1847 });
1848 assert!(
1849 editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
1850 "Should not switch focus to editor if SelectAllMatches does not find any matches"
1851 );
1852 search_bar.update(cx, |search_bar, cx| {
1853 let all_selections =
1854 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1855 assert_eq!(
1856 all_selections, last_match_selections,
1857 "Should not select anything new if there are no matches"
1858 );
1859 assert!(
1860 search_bar.active_match_index.is_none(),
1861 "For no matches, there should be no active match index"
1862 );
1863 });
1864 })
1865 .unwrap();
1866 }
1867
1868 #[gpui::test]
1869 async fn test_search_query_history(cx: &mut TestAppContext) {
1870 init_globals(cx);
1871 let buffer_text = r#"
1872 A regular expression (shortened as regex or regexp;[1] also referred to as
1873 rational expression[2][3]) is a sequence of characters that specifies a search
1874 pattern in text. Usually such patterns are used by string-searching algorithms
1875 for "find" or "find and replace" operations on strings, or for input validation.
1876 "#
1877 .unindent();
1878 let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1879 let cx = cx.add_empty_window();
1880
1881 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1882
1883 let search_bar = cx.new_view(|cx| {
1884 let mut search_bar = BufferSearchBar::new(cx);
1885 search_bar.set_active_pane_item(Some(&editor), cx);
1886 search_bar.show(cx);
1887 search_bar
1888 });
1889
1890 // Add 3 search items into the history.
1891 search_bar
1892 .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1893 .await
1894 .unwrap();
1895 search_bar
1896 .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1897 .await
1898 .unwrap();
1899 search_bar
1900 .update(cx, |search_bar, cx| {
1901 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1902 })
1903 .await
1904 .unwrap();
1905 // Ensure that the latest search is active.
1906 search_bar.update(cx, |search_bar, cx| {
1907 assert_eq!(search_bar.query(cx), "c");
1908 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1909 });
1910
1911 // Next history query after the latest should set the query to the empty string.
1912 search_bar.update(cx, |search_bar, cx| {
1913 search_bar.next_history_query(&NextHistoryQuery, cx);
1914 });
1915 search_bar.update(cx, |search_bar, cx| {
1916 assert_eq!(search_bar.query(cx), "");
1917 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1918 });
1919 search_bar.update(cx, |search_bar, cx| {
1920 search_bar.next_history_query(&NextHistoryQuery, cx);
1921 });
1922 search_bar.update(cx, |search_bar, cx| {
1923 assert_eq!(search_bar.query(cx), "");
1924 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1925 });
1926
1927 // First previous query for empty current query should set the query to the latest.
1928 search_bar.update(cx, |search_bar, cx| {
1929 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1930 });
1931 search_bar.update(cx, |search_bar, cx| {
1932 assert_eq!(search_bar.query(cx), "c");
1933 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1934 });
1935
1936 // Further previous items should go over the history in reverse order.
1937 search_bar.update(cx, |search_bar, cx| {
1938 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1939 });
1940 search_bar.update(cx, |search_bar, cx| {
1941 assert_eq!(search_bar.query(cx), "b");
1942 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1943 });
1944
1945 // Previous items should never go behind the first history item.
1946 search_bar.update(cx, |search_bar, cx| {
1947 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1948 });
1949 search_bar.update(cx, |search_bar, cx| {
1950 assert_eq!(search_bar.query(cx), "a");
1951 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1952 });
1953 search_bar.update(cx, |search_bar, cx| {
1954 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1955 });
1956 search_bar.update(cx, |search_bar, cx| {
1957 assert_eq!(search_bar.query(cx), "a");
1958 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1959 });
1960
1961 // Next items should go over the history in the original order.
1962 search_bar.update(cx, |search_bar, cx| {
1963 search_bar.next_history_query(&NextHistoryQuery, cx);
1964 });
1965 search_bar.update(cx, |search_bar, cx| {
1966 assert_eq!(search_bar.query(cx), "b");
1967 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1968 });
1969
1970 search_bar
1971 .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1972 .await
1973 .unwrap();
1974 search_bar.update(cx, |search_bar, cx| {
1975 assert_eq!(search_bar.query(cx), "ba");
1976 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1977 });
1978
1979 // New search input should add another entry to history and move the selection to the end of the history.
1980 search_bar.update(cx, |search_bar, cx| {
1981 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1982 });
1983 search_bar.update(cx, |search_bar, cx| {
1984 assert_eq!(search_bar.query(cx), "c");
1985 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1986 });
1987 search_bar.update(cx, |search_bar, cx| {
1988 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1989 });
1990 search_bar.update(cx, |search_bar, cx| {
1991 assert_eq!(search_bar.query(cx), "b");
1992 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1993 });
1994 search_bar.update(cx, |search_bar, cx| {
1995 search_bar.next_history_query(&NextHistoryQuery, cx);
1996 });
1997 search_bar.update(cx, |search_bar, cx| {
1998 assert_eq!(search_bar.query(cx), "c");
1999 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2000 });
2001 search_bar.update(cx, |search_bar, cx| {
2002 search_bar.next_history_query(&NextHistoryQuery, cx);
2003 });
2004 search_bar.update(cx, |search_bar, cx| {
2005 assert_eq!(search_bar.query(cx), "ba");
2006 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2007 });
2008 search_bar.update(cx, |search_bar, cx| {
2009 search_bar.next_history_query(&NextHistoryQuery, cx);
2010 });
2011 search_bar.update(cx, |search_bar, cx| {
2012 assert_eq!(search_bar.query(cx), "");
2013 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2014 });
2015 }
2016
2017 #[gpui::test]
2018 async fn test_replace_simple(cx: &mut TestAppContext) {
2019 let (editor, search_bar, cx) = init_test(cx);
2020
2021 search_bar
2022 .update(cx, |search_bar, cx| {
2023 search_bar.search("expression", None, cx)
2024 })
2025 .await
2026 .unwrap();
2027
2028 search_bar.update(cx, |search_bar, cx| {
2029 search_bar.replacement_editor.update(cx, |editor, cx| {
2030 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2031 editor.set_text("expr$1", cx);
2032 });
2033 search_bar.replace_all(&ReplaceAll, cx)
2034 });
2035 assert_eq!(
2036 editor.update(cx, |this, cx| { this.text(cx) }),
2037 r#"
2038 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2039 rational expr$1[2][3]) is a sequence of characters that specifies a search
2040 pattern in text. Usually such patterns are used by string-searching algorithms
2041 for "find" or "find and replace" operations on strings, or for input validation.
2042 "#
2043 .unindent()
2044 );
2045
2046 // Search for word boundaries and replace just a single one.
2047 search_bar
2048 .update(cx, |search_bar, cx| {
2049 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
2050 })
2051 .await
2052 .unwrap();
2053
2054 search_bar.update(cx, |search_bar, cx| {
2055 search_bar.replacement_editor.update(cx, |editor, cx| {
2056 editor.set_text("banana", cx);
2057 });
2058 search_bar.replace_next(&ReplaceNext, cx)
2059 });
2060 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2061 assert_eq!(
2062 editor.update(cx, |this, cx| { this.text(cx) }),
2063 r#"
2064 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2065 rational expr$1[2][3]) is a sequence of characters that specifies a search
2066 pattern in text. Usually such patterns are used by string-searching algorithms
2067 for "find" or "find and replace" operations on strings, or for input validation.
2068 "#
2069 .unindent()
2070 );
2071 // Let's turn on regex mode.
2072 search_bar
2073 .update(cx, |search_bar, cx| {
2074 search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), cx)
2075 })
2076 .await
2077 .unwrap();
2078 search_bar.update(cx, |search_bar, cx| {
2079 search_bar.replacement_editor.update(cx, |editor, cx| {
2080 editor.set_text("${1}number", cx);
2081 });
2082 search_bar.replace_all(&ReplaceAll, cx)
2083 });
2084 assert_eq!(
2085 editor.update(cx, |this, cx| { this.text(cx) }),
2086 r#"
2087 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2088 rational expr$12number3number) is a sequence of characters that specifies a search
2089 pattern in text. Usually such patterns are used by string-searching algorithms
2090 for "find" or "find and replace" operations on strings, or for input validation.
2091 "#
2092 .unindent()
2093 );
2094 // Now with a whole-word twist.
2095 search_bar
2096 .update(cx, |search_bar, cx| {
2097 search_bar.search(
2098 "a\\w+s",
2099 Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2100 cx,
2101 )
2102 })
2103 .await
2104 .unwrap();
2105 search_bar.update(cx, |search_bar, cx| {
2106 search_bar.replacement_editor.update(cx, |editor, cx| {
2107 editor.set_text("things", cx);
2108 });
2109 search_bar.replace_all(&ReplaceAll, cx)
2110 });
2111 // The only word affected by this edit should be `algorithms`, even though there's a bunch
2112 // of words in this text that would match this regex if not for WHOLE_WORD.
2113 assert_eq!(
2114 editor.update(cx, |this, cx| { this.text(cx) }),
2115 r#"
2116 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2117 rational expr$12number3number) is a sequence of characters that specifies a search
2118 pattern in text. Usually such patterns are used by string-searching things
2119 for "find" or "find and replace" operations on strings, or for input validation.
2120 "#
2121 .unindent()
2122 );
2123 }
2124
2125 struct ReplacementTestParams<'a> {
2126 editor: &'a View<Editor>,
2127 search_bar: &'a View<BufferSearchBar>,
2128 cx: &'a mut VisualTestContext,
2129 search_text: &'static str,
2130 search_options: Option<SearchOptions>,
2131 replacement_text: &'static str,
2132 replace_all: bool,
2133 expected_text: String,
2134 }
2135
2136 async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2137 options
2138 .search_bar
2139 .update(options.cx, |search_bar, cx| {
2140 if let Some(options) = options.search_options {
2141 search_bar.set_search_options(options, cx);
2142 }
2143 search_bar.search(options.search_text, options.search_options, cx)
2144 })
2145 .await
2146 .unwrap();
2147
2148 options.search_bar.update(options.cx, |search_bar, cx| {
2149 search_bar.replacement_editor.update(cx, |editor, cx| {
2150 editor.set_text(options.replacement_text, cx);
2151 });
2152
2153 if options.replace_all {
2154 search_bar.replace_all(&ReplaceAll, cx)
2155 } else {
2156 search_bar.replace_next(&ReplaceNext, cx)
2157 }
2158 });
2159
2160 assert_eq!(
2161 options
2162 .editor
2163 .update(options.cx, |this, cx| { this.text(cx) }),
2164 options.expected_text
2165 );
2166 }
2167
2168 #[gpui::test]
2169 async fn test_replace_special_characters(cx: &mut TestAppContext) {
2170 let (editor, search_bar, cx) = init_test(cx);
2171
2172 run_replacement_test(ReplacementTestParams {
2173 editor: &editor,
2174 search_bar: &search_bar,
2175 cx,
2176 search_text: "expression",
2177 search_options: None,
2178 replacement_text: r"\n",
2179 replace_all: true,
2180 expected_text: r#"
2181 A regular \n (shortened as regex or regexp;[1] also referred to as
2182 rational \n[2][3]) is a sequence of characters that specifies a search
2183 pattern in text. Usually such patterns are used by string-searching algorithms
2184 for "find" or "find and replace" operations on strings, or for input validation.
2185 "#
2186 .unindent(),
2187 })
2188 .await;
2189
2190 run_replacement_test(ReplacementTestParams {
2191 editor: &editor,
2192 search_bar: &search_bar,
2193 cx,
2194 search_text: "or",
2195 search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2196 replacement_text: r"\\\n\\\\",
2197 replace_all: false,
2198 expected_text: r#"
2199 A regular \n (shortened as regex \
2200 \\ regexp;[1] also referred to as
2201 rational \n[2][3]) is a sequence of characters that specifies a search
2202 pattern in text. Usually such patterns are used by string-searching algorithms
2203 for "find" or "find and replace" operations on strings, or for input validation.
2204 "#
2205 .unindent(),
2206 })
2207 .await;
2208
2209 run_replacement_test(ReplacementTestParams {
2210 editor: &editor,
2211 search_bar: &search_bar,
2212 cx,
2213 search_text: r"(that|used) ",
2214 search_options: Some(SearchOptions::REGEX),
2215 replacement_text: r"$1\n",
2216 replace_all: true,
2217 expected_text: r#"
2218 A regular \n (shortened as regex \
2219 \\ regexp;[1] also referred to as
2220 rational \n[2][3]) is a sequence of characters that
2221 specifies a search
2222 pattern in text. Usually such patterns are used
2223 by string-searching algorithms
2224 for "find" or "find and replace" operations on strings, or for input validation.
2225 "#
2226 .unindent(),
2227 })
2228 .await;
2229 }
2230
2231 #[gpui::test]
2232 async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2233 cx: &mut TestAppContext,
2234 ) {
2235 init_globals(cx);
2236 let buffer = cx.new_model(|cx| {
2237 Buffer::local(
2238 r#"
2239 aaa bbb aaa ccc
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 "#
2246 .unindent(),
2247 cx,
2248 )
2249 });
2250 let cx = cx.add_empty_window();
2251 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
2252
2253 let search_bar = cx.new_view(|cx| {
2254 let mut search_bar = BufferSearchBar::new(cx);
2255 search_bar.set_active_pane_item(Some(&editor), cx);
2256 search_bar.show(cx);
2257 search_bar
2258 });
2259
2260 editor.update(cx, |editor, cx| {
2261 editor.change_selections(None, cx, |s| {
2262 s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2263 })
2264 });
2265
2266 search_bar.update(cx, |search_bar, cx| {
2267 let deploy = Deploy {
2268 focus: true,
2269 replace_enabled: false,
2270 selection_search_enabled: true,
2271 };
2272 search_bar.deploy(&deploy, cx);
2273 });
2274
2275 cx.run_until_parked();
2276
2277 search_bar
2278 .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2279 .await
2280 .unwrap();
2281
2282 editor.update(cx, |editor, cx| {
2283 assert_eq!(
2284 editor.search_background_highlights(cx),
2285 &[
2286 Point::new(1, 0)..Point::new(1, 3),
2287 Point::new(1, 8)..Point::new(1, 11),
2288 Point::new(2, 0)..Point::new(2, 3),
2289 ]
2290 );
2291 });
2292 }
2293
2294 #[gpui::test]
2295 async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2296 cx: &mut TestAppContext,
2297 ) {
2298 init_globals(cx);
2299 let text = r#"
2300 aaa bbb aaa ccc
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
2307 aaa bbb aaa ccc
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 "#
2314 .unindent();
2315
2316 let cx = cx.add_empty_window();
2317 let editor = cx.new_view(|cx| {
2318 let multibuffer = MultiBuffer::build_multi(
2319 [
2320 (
2321 &text,
2322 vec![
2323 Point::new(0, 0)..Point::new(2, 0),
2324 Point::new(4, 0)..Point::new(5, 0),
2325 ],
2326 ),
2327 (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2328 ],
2329 cx,
2330 );
2331 Editor::for_multibuffer(multibuffer, None, false, cx)
2332 });
2333
2334 let search_bar = cx.new_view(|cx| {
2335 let mut search_bar = BufferSearchBar::new(cx);
2336 search_bar.set_active_pane_item(Some(&editor), cx);
2337 search_bar.show(cx);
2338 search_bar
2339 });
2340
2341 editor.update(cx, |editor, cx| {
2342 editor.change_selections(None, cx, |s| {
2343 s.select_ranges(vec![
2344 Point::new(1, 0)..Point::new(1, 4),
2345 Point::new(5, 3)..Point::new(6, 4),
2346 ])
2347 })
2348 });
2349
2350 search_bar.update(cx, |search_bar, cx| {
2351 let deploy = Deploy {
2352 focus: true,
2353 replace_enabled: false,
2354 selection_search_enabled: true,
2355 };
2356 search_bar.deploy(&deploy, cx);
2357 });
2358
2359 cx.run_until_parked();
2360
2361 search_bar
2362 .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2363 .await
2364 .unwrap();
2365
2366 editor.update(cx, |editor, cx| {
2367 assert_eq!(
2368 editor.search_background_highlights(cx),
2369 &[
2370 Point::new(1, 0)..Point::new(1, 3),
2371 Point::new(5, 8)..Point::new(5, 11),
2372 Point::new(6, 0)..Point::new(6, 3),
2373 ]
2374 );
2375 });
2376 }
2377
2378 #[gpui::test]
2379 async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2380 let (editor, search_bar, cx) = init_test(cx);
2381 // Search using valid regexp
2382 search_bar
2383 .update(cx, |search_bar, cx| {
2384 search_bar.enable_search_option(SearchOptions::REGEX, cx);
2385 search_bar.search("expression", None, cx)
2386 })
2387 .await
2388 .unwrap();
2389 editor.update(cx, |editor, cx| {
2390 assert_eq!(
2391 display_points_of(editor.all_text_background_highlights(cx)),
2392 &[
2393 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2394 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2395 ],
2396 );
2397 });
2398
2399 // Now, the expression is invalid
2400 search_bar
2401 .update(cx, |search_bar, cx| {
2402 search_bar.search("expression (", None, cx)
2403 })
2404 .await
2405 .unwrap_err();
2406 editor.update(cx, |editor, cx| {
2407 assert!(display_points_of(editor.all_text_background_highlights(cx)).is_empty(),);
2408 });
2409 }
2410
2411 #[gpui::test]
2412 async fn test_search_options_changes(cx: &mut TestAppContext) {
2413 let (_editor, search_bar, cx) = init_test(cx);
2414 update_search_settings(
2415 SearchSettings {
2416 whole_word: false,
2417 case_sensitive: false,
2418 include_ignored: false,
2419 regex: false,
2420 },
2421 cx,
2422 );
2423
2424 let deploy = Deploy {
2425 focus: true,
2426 replace_enabled: false,
2427 selection_search_enabled: true,
2428 };
2429
2430 search_bar.update(cx, |search_bar, cx| {
2431 assert_eq!(
2432 search_bar.search_options,
2433 SearchOptions::NONE,
2434 "Should have no search options enabled by default"
2435 );
2436 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
2437 assert_eq!(
2438 search_bar.search_options,
2439 SearchOptions::WHOLE_WORD,
2440 "Should enable the option toggled"
2441 );
2442 assert!(
2443 !search_bar.dismissed,
2444 "Search bar should be present and visible"
2445 );
2446 search_bar.deploy(&deploy, cx);
2447 assert_eq!(
2448 search_bar.configured_options,
2449 SearchOptions::NONE,
2450 "Should have configured search options matching the settings"
2451 );
2452 assert_eq!(
2453 search_bar.search_options,
2454 SearchOptions::WHOLE_WORD,
2455 "After (re)deploying, the option should still be enabled"
2456 );
2457
2458 search_bar.dismiss(&Dismiss, cx);
2459 search_bar.deploy(&deploy, cx);
2460 assert_eq!(
2461 search_bar.search_options,
2462 SearchOptions::NONE,
2463 "After hiding and showing the search bar, default options should be used"
2464 );
2465
2466 search_bar.toggle_search_option(SearchOptions::REGEX, cx);
2467 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
2468 assert_eq!(
2469 search_bar.search_options,
2470 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2471 "Should enable the options toggled"
2472 );
2473 assert!(
2474 !search_bar.dismissed,
2475 "Search bar should be present and visible"
2476 );
2477 });
2478
2479 update_search_settings(
2480 SearchSettings {
2481 whole_word: false,
2482 case_sensitive: true,
2483 include_ignored: false,
2484 regex: false,
2485 },
2486 cx,
2487 );
2488 search_bar.update(cx, |search_bar, cx| {
2489 assert_eq!(
2490 search_bar.search_options,
2491 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2492 "Should have no search options enabled by default"
2493 );
2494
2495 search_bar.deploy(&deploy, cx);
2496 assert_eq!(
2497 search_bar.configured_options,
2498 SearchOptions::CASE_SENSITIVE,
2499 "Should have configured search options matching the settings"
2500 );
2501 assert_eq!(
2502 search_bar.search_options,
2503 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2504 "Toggling a non-dismissed search bar with custom options should not change the default options"
2505 );
2506 search_bar.dismiss(&Dismiss, cx);
2507 search_bar.deploy(&deploy, cx);
2508 assert_eq!(
2509 search_bar.search_options,
2510 SearchOptions::CASE_SENSITIVE,
2511 "After hiding and showing the search bar, default options should be used"
2512 );
2513 });
2514 }
2515
2516 fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2517 cx.update(|cx| {
2518 SettingsStore::update_global(cx, |store, cx| {
2519 store.update_user_settings::<EditorSettings>(cx, |settings| {
2520 settings.search = Some(search_settings);
2521 });
2522 });
2523 });
2524 }
2525}