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