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