1mod registrar;
2
3use crate::{
4 FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOption,
5 SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, SelectPreviousMatch,
6 ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
7 buffer_search::registrar::WithResultsOrExternalQuery,
8 search_bar::{
9 ActionButtonState, alignment_element, filter_search_results_input, input_base_styles,
10 render_action_button, render_text_input,
11 },
12};
13use any_vec::AnyVec;
14use collections::HashMap;
15use editor::{
16 DisplayPoint, Editor, EditorSettings, MultiBufferOffset, SplittableEditor, ToggleSplitDiff,
17 actions::{Backtab, FoldAll, Tab, ToggleFoldAll, UnfoldAll},
18};
19use futures::channel::oneshot;
20use gpui::{
21 Action, App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _,
22 IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task,
23 WeakEntity, Window, div,
24};
25use language::{Language, LanguageRegistry};
26use project::{
27 search::SearchQuery,
28 search_history::{SearchHistory, SearchHistoryCursor},
29};
30
31use fs::Fs;
32use settings::{DiffViewStyle, Settings, update_settings_file};
33use std::{any::TypeId, sync::Arc};
34use zed_actions::{outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath};
35
36use ui::{
37 BASE_REM_SIZE_IN_PX, IconButtonShape, PlatformStyle, TextSize, Tooltip, prelude::*,
38 render_modifiers, utils::SearchInputWidth,
39};
40use util::{ResultExt, paths::PathMatcher};
41use workspace::{
42 ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
43 item::{ItemBufferKind, ItemHandle},
44 searchable::{
45 CollapseDirection, Direction, FilteredSearchRange, SearchEvent, SearchToken,
46 SearchableItemHandle, WeakSearchableItemHandle,
47 },
48};
49
50pub use registrar::{DivRegistrar, register_pane_search_actions};
51use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar};
52
53const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
54
55pub use zed_actions::buffer_search::{Deploy, DeployReplace, Dismiss, FocusEditor};
56
57pub enum Event {
58 UpdateLocation,
59 Dismissed,
60}
61
62pub fn init(cx: &mut App) {
63 cx.observe_new(|workspace: &mut Workspace, _, _| BufferSearchBar::register(workspace))
64 .detach();
65}
66
67pub struct BufferSearchBar {
68 query_editor: Entity<Editor>,
69 query_editor_focused: bool,
70 replacement_editor: Entity<Editor>,
71 replacement_editor_focused: bool,
72 active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
73 active_match_index: Option<usize>,
74 #[cfg(target_os = "macos")]
75 active_searchable_item_subscriptions: Option<[Subscription; 2]>,
76 #[cfg(not(target_os = "macos"))]
77 active_searchable_item_subscriptions: Option<Subscription>,
78 #[cfg(target_os = "macos")]
79 pending_external_query: Option<(String, SearchOptions)>,
80 active_search: Option<Arc<SearchQuery>>,
81 searchable_items_with_matches:
82 HashMap<Box<dyn WeakSearchableItemHandle>, (AnyVec<dyn Send>, SearchToken)>,
83 pending_search: Option<Task<()>>,
84 search_options: SearchOptions,
85 default_options: SearchOptions,
86 configured_options: SearchOptions,
87 query_error: Option<String>,
88 dismissed: bool,
89 search_history: SearchHistory,
90 search_history_cursor: SearchHistoryCursor,
91 replace_enabled: bool,
92 selection_search_enabled: Option<FilteredSearchRange>,
93 scroll_handle: ScrollHandle,
94 editor_scroll_handle: ScrollHandle,
95 editor_needed_width: Pixels,
96 regex_language: Option<Arc<Language>>,
97 is_collapsed: bool,
98 splittable_editor: Option<WeakEntity<SplittableEditor>>,
99 _splittable_editor_subscription: Option<Subscription>,
100}
101
102impl EventEmitter<Event> for BufferSearchBar {}
103impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
104impl Render for BufferSearchBar {
105 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
106 let focus_handle = self.focus_handle(cx);
107
108 let has_splittable_editor = self.splittable_editor.is_some();
109 let split_buttons = if has_splittable_editor {
110 self.splittable_editor
111 .as_ref()
112 .and_then(|weak| weak.upgrade())
113 .map(|splittable_editor| {
114 let is_split = splittable_editor.read(cx).is_split();
115 let focus_handle = splittable_editor.focus_handle(cx);
116 h_flex()
117 .gap_1()
118 .child(
119 IconButton::new("diff-unified", IconName::DiffUnified)
120 .shape(IconButtonShape::Square)
121 .toggle_state(!is_split)
122 .tooltip(Tooltip::element(move |_, cx| {
123 v_flex()
124 .gap_1()
125 .child(Label::new("Unified"))
126 .child(
127 h_flex()
128 .gap_0p5()
129 .text_sm()
130 .text_color(Color::Muted.color(cx))
131 .children(render_modifiers(
132 &gpui::Modifiers::secondary_key(),
133 PlatformStyle::platform(),
134 None,
135 Some(TextSize::Default.rems(cx).into()),
136 false,
137 ))
138 .child("click to set as default"),
139 )
140 .into_any()
141 }))
142 .on_click({
143 let focus_handle = focus_handle.clone();
144 move |_, window, cx| {
145 if window.modifiers().secondary() {
146 update_settings_file(
147 <dyn Fs>::global(cx),
148 cx,
149 |settings, _| {
150 settings.editor.diff_view_style =
151 Some(DiffViewStyle::Unified);
152 },
153 );
154 }
155 if is_split {
156 focus_handle.focus(window, cx);
157 window
158 .dispatch_action(ToggleSplitDiff.boxed_clone(), cx);
159 }
160 }
161 }),
162 )
163 .child(
164 IconButton::new("diff-split", IconName::DiffSplit)
165 .shape(IconButtonShape::Square)
166 .toggle_state(is_split)
167 .tooltip(Tooltip::element(move |_, cx| {
168 v_flex()
169 .gap_1()
170 .child(Label::new("Split"))
171 .child(
172 h_flex()
173 .gap_0p5()
174 .text_sm()
175 .text_color(Color::Muted.color(cx))
176 .children(render_modifiers(
177 &gpui::Modifiers::secondary_key(),
178 PlatformStyle::platform(),
179 None,
180 Some(TextSize::Default.rems(cx).into()),
181 false,
182 ))
183 .child("click to set as default"),
184 )
185 .into_any()
186 }))
187 .on_click({
188 move |_, window, cx| {
189 if window.modifiers().secondary() {
190 update_settings_file(
191 <dyn Fs>::global(cx),
192 cx,
193 |settings, _| {
194 settings.editor.diff_view_style =
195 Some(DiffViewStyle::Split);
196 },
197 );
198 }
199 if !is_split {
200 focus_handle.focus(window, cx);
201 window
202 .dispatch_action(ToggleSplitDiff.boxed_clone(), cx);
203 }
204 }
205 }),
206 )
207 })
208 } else {
209 None
210 };
211
212 let collapse_expand_button = if self.needs_expand_collapse_option(cx) {
213 let query_editor_focus = self.query_editor.focus_handle(cx);
214
215 let (icon, tooltip_label) = if self.is_collapsed {
216 (IconName::ChevronUpDown, "Expand All Files")
217 } else {
218 (IconName::ChevronDownUp, "Collapse All Files")
219 };
220
221 let collapse_expand_icon_button = |id| {
222 IconButton::new(id, icon)
223 .shape(IconButtonShape::Square)
224 .tooltip(move |_, cx| {
225 Tooltip::for_action_in(
226 tooltip_label,
227 &ToggleFoldAll,
228 &query_editor_focus,
229 cx,
230 )
231 })
232 .on_click(|_event, window, cx| {
233 window.dispatch_action(ToggleFoldAll.boxed_clone(), cx)
234 })
235 };
236
237 if self.dismissed {
238 return h_flex()
239 .pl_0p5()
240 .gap_1()
241 .child(collapse_expand_icon_button(
242 "multibuffer-collapse-expand-empty",
243 ))
244 .when(has_splittable_editor, |this| this.children(split_buttons))
245 .into_any_element();
246 }
247
248 Some(
249 h_flex()
250 .gap_1()
251 .child(collapse_expand_icon_button("multibuffer-collapse-expand"))
252 .children(split_buttons)
253 .into_any_element(),
254 )
255 } else {
256 None
257 };
258
259 let narrow_mode =
260 self.scroll_handle.bounds().size.width / window.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
261 let hide_inline_icons = self.editor_needed_width
262 > self.editor_scroll_handle.bounds().size.width - window.rem_size() * 6.;
263
264 let workspace::searchable::SearchOptions {
265 case,
266 word,
267 regex,
268 replacement,
269 selection,
270 find_in_results,
271 } = self.supported_options(cx);
272
273 self.query_editor.update(cx, |query_editor, cx| {
274 if query_editor.placeholder_text(cx).is_none() {
275 query_editor.set_placeholder_text("Search…", window, cx);
276 }
277 });
278
279 self.replacement_editor.update(cx, |editor, cx| {
280 editor.set_placeholder_text("Replace with…", window, cx);
281 });
282
283 let mut color_override = None;
284 let match_text = self
285 .active_searchable_item
286 .as_ref()
287 .and_then(|searchable_item| {
288 if self.query(cx).is_empty() {
289 return None;
290 }
291 let matches_count = self
292 .searchable_items_with_matches
293 .get(&searchable_item.downgrade())
294 .map(|(matches, _)| matches.len())
295 .unwrap_or(0);
296 if let Some(match_ix) = self.active_match_index {
297 Some(format!("{}/{}", match_ix + 1, matches_count))
298 } else {
299 color_override = Some(Color::Error); // No matches found
300 None
301 }
302 })
303 .unwrap_or_else(|| "0/0".to_string());
304 let should_show_replace_input = self.replace_enabled && replacement;
305 let in_replace = self.replacement_editor.focus_handle(cx).is_focused(window);
306
307 let theme_colors = cx.theme().colors();
308 let query_border = if self.query_error.is_some() {
309 Color::Error.color(cx)
310 } else {
311 theme_colors.border
312 };
313 let replacement_border = theme_colors.border;
314
315 let container_width = window.viewport_size().width;
316 let input_width = SearchInputWidth::calc_width(container_width);
317
318 let input_base_styles =
319 |border_color| input_base_styles(border_color, |div| div.w(input_width));
320
321 let input_style = if find_in_results {
322 filter_search_results_input(query_border, |div| div.w(input_width), cx)
323 } else {
324 input_base_styles(query_border)
325 };
326
327 let query_column = input_style
328 .id("editor-scroll")
329 .track_scroll(&self.editor_scroll_handle)
330 .child(render_text_input(&self.query_editor, color_override, cx))
331 .when(!hide_inline_icons, |div| {
332 div.child(
333 h_flex()
334 .gap_1()
335 .when(case, |div| {
336 div.child(SearchOption::CaseSensitive.as_button(
337 self.search_options,
338 SearchSource::Buffer,
339 focus_handle.clone(),
340 ))
341 })
342 .when(word, |div| {
343 div.child(SearchOption::WholeWord.as_button(
344 self.search_options,
345 SearchSource::Buffer,
346 focus_handle.clone(),
347 ))
348 })
349 .when(regex, |div| {
350 div.child(SearchOption::Regex.as_button(
351 self.search_options,
352 SearchSource::Buffer,
353 focus_handle.clone(),
354 ))
355 }),
356 )
357 });
358
359 let mode_column = h_flex()
360 .gap_1()
361 .min_w_64()
362 .when(replacement, |this| {
363 this.child(render_action_button(
364 "buffer-search-bar-toggle",
365 IconName::Replace,
366 self.replace_enabled.then_some(ActionButtonState::Toggled),
367 "Toggle Replace",
368 &ToggleReplace,
369 focus_handle.clone(),
370 ))
371 })
372 .when(selection, |this| {
373 this.child(
374 IconButton::new(
375 "buffer-search-bar-toggle-search-selection-button",
376 IconName::Quote,
377 )
378 .style(ButtonStyle::Subtle)
379 .shape(IconButtonShape::Square)
380 .when(self.selection_search_enabled.is_some(), |button| {
381 button.style(ButtonStyle::Filled)
382 })
383 .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
384 this.toggle_selection(&ToggleSelection, window, cx);
385 }))
386 .toggle_state(self.selection_search_enabled.is_some())
387 .tooltip({
388 let focus_handle = focus_handle.clone();
389 move |_window, cx| {
390 Tooltip::for_action_in(
391 "Toggle Search Selection",
392 &ToggleSelection,
393 &focus_handle,
394 cx,
395 )
396 }
397 }),
398 )
399 })
400 .when(!find_in_results, |el| {
401 let query_focus = self.query_editor.focus_handle(cx);
402 let matches_column = h_flex()
403 .pl_2()
404 .ml_2()
405 .border_l_1()
406 .border_color(theme_colors.border_variant)
407 .child(render_action_button(
408 "buffer-search-nav-button",
409 ui::IconName::ChevronLeft,
410 self.active_match_index
411 .is_none()
412 .then_some(ActionButtonState::Disabled),
413 "Select Previous Match",
414 &SelectPreviousMatch,
415 query_focus.clone(),
416 ))
417 .child(render_action_button(
418 "buffer-search-nav-button",
419 ui::IconName::ChevronRight,
420 self.active_match_index
421 .is_none()
422 .then_some(ActionButtonState::Disabled),
423 "Select Next Match",
424 &SelectNextMatch,
425 query_focus.clone(),
426 ))
427 .when(!narrow_mode, |this| {
428 this.child(div().ml_2().min_w(rems_from_px(40.)).child(
429 Label::new(match_text).size(LabelSize::Small).color(
430 if self.active_match_index.is_some() {
431 Color::Default
432 } else {
433 Color::Disabled
434 },
435 ),
436 ))
437 });
438
439 el.child(render_action_button(
440 "buffer-search-nav-button",
441 IconName::SelectAll,
442 Default::default(),
443 "Select All Matches",
444 &SelectAllMatches,
445 query_focus,
446 ))
447 .child(matches_column)
448 })
449 .when(find_in_results, |el| {
450 el.child(render_action_button(
451 "buffer-search",
452 IconName::Close,
453 Default::default(),
454 "Close Search Bar",
455 &Dismiss,
456 focus_handle.clone(),
457 ))
458 });
459
460 let has_collapse_button = collapse_expand_button.is_some();
461
462 let search_line = h_flex()
463 .w_full()
464 .gap_2()
465 .when(find_in_results, |el| el.child(alignment_element()))
466 .when(!find_in_results && has_collapse_button, |el| {
467 el.pl_0p5().child(collapse_expand_button.expect("button"))
468 })
469 .child(query_column)
470 .child(mode_column);
471
472 let replace_line =
473 should_show_replace_input.then(|| {
474 let replace_column = input_base_styles(replacement_border)
475 .child(render_text_input(&self.replacement_editor, None, cx));
476 let focus_handle = self.replacement_editor.read(cx).focus_handle(cx);
477
478 let replace_actions = h_flex()
479 .min_w_64()
480 .gap_1()
481 .child(render_action_button(
482 "buffer-search-replace-button",
483 IconName::ReplaceNext,
484 Default::default(),
485 "Replace Next Match",
486 &ReplaceNext,
487 focus_handle.clone(),
488 ))
489 .child(render_action_button(
490 "buffer-search-replace-button",
491 IconName::ReplaceAll,
492 Default::default(),
493 "Replace All Matches",
494 &ReplaceAll,
495 focus_handle,
496 ));
497
498 h_flex()
499 .w_full()
500 .gap_2()
501 .when(has_collapse_button, |this| this.child(alignment_element()))
502 .child(replace_column)
503 .child(replace_actions)
504 });
505
506 let mut key_context = KeyContext::new_with_defaults();
507 key_context.add("BufferSearchBar");
508 if in_replace {
509 key_context.add("in_replace");
510 }
511
512 let query_error_line = self.query_error.as_ref().map(|error| {
513 Label::new(error)
514 .size(LabelSize::Small)
515 .color(Color::Error)
516 .mt_neg_1()
517 .ml_2()
518 });
519
520 let search_line =
521 h_flex()
522 .relative()
523 .child(search_line)
524 .when(!narrow_mode && !find_in_results, |this| {
525 this.child(
526 h_flex()
527 .absolute()
528 .right_0()
529 .when(has_collapse_button, |this| {
530 this.pr_2()
531 .border_r_1()
532 .border_color(cx.theme().colors().border_variant)
533 })
534 .child(render_action_button(
535 "buffer-search",
536 IconName::Close,
537 Default::default(),
538 "Close Search Bar",
539 &Dismiss,
540 focus_handle.clone(),
541 )),
542 )
543 });
544
545 v_flex()
546 .id("buffer_search")
547 .gap_2()
548 .w_full()
549 .track_scroll(&self.scroll_handle)
550 .key_context(key_context)
551 .capture_action(cx.listener(Self::tab))
552 .capture_action(cx.listener(Self::backtab))
553 .capture_action(cx.listener(Self::toggle_fold_all))
554 .on_action(cx.listener(Self::previous_history_query))
555 .on_action(cx.listener(Self::next_history_query))
556 .on_action(cx.listener(Self::dismiss))
557 .on_action(cx.listener(Self::select_next_match))
558 .on_action(cx.listener(Self::select_prev_match))
559 .on_action(cx.listener(|this, _: &ToggleOutline, window, cx| {
560 if let Some(active_searchable_item) = &mut this.active_searchable_item {
561 active_searchable_item.relay_action(Box::new(ToggleOutline), window, cx);
562 }
563 }))
564 .on_action(cx.listener(|this, _: &CopyPath, window, cx| {
565 if let Some(active_searchable_item) = &mut this.active_searchable_item {
566 active_searchable_item.relay_action(Box::new(CopyPath), window, cx);
567 }
568 }))
569 .on_action(cx.listener(|this, _: &CopyRelativePath, window, cx| {
570 if let Some(active_searchable_item) = &mut this.active_searchable_item {
571 active_searchable_item.relay_action(Box::new(CopyRelativePath), window, cx);
572 }
573 }))
574 .when(replacement, |this| {
575 this.on_action(cx.listener(Self::toggle_replace))
576 .on_action(cx.listener(Self::replace_next))
577 .on_action(cx.listener(Self::replace_all))
578 })
579 .when(case, |this| {
580 this.on_action(cx.listener(Self::toggle_case_sensitive))
581 })
582 .when(word, |this| {
583 this.on_action(cx.listener(Self::toggle_whole_word))
584 })
585 .when(regex, |this| {
586 this.on_action(cx.listener(Self::toggle_regex))
587 })
588 .when(selection, |this| {
589 this.on_action(cx.listener(Self::toggle_selection))
590 })
591 .child(search_line)
592 .children(query_error_line)
593 .children(replace_line)
594 .into_any_element()
595 }
596}
597
598impl Focusable for BufferSearchBar {
599 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
600 self.query_editor.focus_handle(cx)
601 }
602}
603
604impl ToolbarItemView for BufferSearchBar {
605 fn contribute_context(&self, context: &mut KeyContext, _cx: &App) {
606 if !self.dismissed {
607 context.add("buffer_search_deployed");
608 }
609 }
610
611 fn set_active_pane_item(
612 &mut self,
613 item: Option<&dyn ItemHandle>,
614 window: &mut Window,
615 cx: &mut Context<Self>,
616 ) -> ToolbarItemLocation {
617 cx.notify();
618 self.active_searchable_item_subscriptions.take();
619 self.active_searchable_item.take();
620 self.splittable_editor = None;
621 self._splittable_editor_subscription = None;
622
623 self.pending_search.take();
624
625 if let Some(splittable_editor) = item
626 .and_then(|item| item.act_as_type(TypeId::of::<SplittableEditor>(), cx))
627 .and_then(|entity| entity.downcast::<SplittableEditor>().ok())
628 {
629 self._splittable_editor_subscription =
630 Some(cx.observe(&splittable_editor, |_, _, cx| cx.notify()));
631 self.splittable_editor = Some(splittable_editor.downgrade());
632 }
633
634 if let Some(searchable_item_handle) =
635 item.and_then(|item| item.to_searchable_item_handle(cx))
636 {
637 let this = cx.entity().downgrade();
638
639 let search_event_subscription = searchable_item_handle.subscribe_to_search_events(
640 window,
641 cx,
642 Box::new(move |search_event, window, cx| {
643 if let Some(this) = this.upgrade() {
644 this.update(cx, |this, cx| {
645 this.on_active_searchable_item_event(search_event, window, cx)
646 });
647 }
648 }),
649 );
650
651 #[cfg(target_os = "macos")]
652 {
653 let item_focus_handle = searchable_item_handle.item_focus_handle(cx);
654
655 self.active_searchable_item_subscriptions = Some([
656 search_event_subscription,
657 cx.on_focus(&item_focus_handle, window, |this, window, cx| {
658 if this.query_editor_focused || this.replacement_editor_focused {
659 // no need to read pasteboard since focus came from toolbar
660 return;
661 }
662
663 cx.defer_in(window, |this, window, cx| {
664 let Some(item) = cx.read_from_find_pasteboard() else {
665 return;
666 };
667 let Some(text) = item.text() else {
668 return;
669 };
670
671 if this.query(cx) == text {
672 return;
673 }
674
675 let search_options = item
676 .metadata()
677 .and_then(|m| m.parse().ok())
678 .and_then(SearchOptions::from_bits)
679 .unwrap_or(this.search_options);
680
681 if this.dismissed {
682 this.pending_external_query = Some((text, search_options));
683 } else {
684 drop(this.search(&text, Some(search_options), true, window, cx));
685 }
686 });
687 }),
688 ]);
689 }
690 #[cfg(not(target_os = "macos"))]
691 {
692 self.active_searchable_item_subscriptions = Some(search_event_subscription);
693 }
694
695 let is_project_search = searchable_item_handle.supported_options(cx).find_in_results;
696 self.active_searchable_item = Some(searchable_item_handle);
697 drop(self.update_matches(true, false, window, cx));
698 if self.needs_expand_collapse_option(cx) {
699 return ToolbarItemLocation::PrimaryLeft;
700 } else if !self.is_dismissed() {
701 if is_project_search {
702 self.dismiss(&Default::default(), window, cx);
703 } else {
704 return ToolbarItemLocation::Secondary;
705 }
706 }
707 }
708 ToolbarItemLocation::Hidden
709 }
710}
711
712impl BufferSearchBar {
713 pub fn query_editor_focused(&self) -> bool {
714 self.query_editor_focused
715 }
716
717 pub fn register(registrar: &mut impl SearchActionsRegistrar) {
718 registrar.register_handler(ForDeployed(|this, _: &FocusSearch, window, cx| {
719 this.query_editor.focus_handle(cx).focus(window, cx);
720 this.select_query(window, cx);
721 }));
722 registrar.register_handler(ForDeployed(
723 |this, action: &ToggleCaseSensitive, window, cx| {
724 if this.supported_options(cx).case {
725 this.toggle_case_sensitive(action, window, cx);
726 }
727 },
728 ));
729 registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, window, cx| {
730 if this.supported_options(cx).word {
731 this.toggle_whole_word(action, window, cx);
732 }
733 }));
734 registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, window, cx| {
735 if this.supported_options(cx).regex {
736 this.toggle_regex(action, window, cx);
737 }
738 }));
739 registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, window, cx| {
740 if this.supported_options(cx).selection {
741 this.toggle_selection(action, window, cx);
742 } else {
743 cx.propagate();
744 }
745 }));
746 registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, window, cx| {
747 if this.supported_options(cx).replacement {
748 this.toggle_replace(action, window, cx);
749 } else {
750 cx.propagate();
751 }
752 }));
753 registrar.register_handler(WithResultsOrExternalQuery(
754 |this, action: &SelectNextMatch, window, cx| {
755 if this.supported_options(cx).find_in_results {
756 cx.propagate();
757 } else {
758 this.select_next_match(action, window, cx);
759 }
760 },
761 ));
762 registrar.register_handler(WithResultsOrExternalQuery(
763 |this, action: &SelectPreviousMatch, window, cx| {
764 if this.supported_options(cx).find_in_results {
765 cx.propagate();
766 } else {
767 this.select_prev_match(action, window, cx);
768 }
769 },
770 ));
771 registrar.register_handler(WithResultsOrExternalQuery(
772 |this, action: &SelectAllMatches, window, cx| {
773 if this.supported_options(cx).find_in_results {
774 cx.propagate();
775 } else {
776 this.select_all_matches(action, window, cx);
777 }
778 },
779 ));
780 registrar.register_handler(ForDeployed(
781 |this, _: &editor::actions::Cancel, window, cx| {
782 this.dismiss(&Dismiss, window, cx);
783 },
784 ));
785 registrar.register_handler(ForDeployed(|this, _: &Dismiss, window, cx| {
786 this.dismiss(&Dismiss, window, cx);
787 }));
788
789 // register deploy buffer search for both search bar states, since we want to focus into the search bar
790 // when the deploy action is triggered in the buffer.
791 registrar.register_handler(ForDeployed(|this, deploy, window, cx| {
792 this.deploy(deploy, window, cx);
793 }));
794 registrar.register_handler(ForDismissed(|this, deploy, window, cx| {
795 this.deploy(deploy, window, cx);
796 }));
797 registrar.register_handler(ForDeployed(|this, _: &DeployReplace, window, cx| {
798 if this.supported_options(cx).find_in_results {
799 cx.propagate();
800 } else {
801 this.deploy(&Deploy::replace(), window, cx);
802 }
803 }));
804 registrar.register_handler(ForDismissed(|this, _: &DeployReplace, window, cx| {
805 if this.supported_options(cx).find_in_results {
806 cx.propagate();
807 } else {
808 this.deploy(&Deploy::replace(), window, cx);
809 }
810 }));
811 }
812
813 pub fn new(
814 languages: Option<Arc<LanguageRegistry>>,
815 window: &mut Window,
816 cx: &mut Context<Self>,
817 ) -> Self {
818 let query_editor = cx.new(|cx| {
819 let mut editor = Editor::single_line(window, cx);
820 editor.set_use_autoclose(false);
821 editor
822 });
823 cx.subscribe_in(&query_editor, window, Self::on_query_editor_event)
824 .detach();
825 let replacement_editor = cx.new(|cx| Editor::single_line(window, cx));
826 cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
827 .detach();
828
829 let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
830 if let Some(languages) = languages {
831 let query_buffer = query_editor
832 .read(cx)
833 .buffer()
834 .read(cx)
835 .as_singleton()
836 .expect("query editor should be backed by a singleton buffer");
837
838 query_buffer
839 .read(cx)
840 .set_language_registry(languages.clone());
841
842 cx.spawn(async move |buffer_search_bar, cx| {
843 use anyhow::Context as _;
844
845 let regex_language = languages
846 .language_for_name("regex")
847 .await
848 .context("loading regex language")?;
849
850 buffer_search_bar
851 .update(cx, |buffer_search_bar, cx| {
852 buffer_search_bar.regex_language = Some(regex_language);
853 buffer_search_bar.adjust_query_regex_language(cx);
854 })
855 .ok();
856 anyhow::Ok(())
857 })
858 .detach_and_log_err(cx);
859 }
860
861 Self {
862 query_editor,
863 query_editor_focused: false,
864 replacement_editor,
865 replacement_editor_focused: false,
866 active_searchable_item: None,
867 active_searchable_item_subscriptions: None,
868 #[cfg(target_os = "macos")]
869 pending_external_query: None,
870 active_match_index: None,
871 searchable_items_with_matches: Default::default(),
872 default_options: search_options,
873 configured_options: search_options,
874 search_options,
875 pending_search: None,
876 query_error: None,
877 dismissed: true,
878 search_history: SearchHistory::new(
879 Some(MAX_BUFFER_SEARCH_HISTORY_SIZE),
880 project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
881 ),
882 search_history_cursor: Default::default(),
883 active_search: None,
884 replace_enabled: false,
885 selection_search_enabled: None,
886 scroll_handle: ScrollHandle::new(),
887 editor_scroll_handle: ScrollHandle::new(),
888 editor_needed_width: px(0.),
889 regex_language: None,
890 is_collapsed: false,
891 splittable_editor: None,
892 _splittable_editor_subscription: None,
893 }
894 }
895
896 pub fn is_dismissed(&self) -> bool {
897 self.dismissed
898 }
899
900 pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
901 self.dismissed = true;
902 cx.emit(Event::Dismissed);
903 self.query_error = None;
904 self.sync_select_next_case_sensitivity(cx);
905
906 for searchable_item in self.searchable_items_with_matches.keys() {
907 if let Some(searchable_item) =
908 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
909 {
910 searchable_item.clear_matches(window, cx);
911 }
912 }
913
914 let needs_collapse_expand = self.needs_expand_collapse_option(cx);
915
916 if let Some(active_editor) = self.active_searchable_item.as_mut() {
917 self.selection_search_enabled = None;
918 self.replace_enabled = false;
919 active_editor.search_bar_visibility_changed(false, window, cx);
920 active_editor.toggle_filtered_search_ranges(None, window, cx);
921 let handle = active_editor.item_focus_handle(cx);
922 self.focus(&handle, window, cx);
923 }
924
925 if needs_collapse_expand {
926 cx.emit(Event::UpdateLocation);
927 cx.emit(ToolbarItemEvent::ChangeLocation(
928 ToolbarItemLocation::PrimaryLeft,
929 ));
930 cx.notify();
931 return;
932 }
933 cx.emit(Event::UpdateLocation);
934 cx.emit(ToolbarItemEvent::ChangeLocation(
935 ToolbarItemLocation::Hidden,
936 ));
937 cx.notify();
938 }
939
940 pub fn deploy(&mut self, deploy: &Deploy, window: &mut Window, cx: &mut Context<Self>) -> bool {
941 let filtered_search_range = if deploy.selection_search_enabled {
942 Some(FilteredSearchRange::Default)
943 } else {
944 None
945 };
946 if self.show(window, cx) {
947 if let Some(active_item) = self.active_searchable_item.as_mut() {
948 active_item.toggle_filtered_search_ranges(filtered_search_range, window, cx);
949 }
950 self.search_suggested(window, cx);
951 self.smartcase(window, cx);
952 self.sync_select_next_case_sensitivity(cx);
953 self.replace_enabled |= deploy.replace_enabled;
954 self.selection_search_enabled =
955 self.selection_search_enabled
956 .or(if deploy.selection_search_enabled {
957 Some(FilteredSearchRange::Default)
958 } else {
959 None
960 });
961 if deploy.focus {
962 let mut handle = self.query_editor.focus_handle(cx);
963 let mut select_query = true;
964 if deploy.replace_enabled && handle.is_focused(window) {
965 handle = self.replacement_editor.focus_handle(cx);
966 select_query = false;
967 };
968
969 if select_query {
970 self.select_query(window, cx);
971 }
972
973 window.focus(&handle, cx);
974 }
975 return true;
976 }
977
978 cx.propagate();
979 false
980 }
981
982 pub fn toggle(&mut self, action: &Deploy, window: &mut Window, cx: &mut Context<Self>) {
983 if self.is_dismissed() {
984 self.deploy(action, window, cx);
985 } else {
986 self.dismiss(&Dismiss, window, cx);
987 }
988 }
989
990 pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
991 let Some(handle) = self.active_searchable_item.as_ref() else {
992 return false;
993 };
994
995 let configured_options =
996 SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
997 let settings_changed = configured_options != self.configured_options;
998
999 if self.dismissed && settings_changed {
1000 // Only update configuration options when search bar is dismissed,
1001 // so we don't miss updates even after calling show twice
1002 self.configured_options = configured_options;
1003 self.search_options = configured_options;
1004 self.default_options = configured_options;
1005 }
1006
1007 // This isn't a normal setting; it's only applicable to vim search.
1008 self.search_options.remove(SearchOptions::BACKWARDS);
1009
1010 self.dismissed = false;
1011 self.adjust_query_regex_language(cx);
1012 handle.search_bar_visibility_changed(true, window, cx);
1013 cx.notify();
1014 cx.emit(Event::UpdateLocation);
1015 cx.emit(ToolbarItemEvent::ChangeLocation(
1016 if self.needs_expand_collapse_option(cx) {
1017 ToolbarItemLocation::PrimaryLeft
1018 } else {
1019 ToolbarItemLocation::Secondary
1020 },
1021 ));
1022 true
1023 }
1024
1025 fn supported_options(&self, cx: &mut Context<Self>) -> workspace::searchable::SearchOptions {
1026 self.active_searchable_item
1027 .as_ref()
1028 .map(|item| item.supported_options(cx))
1029 .unwrap_or_default()
1030 }
1031
1032 // We provide an expand/collapse button if we are in a multibuffer
1033 // and not doing a project search.
1034 fn needs_expand_collapse_option(&self, cx: &App) -> bool {
1035 if let Some(item) = &self.active_searchable_item {
1036 let buffer_kind = item.buffer_kind(cx);
1037
1038 if buffer_kind == ItemBufferKind::Singleton {
1039 return false;
1040 }
1041
1042 let workspace::searchable::SearchOptions {
1043 find_in_results, ..
1044 } = item.supported_options(cx);
1045 !find_in_results
1046 } else {
1047 false
1048 }
1049 }
1050
1051 fn toggle_fold_all(&mut self, _: &ToggleFoldAll, window: &mut Window, cx: &mut Context<Self>) {
1052 self.toggle_fold_all_in_item(window, cx);
1053 }
1054
1055 fn toggle_fold_all_in_item(&self, window: &mut Window, cx: &mut Context<Self>) {
1056 let is_collapsed = self.is_collapsed;
1057 if let Some(item) = &self.active_searchable_item {
1058 if let Some(item) = item.act_as_type(TypeId::of::<Editor>(), cx) {
1059 let editor = item.downcast::<Editor>().expect("Is an editor");
1060 editor.update(cx, |editor, cx| {
1061 if is_collapsed {
1062 editor.unfold_all(&UnfoldAll, window, cx);
1063 } else {
1064 editor.fold_all(&FoldAll, window, cx);
1065 }
1066 })
1067 }
1068 }
1069 }
1070
1071 pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1072 let search = self.query_suggestion(window, cx).map(|suggestion| {
1073 self.search(&suggestion, Some(self.default_options), true, window, cx)
1074 });
1075
1076 #[cfg(target_os = "macos")]
1077 let search = search.or_else(|| {
1078 self.pending_external_query
1079 .take()
1080 .map(|(query, options)| self.search(&query, Some(options), true, window, cx))
1081 });
1082
1083 if let Some(search) = search {
1084 cx.spawn_in(window, async move |this, cx| {
1085 if search.await.is_ok() {
1086 this.update_in(cx, |this, window, cx| {
1087 if !this.dismissed {
1088 this.activate_current_match(window, cx)
1089 }
1090 })
1091 } else {
1092 Ok(())
1093 }
1094 })
1095 .detach_and_log_err(cx);
1096 }
1097 }
1098
1099 pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1100 if let Some(match_ix) = self.active_match_index
1101 && let Some(active_searchable_item) = self.active_searchable_item.as_ref()
1102 && let Some((matches, token)) = self
1103 .searchable_items_with_matches
1104 .get(&active_searchable_item.downgrade())
1105 {
1106 active_searchable_item.activate_match(match_ix, matches, *token, window, cx)
1107 }
1108 }
1109
1110 pub fn select_query(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1111 self.query_editor.update(cx, |query_editor, cx| {
1112 query_editor.select_all(&Default::default(), window, cx);
1113 });
1114 }
1115
1116 pub fn query(&self, cx: &App) -> String {
1117 self.query_editor.read(cx).text(cx)
1118 }
1119
1120 pub fn replacement(&self, cx: &mut App) -> String {
1121 self.replacement_editor.read(cx).text(cx)
1122 }
1123
1124 pub fn query_suggestion(
1125 &mut self,
1126 window: &mut Window,
1127 cx: &mut Context<Self>,
1128 ) -> Option<String> {
1129 self.active_searchable_item
1130 .as_ref()
1131 .map(|searchable_item| searchable_item.query_suggestion(window, cx))
1132 .filter(|suggestion| !suggestion.is_empty())
1133 }
1134
1135 pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut Context<Self>) {
1136 if replacement.is_none() {
1137 self.replace_enabled = false;
1138 return;
1139 }
1140 self.replace_enabled = true;
1141 self.replacement_editor
1142 .update(cx, |replacement_editor, cx| {
1143 replacement_editor
1144 .buffer()
1145 .update(cx, |replacement_buffer, cx| {
1146 let len = replacement_buffer.len(cx);
1147 replacement_buffer.edit(
1148 [(MultiBufferOffset(0)..len, replacement.unwrap())],
1149 None,
1150 cx,
1151 );
1152 });
1153 });
1154 }
1155
1156 pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1157 self.focus(&self.replacement_editor.focus_handle(cx), window, cx);
1158 cx.notify();
1159 }
1160
1161 pub fn search(
1162 &mut self,
1163 query: &str,
1164 options: Option<SearchOptions>,
1165 add_to_history: bool,
1166 window: &mut Window,
1167 cx: &mut Context<Self>,
1168 ) -> oneshot::Receiver<()> {
1169 let options = options.unwrap_or(self.default_options);
1170 let updated = query != self.query(cx) || self.search_options != options;
1171 if updated {
1172 self.query_editor.update(cx, |query_editor, cx| {
1173 query_editor.buffer().update(cx, |query_buffer, cx| {
1174 let len = query_buffer.len(cx);
1175 query_buffer.edit([(MultiBufferOffset(0)..len, query)], None, cx);
1176 });
1177 });
1178 self.set_search_options(options, cx);
1179 self.clear_matches(window, cx);
1180 #[cfg(target_os = "macos")]
1181 self.update_find_pasteboard(cx);
1182 cx.notify();
1183 }
1184 self.update_matches(!updated, add_to_history, window, cx)
1185 }
1186
1187 #[cfg(target_os = "macos")]
1188 pub fn update_find_pasteboard(&mut self, cx: &mut App) {
1189 cx.write_to_find_pasteboard(gpui::ClipboardItem::new_string_with_metadata(
1190 self.query(cx),
1191 self.search_options.bits().to_string(),
1192 ));
1193 }
1194
1195 pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
1196 if let Some(active_editor) = self.active_searchable_item.as_ref() {
1197 let handle = active_editor.item_focus_handle(cx);
1198 window.focus(&handle, cx);
1199 }
1200 }
1201
1202 pub fn toggle_search_option(
1203 &mut self,
1204 search_option: SearchOptions,
1205 window: &mut Window,
1206 cx: &mut Context<Self>,
1207 ) {
1208 self.search_options.toggle(search_option);
1209 self.default_options = self.search_options;
1210 drop(self.update_matches(false, false, window, cx));
1211 self.adjust_query_regex_language(cx);
1212 self.sync_select_next_case_sensitivity(cx);
1213 cx.notify();
1214 }
1215
1216 pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
1217 self.search_options.contains(search_option)
1218 }
1219
1220 pub fn enable_search_option(
1221 &mut self,
1222 search_option: SearchOptions,
1223 window: &mut Window,
1224 cx: &mut Context<Self>,
1225 ) {
1226 if !self.search_options.contains(search_option) {
1227 self.toggle_search_option(search_option, window, cx)
1228 }
1229 }
1230
1231 pub fn set_search_within_selection(
1232 &mut self,
1233 search_within_selection: Option<FilteredSearchRange>,
1234 window: &mut Window,
1235 cx: &mut Context<Self>,
1236 ) -> Option<oneshot::Receiver<()>> {
1237 let active_item = self.active_searchable_item.as_mut()?;
1238 self.selection_search_enabled = search_within_selection;
1239 active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx);
1240 cx.notify();
1241 Some(self.update_matches(false, false, window, cx))
1242 }
1243
1244 pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context<Self>) {
1245 self.search_options = search_options;
1246 self.adjust_query_regex_language(cx);
1247 self.sync_select_next_case_sensitivity(cx);
1248 cx.notify();
1249 }
1250
1251 pub fn clear_search_within_ranges(
1252 &mut self,
1253 search_options: SearchOptions,
1254 cx: &mut Context<Self>,
1255 ) {
1256 self.search_options = search_options;
1257 self.adjust_query_regex_language(cx);
1258 cx.notify();
1259 }
1260
1261 fn select_next_match(
1262 &mut self,
1263 _: &SelectNextMatch,
1264 window: &mut Window,
1265 cx: &mut Context<Self>,
1266 ) {
1267 self.select_match(Direction::Next, 1, window, cx);
1268 }
1269
1270 fn select_prev_match(
1271 &mut self,
1272 _: &SelectPreviousMatch,
1273 window: &mut Window,
1274 cx: &mut Context<Self>,
1275 ) {
1276 self.select_match(Direction::Prev, 1, window, cx);
1277 }
1278
1279 pub fn select_all_matches(
1280 &mut self,
1281 _: &SelectAllMatches,
1282 window: &mut Window,
1283 cx: &mut Context<Self>,
1284 ) {
1285 if !self.dismissed
1286 && self.active_match_index.is_some()
1287 && let Some(searchable_item) = self.active_searchable_item.as_ref()
1288 && let Some((matches, token)) = self
1289 .searchable_items_with_matches
1290 .get(&searchable_item.downgrade())
1291 {
1292 searchable_item.select_matches(matches, *token, window, cx);
1293 self.focus_editor(&FocusEditor, window, cx);
1294 }
1295 }
1296
1297 pub fn select_match(
1298 &mut self,
1299 direction: Direction,
1300 count: usize,
1301 window: &mut Window,
1302 cx: &mut Context<Self>,
1303 ) {
1304 #[cfg(target_os = "macos")]
1305 if let Some((query, options)) = self.pending_external_query.take() {
1306 let search_rx = self.search(&query, Some(options), true, window, cx);
1307 cx.spawn_in(window, async move |this, cx| {
1308 if search_rx.await.is_ok() {
1309 this.update_in(cx, |this, window, cx| {
1310 this.activate_current_match(window, cx);
1311 })
1312 .ok();
1313 }
1314 })
1315 .detach();
1316
1317 return;
1318 }
1319
1320 if let Some(index) = self.active_match_index
1321 && let Some(searchable_item) = self.active_searchable_item.as_ref()
1322 && let Some((matches, token)) = self
1323 .searchable_items_with_matches
1324 .get(&searchable_item.downgrade())
1325 .filter(|(matches, _)| !matches.is_empty())
1326 {
1327 // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
1328 if !EditorSettings::get_global(cx).search_wrap
1329 && ((direction == Direction::Next && index + count >= matches.len())
1330 || (direction == Direction::Prev && index < count))
1331 {
1332 crate::show_no_more_matches(window, cx);
1333 return;
1334 }
1335 let new_match_index = searchable_item
1336 .match_index_for_direction(matches, index, direction, count, *token, window, cx);
1337
1338 searchable_item.update_matches(matches, Some(new_match_index), *token, window, cx);
1339 searchable_item.activate_match(new_match_index, matches, *token, window, cx);
1340 }
1341 }
1342
1343 pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1344 if let Some(searchable_item) = self.active_searchable_item.as_ref()
1345 && let Some((matches, token)) = self
1346 .searchable_items_with_matches
1347 .get(&searchable_item.downgrade())
1348 {
1349 if matches.is_empty() {
1350 return;
1351 }
1352 searchable_item.update_matches(matches, Some(0), *token, window, cx);
1353 searchable_item.activate_match(0, matches, *token, window, cx);
1354 }
1355 }
1356
1357 pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1358 if let Some(searchable_item) = self.active_searchable_item.as_ref()
1359 && let Some((matches, token)) = self
1360 .searchable_items_with_matches
1361 .get(&searchable_item.downgrade())
1362 {
1363 if matches.is_empty() {
1364 return;
1365 }
1366 let new_match_index = matches.len() - 1;
1367 searchable_item.update_matches(matches, Some(new_match_index), *token, window, cx);
1368 searchable_item.activate_match(new_match_index, matches, *token, window, cx);
1369 }
1370 }
1371
1372 fn on_query_editor_event(
1373 &mut self,
1374 editor: &Entity<Editor>,
1375 event: &editor::EditorEvent,
1376 window: &mut Window,
1377 cx: &mut Context<Self>,
1378 ) {
1379 match event {
1380 editor::EditorEvent::Focused => self.query_editor_focused = true,
1381 editor::EditorEvent::Blurred => self.query_editor_focused = false,
1382 editor::EditorEvent::Edited { .. } => {
1383 self.smartcase(window, cx);
1384 self.clear_matches(window, cx);
1385 let search = self.update_matches(false, true, window, cx);
1386
1387 let width = editor.update(cx, |editor, cx| {
1388 let text_layout_details = editor.text_layout_details(window, cx);
1389 let snapshot = editor.snapshot(window, cx).display_snapshot;
1390
1391 snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
1392 - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
1393 });
1394 self.editor_needed_width = width;
1395 cx.notify();
1396
1397 cx.spawn_in(window, async move |this, cx| {
1398 if search.await.is_ok() {
1399 this.update_in(cx, |this, window, cx| {
1400 this.activate_current_match(window, cx);
1401 #[cfg(target_os = "macos")]
1402 this.update_find_pasteboard(cx);
1403 })?;
1404 }
1405 anyhow::Ok(())
1406 })
1407 .detach_and_log_err(cx);
1408 }
1409 _ => {}
1410 }
1411 }
1412
1413 fn on_replacement_editor_event(
1414 &mut self,
1415 _: Entity<Editor>,
1416 event: &editor::EditorEvent,
1417 _: &mut Context<Self>,
1418 ) {
1419 match event {
1420 editor::EditorEvent::Focused => self.replacement_editor_focused = true,
1421 editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
1422 _ => {}
1423 }
1424 }
1425
1426 fn on_active_searchable_item_event(
1427 &mut self,
1428 event: &SearchEvent,
1429 window: &mut Window,
1430 cx: &mut Context<Self>,
1431 ) {
1432 match event {
1433 SearchEvent::MatchesInvalidated => {
1434 drop(self.update_matches(false, false, window, cx));
1435 }
1436 SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx),
1437 SearchEvent::ResultsCollapsedChanged(collapse_direction) => {
1438 if self.needs_expand_collapse_option(cx) {
1439 match collapse_direction {
1440 CollapseDirection::Collapsed => self.is_collapsed = true,
1441 CollapseDirection::Expanded => self.is_collapsed = false,
1442 }
1443 }
1444 cx.notify();
1445 }
1446 }
1447 }
1448
1449 fn toggle_case_sensitive(
1450 &mut self,
1451 _: &ToggleCaseSensitive,
1452 window: &mut Window,
1453 cx: &mut Context<Self>,
1454 ) {
1455 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx)
1456 }
1457
1458 fn toggle_whole_word(
1459 &mut self,
1460 _: &ToggleWholeWord,
1461 window: &mut Window,
1462 cx: &mut Context<Self>,
1463 ) {
1464 self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1465 }
1466
1467 fn toggle_selection(
1468 &mut self,
1469 _: &ToggleSelection,
1470 window: &mut Window,
1471 cx: &mut Context<Self>,
1472 ) {
1473 self.set_search_within_selection(
1474 if let Some(_) = self.selection_search_enabled {
1475 None
1476 } else {
1477 Some(FilteredSearchRange::Default)
1478 },
1479 window,
1480 cx,
1481 );
1482 }
1483
1484 fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context<Self>) {
1485 self.toggle_search_option(SearchOptions::REGEX, window, cx)
1486 }
1487
1488 fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) {
1489 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1490 self.active_match_index = None;
1491 self.searchable_items_with_matches
1492 .remove(&active_searchable_item.downgrade());
1493 active_searchable_item.clear_matches(window, cx);
1494 }
1495 }
1496
1497 pub fn has_active_match(&self) -> bool {
1498 self.active_match_index.is_some()
1499 }
1500
1501 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1502 let mut active_item_matches = None;
1503 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
1504 if let Some(searchable_item) =
1505 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
1506 {
1507 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
1508 active_item_matches = Some((searchable_item.downgrade(), matches));
1509 } else {
1510 searchable_item.clear_matches(window, cx);
1511 }
1512 }
1513 }
1514
1515 self.searchable_items_with_matches
1516 .extend(active_item_matches);
1517 }
1518
1519 fn update_matches(
1520 &mut self,
1521 reuse_existing_query: bool,
1522 add_to_history: bool,
1523 window: &mut Window,
1524 cx: &mut Context<Self>,
1525 ) -> oneshot::Receiver<()> {
1526 let (done_tx, done_rx) = oneshot::channel();
1527 let query = self.query(cx);
1528 self.pending_search.take();
1529 #[cfg(target_os = "macos")]
1530 self.pending_external_query.take();
1531
1532 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1533 self.query_error = None;
1534 if query.is_empty() {
1535 self.clear_active_searchable_item_matches(window, cx);
1536 let _ = done_tx.send(());
1537 cx.notify();
1538 } else {
1539 let query: Arc<_> = if let Some(search) =
1540 self.active_search.take().filter(|_| reuse_existing_query)
1541 {
1542 search
1543 } else {
1544 // Value doesn't matter, we only construct empty matchers with it
1545
1546 if self.search_options.contains(SearchOptions::REGEX) {
1547 match SearchQuery::regex(
1548 query,
1549 self.search_options.contains(SearchOptions::WHOLE_WORD),
1550 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1551 false,
1552 self.search_options
1553 .contains(SearchOptions::ONE_MATCH_PER_LINE),
1554 PathMatcher::default(),
1555 PathMatcher::default(),
1556 false,
1557 None,
1558 ) {
1559 Ok(query) => query.with_replacement(self.replacement(cx)),
1560 Err(e) => {
1561 self.query_error = Some(e.to_string());
1562 self.clear_active_searchable_item_matches(window, cx);
1563 cx.notify();
1564 return done_rx;
1565 }
1566 }
1567 } else {
1568 match SearchQuery::text(
1569 query,
1570 self.search_options.contains(SearchOptions::WHOLE_WORD),
1571 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1572 false,
1573 PathMatcher::default(),
1574 PathMatcher::default(),
1575 false,
1576 None,
1577 ) {
1578 Ok(query) => query.with_replacement(self.replacement(cx)),
1579 Err(e) => {
1580 self.query_error = Some(e.to_string());
1581 self.clear_active_searchable_item_matches(window, cx);
1582 cx.notify();
1583 return done_rx;
1584 }
1585 }
1586 }
1587 .into()
1588 };
1589
1590 self.active_search = Some(query.clone());
1591 let query_text = query.as_str().to_string();
1592
1593 let matches_with_token =
1594 active_searchable_item.find_matches_with_token(query, window, cx);
1595
1596 let active_searchable_item = active_searchable_item.downgrade();
1597 self.pending_search = Some(cx.spawn_in(window, async move |this, cx| {
1598 let (matches, token) = matches_with_token.await;
1599
1600 this.update_in(cx, |this, window, cx| {
1601 if let Some(active_searchable_item) =
1602 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1603 {
1604 this.searchable_items_with_matches
1605 .insert(active_searchable_item.downgrade(), (matches, token));
1606
1607 this.update_match_index(window, cx);
1608
1609 if add_to_history {
1610 this.search_history
1611 .add(&mut this.search_history_cursor, query_text);
1612 }
1613 if !this.dismissed {
1614 let (matches, token) = this
1615 .searchable_items_with_matches
1616 .get(&active_searchable_item.downgrade())
1617 .unwrap();
1618 if matches.is_empty() {
1619 active_searchable_item.clear_matches(window, cx);
1620 } else {
1621 active_searchable_item.update_matches(
1622 matches,
1623 this.active_match_index,
1624 *token,
1625 window,
1626 cx,
1627 );
1628 }
1629 }
1630 let _ = done_tx.send(());
1631 cx.notify();
1632 }
1633 })
1634 .log_err();
1635 }));
1636 }
1637 }
1638 done_rx
1639 }
1640
1641 fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction {
1642 if self.search_options.contains(SearchOptions::BACKWARDS) {
1643 direction.opposite()
1644 } else {
1645 direction
1646 }
1647 }
1648
1649 pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1650 let direction = self.reverse_direction_if_backwards(Direction::Next);
1651 let new_index = self
1652 .active_searchable_item
1653 .as_ref()
1654 .and_then(|searchable_item| {
1655 let (matches, token) = self
1656 .searchable_items_with_matches
1657 .get(&searchable_item.downgrade())?;
1658 searchable_item.active_match_index(direction, matches, *token, window, cx)
1659 });
1660 if new_index != self.active_match_index {
1661 self.active_match_index = new_index;
1662 if !self.dismissed {
1663 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1664 if let Some((matches, token)) = self
1665 .searchable_items_with_matches
1666 .get(&searchable_item.downgrade())
1667 {
1668 if !matches.is_empty() {
1669 searchable_item.update_matches(matches, new_index, *token, window, cx);
1670 }
1671 }
1672 }
1673 }
1674 cx.notify();
1675 }
1676 }
1677
1678 fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1679 self.cycle_field(Direction::Next, window, cx);
1680 }
1681
1682 fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1683 self.cycle_field(Direction::Prev, window, cx);
1684 }
1685 fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1686 let mut handles = vec![self.query_editor.focus_handle(cx)];
1687 if self.replace_enabled {
1688 handles.push(self.replacement_editor.focus_handle(cx));
1689 }
1690 if let Some(item) = self.active_searchable_item.as_ref() {
1691 handles.push(item.item_focus_handle(cx));
1692 }
1693 let current_index = match handles.iter().position(|focus| focus.is_focused(window)) {
1694 Some(index) => index,
1695 None => return,
1696 };
1697
1698 let new_index = match direction {
1699 Direction::Next => (current_index + 1) % handles.len(),
1700 Direction::Prev if current_index == 0 => handles.len() - 1,
1701 Direction::Prev => (current_index - 1) % handles.len(),
1702 };
1703 let next_focus_handle = &handles[new_index];
1704 self.focus(next_focus_handle, window, cx);
1705 cx.stop_propagation();
1706 }
1707
1708 fn next_history_query(
1709 &mut self,
1710 _: &NextHistoryQuery,
1711 window: &mut Window,
1712 cx: &mut Context<Self>,
1713 ) {
1714 if let Some(new_query) = self
1715 .search_history
1716 .next(&mut self.search_history_cursor)
1717 .map(str::to_string)
1718 {
1719 drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1720 } else {
1721 self.search_history_cursor.reset();
1722 drop(self.search("", Some(self.search_options), false, window, cx));
1723 }
1724 }
1725
1726 fn previous_history_query(
1727 &mut self,
1728 _: &PreviousHistoryQuery,
1729 window: &mut Window,
1730 cx: &mut Context<Self>,
1731 ) {
1732 if self.query(cx).is_empty()
1733 && let Some(new_query) = self
1734 .search_history
1735 .current(&self.search_history_cursor)
1736 .map(str::to_string)
1737 {
1738 drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1739 return;
1740 }
1741
1742 if let Some(new_query) = self
1743 .search_history
1744 .previous(&mut self.search_history_cursor)
1745 .map(str::to_string)
1746 {
1747 drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1748 }
1749 }
1750
1751 fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut App) {
1752 window.invalidate_character_coordinates();
1753 window.focus(handle, cx);
1754 }
1755
1756 fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1757 if self.active_searchable_item.is_some() {
1758 self.replace_enabled = !self.replace_enabled;
1759 let handle = if self.replace_enabled {
1760 self.replacement_editor.focus_handle(cx)
1761 } else {
1762 self.query_editor.focus_handle(cx)
1763 };
1764 self.focus(&handle, window, cx);
1765 cx.notify();
1766 }
1767 }
1768
1769 fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1770 let mut should_propagate = true;
1771 if !self.dismissed
1772 && self.active_search.is_some()
1773 && let Some(searchable_item) = self.active_searchable_item.as_ref()
1774 && let Some(query) = self.active_search.as_ref()
1775 && let Some((matches, token)) = self
1776 .searchable_items_with_matches
1777 .get(&searchable_item.downgrade())
1778 {
1779 if let Some(active_index) = self.active_match_index {
1780 let query = query
1781 .as_ref()
1782 .clone()
1783 .with_replacement(self.replacement(cx));
1784 searchable_item.replace(matches.at(active_index), &query, *token, window, cx);
1785 self.select_next_match(&SelectNextMatch, window, cx);
1786 }
1787 should_propagate = false;
1788 }
1789 if !should_propagate {
1790 cx.stop_propagation();
1791 }
1792 }
1793
1794 pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1795 if !self.dismissed
1796 && self.active_search.is_some()
1797 && let Some(searchable_item) = self.active_searchable_item.as_ref()
1798 && let Some(query) = self.active_search.as_ref()
1799 && let Some((matches, token)) = self
1800 .searchable_items_with_matches
1801 .get(&searchable_item.downgrade())
1802 {
1803 let query = query
1804 .as_ref()
1805 .clone()
1806 .with_replacement(self.replacement(cx));
1807 searchable_item.replace_all(&mut matches.iter(), &query, *token, window, cx);
1808 }
1809 }
1810
1811 pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1812 self.update_match_index(window, cx);
1813 self.active_match_index.is_some()
1814 }
1815
1816 pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1817 EditorSettings::get_global(cx).use_smartcase_search
1818 }
1819
1820 pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1821 str.chars().any(|c| c.is_uppercase())
1822 }
1823
1824 fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1825 if self.should_use_smartcase_search(cx) {
1826 let query = self.query(cx);
1827 if !query.is_empty() {
1828 let is_case = self.is_contains_uppercase(&query);
1829 if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1830 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1831 }
1832 }
1833 }
1834 }
1835
1836 fn adjust_query_regex_language(&self, cx: &mut App) {
1837 let enable = self.search_options.contains(SearchOptions::REGEX);
1838 let query_buffer = self
1839 .query_editor
1840 .read(cx)
1841 .buffer()
1842 .read(cx)
1843 .as_singleton()
1844 .expect("query editor should be backed by a singleton buffer");
1845
1846 if enable {
1847 if let Some(regex_language) = self.regex_language.clone() {
1848 query_buffer.update(cx, |query_buffer, cx| {
1849 query_buffer.set_language(Some(regex_language), cx);
1850 })
1851 }
1852 } else {
1853 query_buffer.update(cx, |query_buffer, cx| {
1854 query_buffer.set_language(None, cx);
1855 })
1856 }
1857 }
1858
1859 /// Updates the searchable item's case sensitivity option to match the
1860 /// search bar's current case sensitivity setting. This ensures that
1861 /// editor's `select_next`/ `select_previous` operations respect the buffer
1862 /// search bar's search options.
1863 ///
1864 /// Clears the case sensitivity when the search bar is dismissed so that
1865 /// only the editor's settings are respected.
1866 fn sync_select_next_case_sensitivity(&self, cx: &mut Context<Self>) {
1867 let case_sensitive = match self.dismissed {
1868 true => None,
1869 false => Some(self.search_options.contains(SearchOptions::CASE_SENSITIVE)),
1870 };
1871
1872 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1873 active_searchable_item.set_search_is_case_sensitive(case_sensitive, cx);
1874 }
1875 }
1876}
1877
1878#[cfg(test)]
1879mod tests {
1880 use std::ops::Range;
1881
1882 use super::*;
1883 use editor::{
1884 DisplayPoint, Editor, ExcerptRange, MultiBuffer, SearchSettings, SelectionEffects,
1885 display_map::DisplayRow, test::editor_test_context::EditorTestContext,
1886 };
1887 use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1888 use language::{Buffer, Point};
1889 use settings::{SearchSettingsContent, SettingsStore};
1890 use smol::stream::StreamExt as _;
1891 use unindent::Unindent as _;
1892 use util_macros::perf;
1893
1894 fn init_globals(cx: &mut TestAppContext) {
1895 cx.update(|cx| {
1896 let store = settings::SettingsStore::test(cx);
1897 cx.set_global(store);
1898 editor::init(cx);
1899
1900 theme::init(theme::LoadThemes::JustBase, cx);
1901 crate::init(cx);
1902 });
1903 }
1904
1905 fn init_multibuffer_test(
1906 cx: &mut TestAppContext,
1907 ) -> (
1908 Entity<Editor>,
1909 Entity<BufferSearchBar>,
1910 &mut VisualTestContext,
1911 ) {
1912 init_globals(cx);
1913
1914 let buffer1 = cx.new(|cx| {
1915 Buffer::local(
1916 r#"
1917 A regular expression (shortened as regex or regexp;[1] also referred to as
1918 rational expression[2][3]) is a sequence of characters that specifies a search
1919 pattern in text. Usually such patterns are used by string-searching algorithms
1920 for "find" or "find and replace" operations on strings, or for input validation.
1921 "#
1922 .unindent(),
1923 cx,
1924 )
1925 });
1926
1927 let buffer2 = cx.new(|cx| {
1928 Buffer::local(
1929 r#"
1930 Some Additional text with the term regular expression in it.
1931 There two lines.
1932 "#
1933 .unindent(),
1934 cx,
1935 )
1936 });
1937
1938 let multibuffer = cx.new(|cx| {
1939 let mut buffer = MultiBuffer::new(language::Capability::ReadWrite);
1940
1941 //[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))]
1942 buffer.push_excerpts(
1943 buffer1,
1944 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
1945 cx,
1946 );
1947 buffer.push_excerpts(
1948 buffer2,
1949 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
1950 cx,
1951 );
1952
1953 buffer
1954 });
1955 let mut editor = None;
1956 let window = cx.add_window(|window, cx| {
1957 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
1958 "keymaps/default-macos.json",
1959 cx,
1960 )
1961 .unwrap();
1962 cx.bind_keys(default_key_bindings);
1963 editor =
1964 Some(cx.new(|cx| Editor::for_multibuffer(multibuffer.clone(), None, window, cx)));
1965
1966 let mut search_bar = BufferSearchBar::new(None, window, cx);
1967 search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
1968 search_bar.show(window, cx);
1969 search_bar
1970 });
1971 let search_bar = window.root(cx).unwrap();
1972
1973 let cx = VisualTestContext::from_window(*window, cx).into_mut();
1974
1975 (editor.unwrap(), search_bar, cx)
1976 }
1977
1978 fn init_test(
1979 cx: &mut TestAppContext,
1980 ) -> (
1981 Entity<Editor>,
1982 Entity<BufferSearchBar>,
1983 &mut VisualTestContext,
1984 ) {
1985 init_globals(cx);
1986 let buffer = cx.new(|cx| {
1987 Buffer::local(
1988 r#"
1989 A regular expression (shortened as regex or regexp;[1] also referred to as
1990 rational expression[2][3]) is a sequence of characters that specifies a search
1991 pattern in text. Usually such patterns are used by string-searching algorithms
1992 for "find" or "find and replace" operations on strings, or for input validation.
1993 "#
1994 .unindent(),
1995 cx,
1996 )
1997 });
1998 let mut editor = None;
1999 let window = cx.add_window(|window, cx| {
2000 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
2001 "keymaps/default-macos.json",
2002 cx,
2003 )
2004 .unwrap();
2005 cx.bind_keys(default_key_bindings);
2006 editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
2007 let mut search_bar = BufferSearchBar::new(None, window, cx);
2008 search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
2009 search_bar.show(window, cx);
2010 search_bar
2011 });
2012 let search_bar = window.root(cx).unwrap();
2013
2014 let cx = VisualTestContext::from_window(*window, cx).into_mut();
2015
2016 (editor.unwrap(), search_bar, cx)
2017 }
2018
2019 #[perf]
2020 #[gpui::test]
2021 async fn test_search_simple(cx: &mut TestAppContext) {
2022 let (editor, search_bar, cx) = init_test(cx);
2023 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
2024 background_highlights
2025 .into_iter()
2026 .map(|(range, _)| range)
2027 .collect::<Vec<_>>()
2028 };
2029 // Search for a string that appears with different casing.
2030 // By default, search is case-insensitive.
2031 search_bar
2032 .update_in(cx, |search_bar, window, cx| {
2033 search_bar.search("us", None, true, window, cx)
2034 })
2035 .await
2036 .unwrap();
2037 editor.update_in(cx, |editor, window, cx| {
2038 assert_eq!(
2039 display_points_of(editor.all_text_background_highlights(window, cx)),
2040 &[
2041 DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
2042 DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
2043 ]
2044 );
2045 });
2046
2047 // Switch to a case sensitive search.
2048 search_bar.update_in(cx, |search_bar, window, cx| {
2049 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
2050 });
2051 let mut editor_notifications = cx.notifications(&editor);
2052 editor_notifications.next().await;
2053 editor.update_in(cx, |editor, window, cx| {
2054 assert_eq!(
2055 display_points_of(editor.all_text_background_highlights(window, cx)),
2056 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
2057 );
2058 });
2059
2060 // Search for a string that appears both as a whole word and
2061 // within other words. By default, all results are found.
2062 search_bar
2063 .update_in(cx, |search_bar, window, cx| {
2064 search_bar.search("or", None, true, window, cx)
2065 })
2066 .await
2067 .unwrap();
2068 editor.update_in(cx, |editor, window, cx| {
2069 assert_eq!(
2070 display_points_of(editor.all_text_background_highlights(window, cx)),
2071 &[
2072 DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
2073 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
2074 DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
2075 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
2076 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
2077 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
2078 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
2079 ]
2080 );
2081 });
2082
2083 // Switch to a whole word search.
2084 search_bar.update_in(cx, |search_bar, window, cx| {
2085 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2086 });
2087 let mut editor_notifications = cx.notifications(&editor);
2088 editor_notifications.next().await;
2089 editor.update_in(cx, |editor, window, cx| {
2090 assert_eq!(
2091 display_points_of(editor.all_text_background_highlights(window, cx)),
2092 &[
2093 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
2094 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
2095 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
2096 ]
2097 );
2098 });
2099
2100 editor.update_in(cx, |editor, window, cx| {
2101 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2102 s.select_display_ranges([
2103 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2104 ])
2105 });
2106 });
2107 search_bar.update_in(cx, |search_bar, window, cx| {
2108 assert_eq!(search_bar.active_match_index, Some(0));
2109 search_bar.select_next_match(&SelectNextMatch, window, cx);
2110 assert_eq!(
2111 editor.update(cx, |editor, cx| editor
2112 .selections
2113 .display_ranges(&editor.display_snapshot(cx))),
2114 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2115 );
2116 });
2117 search_bar.read_with(cx, |search_bar, _| {
2118 assert_eq!(search_bar.active_match_index, Some(0));
2119 });
2120
2121 search_bar.update_in(cx, |search_bar, window, cx| {
2122 search_bar.select_next_match(&SelectNextMatch, window, cx);
2123 assert_eq!(
2124 editor.update(cx, |editor, cx| editor
2125 .selections
2126 .display_ranges(&editor.display_snapshot(cx))),
2127 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2128 );
2129 });
2130 search_bar.read_with(cx, |search_bar, _| {
2131 assert_eq!(search_bar.active_match_index, Some(1));
2132 });
2133
2134 search_bar.update_in(cx, |search_bar, window, cx| {
2135 search_bar.select_next_match(&SelectNextMatch, window, cx);
2136 assert_eq!(
2137 editor.update(cx, |editor, cx| editor
2138 .selections
2139 .display_ranges(&editor.display_snapshot(cx))),
2140 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2141 );
2142 });
2143 search_bar.read_with(cx, |search_bar, _| {
2144 assert_eq!(search_bar.active_match_index, Some(2));
2145 });
2146
2147 search_bar.update_in(cx, |search_bar, window, cx| {
2148 search_bar.select_next_match(&SelectNextMatch, window, cx);
2149 assert_eq!(
2150 editor.update(cx, |editor, cx| editor
2151 .selections
2152 .display_ranges(&editor.display_snapshot(cx))),
2153 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2154 );
2155 });
2156 search_bar.read_with(cx, |search_bar, _| {
2157 assert_eq!(search_bar.active_match_index, Some(0));
2158 });
2159
2160 search_bar.update_in(cx, |search_bar, window, cx| {
2161 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2162 assert_eq!(
2163 editor.update(cx, |editor, cx| editor
2164 .selections
2165 .display_ranges(&editor.display_snapshot(cx))),
2166 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2167 );
2168 });
2169 search_bar.read_with(cx, |search_bar, _| {
2170 assert_eq!(search_bar.active_match_index, Some(2));
2171 });
2172
2173 search_bar.update_in(cx, |search_bar, window, cx| {
2174 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2175 assert_eq!(
2176 editor.update(cx, |editor, cx| editor
2177 .selections
2178 .display_ranges(&editor.display_snapshot(cx))),
2179 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2180 );
2181 });
2182 search_bar.read_with(cx, |search_bar, _| {
2183 assert_eq!(search_bar.active_match_index, Some(1));
2184 });
2185
2186 search_bar.update_in(cx, |search_bar, window, cx| {
2187 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2188 assert_eq!(
2189 editor.update(cx, |editor, cx| editor
2190 .selections
2191 .display_ranges(&editor.display_snapshot(cx))),
2192 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2193 );
2194 });
2195 search_bar.read_with(cx, |search_bar, _| {
2196 assert_eq!(search_bar.active_match_index, Some(0));
2197 });
2198
2199 // Park the cursor in between matches and ensure that going to the previous match selects
2200 // the closest match to the left.
2201 editor.update_in(cx, |editor, window, cx| {
2202 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2203 s.select_display_ranges([
2204 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
2205 ])
2206 });
2207 });
2208 search_bar.update_in(cx, |search_bar, window, cx| {
2209 assert_eq!(search_bar.active_match_index, Some(1));
2210 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2211 assert_eq!(
2212 editor.update(cx, |editor, cx| editor
2213 .selections
2214 .display_ranges(&editor.display_snapshot(cx))),
2215 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2216 );
2217 });
2218 search_bar.read_with(cx, |search_bar, _| {
2219 assert_eq!(search_bar.active_match_index, Some(0));
2220 });
2221
2222 // Park the cursor in between matches and ensure that going to the next match selects the
2223 // closest match to the right.
2224 editor.update_in(cx, |editor, window, cx| {
2225 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2226 s.select_display_ranges([
2227 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
2228 ])
2229 });
2230 });
2231 search_bar.update_in(cx, |search_bar, window, cx| {
2232 assert_eq!(search_bar.active_match_index, Some(1));
2233 search_bar.select_next_match(&SelectNextMatch, window, cx);
2234 assert_eq!(
2235 editor.update(cx, |editor, cx| editor
2236 .selections
2237 .display_ranges(&editor.display_snapshot(cx))),
2238 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2239 );
2240 });
2241 search_bar.read_with(cx, |search_bar, _| {
2242 assert_eq!(search_bar.active_match_index, Some(1));
2243 });
2244
2245 // Park the cursor after the last match and ensure that going to the previous match selects
2246 // the last match.
2247 editor.update_in(cx, |editor, window, cx| {
2248 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2249 s.select_display_ranges([
2250 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2251 ])
2252 });
2253 });
2254 search_bar.update_in(cx, |search_bar, window, cx| {
2255 assert_eq!(search_bar.active_match_index, Some(2));
2256 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2257 assert_eq!(
2258 editor.update(cx, |editor, cx| editor
2259 .selections
2260 .display_ranges(&editor.display_snapshot(cx))),
2261 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2262 );
2263 });
2264 search_bar.read_with(cx, |search_bar, _| {
2265 assert_eq!(search_bar.active_match_index, Some(2));
2266 });
2267
2268 // Park the cursor after the last match and ensure that going to the next match selects the
2269 // first match.
2270 editor.update_in(cx, |editor, window, cx| {
2271 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2272 s.select_display_ranges([
2273 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2274 ])
2275 });
2276 });
2277 search_bar.update_in(cx, |search_bar, window, cx| {
2278 assert_eq!(search_bar.active_match_index, Some(2));
2279 search_bar.select_next_match(&SelectNextMatch, window, cx);
2280 assert_eq!(
2281 editor.update(cx, |editor, cx| editor
2282 .selections
2283 .display_ranges(&editor.display_snapshot(cx))),
2284 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2285 );
2286 });
2287 search_bar.read_with(cx, |search_bar, _| {
2288 assert_eq!(search_bar.active_match_index, Some(0));
2289 });
2290
2291 // Park the cursor before the first match and ensure that going to the previous match
2292 // selects the last match.
2293 editor.update_in(cx, |editor, window, cx| {
2294 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2295 s.select_display_ranges([
2296 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2297 ])
2298 });
2299 });
2300 search_bar.update_in(cx, |search_bar, window, cx| {
2301 assert_eq!(search_bar.active_match_index, Some(0));
2302 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2303 assert_eq!(
2304 editor.update(cx, |editor, cx| editor
2305 .selections
2306 .display_ranges(&editor.display_snapshot(cx))),
2307 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2308 );
2309 });
2310 search_bar.read_with(cx, |search_bar, _| {
2311 assert_eq!(search_bar.active_match_index, Some(2));
2312 });
2313 }
2314
2315 fn display_points_of(
2316 background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
2317 ) -> Vec<Range<DisplayPoint>> {
2318 background_highlights
2319 .into_iter()
2320 .map(|(range, _)| range)
2321 .collect::<Vec<_>>()
2322 }
2323
2324 #[perf]
2325 #[gpui::test]
2326 async fn test_search_option_handling(cx: &mut TestAppContext) {
2327 let (editor, search_bar, cx) = init_test(cx);
2328
2329 // show with options should make current search case sensitive
2330 search_bar
2331 .update_in(cx, |search_bar, window, cx| {
2332 search_bar.show(window, cx);
2333 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2334 })
2335 .await
2336 .unwrap();
2337 editor.update_in(cx, |editor, window, cx| {
2338 assert_eq!(
2339 display_points_of(editor.all_text_background_highlights(window, cx)),
2340 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
2341 );
2342 });
2343
2344 // search_suggested should restore default options
2345 search_bar.update_in(cx, |search_bar, window, cx| {
2346 search_bar.search_suggested(window, cx);
2347 assert_eq!(search_bar.search_options, SearchOptions::NONE)
2348 });
2349
2350 // toggling a search option should update the defaults
2351 search_bar
2352 .update_in(cx, |search_bar, window, cx| {
2353 search_bar.search(
2354 "regex",
2355 Some(SearchOptions::CASE_SENSITIVE),
2356 true,
2357 window,
2358 cx,
2359 )
2360 })
2361 .await
2362 .unwrap();
2363 search_bar.update_in(cx, |search_bar, window, cx| {
2364 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
2365 });
2366 let mut editor_notifications = cx.notifications(&editor);
2367 editor_notifications.next().await;
2368 editor.update_in(cx, |editor, window, cx| {
2369 assert_eq!(
2370 display_points_of(editor.all_text_background_highlights(window, cx)),
2371 &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
2372 );
2373 });
2374
2375 // defaults should still include whole word
2376 search_bar.update_in(cx, |search_bar, window, cx| {
2377 search_bar.search_suggested(window, cx);
2378 assert_eq!(
2379 search_bar.search_options,
2380 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
2381 )
2382 });
2383 }
2384
2385 #[perf]
2386 #[gpui::test]
2387 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
2388 init_globals(cx);
2389 let buffer_text = r#"
2390 A regular expression (shortened as regex or regexp;[1] also referred to as
2391 rational expression[2][3]) is a sequence of characters that specifies a search
2392 pattern in text. Usually such patterns are used by string-searching algorithms
2393 for "find" or "find and replace" operations on strings, or for input validation.
2394 "#
2395 .unindent();
2396 let expected_query_matches_count = buffer_text
2397 .chars()
2398 .filter(|c| c.eq_ignore_ascii_case(&'a'))
2399 .count();
2400 assert!(
2401 expected_query_matches_count > 1,
2402 "Should pick a query with multiple results"
2403 );
2404 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2405 let window = cx.add_window(|_, _| gpui::Empty);
2406
2407 let editor = window.build_entity(cx, |window, cx| {
2408 Editor::for_buffer(buffer.clone(), None, window, cx)
2409 });
2410
2411 let search_bar = window.build_entity(cx, |window, cx| {
2412 let mut search_bar = BufferSearchBar::new(None, window, cx);
2413 search_bar.set_active_pane_item(Some(&editor), window, cx);
2414 search_bar.show(window, cx);
2415 search_bar
2416 });
2417
2418 window
2419 .update(cx, |_, window, cx| {
2420 search_bar.update(cx, |search_bar, cx| {
2421 search_bar.search("a", None, true, window, cx)
2422 })
2423 })
2424 .unwrap()
2425 .await
2426 .unwrap();
2427 let initial_selections = window
2428 .update(cx, |_, window, cx| {
2429 search_bar.update(cx, |search_bar, cx| {
2430 let handle = search_bar.query_editor.focus_handle(cx);
2431 window.focus(&handle, cx);
2432 search_bar.activate_current_match(window, cx);
2433 });
2434 assert!(
2435 !editor.read(cx).is_focused(window),
2436 "Initially, the editor should not be focused"
2437 );
2438 let initial_selections = editor.update(cx, |editor, cx| {
2439 let initial_selections = editor.selections.display_ranges(&editor.display_snapshot(cx));
2440 assert_eq!(
2441 initial_selections.len(), 1,
2442 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
2443 );
2444 initial_selections
2445 });
2446 search_bar.update(cx, |search_bar, cx| {
2447 assert_eq!(search_bar.active_match_index, Some(0));
2448 let handle = search_bar.query_editor.focus_handle(cx);
2449 window.focus(&handle, cx);
2450 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2451 });
2452 assert!(
2453 editor.read(cx).is_focused(window),
2454 "Should focus editor after successful SelectAllMatches"
2455 );
2456 search_bar.update(cx, |search_bar, cx| {
2457 let all_selections =
2458 editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2459 assert_eq!(
2460 all_selections.len(),
2461 expected_query_matches_count,
2462 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2463 );
2464 assert_eq!(
2465 search_bar.active_match_index,
2466 Some(0),
2467 "Match index should not change after selecting all matches"
2468 );
2469 });
2470
2471 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
2472 initial_selections
2473 }).unwrap();
2474
2475 window
2476 .update(cx, |_, window, cx| {
2477 assert!(
2478 editor.read(cx).is_focused(window),
2479 "Should still have editor focused after SelectNextMatch"
2480 );
2481 search_bar.update(cx, |search_bar, cx| {
2482 let all_selections = editor.update(cx, |editor, cx| {
2483 editor
2484 .selections
2485 .display_ranges(&editor.display_snapshot(cx))
2486 });
2487 assert_eq!(
2488 all_selections.len(),
2489 1,
2490 "On next match, should deselect items and select the next match"
2491 );
2492 assert_ne!(
2493 all_selections, initial_selections,
2494 "Next match should be different from the first selection"
2495 );
2496 assert_eq!(
2497 search_bar.active_match_index,
2498 Some(1),
2499 "Match index should be updated to the next one"
2500 );
2501 let handle = search_bar.query_editor.focus_handle(cx);
2502 window.focus(&handle, cx);
2503 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2504 });
2505 })
2506 .unwrap();
2507 window
2508 .update(cx, |_, window, cx| {
2509 assert!(
2510 editor.read(cx).is_focused(window),
2511 "Should focus editor after successful SelectAllMatches"
2512 );
2513 search_bar.update(cx, |search_bar, cx| {
2514 let all_selections =
2515 editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2516 assert_eq!(
2517 all_selections.len(),
2518 expected_query_matches_count,
2519 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2520 );
2521 assert_eq!(
2522 search_bar.active_match_index,
2523 Some(1),
2524 "Match index should not change after selecting all matches"
2525 );
2526 });
2527 search_bar.update(cx, |search_bar, cx| {
2528 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2529 });
2530 })
2531 .unwrap();
2532 let last_match_selections = window
2533 .update(cx, |_, window, cx| {
2534 assert!(
2535 editor.read(cx).is_focused(window),
2536 "Should still have editor focused after SelectPreviousMatch"
2537 );
2538
2539 search_bar.update(cx, |search_bar, cx| {
2540 let all_selections = editor.update(cx, |editor, cx| {
2541 editor
2542 .selections
2543 .display_ranges(&editor.display_snapshot(cx))
2544 });
2545 assert_eq!(
2546 all_selections.len(),
2547 1,
2548 "On previous match, should deselect items and select the previous item"
2549 );
2550 assert_eq!(
2551 all_selections, initial_selections,
2552 "Previous match should be the same as the first selection"
2553 );
2554 assert_eq!(
2555 search_bar.active_match_index,
2556 Some(0),
2557 "Match index should be updated to the previous one"
2558 );
2559 all_selections
2560 })
2561 })
2562 .unwrap();
2563
2564 window
2565 .update(cx, |_, window, cx| {
2566 search_bar.update(cx, |search_bar, cx| {
2567 let handle = search_bar.query_editor.focus_handle(cx);
2568 window.focus(&handle, cx);
2569 search_bar.search("abas_nonexistent_match", None, true, window, cx)
2570 })
2571 })
2572 .unwrap()
2573 .await
2574 .unwrap();
2575 window
2576 .update(cx, |_, window, cx| {
2577 search_bar.update(cx, |search_bar, cx| {
2578 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2579 });
2580 assert!(
2581 editor.update(cx, |this, _cx| !this.is_focused(window)),
2582 "Should not switch focus to editor if SelectAllMatches does not find any matches"
2583 );
2584 search_bar.update(cx, |search_bar, cx| {
2585 let all_selections =
2586 editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2587 assert_eq!(
2588 all_selections, last_match_selections,
2589 "Should not select anything new if there are no matches"
2590 );
2591 assert!(
2592 search_bar.active_match_index.is_none(),
2593 "For no matches, there should be no active match index"
2594 );
2595 });
2596 })
2597 .unwrap();
2598 }
2599
2600 #[perf]
2601 #[gpui::test]
2602 async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2603 init_globals(cx);
2604 let buffer_text = r#"
2605 self.buffer.update(cx, |buffer, cx| {
2606 buffer.edit(
2607 edits,
2608 Some(AutoindentMode::Block {
2609 original_indent_columns,
2610 }),
2611 cx,
2612 )
2613 });
2614
2615 this.buffer.update(cx, |buffer, cx| {
2616 buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2617 });
2618 "#
2619 .unindent();
2620 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2621 let cx = cx.add_empty_window();
2622
2623 let editor =
2624 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2625
2626 let search_bar = cx.new_window_entity(|window, cx| {
2627 let mut search_bar = BufferSearchBar::new(None, window, cx);
2628 search_bar.set_active_pane_item(Some(&editor), window, cx);
2629 search_bar.show(window, cx);
2630 search_bar
2631 });
2632
2633 search_bar
2634 .update_in(cx, |search_bar, window, cx| {
2635 search_bar.search(
2636 "edit\\(",
2637 Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2638 true,
2639 window,
2640 cx,
2641 )
2642 })
2643 .await
2644 .unwrap();
2645
2646 search_bar.update_in(cx, |search_bar, window, cx| {
2647 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2648 });
2649 search_bar.update(cx, |_, cx| {
2650 let all_selections = editor.update(cx, |editor, cx| {
2651 editor
2652 .selections
2653 .display_ranges(&editor.display_snapshot(cx))
2654 });
2655 assert_eq!(
2656 all_selections.len(),
2657 2,
2658 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2659 );
2660 });
2661
2662 search_bar
2663 .update_in(cx, |search_bar, window, cx| {
2664 search_bar.search(
2665 "edit(",
2666 Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2667 true,
2668 window,
2669 cx,
2670 )
2671 })
2672 .await
2673 .unwrap();
2674
2675 search_bar.update_in(cx, |search_bar, window, cx| {
2676 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2677 });
2678 search_bar.update(cx, |_, cx| {
2679 let all_selections = editor.update(cx, |editor, cx| {
2680 editor
2681 .selections
2682 .display_ranges(&editor.display_snapshot(cx))
2683 });
2684 assert_eq!(
2685 all_selections.len(),
2686 2,
2687 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2688 );
2689 });
2690 }
2691
2692 #[perf]
2693 #[gpui::test]
2694 async fn test_search_query_history(cx: &mut TestAppContext) {
2695 let (_editor, search_bar, cx) = init_test(cx);
2696
2697 // Add 3 search items into the history.
2698 search_bar
2699 .update_in(cx, |search_bar, window, cx| {
2700 search_bar.search("a", None, true, window, cx)
2701 })
2702 .await
2703 .unwrap();
2704 search_bar
2705 .update_in(cx, |search_bar, window, cx| {
2706 search_bar.search("b", None, true, window, cx)
2707 })
2708 .await
2709 .unwrap();
2710 search_bar
2711 .update_in(cx, |search_bar, window, cx| {
2712 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2713 })
2714 .await
2715 .unwrap();
2716 // Ensure that the latest search is active.
2717 search_bar.update(cx, |search_bar, cx| {
2718 assert_eq!(search_bar.query(cx), "c");
2719 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2720 });
2721
2722 // Next history query after the latest should set the query to the empty string.
2723 search_bar.update_in(cx, |search_bar, window, cx| {
2724 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2725 });
2726 cx.background_executor.run_until_parked();
2727 search_bar.update(cx, |search_bar, cx| {
2728 assert_eq!(search_bar.query(cx), "");
2729 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2730 });
2731 search_bar.update_in(cx, |search_bar, window, cx| {
2732 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2733 });
2734 cx.background_executor.run_until_parked();
2735 search_bar.update(cx, |search_bar, cx| {
2736 assert_eq!(search_bar.query(cx), "");
2737 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2738 });
2739
2740 // First previous query for empty current query should set the query to the latest.
2741 search_bar.update_in(cx, |search_bar, window, cx| {
2742 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2743 });
2744 cx.background_executor.run_until_parked();
2745 search_bar.update(cx, |search_bar, cx| {
2746 assert_eq!(search_bar.query(cx), "c");
2747 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2748 });
2749
2750 // Further previous items should go over the history in reverse order.
2751 search_bar.update_in(cx, |search_bar, window, cx| {
2752 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2753 });
2754 cx.background_executor.run_until_parked();
2755 search_bar.update(cx, |search_bar, cx| {
2756 assert_eq!(search_bar.query(cx), "b");
2757 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2758 });
2759
2760 // Previous items should never go behind the first history item.
2761 search_bar.update_in(cx, |search_bar, window, cx| {
2762 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2763 });
2764 cx.background_executor.run_until_parked();
2765 search_bar.update(cx, |search_bar, cx| {
2766 assert_eq!(search_bar.query(cx), "a");
2767 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2768 });
2769 search_bar.update_in(cx, |search_bar, window, cx| {
2770 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2771 });
2772 cx.background_executor.run_until_parked();
2773 search_bar.update(cx, |search_bar, cx| {
2774 assert_eq!(search_bar.query(cx), "a");
2775 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2776 });
2777
2778 // Next items should go over the history in the original order.
2779 search_bar.update_in(cx, |search_bar, window, cx| {
2780 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2781 });
2782 cx.background_executor.run_until_parked();
2783 search_bar.update(cx, |search_bar, cx| {
2784 assert_eq!(search_bar.query(cx), "b");
2785 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2786 });
2787
2788 search_bar
2789 .update_in(cx, |search_bar, window, cx| {
2790 search_bar.search("ba", None, true, window, cx)
2791 })
2792 .await
2793 .unwrap();
2794 search_bar.update(cx, |search_bar, cx| {
2795 assert_eq!(search_bar.query(cx), "ba");
2796 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2797 });
2798
2799 // New search input should add another entry to history and move the selection to the end of the history.
2800 search_bar.update_in(cx, |search_bar, window, cx| {
2801 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2802 });
2803 cx.background_executor.run_until_parked();
2804 search_bar.update(cx, |search_bar, cx| {
2805 assert_eq!(search_bar.query(cx), "c");
2806 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2807 });
2808 search_bar.update_in(cx, |search_bar, window, cx| {
2809 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2810 });
2811 cx.background_executor.run_until_parked();
2812 search_bar.update(cx, |search_bar, cx| {
2813 assert_eq!(search_bar.query(cx), "b");
2814 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2815 });
2816 search_bar.update_in(cx, |search_bar, window, cx| {
2817 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2818 });
2819 cx.background_executor.run_until_parked();
2820 search_bar.update(cx, |search_bar, cx| {
2821 assert_eq!(search_bar.query(cx), "c");
2822 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2823 });
2824 search_bar.update_in(cx, |search_bar, window, cx| {
2825 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2826 });
2827 cx.background_executor.run_until_parked();
2828 search_bar.update(cx, |search_bar, cx| {
2829 assert_eq!(search_bar.query(cx), "ba");
2830 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2831 });
2832 search_bar.update_in(cx, |search_bar, window, cx| {
2833 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2834 });
2835 cx.background_executor.run_until_parked();
2836 search_bar.update(cx, |search_bar, cx| {
2837 assert_eq!(search_bar.query(cx), "");
2838 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2839 });
2840 }
2841
2842 #[perf]
2843 #[gpui::test]
2844 async fn test_replace_simple(cx: &mut TestAppContext) {
2845 let (editor, search_bar, cx) = init_test(cx);
2846
2847 search_bar
2848 .update_in(cx, |search_bar, window, cx| {
2849 search_bar.search("expression", None, true, window, cx)
2850 })
2851 .await
2852 .unwrap();
2853
2854 search_bar.update_in(cx, |search_bar, window, cx| {
2855 search_bar.replacement_editor.update(cx, |editor, cx| {
2856 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2857 editor.set_text("expr$1", window, cx);
2858 });
2859 search_bar.replace_all(&ReplaceAll, window, cx)
2860 });
2861 assert_eq!(
2862 editor.read_with(cx, |this, cx| { this.text(cx) }),
2863 r#"
2864 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2865 rational expr$1[2][3]) is a sequence of characters that specifies a search
2866 pattern in text. Usually such patterns are used by string-searching algorithms
2867 for "find" or "find and replace" operations on strings, or for input validation.
2868 "#
2869 .unindent()
2870 );
2871
2872 // Search for word boundaries and replace just a single one.
2873 search_bar
2874 .update_in(cx, |search_bar, window, cx| {
2875 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx)
2876 })
2877 .await
2878 .unwrap();
2879
2880 search_bar.update_in(cx, |search_bar, window, cx| {
2881 search_bar.replacement_editor.update(cx, |editor, cx| {
2882 editor.set_text("banana", window, cx);
2883 });
2884 search_bar.replace_next(&ReplaceNext, window, cx)
2885 });
2886 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2887 assert_eq!(
2888 editor.read_with(cx, |this, cx| { this.text(cx) }),
2889 r#"
2890 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2891 rational expr$1[2][3]) is a sequence of characters that specifies a search
2892 pattern in text. Usually such patterns are used by string-searching algorithms
2893 for "find" or "find and replace" operations on strings, or for input validation.
2894 "#
2895 .unindent()
2896 );
2897 // Let's turn on regex mode.
2898 search_bar
2899 .update_in(cx, |search_bar, window, cx| {
2900 search_bar.search(
2901 "\\[([^\\]]+)\\]",
2902 Some(SearchOptions::REGEX),
2903 true,
2904 window,
2905 cx,
2906 )
2907 })
2908 .await
2909 .unwrap();
2910 search_bar.update_in(cx, |search_bar, window, cx| {
2911 search_bar.replacement_editor.update(cx, |editor, cx| {
2912 editor.set_text("${1}number", window, cx);
2913 });
2914 search_bar.replace_all(&ReplaceAll, window, cx)
2915 });
2916 assert_eq!(
2917 editor.read_with(cx, |this, cx| { this.text(cx) }),
2918 r#"
2919 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2920 rational expr$12number3number) is a sequence of characters that specifies a search
2921 pattern in text. Usually such patterns are used by string-searching algorithms
2922 for "find" or "find and replace" operations on strings, or for input validation.
2923 "#
2924 .unindent()
2925 );
2926 // Now with a whole-word twist.
2927 search_bar
2928 .update_in(cx, |search_bar, window, cx| {
2929 search_bar.search(
2930 "a\\w+s",
2931 Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2932 true,
2933 window,
2934 cx,
2935 )
2936 })
2937 .await
2938 .unwrap();
2939 search_bar.update_in(cx, |search_bar, window, cx| {
2940 search_bar.replacement_editor.update(cx, |editor, cx| {
2941 editor.set_text("things", window, cx);
2942 });
2943 search_bar.replace_all(&ReplaceAll, window, cx)
2944 });
2945 // The only word affected by this edit should be `algorithms`, even though there's a bunch
2946 // of words in this text that would match this regex if not for WHOLE_WORD.
2947 assert_eq!(
2948 editor.read_with(cx, |this, cx| { this.text(cx) }),
2949 r#"
2950 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2951 rational expr$12number3number) is a sequence of characters that specifies a search
2952 pattern in text. Usually such patterns are used by string-searching things
2953 for "find" or "find and replace" operations on strings, or for input validation.
2954 "#
2955 .unindent()
2956 );
2957 }
2958
2959 #[gpui::test]
2960 async fn test_replace_focus(cx: &mut TestAppContext) {
2961 let (editor, search_bar, cx) = init_test(cx);
2962
2963 editor.update_in(cx, |editor, window, cx| {
2964 editor.set_text("What a bad day!", window, cx)
2965 });
2966
2967 search_bar
2968 .update_in(cx, |search_bar, window, cx| {
2969 search_bar.search("bad", None, true, window, cx)
2970 })
2971 .await
2972 .unwrap();
2973
2974 // Calling `toggle_replace` in the search bar ensures that the "Replace
2975 // *" buttons are rendered, so we can then simulate clicking the
2976 // buttons.
2977 search_bar.update_in(cx, |search_bar, window, cx| {
2978 search_bar.toggle_replace(&ToggleReplace, window, cx)
2979 });
2980
2981 search_bar.update_in(cx, |search_bar, window, cx| {
2982 search_bar.replacement_editor.update(cx, |editor, cx| {
2983 editor.set_text("great", window, cx);
2984 });
2985 });
2986
2987 // Focus on the editor instead of the search bar, as we want to ensure
2988 // that pressing the "Replace Next Match" button will work, even if the
2989 // search bar is not focused.
2990 cx.focus(&editor);
2991
2992 // We'll not simulate clicking the "Replace Next Match " button, asserting that
2993 // the replacement was done.
2994 let button_bounds = cx
2995 .debug_bounds("ICON-ReplaceNext")
2996 .expect("'Replace Next Match' button should be visible");
2997 cx.simulate_click(button_bounds.center(), gpui::Modifiers::none());
2998
2999 assert_eq!(
3000 editor.read_with(cx, |editor, cx| editor.text(cx)),
3001 "What a great day!"
3002 );
3003 }
3004
3005 struct ReplacementTestParams<'a> {
3006 editor: &'a Entity<Editor>,
3007 search_bar: &'a Entity<BufferSearchBar>,
3008 cx: &'a mut VisualTestContext,
3009 search_text: &'static str,
3010 search_options: Option<SearchOptions>,
3011 replacement_text: &'static str,
3012 replace_all: bool,
3013 expected_text: String,
3014 }
3015
3016 async fn run_replacement_test(options: ReplacementTestParams<'_>) {
3017 options
3018 .search_bar
3019 .update_in(options.cx, |search_bar, window, cx| {
3020 if let Some(options) = options.search_options {
3021 search_bar.set_search_options(options, cx);
3022 }
3023 search_bar.search(
3024 options.search_text,
3025 options.search_options,
3026 true,
3027 window,
3028 cx,
3029 )
3030 })
3031 .await
3032 .unwrap();
3033
3034 options
3035 .search_bar
3036 .update_in(options.cx, |search_bar, window, cx| {
3037 search_bar.replacement_editor.update(cx, |editor, cx| {
3038 editor.set_text(options.replacement_text, window, cx);
3039 });
3040
3041 if options.replace_all {
3042 search_bar.replace_all(&ReplaceAll, window, cx)
3043 } else {
3044 search_bar.replace_next(&ReplaceNext, window, cx)
3045 }
3046 });
3047
3048 assert_eq!(
3049 options
3050 .editor
3051 .read_with(options.cx, |this, cx| { this.text(cx) }),
3052 options.expected_text
3053 );
3054 }
3055
3056 #[perf]
3057 #[gpui::test]
3058 async fn test_replace_special_characters(cx: &mut TestAppContext) {
3059 let (editor, search_bar, cx) = init_test(cx);
3060
3061 run_replacement_test(ReplacementTestParams {
3062 editor: &editor,
3063 search_bar: &search_bar,
3064 cx,
3065 search_text: "expression",
3066 search_options: None,
3067 replacement_text: r"\n",
3068 replace_all: true,
3069 expected_text: r#"
3070 A regular \n (shortened as regex or regexp;[1] also referred to as
3071 rational \n[2][3]) is a sequence of characters that specifies a search
3072 pattern in text. Usually such patterns are used by string-searching algorithms
3073 for "find" or "find and replace" operations on strings, or for input validation.
3074 "#
3075 .unindent(),
3076 })
3077 .await;
3078
3079 run_replacement_test(ReplacementTestParams {
3080 editor: &editor,
3081 search_bar: &search_bar,
3082 cx,
3083 search_text: "or",
3084 search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
3085 replacement_text: r"\\\n\\\\",
3086 replace_all: false,
3087 expected_text: r#"
3088 A regular \n (shortened as regex \
3089 \\ regexp;[1] also referred to as
3090 rational \n[2][3]) is a sequence of characters that specifies a search
3091 pattern in text. Usually such patterns are used by string-searching algorithms
3092 for "find" or "find and replace" operations on strings, or for input validation.
3093 "#
3094 .unindent(),
3095 })
3096 .await;
3097
3098 run_replacement_test(ReplacementTestParams {
3099 editor: &editor,
3100 search_bar: &search_bar,
3101 cx,
3102 search_text: r"(that|used) ",
3103 search_options: Some(SearchOptions::REGEX),
3104 replacement_text: r"$1\n",
3105 replace_all: true,
3106 expected_text: r#"
3107 A regular \n (shortened as regex \
3108 \\ regexp;[1] also referred to as
3109 rational \n[2][3]) is a sequence of characters that
3110 specifies a search
3111 pattern in text. Usually such patterns are used
3112 by string-searching algorithms
3113 for "find" or "find and replace" operations on strings, or for input validation.
3114 "#
3115 .unindent(),
3116 })
3117 .await;
3118 }
3119
3120 #[perf]
3121 #[gpui::test]
3122 async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
3123 cx: &mut TestAppContext,
3124 ) {
3125 init_globals(cx);
3126 let buffer = cx.new(|cx| {
3127 Buffer::local(
3128 r#"
3129 aaa bbb aaa ccc
3130 aaa bbb aaa ccc
3131 aaa bbb aaa ccc
3132 aaa bbb aaa ccc
3133 aaa bbb aaa ccc
3134 aaa bbb aaa ccc
3135 "#
3136 .unindent(),
3137 cx,
3138 )
3139 });
3140 let cx = cx.add_empty_window();
3141 let editor =
3142 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
3143
3144 let search_bar = cx.new_window_entity(|window, cx| {
3145 let mut search_bar = BufferSearchBar::new(None, window, cx);
3146 search_bar.set_active_pane_item(Some(&editor), window, cx);
3147 search_bar.show(window, cx);
3148 search_bar
3149 });
3150
3151 editor.update_in(cx, |editor, window, cx| {
3152 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3153 s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
3154 })
3155 });
3156
3157 search_bar.update_in(cx, |search_bar, window, cx| {
3158 let deploy = Deploy {
3159 focus: true,
3160 replace_enabled: false,
3161 selection_search_enabled: true,
3162 };
3163 search_bar.deploy(&deploy, window, cx);
3164 });
3165
3166 cx.run_until_parked();
3167
3168 search_bar
3169 .update_in(cx, |search_bar, window, cx| {
3170 search_bar.search("aaa", None, true, window, cx)
3171 })
3172 .await
3173 .unwrap();
3174
3175 editor.update(cx, |editor, cx| {
3176 assert_eq!(
3177 editor.search_background_highlights(cx),
3178 &[
3179 Point::new(1, 0)..Point::new(1, 3),
3180 Point::new(1, 8)..Point::new(1, 11),
3181 Point::new(2, 0)..Point::new(2, 3),
3182 ]
3183 );
3184 });
3185 }
3186
3187 #[perf]
3188 #[gpui::test]
3189 async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
3190 cx: &mut TestAppContext,
3191 ) {
3192 init_globals(cx);
3193 let text = r#"
3194 aaa bbb aaa ccc
3195 aaa bbb aaa ccc
3196 aaa bbb aaa ccc
3197 aaa bbb aaa ccc
3198 aaa bbb aaa ccc
3199 aaa bbb aaa ccc
3200
3201 aaa bbb aaa ccc
3202 aaa bbb aaa ccc
3203 aaa bbb aaa ccc
3204 aaa bbb aaa ccc
3205 aaa bbb aaa ccc
3206 aaa bbb aaa ccc
3207 "#
3208 .unindent();
3209
3210 let cx = cx.add_empty_window();
3211 let editor = cx.new_window_entity(|window, cx| {
3212 let multibuffer = MultiBuffer::build_multi(
3213 [
3214 (
3215 &text,
3216 vec![
3217 Point::new(0, 0)..Point::new(2, 0),
3218 Point::new(4, 0)..Point::new(5, 0),
3219 ],
3220 ),
3221 (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
3222 ],
3223 cx,
3224 );
3225 Editor::for_multibuffer(multibuffer, None, window, cx)
3226 });
3227
3228 let search_bar = cx.new_window_entity(|window, cx| {
3229 let mut search_bar = BufferSearchBar::new(None, window, cx);
3230 search_bar.set_active_pane_item(Some(&editor), window, cx);
3231 search_bar.show(window, cx);
3232 search_bar
3233 });
3234
3235 editor.update_in(cx, |editor, window, cx| {
3236 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3237 s.select_ranges(vec![
3238 Point::new(1, 0)..Point::new(1, 4),
3239 Point::new(5, 3)..Point::new(6, 4),
3240 ])
3241 })
3242 });
3243
3244 search_bar.update_in(cx, |search_bar, window, cx| {
3245 let deploy = Deploy {
3246 focus: true,
3247 replace_enabled: false,
3248 selection_search_enabled: true,
3249 };
3250 search_bar.deploy(&deploy, window, cx);
3251 });
3252
3253 cx.run_until_parked();
3254
3255 search_bar
3256 .update_in(cx, |search_bar, window, cx| {
3257 search_bar.search("aaa", None, true, window, cx)
3258 })
3259 .await
3260 .unwrap();
3261
3262 editor.update(cx, |editor, cx| {
3263 assert_eq!(
3264 editor.search_background_highlights(cx),
3265 &[
3266 Point::new(1, 0)..Point::new(1, 3),
3267 Point::new(5, 8)..Point::new(5, 11),
3268 Point::new(6, 0)..Point::new(6, 3),
3269 ]
3270 );
3271 });
3272 }
3273
3274 #[perf]
3275 #[gpui::test]
3276 async fn test_hides_and_uses_secondary_when_in_singleton_buffer(cx: &mut TestAppContext) {
3277 let (editor, search_bar, cx) = init_test(cx);
3278
3279 let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3280 search_bar.set_active_pane_item(Some(&editor), window, cx)
3281 });
3282
3283 assert_eq!(initial_location, ToolbarItemLocation::Secondary);
3284
3285 let mut events = cx.events(&search_bar);
3286
3287 search_bar.update_in(cx, |search_bar, window, cx| {
3288 search_bar.dismiss(&Dismiss, window, cx);
3289 });
3290
3291 assert_eq!(
3292 events.try_next().unwrap(),
3293 Some(ToolbarItemEvent::ChangeLocation(
3294 ToolbarItemLocation::Hidden
3295 ))
3296 );
3297
3298 search_bar.update_in(cx, |search_bar, window, cx| {
3299 search_bar.show(window, cx);
3300 });
3301
3302 assert_eq!(
3303 events.try_next().unwrap(),
3304 Some(ToolbarItemEvent::ChangeLocation(
3305 ToolbarItemLocation::Secondary
3306 ))
3307 );
3308 }
3309
3310 #[perf]
3311 #[gpui::test]
3312 async fn test_uses_primary_left_when_in_multi_buffer(cx: &mut TestAppContext) {
3313 let (editor, search_bar, cx) = init_multibuffer_test(cx);
3314
3315 let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3316 search_bar.set_active_pane_item(Some(&editor), window, cx)
3317 });
3318
3319 assert_eq!(initial_location, ToolbarItemLocation::PrimaryLeft);
3320
3321 let mut events = cx.events(&search_bar);
3322
3323 search_bar.update_in(cx, |search_bar, window, cx| {
3324 search_bar.dismiss(&Dismiss, window, cx);
3325 });
3326
3327 assert_eq!(
3328 events.try_next().unwrap(),
3329 Some(ToolbarItemEvent::ChangeLocation(
3330 ToolbarItemLocation::PrimaryLeft
3331 ))
3332 );
3333
3334 search_bar.update_in(cx, |search_bar, window, cx| {
3335 search_bar.show(window, cx);
3336 });
3337
3338 assert_eq!(
3339 events.try_next().unwrap(),
3340 Some(ToolbarItemEvent::ChangeLocation(
3341 ToolbarItemLocation::PrimaryLeft
3342 ))
3343 );
3344 }
3345
3346 #[perf]
3347 #[gpui::test]
3348 async fn test_hides_and_uses_secondary_when_part_of_project_search(cx: &mut TestAppContext) {
3349 let (editor, search_bar, cx) = init_multibuffer_test(cx);
3350
3351 editor.update(cx, |editor, _| {
3352 editor.set_in_project_search(true);
3353 });
3354
3355 let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3356 search_bar.set_active_pane_item(Some(&editor), window, cx)
3357 });
3358
3359 assert_eq!(initial_location, ToolbarItemLocation::Hidden);
3360
3361 let mut events = cx.events(&search_bar);
3362
3363 search_bar.update_in(cx, |search_bar, window, cx| {
3364 search_bar.dismiss(&Dismiss, window, cx);
3365 });
3366
3367 assert_eq!(
3368 events.try_next().unwrap(),
3369 Some(ToolbarItemEvent::ChangeLocation(
3370 ToolbarItemLocation::Hidden
3371 ))
3372 );
3373
3374 search_bar.update_in(cx, |search_bar, window, cx| {
3375 search_bar.show(window, cx);
3376 });
3377
3378 assert_eq!(
3379 events.try_next().unwrap(),
3380 Some(ToolbarItemEvent::ChangeLocation(
3381 ToolbarItemLocation::Secondary
3382 ))
3383 );
3384 }
3385
3386 #[perf]
3387 #[gpui::test]
3388 async fn test_sets_collapsed_when_editor_fold_events_emitted(cx: &mut TestAppContext) {
3389 let (editor, search_bar, cx) = init_multibuffer_test(cx);
3390
3391 search_bar.update_in(cx, |search_bar, window, cx| {
3392 search_bar.set_active_pane_item(Some(&editor), window, cx);
3393 });
3394
3395 editor.update_in(cx, |editor, window, cx| {
3396 editor.fold_all(&FoldAll, window, cx);
3397 });
3398
3399 let is_collapsed = search_bar.read_with(cx, |search_bar, _| search_bar.is_collapsed);
3400
3401 assert!(is_collapsed);
3402
3403 editor.update_in(cx, |editor, window, cx| {
3404 editor.unfold_all(&UnfoldAll, window, cx);
3405 });
3406
3407 let is_collapsed = search_bar.read_with(cx, |search_bar, _| search_bar.is_collapsed);
3408
3409 assert!(!is_collapsed);
3410 }
3411
3412 #[perf]
3413 #[gpui::test]
3414 async fn test_search_options_changes(cx: &mut TestAppContext) {
3415 let (_editor, search_bar, cx) = init_test(cx);
3416 update_search_settings(
3417 SearchSettings {
3418 button: true,
3419 whole_word: false,
3420 case_sensitive: false,
3421 include_ignored: false,
3422 regex: false,
3423 center_on_match: false,
3424 },
3425 cx,
3426 );
3427
3428 let deploy = Deploy {
3429 focus: true,
3430 replace_enabled: false,
3431 selection_search_enabled: true,
3432 };
3433
3434 search_bar.update_in(cx, |search_bar, window, cx| {
3435 assert_eq!(
3436 search_bar.search_options,
3437 SearchOptions::NONE,
3438 "Should have no search options enabled by default"
3439 );
3440 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3441 assert_eq!(
3442 search_bar.search_options,
3443 SearchOptions::WHOLE_WORD,
3444 "Should enable the option toggled"
3445 );
3446 assert!(
3447 !search_bar.dismissed,
3448 "Search bar should be present and visible"
3449 );
3450 search_bar.deploy(&deploy, window, cx);
3451 assert_eq!(
3452 search_bar.search_options,
3453 SearchOptions::WHOLE_WORD,
3454 "After (re)deploying, the option should still be enabled"
3455 );
3456
3457 search_bar.dismiss(&Dismiss, window, cx);
3458 search_bar.deploy(&deploy, window, cx);
3459 assert_eq!(
3460 search_bar.search_options,
3461 SearchOptions::WHOLE_WORD,
3462 "After hiding and showing the search bar, search options should be preserved"
3463 );
3464
3465 search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
3466 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3467 assert_eq!(
3468 search_bar.search_options,
3469 SearchOptions::REGEX,
3470 "Should enable the options toggled"
3471 );
3472 assert!(
3473 !search_bar.dismissed,
3474 "Search bar should be present and visible"
3475 );
3476 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3477 });
3478
3479 update_search_settings(
3480 SearchSettings {
3481 button: true,
3482 whole_word: false,
3483 case_sensitive: true,
3484 include_ignored: false,
3485 regex: false,
3486 center_on_match: false,
3487 },
3488 cx,
3489 );
3490 search_bar.update_in(cx, |search_bar, window, cx| {
3491 assert_eq!(
3492 search_bar.search_options,
3493 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3494 "Should have no search options enabled by default"
3495 );
3496
3497 search_bar.deploy(&deploy, window, cx);
3498 assert_eq!(
3499 search_bar.search_options,
3500 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3501 "Toggling a non-dismissed search bar with custom options should not change the default options"
3502 );
3503 search_bar.dismiss(&Dismiss, window, cx);
3504 search_bar.deploy(&deploy, window, cx);
3505 assert_eq!(
3506 search_bar.configured_options,
3507 SearchOptions::CASE_SENSITIVE,
3508 "After a settings update and toggling the search bar, configured options should be updated"
3509 );
3510 assert_eq!(
3511 search_bar.search_options,
3512 SearchOptions::CASE_SENSITIVE,
3513 "After a settings update and toggling the search bar, configured options should be used"
3514 );
3515 });
3516
3517 update_search_settings(
3518 SearchSettings {
3519 button: true,
3520 whole_word: true,
3521 case_sensitive: true,
3522 include_ignored: false,
3523 regex: false,
3524 center_on_match: false,
3525 },
3526 cx,
3527 );
3528
3529 search_bar.update_in(cx, |search_bar, window, cx| {
3530 search_bar.deploy(&deploy, window, cx);
3531 search_bar.dismiss(&Dismiss, window, cx);
3532 search_bar.show(window, cx);
3533 assert_eq!(
3534 search_bar.search_options,
3535 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
3536 "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
3537 );
3538 });
3539 }
3540
3541 #[gpui::test]
3542 async fn test_select_occurrence_case_sensitivity(cx: &mut TestAppContext) {
3543 let (editor, search_bar, cx) = init_test(cx);
3544 let mut editor_cx = EditorTestContext::for_editor_in(editor, cx).await;
3545
3546 // Start with case sensitive search settings.
3547 let mut search_settings = SearchSettings::default();
3548 search_settings.case_sensitive = true;
3549 update_search_settings(search_settings, cx);
3550 search_bar.update(cx, |search_bar, cx| {
3551 let mut search_options = search_bar.search_options;
3552 search_options.insert(SearchOptions::CASE_SENSITIVE);
3553 search_bar.set_search_options(search_options, cx);
3554 });
3555
3556 editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3557 editor_cx.update_editor(|e, window, cx| {
3558 e.select_next(&Default::default(), window, cx).unwrap();
3559 });
3560 editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
3561
3562 // Update the search bar's case sensitivite toggle, so we can later
3563 // confirm that `select_next` will now be case-insensitive.
3564 editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3565 search_bar.update_in(cx, |search_bar, window, cx| {
3566 search_bar.toggle_case_sensitive(&Default::default(), window, cx);
3567 });
3568 editor_cx.update_editor(|e, window, cx| {
3569 e.select_next(&Default::default(), window, cx).unwrap();
3570 });
3571 editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
3572
3573 // Confirm that, after dismissing the search bar, only the editor's
3574 // search settings actually affect the behavior of `select_next`.
3575 search_bar.update_in(cx, |search_bar, window, cx| {
3576 search_bar.dismiss(&Default::default(), window, cx);
3577 });
3578 editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3579 editor_cx.update_editor(|e, window, cx| {
3580 e.select_next(&Default::default(), window, cx).unwrap();
3581 });
3582 editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
3583
3584 // Update the editor's search settings, disabling case sensitivity, to
3585 // check that the value is respected.
3586 let mut search_settings = SearchSettings::default();
3587 search_settings.case_sensitive = false;
3588 update_search_settings(search_settings, cx);
3589 editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3590 editor_cx.update_editor(|e, window, cx| {
3591 e.select_next(&Default::default(), window, cx).unwrap();
3592 });
3593 editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
3594 }
3595
3596 fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
3597 cx.update(|cx| {
3598 SettingsStore::update_global(cx, |store, cx| {
3599 store.update_user_settings(cx, |settings| {
3600 settings.editor.search = Some(SearchSettingsContent {
3601 button: Some(search_settings.button),
3602 whole_word: Some(search_settings.whole_word),
3603 case_sensitive: Some(search_settings.case_sensitive),
3604 include_ignored: Some(search_settings.include_ignored),
3605 regex: Some(search_settings.regex),
3606 center_on_match: Some(search_settings.center_on_match),
3607 });
3608 });
3609 });
3610 });
3611 }
3612}