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