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