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