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