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