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