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