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