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 // This isn't a normal setting; it's only applicable to vim search.
934 self.search_options.remove(SearchOptions::BACKWARDS);
935
936 self.dismissed = false;
937 self.adjust_query_regex_language(cx);
938 handle.search_bar_visibility_changed(true, window, cx);
939 cx.notify();
940 cx.emit(Event::UpdateLocation);
941 cx.emit(ToolbarItemEvent::ChangeLocation(
942 if self.needs_expand_collapse_option(cx) {
943 ToolbarItemLocation::PrimaryLeft
944 } else {
945 ToolbarItemLocation::Secondary
946 },
947 ));
948 true
949 }
950
951 fn supported_options(&self, cx: &mut Context<Self>) -> workspace::searchable::SearchOptions {
952 self.active_searchable_item
953 .as_ref()
954 .map(|item| item.supported_options(cx))
955 .unwrap_or_default()
956 }
957
958 // We provide an expand/collapse button if we are in a multibuffer
959 // and not doing a project search.
960 fn needs_expand_collapse_option(&self, cx: &App) -> bool {
961 if let Some(item) = &self.active_searchable_item {
962 let buffer_kind = item.buffer_kind(cx);
963
964 if buffer_kind == ItemBufferKind::Singleton {
965 return false;
966 }
967
968 let workspace::searchable::SearchOptions {
969 find_in_results, ..
970 } = item.supported_options(cx);
971 !find_in_results
972 } else {
973 false
974 }
975 }
976
977 fn toggle_fold_all(&mut self, _: &ToggleFoldAll, window: &mut Window, cx: &mut Context<Self>) {
978 self.toggle_fold_all_in_item(window, cx);
979 }
980
981 fn toggle_fold_all_in_item(&self, window: &mut Window, cx: &mut Context<Self>) {
982 let is_collapsed = self.is_collapsed;
983 if let Some(item) = &self.active_searchable_item {
984 if let Some(item) = item.act_as_type(TypeId::of::<Editor>(), cx) {
985 let editor = item.downcast::<Editor>().expect("Is an editor");
986 editor.update(cx, |editor, cx| {
987 if is_collapsed {
988 editor.unfold_all(&UnfoldAll, window, cx);
989 } else {
990 editor.fold_all(&FoldAll, window, cx);
991 }
992 })
993 }
994 }
995 }
996
997 pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context<Self>) {
998 let search = self.query_suggestion(window, cx).map(|suggestion| {
999 self.search(&suggestion, Some(self.default_options), true, window, cx)
1000 });
1001
1002 #[cfg(target_os = "macos")]
1003 let search = search.or_else(|| {
1004 self.pending_external_query
1005 .take()
1006 .map(|(query, options)| self.search(&query, Some(options), true, window, cx))
1007 });
1008
1009 if let Some(search) = search {
1010 cx.spawn_in(window, async move |this, cx| {
1011 if search.await.is_ok() {
1012 this.update_in(cx, |this, window, cx| {
1013 if !this.dismissed {
1014 this.activate_current_match(window, cx)
1015 }
1016 })
1017 } else {
1018 Ok(())
1019 }
1020 })
1021 .detach_and_log_err(cx);
1022 }
1023 }
1024
1025 pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1026 if let Some(match_ix) = self.active_match_index
1027 && let Some(active_searchable_item) = self.active_searchable_item.as_ref()
1028 && let Some(matches) = self
1029 .searchable_items_with_matches
1030 .get(&active_searchable_item.downgrade())
1031 {
1032 active_searchable_item.activate_match(match_ix, matches, window, cx)
1033 }
1034 }
1035
1036 pub fn select_query(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1037 self.query_editor.update(cx, |query_editor, cx| {
1038 query_editor.select_all(&Default::default(), window, cx);
1039 });
1040 }
1041
1042 pub fn query(&self, cx: &App) -> String {
1043 self.query_editor.read(cx).text(cx)
1044 }
1045
1046 pub fn replacement(&self, cx: &mut App) -> String {
1047 self.replacement_editor.read(cx).text(cx)
1048 }
1049
1050 pub fn query_suggestion(
1051 &mut self,
1052 window: &mut Window,
1053 cx: &mut Context<Self>,
1054 ) -> Option<String> {
1055 self.active_searchable_item
1056 .as_ref()
1057 .map(|searchable_item| searchable_item.query_suggestion(window, cx))
1058 .filter(|suggestion| !suggestion.is_empty())
1059 }
1060
1061 pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut Context<Self>) {
1062 if replacement.is_none() {
1063 self.replace_enabled = false;
1064 return;
1065 }
1066 self.replace_enabled = true;
1067 self.replacement_editor
1068 .update(cx, |replacement_editor, cx| {
1069 replacement_editor
1070 .buffer()
1071 .update(cx, |replacement_buffer, cx| {
1072 let len = replacement_buffer.len(cx);
1073 replacement_buffer.edit(
1074 [(MultiBufferOffset(0)..len, replacement.unwrap())],
1075 None,
1076 cx,
1077 );
1078 });
1079 });
1080 }
1081
1082 pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1083 self.focus(&self.replacement_editor.focus_handle(cx), window, cx);
1084 cx.notify();
1085 }
1086
1087 pub fn search(
1088 &mut self,
1089 query: &str,
1090 options: Option<SearchOptions>,
1091 add_to_history: bool,
1092 window: &mut Window,
1093 cx: &mut Context<Self>,
1094 ) -> oneshot::Receiver<()> {
1095 let options = options.unwrap_or(self.default_options);
1096 let updated = query != self.query(cx) || self.search_options != options;
1097 if updated {
1098 self.query_editor.update(cx, |query_editor, cx| {
1099 query_editor.buffer().update(cx, |query_buffer, cx| {
1100 let len = query_buffer.len(cx);
1101 query_buffer.edit([(MultiBufferOffset(0)..len, query)], None, cx);
1102 });
1103 });
1104 self.set_search_options(options, cx);
1105 self.clear_matches(window, cx);
1106 #[cfg(target_os = "macos")]
1107 self.update_find_pasteboard(cx);
1108 cx.notify();
1109 }
1110 self.update_matches(!updated, add_to_history, window, cx)
1111 }
1112
1113 #[cfg(target_os = "macos")]
1114 pub fn update_find_pasteboard(&mut self, cx: &mut App) {
1115 cx.write_to_find_pasteboard(gpui::ClipboardItem::new_string_with_metadata(
1116 self.query(cx),
1117 self.search_options.bits().to_string(),
1118 ));
1119 }
1120
1121 pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
1122 if let Some(active_editor) = self.active_searchable_item.as_ref() {
1123 let handle = active_editor.item_focus_handle(cx);
1124 window.focus(&handle, cx);
1125 }
1126 }
1127
1128 pub fn toggle_search_option(
1129 &mut self,
1130 search_option: SearchOptions,
1131 window: &mut Window,
1132 cx: &mut Context<Self>,
1133 ) {
1134 self.search_options.toggle(search_option);
1135 self.default_options = self.search_options;
1136 drop(self.update_matches(false, false, window, cx));
1137 self.adjust_query_regex_language(cx);
1138 self.sync_select_next_case_sensitivity(cx);
1139 cx.notify();
1140 }
1141
1142 pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
1143 self.search_options.contains(search_option)
1144 }
1145
1146 pub fn enable_search_option(
1147 &mut self,
1148 search_option: SearchOptions,
1149 window: &mut Window,
1150 cx: &mut Context<Self>,
1151 ) {
1152 if !self.search_options.contains(search_option) {
1153 self.toggle_search_option(search_option, window, cx)
1154 }
1155 }
1156
1157 pub fn set_search_within_selection(
1158 &mut self,
1159 search_within_selection: Option<FilteredSearchRange>,
1160 window: &mut Window,
1161 cx: &mut Context<Self>,
1162 ) -> Option<oneshot::Receiver<()>> {
1163 let active_item = self.active_searchable_item.as_mut()?;
1164 self.selection_search_enabled = search_within_selection;
1165 active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx);
1166 cx.notify();
1167 Some(self.update_matches(false, false, window, cx))
1168 }
1169
1170 pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context<Self>) {
1171 self.search_options = search_options;
1172 self.adjust_query_regex_language(cx);
1173 self.sync_select_next_case_sensitivity(cx);
1174 cx.notify();
1175 }
1176
1177 pub fn clear_search_within_ranges(
1178 &mut self,
1179 search_options: SearchOptions,
1180 cx: &mut Context<Self>,
1181 ) {
1182 self.search_options = search_options;
1183 self.adjust_query_regex_language(cx);
1184 cx.notify();
1185 }
1186
1187 fn select_next_match(
1188 &mut self,
1189 _: &SelectNextMatch,
1190 window: &mut Window,
1191 cx: &mut Context<Self>,
1192 ) {
1193 self.select_match(Direction::Next, 1, window, cx);
1194 }
1195
1196 fn select_prev_match(
1197 &mut self,
1198 _: &SelectPreviousMatch,
1199 window: &mut Window,
1200 cx: &mut Context<Self>,
1201 ) {
1202 self.select_match(Direction::Prev, 1, window, cx);
1203 }
1204
1205 pub fn select_all_matches(
1206 &mut self,
1207 _: &SelectAllMatches,
1208 window: &mut Window,
1209 cx: &mut Context<Self>,
1210 ) {
1211 if !self.dismissed
1212 && self.active_match_index.is_some()
1213 && let Some(searchable_item) = self.active_searchable_item.as_ref()
1214 && let Some(matches) = self
1215 .searchable_items_with_matches
1216 .get(&searchable_item.downgrade())
1217 {
1218 searchable_item.select_matches(matches, window, cx);
1219 self.focus_editor(&FocusEditor, window, cx);
1220 }
1221 }
1222
1223 pub fn select_match(
1224 &mut self,
1225 direction: Direction,
1226 count: usize,
1227 window: &mut Window,
1228 cx: &mut Context<Self>,
1229 ) {
1230 #[cfg(target_os = "macos")]
1231 if let Some((query, options)) = self.pending_external_query.take() {
1232 let search_rx = self.search(&query, Some(options), true, window, cx);
1233 cx.spawn_in(window, async move |this, cx| {
1234 if search_rx.await.is_ok() {
1235 this.update_in(cx, |this, window, cx| {
1236 this.activate_current_match(window, cx);
1237 })
1238 .ok();
1239 }
1240 })
1241 .detach();
1242
1243 return;
1244 }
1245
1246 if let Some(index) = self.active_match_index
1247 && let Some(searchable_item) = self.active_searchable_item.as_ref()
1248 && let Some(matches) = self
1249 .searchable_items_with_matches
1250 .get(&searchable_item.downgrade())
1251 .filter(|matches| !matches.is_empty())
1252 {
1253 // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
1254 if !EditorSettings::get_global(cx).search_wrap
1255 && ((direction == Direction::Next && index + count >= matches.len())
1256 || (direction == Direction::Prev && index < count))
1257 {
1258 crate::show_no_more_matches(window, cx);
1259 return;
1260 }
1261 let new_match_index = searchable_item
1262 .match_index_for_direction(matches, index, direction, count, window, cx);
1263
1264 searchable_item.update_matches(matches, Some(new_match_index), window, cx);
1265 searchable_item.activate_match(new_match_index, matches, window, cx);
1266 }
1267 }
1268
1269 pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1270 if let Some(searchable_item) = self.active_searchable_item.as_ref()
1271 && let Some(matches) = self
1272 .searchable_items_with_matches
1273 .get(&searchable_item.downgrade())
1274 {
1275 if matches.is_empty() {
1276 return;
1277 }
1278 searchable_item.update_matches(matches, Some(0), window, cx);
1279 searchable_item.activate_match(0, matches, window, cx);
1280 }
1281 }
1282
1283 pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1284 if let Some(searchable_item) = self.active_searchable_item.as_ref()
1285 && let Some(matches) = self
1286 .searchable_items_with_matches
1287 .get(&searchable_item.downgrade())
1288 {
1289 if matches.is_empty() {
1290 return;
1291 }
1292 let new_match_index = matches.len() - 1;
1293 searchable_item.update_matches(matches, Some(new_match_index), window, cx);
1294 searchable_item.activate_match(new_match_index, matches, window, cx);
1295 }
1296 }
1297
1298 fn on_query_editor_event(
1299 &mut self,
1300 editor: &Entity<Editor>,
1301 event: &editor::EditorEvent,
1302 window: &mut Window,
1303 cx: &mut Context<Self>,
1304 ) {
1305 match event {
1306 editor::EditorEvent::Focused => self.query_editor_focused = true,
1307 editor::EditorEvent::Blurred => self.query_editor_focused = false,
1308 editor::EditorEvent::Edited { .. } => {
1309 self.smartcase(window, cx);
1310 self.clear_matches(window, cx);
1311 let search = self.update_matches(false, true, window, cx);
1312
1313 let width = editor.update(cx, |editor, cx| {
1314 let text_layout_details = editor.text_layout_details(window);
1315 let snapshot = editor.snapshot(window, cx).display_snapshot;
1316
1317 snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
1318 - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
1319 });
1320 self.editor_needed_width = width;
1321 cx.notify();
1322
1323 cx.spawn_in(window, async move |this, cx| {
1324 if search.await.is_ok() {
1325 this.update_in(cx, |this, window, cx| {
1326 this.activate_current_match(window, cx);
1327 #[cfg(target_os = "macos")]
1328 this.update_find_pasteboard(cx);
1329 })?;
1330 }
1331 anyhow::Ok(())
1332 })
1333 .detach_and_log_err(cx);
1334 }
1335 _ => {}
1336 }
1337 }
1338
1339 fn on_replacement_editor_event(
1340 &mut self,
1341 _: Entity<Editor>,
1342 event: &editor::EditorEvent,
1343 _: &mut Context<Self>,
1344 ) {
1345 match event {
1346 editor::EditorEvent::Focused => self.replacement_editor_focused = true,
1347 editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
1348 _ => {}
1349 }
1350 }
1351
1352 fn on_active_searchable_item_event(
1353 &mut self,
1354 event: &SearchEvent,
1355 window: &mut Window,
1356 cx: &mut Context<Self>,
1357 ) {
1358 match event {
1359 SearchEvent::MatchesInvalidated => {
1360 drop(self.update_matches(false, false, window, cx));
1361 }
1362 SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx),
1363 SearchEvent::ResultsCollapsedChanged(collapse_direction) => {
1364 if self.needs_expand_collapse_option(cx) {
1365 match collapse_direction {
1366 CollapseDirection::Collapsed => self.is_collapsed = true,
1367 CollapseDirection::Expanded => self.is_collapsed = false,
1368 }
1369 }
1370 cx.notify();
1371 }
1372 }
1373 }
1374
1375 fn toggle_case_sensitive(
1376 &mut self,
1377 _: &ToggleCaseSensitive,
1378 window: &mut Window,
1379 cx: &mut Context<Self>,
1380 ) {
1381 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx)
1382 }
1383
1384 fn toggle_whole_word(
1385 &mut self,
1386 _: &ToggleWholeWord,
1387 window: &mut Window,
1388 cx: &mut Context<Self>,
1389 ) {
1390 self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1391 }
1392
1393 fn toggle_selection(
1394 &mut self,
1395 _: &ToggleSelection,
1396 window: &mut Window,
1397 cx: &mut Context<Self>,
1398 ) {
1399 self.set_search_within_selection(
1400 if let Some(_) = self.selection_search_enabled {
1401 None
1402 } else {
1403 Some(FilteredSearchRange::Default)
1404 },
1405 window,
1406 cx,
1407 );
1408 }
1409
1410 fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context<Self>) {
1411 self.toggle_search_option(SearchOptions::REGEX, window, cx)
1412 }
1413
1414 fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) {
1415 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1416 self.active_match_index = None;
1417 self.searchable_items_with_matches
1418 .remove(&active_searchable_item.downgrade());
1419 active_searchable_item.clear_matches(window, cx);
1420 }
1421 }
1422
1423 pub fn has_active_match(&self) -> bool {
1424 self.active_match_index.is_some()
1425 }
1426
1427 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1428 let mut active_item_matches = None;
1429 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
1430 if let Some(searchable_item) =
1431 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
1432 {
1433 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
1434 active_item_matches = Some((searchable_item.downgrade(), matches));
1435 } else {
1436 searchable_item.clear_matches(window, cx);
1437 }
1438 }
1439 }
1440
1441 self.searchable_items_with_matches
1442 .extend(active_item_matches);
1443 }
1444
1445 fn update_matches(
1446 &mut self,
1447 reuse_existing_query: bool,
1448 add_to_history: bool,
1449 window: &mut Window,
1450 cx: &mut Context<Self>,
1451 ) -> oneshot::Receiver<()> {
1452 let (done_tx, done_rx) = oneshot::channel();
1453 let query = self.query(cx);
1454 self.pending_search.take();
1455 #[cfg(target_os = "macos")]
1456 self.pending_external_query.take();
1457
1458 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1459 self.query_error = None;
1460 if query.is_empty() {
1461 self.clear_active_searchable_item_matches(window, cx);
1462 let _ = done_tx.send(());
1463 cx.notify();
1464 } else {
1465 let query: Arc<_> = if let Some(search) =
1466 self.active_search.take().filter(|_| reuse_existing_query)
1467 {
1468 search
1469 } else {
1470 // Value doesn't matter, we only construct empty matchers with it
1471
1472 if self.search_options.contains(SearchOptions::REGEX) {
1473 match SearchQuery::regex(
1474 query,
1475 self.search_options.contains(SearchOptions::WHOLE_WORD),
1476 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1477 false,
1478 self.search_options
1479 .contains(SearchOptions::ONE_MATCH_PER_LINE),
1480 PathMatcher::default(),
1481 PathMatcher::default(),
1482 false,
1483 None,
1484 ) {
1485 Ok(query) => query.with_replacement(self.replacement(cx)),
1486 Err(e) => {
1487 self.query_error = Some(e.to_string());
1488 self.clear_active_searchable_item_matches(window, cx);
1489 cx.notify();
1490 return done_rx;
1491 }
1492 }
1493 } else {
1494 match SearchQuery::text(
1495 query,
1496 self.search_options.contains(SearchOptions::WHOLE_WORD),
1497 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1498 false,
1499 PathMatcher::default(),
1500 PathMatcher::default(),
1501 false,
1502 None,
1503 ) {
1504 Ok(query) => query.with_replacement(self.replacement(cx)),
1505 Err(e) => {
1506 self.query_error = Some(e.to_string());
1507 self.clear_active_searchable_item_matches(window, cx);
1508 cx.notify();
1509 return done_rx;
1510 }
1511 }
1512 }
1513 .into()
1514 };
1515
1516 self.active_search = Some(query.clone());
1517 let query_text = query.as_str().to_string();
1518
1519 let matches = active_searchable_item.find_matches(query, window, cx);
1520
1521 let active_searchable_item = active_searchable_item.downgrade();
1522 self.pending_search = Some(cx.spawn_in(window, async move |this, cx| {
1523 let matches = matches.await;
1524
1525 this.update_in(cx, |this, window, cx| {
1526 if let Some(active_searchable_item) =
1527 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1528 {
1529 this.searchable_items_with_matches
1530 .insert(active_searchable_item.downgrade(), matches);
1531
1532 this.update_match_index(window, cx);
1533
1534 if add_to_history {
1535 this.search_history
1536 .add(&mut this.search_history_cursor, query_text);
1537 }
1538 if !this.dismissed {
1539 let matches = this
1540 .searchable_items_with_matches
1541 .get(&active_searchable_item.downgrade())
1542 .unwrap();
1543 if matches.is_empty() {
1544 active_searchable_item.clear_matches(window, cx);
1545 } else {
1546 active_searchable_item.update_matches(
1547 matches,
1548 this.active_match_index,
1549 window,
1550 cx,
1551 );
1552 }
1553 }
1554 let _ = done_tx.send(());
1555 cx.notify();
1556 }
1557 })
1558 .log_err();
1559 }));
1560 }
1561 }
1562 done_rx
1563 }
1564
1565 fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction {
1566 if self.search_options.contains(SearchOptions::BACKWARDS) {
1567 direction.opposite()
1568 } else {
1569 direction
1570 }
1571 }
1572
1573 pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1574 let direction = self.reverse_direction_if_backwards(Direction::Next);
1575 let new_index = self
1576 .active_searchable_item
1577 .as_ref()
1578 .and_then(|searchable_item| {
1579 let matches = self
1580 .searchable_items_with_matches
1581 .get(&searchable_item.downgrade())?;
1582 searchable_item.active_match_index(direction, matches, window, cx)
1583 });
1584 if new_index != self.active_match_index {
1585 self.active_match_index = new_index;
1586 if !self.dismissed {
1587 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1588 if let Some(matches) = self
1589 .searchable_items_with_matches
1590 .get(&searchable_item.downgrade())
1591 {
1592 if !matches.is_empty() {
1593 searchable_item.update_matches(matches, new_index, window, cx);
1594 }
1595 }
1596 }
1597 }
1598 cx.notify();
1599 }
1600 }
1601
1602 fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1603 self.cycle_field(Direction::Next, window, cx);
1604 }
1605
1606 fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1607 self.cycle_field(Direction::Prev, window, cx);
1608 }
1609 fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1610 let mut handles = vec![self.query_editor.focus_handle(cx)];
1611 if self.replace_enabled {
1612 handles.push(self.replacement_editor.focus_handle(cx));
1613 }
1614 if let Some(item) = self.active_searchable_item.as_ref() {
1615 handles.push(item.item_focus_handle(cx));
1616 }
1617 let current_index = match handles.iter().position(|focus| focus.is_focused(window)) {
1618 Some(index) => index,
1619 None => return,
1620 };
1621
1622 let new_index = match direction {
1623 Direction::Next => (current_index + 1) % handles.len(),
1624 Direction::Prev if current_index == 0 => handles.len() - 1,
1625 Direction::Prev => (current_index - 1) % handles.len(),
1626 };
1627 let next_focus_handle = &handles[new_index];
1628 self.focus(next_focus_handle, window, cx);
1629 cx.stop_propagation();
1630 }
1631
1632 fn next_history_query(
1633 &mut self,
1634 _: &NextHistoryQuery,
1635 window: &mut Window,
1636 cx: &mut Context<Self>,
1637 ) {
1638 if let Some(new_query) = self
1639 .search_history
1640 .next(&mut self.search_history_cursor)
1641 .map(str::to_string)
1642 {
1643 drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1644 } else {
1645 self.search_history_cursor.reset();
1646 drop(self.search("", Some(self.search_options), false, window, cx));
1647 }
1648 }
1649
1650 fn previous_history_query(
1651 &mut self,
1652 _: &PreviousHistoryQuery,
1653 window: &mut Window,
1654 cx: &mut Context<Self>,
1655 ) {
1656 if self.query(cx).is_empty()
1657 && let Some(new_query) = self
1658 .search_history
1659 .current(&self.search_history_cursor)
1660 .map(str::to_string)
1661 {
1662 drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1663 return;
1664 }
1665
1666 if let Some(new_query) = self
1667 .search_history
1668 .previous(&mut self.search_history_cursor)
1669 .map(str::to_string)
1670 {
1671 drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1672 }
1673 }
1674
1675 fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut App) {
1676 window.invalidate_character_coordinates();
1677 window.focus(handle, cx);
1678 }
1679
1680 fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1681 if self.active_searchable_item.is_some() {
1682 self.replace_enabled = !self.replace_enabled;
1683 let handle = if self.replace_enabled {
1684 self.replacement_editor.focus_handle(cx)
1685 } else {
1686 self.query_editor.focus_handle(cx)
1687 };
1688 self.focus(&handle, window, cx);
1689 cx.notify();
1690 }
1691 }
1692
1693 fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1694 let mut should_propagate = true;
1695 if !self.dismissed
1696 && self.active_search.is_some()
1697 && let Some(searchable_item) = self.active_searchable_item.as_ref()
1698 && let Some(query) = self.active_search.as_ref()
1699 && let Some(matches) = self
1700 .searchable_items_with_matches
1701 .get(&searchable_item.downgrade())
1702 {
1703 if let Some(active_index) = self.active_match_index {
1704 let query = query
1705 .as_ref()
1706 .clone()
1707 .with_replacement(self.replacement(cx));
1708 searchable_item.replace(matches.at(active_index), &query, window, cx);
1709 self.select_next_match(&SelectNextMatch, window, cx);
1710 }
1711 should_propagate = false;
1712 }
1713 if !should_propagate {
1714 cx.stop_propagation();
1715 }
1716 }
1717
1718 pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1719 if !self.dismissed
1720 && self.active_search.is_some()
1721 && let Some(searchable_item) = self.active_searchable_item.as_ref()
1722 && let Some(query) = self.active_search.as_ref()
1723 && let Some(matches) = self
1724 .searchable_items_with_matches
1725 .get(&searchable_item.downgrade())
1726 {
1727 let query = query
1728 .as_ref()
1729 .clone()
1730 .with_replacement(self.replacement(cx));
1731 searchable_item.replace_all(&mut matches.iter(), &query, window, cx);
1732 }
1733 }
1734
1735 pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1736 self.update_match_index(window, cx);
1737 self.active_match_index.is_some()
1738 }
1739
1740 pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1741 EditorSettings::get_global(cx).use_smartcase_search
1742 }
1743
1744 pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1745 str.chars().any(|c| c.is_uppercase())
1746 }
1747
1748 fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1749 if self.should_use_smartcase_search(cx) {
1750 let query = self.query(cx);
1751 if !query.is_empty() {
1752 let is_case = self.is_contains_uppercase(&query);
1753 if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1754 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1755 }
1756 }
1757 }
1758 }
1759
1760 fn adjust_query_regex_language(&self, cx: &mut App) {
1761 let enable = self.search_options.contains(SearchOptions::REGEX);
1762 let query_buffer = self
1763 .query_editor
1764 .read(cx)
1765 .buffer()
1766 .read(cx)
1767 .as_singleton()
1768 .expect("query editor should be backed by a singleton buffer");
1769
1770 if enable {
1771 if let Some(regex_language) = self.regex_language.clone() {
1772 query_buffer.update(cx, |query_buffer, cx| {
1773 query_buffer.set_language(Some(regex_language), cx);
1774 })
1775 }
1776 } else {
1777 query_buffer.update(cx, |query_buffer, cx| {
1778 query_buffer.set_language(None, cx);
1779 })
1780 }
1781 }
1782
1783 /// Updates the searchable item's case sensitivity option to match the
1784 /// search bar's current case sensitivity setting. This ensures that
1785 /// editor's `select_next`/ `select_previous` operations respect the buffer
1786 /// search bar's search options.
1787 ///
1788 /// Clears the case sensitivity when the search bar is dismissed so that
1789 /// only the editor's settings are respected.
1790 fn sync_select_next_case_sensitivity(&self, cx: &mut Context<Self>) {
1791 let case_sensitive = match self.dismissed {
1792 true => None,
1793 false => Some(self.search_options.contains(SearchOptions::CASE_SENSITIVE)),
1794 };
1795
1796 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1797 active_searchable_item.set_search_is_case_sensitive(case_sensitive, cx);
1798 }
1799 }
1800}
1801
1802#[cfg(test)]
1803mod tests {
1804 use std::ops::Range;
1805
1806 use super::*;
1807 use editor::{
1808 DisplayPoint, Editor, ExcerptRange, MultiBuffer, SearchSettings, SelectionEffects,
1809 display_map::DisplayRow, test::editor_test_context::EditorTestContext,
1810 };
1811 use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1812 use language::{Buffer, Point};
1813 use settings::{SearchSettingsContent, SettingsStore};
1814 use smol::stream::StreamExt as _;
1815 use unindent::Unindent as _;
1816 use util_macros::perf;
1817
1818 fn init_globals(cx: &mut TestAppContext) {
1819 cx.update(|cx| {
1820 let store = settings::SettingsStore::test(cx);
1821 cx.set_global(store);
1822 editor::init(cx);
1823
1824 theme::init(theme::LoadThemes::JustBase, cx);
1825 crate::init(cx);
1826 });
1827 }
1828
1829 fn init_multibuffer_test(
1830 cx: &mut TestAppContext,
1831 ) -> (
1832 Entity<Editor>,
1833 Entity<BufferSearchBar>,
1834 &mut VisualTestContext,
1835 ) {
1836 init_globals(cx);
1837
1838 let buffer1 = cx.new(|cx| {
1839 Buffer::local(
1840 r#"
1841 A regular expression (shortened as regex or regexp;[1] also referred to as
1842 rational expression[2][3]) is a sequence of characters that specifies a search
1843 pattern in text. Usually such patterns are used by string-searching algorithms
1844 for "find" or "find and replace" operations on strings, or for input validation.
1845 "#
1846 .unindent(),
1847 cx,
1848 )
1849 });
1850
1851 let buffer2 = cx.new(|cx| {
1852 Buffer::local(
1853 r#"
1854 Some Additional text with the term regular expression in it.
1855 There two lines.
1856 "#
1857 .unindent(),
1858 cx,
1859 )
1860 });
1861
1862 let multibuffer = cx.new(|cx| {
1863 let mut buffer = MultiBuffer::new(language::Capability::ReadWrite);
1864
1865 //[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))]
1866 buffer.push_excerpts(
1867 buffer1,
1868 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
1869 cx,
1870 );
1871 buffer.push_excerpts(
1872 buffer2,
1873 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
1874 cx,
1875 );
1876
1877 buffer
1878 });
1879 let mut editor = None;
1880 let window = cx.add_window(|window, cx| {
1881 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
1882 "keymaps/default-macos.json",
1883 cx,
1884 )
1885 .unwrap();
1886 cx.bind_keys(default_key_bindings);
1887 editor =
1888 Some(cx.new(|cx| Editor::for_multibuffer(multibuffer.clone(), None, window, cx)));
1889
1890 let mut search_bar = BufferSearchBar::new(None, window, cx);
1891 search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
1892 search_bar.show(window, cx);
1893 search_bar
1894 });
1895 let search_bar = window.root(cx).unwrap();
1896
1897 let cx = VisualTestContext::from_window(*window, cx).into_mut();
1898
1899 (editor.unwrap(), search_bar, cx)
1900 }
1901
1902 fn init_test(
1903 cx: &mut TestAppContext,
1904 ) -> (
1905 Entity<Editor>,
1906 Entity<BufferSearchBar>,
1907 &mut VisualTestContext,
1908 ) {
1909 init_globals(cx);
1910 let buffer = cx.new(|cx| {
1911 Buffer::local(
1912 r#"
1913 A regular expression (shortened as regex or regexp;[1] also referred to as
1914 rational expression[2][3]) is a sequence of characters that specifies a search
1915 pattern in text. Usually such patterns are used by string-searching algorithms
1916 for "find" or "find and replace" operations on strings, or for input validation.
1917 "#
1918 .unindent(),
1919 cx,
1920 )
1921 });
1922 let mut editor = None;
1923 let window = cx.add_window(|window, cx| {
1924 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
1925 "keymaps/default-macos.json",
1926 cx,
1927 )
1928 .unwrap();
1929 cx.bind_keys(default_key_bindings);
1930 editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
1931 let mut search_bar = BufferSearchBar::new(None, window, cx);
1932 search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
1933 search_bar.show(window, cx);
1934 search_bar
1935 });
1936 let search_bar = window.root(cx).unwrap();
1937
1938 let cx = VisualTestContext::from_window(*window, cx).into_mut();
1939
1940 (editor.unwrap(), search_bar, cx)
1941 }
1942
1943 #[perf]
1944 #[gpui::test]
1945 async fn test_search_simple(cx: &mut TestAppContext) {
1946 let (editor, search_bar, cx) = init_test(cx);
1947 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1948 background_highlights
1949 .into_iter()
1950 .map(|(range, _)| range)
1951 .collect::<Vec<_>>()
1952 };
1953 // Search for a string that appears with different casing.
1954 // By default, search is case-insensitive.
1955 search_bar
1956 .update_in(cx, |search_bar, window, cx| {
1957 search_bar.search("us", None, true, window, cx)
1958 })
1959 .await
1960 .unwrap();
1961 editor.update_in(cx, |editor, window, cx| {
1962 assert_eq!(
1963 display_points_of(editor.all_text_background_highlights(window, cx)),
1964 &[
1965 DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1966 DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1967 ]
1968 );
1969 });
1970
1971 // Switch to a case sensitive search.
1972 search_bar.update_in(cx, |search_bar, window, cx| {
1973 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1974 });
1975 let mut editor_notifications = cx.notifications(&editor);
1976 editor_notifications.next().await;
1977 editor.update_in(cx, |editor, window, cx| {
1978 assert_eq!(
1979 display_points_of(editor.all_text_background_highlights(window, cx)),
1980 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1981 );
1982 });
1983
1984 // Search for a string that appears both as a whole word and
1985 // within other words. By default, all results are found.
1986 search_bar
1987 .update_in(cx, |search_bar, window, cx| {
1988 search_bar.search("or", None, true, window, cx)
1989 })
1990 .await
1991 .unwrap();
1992 editor.update_in(cx, |editor, window, cx| {
1993 assert_eq!(
1994 display_points_of(editor.all_text_background_highlights(window, cx)),
1995 &[
1996 DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1997 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1998 DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1999 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
2000 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
2001 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
2002 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
2003 ]
2004 );
2005 });
2006
2007 // Switch to a whole word search.
2008 search_bar.update_in(cx, |search_bar, window, cx| {
2009 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2010 });
2011 let mut editor_notifications = cx.notifications(&editor);
2012 editor_notifications.next().await;
2013 editor.update_in(cx, |editor, window, cx| {
2014 assert_eq!(
2015 display_points_of(editor.all_text_background_highlights(window, cx)),
2016 &[
2017 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
2018 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
2019 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
2020 ]
2021 );
2022 });
2023
2024 editor.update_in(cx, |editor, window, cx| {
2025 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2026 s.select_display_ranges([
2027 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2028 ])
2029 });
2030 });
2031 search_bar.update_in(cx, |search_bar, window, cx| {
2032 assert_eq!(search_bar.active_match_index, Some(0));
2033 search_bar.select_next_match(&SelectNextMatch, window, cx);
2034 assert_eq!(
2035 editor.update(cx, |editor, cx| editor
2036 .selections
2037 .display_ranges(&editor.display_snapshot(cx))),
2038 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2039 );
2040 });
2041 search_bar.read_with(cx, |search_bar, _| {
2042 assert_eq!(search_bar.active_match_index, Some(0));
2043 });
2044
2045 search_bar.update_in(cx, |search_bar, window, cx| {
2046 search_bar.select_next_match(&SelectNextMatch, window, cx);
2047 assert_eq!(
2048 editor.update(cx, |editor, cx| editor
2049 .selections
2050 .display_ranges(&editor.display_snapshot(cx))),
2051 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2052 );
2053 });
2054 search_bar.read_with(cx, |search_bar, _| {
2055 assert_eq!(search_bar.active_match_index, Some(1));
2056 });
2057
2058 search_bar.update_in(cx, |search_bar, window, cx| {
2059 search_bar.select_next_match(&SelectNextMatch, window, cx);
2060 assert_eq!(
2061 editor.update(cx, |editor, cx| editor
2062 .selections
2063 .display_ranges(&editor.display_snapshot(cx))),
2064 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2065 );
2066 });
2067 search_bar.read_with(cx, |search_bar, _| {
2068 assert_eq!(search_bar.active_match_index, Some(2));
2069 });
2070
2071 search_bar.update_in(cx, |search_bar, window, cx| {
2072 search_bar.select_next_match(&SelectNextMatch, window, cx);
2073 assert_eq!(
2074 editor.update(cx, |editor, cx| editor
2075 .selections
2076 .display_ranges(&editor.display_snapshot(cx))),
2077 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2078 );
2079 });
2080 search_bar.read_with(cx, |search_bar, _| {
2081 assert_eq!(search_bar.active_match_index, Some(0));
2082 });
2083
2084 search_bar.update_in(cx, |search_bar, window, cx| {
2085 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2086 assert_eq!(
2087 editor.update(cx, |editor, cx| editor
2088 .selections
2089 .display_ranges(&editor.display_snapshot(cx))),
2090 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2091 );
2092 });
2093 search_bar.read_with(cx, |search_bar, _| {
2094 assert_eq!(search_bar.active_match_index, Some(2));
2095 });
2096
2097 search_bar.update_in(cx, |search_bar, window, cx| {
2098 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2099 assert_eq!(
2100 editor.update(cx, |editor, cx| editor
2101 .selections
2102 .display_ranges(&editor.display_snapshot(cx))),
2103 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2104 );
2105 });
2106 search_bar.read_with(cx, |search_bar, _| {
2107 assert_eq!(search_bar.active_match_index, Some(1));
2108 });
2109
2110 search_bar.update_in(cx, |search_bar, window, cx| {
2111 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2112 assert_eq!(
2113 editor.update(cx, |editor, cx| editor
2114 .selections
2115 .display_ranges(&editor.display_snapshot(cx))),
2116 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2117 );
2118 });
2119 search_bar.read_with(cx, |search_bar, _| {
2120 assert_eq!(search_bar.active_match_index, Some(0));
2121 });
2122
2123 // Park the cursor in between matches and ensure that going to the previous match selects
2124 // the closest match to the left.
2125 editor.update_in(cx, |editor, window, cx| {
2126 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2127 s.select_display_ranges([
2128 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
2129 ])
2130 });
2131 });
2132 search_bar.update_in(cx, |search_bar, window, cx| {
2133 assert_eq!(search_bar.active_match_index, Some(1));
2134 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2135 assert_eq!(
2136 editor.update(cx, |editor, cx| editor
2137 .selections
2138 .display_ranges(&editor.display_snapshot(cx))),
2139 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2140 );
2141 });
2142 search_bar.read_with(cx, |search_bar, _| {
2143 assert_eq!(search_bar.active_match_index, Some(0));
2144 });
2145
2146 // Park the cursor in between matches and ensure that going to the next match selects the
2147 // closest match to the right.
2148 editor.update_in(cx, |editor, window, cx| {
2149 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2150 s.select_display_ranges([
2151 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
2152 ])
2153 });
2154 });
2155 search_bar.update_in(cx, |search_bar, window, cx| {
2156 assert_eq!(search_bar.active_match_index, Some(1));
2157 search_bar.select_next_match(&SelectNextMatch, window, cx);
2158 assert_eq!(
2159 editor.update(cx, |editor, cx| editor
2160 .selections
2161 .display_ranges(&editor.display_snapshot(cx))),
2162 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2163 );
2164 });
2165 search_bar.read_with(cx, |search_bar, _| {
2166 assert_eq!(search_bar.active_match_index, Some(1));
2167 });
2168
2169 // Park the cursor after the last match and ensure that going to the previous match selects
2170 // the last match.
2171 editor.update_in(cx, |editor, window, cx| {
2172 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2173 s.select_display_ranges([
2174 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2175 ])
2176 });
2177 });
2178 search_bar.update_in(cx, |search_bar, window, cx| {
2179 assert_eq!(search_bar.active_match_index, Some(2));
2180 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2181 assert_eq!(
2182 editor.update(cx, |editor, cx| editor
2183 .selections
2184 .display_ranges(&editor.display_snapshot(cx))),
2185 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2186 );
2187 });
2188 search_bar.read_with(cx, |search_bar, _| {
2189 assert_eq!(search_bar.active_match_index, Some(2));
2190 });
2191
2192 // Park the cursor after the last match and ensure that going to the next match selects the
2193 // first match.
2194 editor.update_in(cx, |editor, window, cx| {
2195 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2196 s.select_display_ranges([
2197 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2198 ])
2199 });
2200 });
2201 search_bar.update_in(cx, |search_bar, window, cx| {
2202 assert_eq!(search_bar.active_match_index, Some(2));
2203 search_bar.select_next_match(&SelectNextMatch, window, cx);
2204 assert_eq!(
2205 editor.update(cx, |editor, cx| editor
2206 .selections
2207 .display_ranges(&editor.display_snapshot(cx))),
2208 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2209 );
2210 });
2211 search_bar.read_with(cx, |search_bar, _| {
2212 assert_eq!(search_bar.active_match_index, Some(0));
2213 });
2214
2215 // Park the cursor before the first match and ensure that going to the previous match
2216 // selects the last match.
2217 editor.update_in(cx, |editor, window, cx| {
2218 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2219 s.select_display_ranges([
2220 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2221 ])
2222 });
2223 });
2224 search_bar.update_in(cx, |search_bar, window, cx| {
2225 assert_eq!(search_bar.active_match_index, Some(0));
2226 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2227 assert_eq!(
2228 editor.update(cx, |editor, cx| editor
2229 .selections
2230 .display_ranges(&editor.display_snapshot(cx))),
2231 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2232 );
2233 });
2234 search_bar.read_with(cx, |search_bar, _| {
2235 assert_eq!(search_bar.active_match_index, Some(2));
2236 });
2237 }
2238
2239 fn display_points_of(
2240 background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
2241 ) -> Vec<Range<DisplayPoint>> {
2242 background_highlights
2243 .into_iter()
2244 .map(|(range, _)| range)
2245 .collect::<Vec<_>>()
2246 }
2247
2248 #[perf]
2249 #[gpui::test]
2250 async fn test_search_option_handling(cx: &mut TestAppContext) {
2251 let (editor, search_bar, cx) = init_test(cx);
2252
2253 // show with options should make current search case sensitive
2254 search_bar
2255 .update_in(cx, |search_bar, window, cx| {
2256 search_bar.show(window, cx);
2257 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2258 })
2259 .await
2260 .unwrap();
2261 editor.update_in(cx, |editor, window, cx| {
2262 assert_eq!(
2263 display_points_of(editor.all_text_background_highlights(window, cx)),
2264 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
2265 );
2266 });
2267
2268 // search_suggested should restore default options
2269 search_bar.update_in(cx, |search_bar, window, cx| {
2270 search_bar.search_suggested(window, cx);
2271 assert_eq!(search_bar.search_options, SearchOptions::NONE)
2272 });
2273
2274 // toggling a search option should update the defaults
2275 search_bar
2276 .update_in(cx, |search_bar, window, cx| {
2277 search_bar.search(
2278 "regex",
2279 Some(SearchOptions::CASE_SENSITIVE),
2280 true,
2281 window,
2282 cx,
2283 )
2284 })
2285 .await
2286 .unwrap();
2287 search_bar.update_in(cx, |search_bar, window, cx| {
2288 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
2289 });
2290 let mut editor_notifications = cx.notifications(&editor);
2291 editor_notifications.next().await;
2292 editor.update_in(cx, |editor, window, cx| {
2293 assert_eq!(
2294 display_points_of(editor.all_text_background_highlights(window, cx)),
2295 &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
2296 );
2297 });
2298
2299 // defaults should still include whole word
2300 search_bar.update_in(cx, |search_bar, window, cx| {
2301 search_bar.search_suggested(window, cx);
2302 assert_eq!(
2303 search_bar.search_options,
2304 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
2305 )
2306 });
2307 }
2308
2309 #[perf]
2310 #[gpui::test]
2311 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
2312 init_globals(cx);
2313 let buffer_text = r#"
2314 A regular expression (shortened as regex or regexp;[1] also referred to as
2315 rational expression[2][3]) is a sequence of characters that specifies a search
2316 pattern in text. Usually such patterns are used by string-searching algorithms
2317 for "find" or "find and replace" operations on strings, or for input validation.
2318 "#
2319 .unindent();
2320 let expected_query_matches_count = buffer_text
2321 .chars()
2322 .filter(|c| c.eq_ignore_ascii_case(&'a'))
2323 .count();
2324 assert!(
2325 expected_query_matches_count > 1,
2326 "Should pick a query with multiple results"
2327 );
2328 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2329 let window = cx.add_window(|_, _| gpui::Empty);
2330
2331 let editor = window.build_entity(cx, |window, cx| {
2332 Editor::for_buffer(buffer.clone(), None, window, cx)
2333 });
2334
2335 let search_bar = window.build_entity(cx, |window, cx| {
2336 let mut search_bar = BufferSearchBar::new(None, window, cx);
2337 search_bar.set_active_pane_item(Some(&editor), window, cx);
2338 search_bar.show(window, cx);
2339 search_bar
2340 });
2341
2342 window
2343 .update(cx, |_, window, cx| {
2344 search_bar.update(cx, |search_bar, cx| {
2345 search_bar.search("a", None, true, window, cx)
2346 })
2347 })
2348 .unwrap()
2349 .await
2350 .unwrap();
2351 let initial_selections = window
2352 .update(cx, |_, window, cx| {
2353 search_bar.update(cx, |search_bar, cx| {
2354 let handle = search_bar.query_editor.focus_handle(cx);
2355 window.focus(&handle, cx);
2356 search_bar.activate_current_match(window, cx);
2357 });
2358 assert!(
2359 !editor.read(cx).is_focused(window),
2360 "Initially, the editor should not be focused"
2361 );
2362 let initial_selections = editor.update(cx, |editor, cx| {
2363 let initial_selections = editor.selections.display_ranges(&editor.display_snapshot(cx));
2364 assert_eq!(
2365 initial_selections.len(), 1,
2366 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
2367 );
2368 initial_selections
2369 });
2370 search_bar.update(cx, |search_bar, cx| {
2371 assert_eq!(search_bar.active_match_index, Some(0));
2372 let handle = search_bar.query_editor.focus_handle(cx);
2373 window.focus(&handle, cx);
2374 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2375 });
2376 assert!(
2377 editor.read(cx).is_focused(window),
2378 "Should focus editor after successful SelectAllMatches"
2379 );
2380 search_bar.update(cx, |search_bar, cx| {
2381 let all_selections =
2382 editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2383 assert_eq!(
2384 all_selections.len(),
2385 expected_query_matches_count,
2386 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2387 );
2388 assert_eq!(
2389 search_bar.active_match_index,
2390 Some(0),
2391 "Match index should not change after selecting all matches"
2392 );
2393 });
2394
2395 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
2396 initial_selections
2397 }).unwrap();
2398
2399 window
2400 .update(cx, |_, window, cx| {
2401 assert!(
2402 editor.read(cx).is_focused(window),
2403 "Should still have editor focused after SelectNextMatch"
2404 );
2405 search_bar.update(cx, |search_bar, cx| {
2406 let all_selections = editor.update(cx, |editor, cx| {
2407 editor
2408 .selections
2409 .display_ranges(&editor.display_snapshot(cx))
2410 });
2411 assert_eq!(
2412 all_selections.len(),
2413 1,
2414 "On next match, should deselect items and select the next match"
2415 );
2416 assert_ne!(
2417 all_selections, initial_selections,
2418 "Next match should be different from the first selection"
2419 );
2420 assert_eq!(
2421 search_bar.active_match_index,
2422 Some(1),
2423 "Match index should be updated to the next one"
2424 );
2425 let handle = search_bar.query_editor.focus_handle(cx);
2426 window.focus(&handle, cx);
2427 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2428 });
2429 })
2430 .unwrap();
2431 window
2432 .update(cx, |_, window, cx| {
2433 assert!(
2434 editor.read(cx).is_focused(window),
2435 "Should focus editor after successful SelectAllMatches"
2436 );
2437 search_bar.update(cx, |search_bar, cx| {
2438 let all_selections =
2439 editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2440 assert_eq!(
2441 all_selections.len(),
2442 expected_query_matches_count,
2443 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2444 );
2445 assert_eq!(
2446 search_bar.active_match_index,
2447 Some(1),
2448 "Match index should not change after selecting all matches"
2449 );
2450 });
2451 search_bar.update(cx, |search_bar, cx| {
2452 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2453 });
2454 })
2455 .unwrap();
2456 let last_match_selections = window
2457 .update(cx, |_, window, cx| {
2458 assert!(
2459 editor.read(cx).is_focused(window),
2460 "Should still have editor focused after SelectPreviousMatch"
2461 );
2462
2463 search_bar.update(cx, |search_bar, cx| {
2464 let all_selections = editor.update(cx, |editor, cx| {
2465 editor
2466 .selections
2467 .display_ranges(&editor.display_snapshot(cx))
2468 });
2469 assert_eq!(
2470 all_selections.len(),
2471 1,
2472 "On previous match, should deselect items and select the previous item"
2473 );
2474 assert_eq!(
2475 all_selections, initial_selections,
2476 "Previous match should be the same as the first selection"
2477 );
2478 assert_eq!(
2479 search_bar.active_match_index,
2480 Some(0),
2481 "Match index should be updated to the previous one"
2482 );
2483 all_selections
2484 })
2485 })
2486 .unwrap();
2487
2488 window
2489 .update(cx, |_, window, cx| {
2490 search_bar.update(cx, |search_bar, cx| {
2491 let handle = search_bar.query_editor.focus_handle(cx);
2492 window.focus(&handle, cx);
2493 search_bar.search("abas_nonexistent_match", None, true, window, cx)
2494 })
2495 })
2496 .unwrap()
2497 .await
2498 .unwrap();
2499 window
2500 .update(cx, |_, window, cx| {
2501 search_bar.update(cx, |search_bar, cx| {
2502 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2503 });
2504 assert!(
2505 editor.update(cx, |this, _cx| !this.is_focused(window)),
2506 "Should not switch focus to editor if SelectAllMatches does not find any matches"
2507 );
2508 search_bar.update(cx, |search_bar, cx| {
2509 let all_selections =
2510 editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2511 assert_eq!(
2512 all_selections, last_match_selections,
2513 "Should not select anything new if there are no matches"
2514 );
2515 assert!(
2516 search_bar.active_match_index.is_none(),
2517 "For no matches, there should be no active match index"
2518 );
2519 });
2520 })
2521 .unwrap();
2522 }
2523
2524 #[perf]
2525 #[gpui::test]
2526 async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2527 init_globals(cx);
2528 let buffer_text = r#"
2529 self.buffer.update(cx, |buffer, cx| {
2530 buffer.edit(
2531 edits,
2532 Some(AutoindentMode::Block {
2533 original_indent_columns,
2534 }),
2535 cx,
2536 )
2537 });
2538
2539 this.buffer.update(cx, |buffer, cx| {
2540 buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2541 });
2542 "#
2543 .unindent();
2544 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2545 let cx = cx.add_empty_window();
2546
2547 let editor =
2548 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2549
2550 let search_bar = cx.new_window_entity(|window, cx| {
2551 let mut search_bar = BufferSearchBar::new(None, window, cx);
2552 search_bar.set_active_pane_item(Some(&editor), window, cx);
2553 search_bar.show(window, cx);
2554 search_bar
2555 });
2556
2557 search_bar
2558 .update_in(cx, |search_bar, window, cx| {
2559 search_bar.search(
2560 "edit\\(",
2561 Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2562 true,
2563 window,
2564 cx,
2565 )
2566 })
2567 .await
2568 .unwrap();
2569
2570 search_bar.update_in(cx, |search_bar, window, cx| {
2571 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2572 });
2573 search_bar.update(cx, |_, cx| {
2574 let all_selections = editor.update(cx, |editor, cx| {
2575 editor
2576 .selections
2577 .display_ranges(&editor.display_snapshot(cx))
2578 });
2579 assert_eq!(
2580 all_selections.len(),
2581 2,
2582 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2583 );
2584 });
2585
2586 search_bar
2587 .update_in(cx, |search_bar, window, cx| {
2588 search_bar.search(
2589 "edit(",
2590 Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2591 true,
2592 window,
2593 cx,
2594 )
2595 })
2596 .await
2597 .unwrap();
2598
2599 search_bar.update_in(cx, |search_bar, window, cx| {
2600 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2601 });
2602 search_bar.update(cx, |_, cx| {
2603 let all_selections = editor.update(cx, |editor, cx| {
2604 editor
2605 .selections
2606 .display_ranges(&editor.display_snapshot(cx))
2607 });
2608 assert_eq!(
2609 all_selections.len(),
2610 2,
2611 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2612 );
2613 });
2614 }
2615
2616 #[perf]
2617 #[gpui::test]
2618 async fn test_search_query_history(cx: &mut TestAppContext) {
2619 let (_editor, search_bar, cx) = init_test(cx);
2620
2621 // Add 3 search items into the history.
2622 search_bar
2623 .update_in(cx, |search_bar, window, cx| {
2624 search_bar.search("a", None, true, window, cx)
2625 })
2626 .await
2627 .unwrap();
2628 search_bar
2629 .update_in(cx, |search_bar, window, cx| {
2630 search_bar.search("b", None, true, window, cx)
2631 })
2632 .await
2633 .unwrap();
2634 search_bar
2635 .update_in(cx, |search_bar, window, cx| {
2636 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2637 })
2638 .await
2639 .unwrap();
2640 // Ensure that the latest search is active.
2641 search_bar.update(cx, |search_bar, cx| {
2642 assert_eq!(search_bar.query(cx), "c");
2643 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2644 });
2645
2646 // Next history query after the latest should set the query to the empty string.
2647 search_bar.update_in(cx, |search_bar, window, cx| {
2648 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2649 });
2650 cx.background_executor.run_until_parked();
2651 search_bar.update(cx, |search_bar, cx| {
2652 assert_eq!(search_bar.query(cx), "");
2653 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2654 });
2655 search_bar.update_in(cx, |search_bar, window, cx| {
2656 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2657 });
2658 cx.background_executor.run_until_parked();
2659 search_bar.update(cx, |search_bar, cx| {
2660 assert_eq!(search_bar.query(cx), "");
2661 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2662 });
2663
2664 // First previous query for empty current query should set the query to the latest.
2665 search_bar.update_in(cx, |search_bar, window, cx| {
2666 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2667 });
2668 cx.background_executor.run_until_parked();
2669 search_bar.update(cx, |search_bar, cx| {
2670 assert_eq!(search_bar.query(cx), "c");
2671 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2672 });
2673
2674 // Further previous items should go over the history in reverse order.
2675 search_bar.update_in(cx, |search_bar, window, cx| {
2676 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2677 });
2678 cx.background_executor.run_until_parked();
2679 search_bar.update(cx, |search_bar, cx| {
2680 assert_eq!(search_bar.query(cx), "b");
2681 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2682 });
2683
2684 // Previous items should never go behind the first history item.
2685 search_bar.update_in(cx, |search_bar, window, cx| {
2686 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2687 });
2688 cx.background_executor.run_until_parked();
2689 search_bar.update(cx, |search_bar, cx| {
2690 assert_eq!(search_bar.query(cx), "a");
2691 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2692 });
2693 search_bar.update_in(cx, |search_bar, window, cx| {
2694 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2695 });
2696 cx.background_executor.run_until_parked();
2697 search_bar.update(cx, |search_bar, cx| {
2698 assert_eq!(search_bar.query(cx), "a");
2699 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2700 });
2701
2702 // Next items should go over the history in the original order.
2703 search_bar.update_in(cx, |search_bar, window, cx| {
2704 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2705 });
2706 cx.background_executor.run_until_parked();
2707 search_bar.update(cx, |search_bar, cx| {
2708 assert_eq!(search_bar.query(cx), "b");
2709 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2710 });
2711
2712 search_bar
2713 .update_in(cx, |search_bar, window, cx| {
2714 search_bar.search("ba", None, true, window, cx)
2715 })
2716 .await
2717 .unwrap();
2718 search_bar.update(cx, |search_bar, cx| {
2719 assert_eq!(search_bar.query(cx), "ba");
2720 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2721 });
2722
2723 // New search input should add another entry to history and move the selection to the end of the history.
2724 search_bar.update_in(cx, |search_bar, window, cx| {
2725 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2726 });
2727 cx.background_executor.run_until_parked();
2728 search_bar.update(cx, |search_bar, cx| {
2729 assert_eq!(search_bar.query(cx), "c");
2730 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2731 });
2732 search_bar.update_in(cx, |search_bar, window, cx| {
2733 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2734 });
2735 cx.background_executor.run_until_parked();
2736 search_bar.update(cx, |search_bar, cx| {
2737 assert_eq!(search_bar.query(cx), "b");
2738 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2739 });
2740 search_bar.update_in(cx, |search_bar, window, cx| {
2741 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2742 });
2743 cx.background_executor.run_until_parked();
2744 search_bar.update(cx, |search_bar, cx| {
2745 assert_eq!(search_bar.query(cx), "c");
2746 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2747 });
2748 search_bar.update_in(cx, |search_bar, window, cx| {
2749 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2750 });
2751 cx.background_executor.run_until_parked();
2752 search_bar.update(cx, |search_bar, cx| {
2753 assert_eq!(search_bar.query(cx), "ba");
2754 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2755 });
2756 search_bar.update_in(cx, |search_bar, window, cx| {
2757 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2758 });
2759 cx.background_executor.run_until_parked();
2760 search_bar.update(cx, |search_bar, cx| {
2761 assert_eq!(search_bar.query(cx), "");
2762 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2763 });
2764 }
2765
2766 #[perf]
2767 #[gpui::test]
2768 async fn test_replace_simple(cx: &mut TestAppContext) {
2769 let (editor, search_bar, cx) = init_test(cx);
2770
2771 search_bar
2772 .update_in(cx, |search_bar, window, cx| {
2773 search_bar.search("expression", None, true, window, cx)
2774 })
2775 .await
2776 .unwrap();
2777
2778 search_bar.update_in(cx, |search_bar, window, cx| {
2779 search_bar.replacement_editor.update(cx, |editor, cx| {
2780 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2781 editor.set_text("expr$1", window, cx);
2782 });
2783 search_bar.replace_all(&ReplaceAll, window, cx)
2784 });
2785 assert_eq!(
2786 editor.read_with(cx, |this, cx| { this.text(cx) }),
2787 r#"
2788 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2789 rational expr$1[2][3]) is a sequence of characters that specifies a search
2790 pattern in text. Usually such patterns are used by string-searching algorithms
2791 for "find" or "find and replace" operations on strings, or for input validation.
2792 "#
2793 .unindent()
2794 );
2795
2796 // Search for word boundaries and replace just a single one.
2797 search_bar
2798 .update_in(cx, |search_bar, window, cx| {
2799 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx)
2800 })
2801 .await
2802 .unwrap();
2803
2804 search_bar.update_in(cx, |search_bar, window, cx| {
2805 search_bar.replacement_editor.update(cx, |editor, cx| {
2806 editor.set_text("banana", window, cx);
2807 });
2808 search_bar.replace_next(&ReplaceNext, window, cx)
2809 });
2810 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2811 assert_eq!(
2812 editor.read_with(cx, |this, cx| { this.text(cx) }),
2813 r#"
2814 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2815 rational expr$1[2][3]) is a sequence of characters that specifies a search
2816 pattern in text. Usually such patterns are used by string-searching algorithms
2817 for "find" or "find and replace" operations on strings, or for input validation.
2818 "#
2819 .unindent()
2820 );
2821 // Let's turn on regex mode.
2822 search_bar
2823 .update_in(cx, |search_bar, window, cx| {
2824 search_bar.search(
2825 "\\[([^\\]]+)\\]",
2826 Some(SearchOptions::REGEX),
2827 true,
2828 window,
2829 cx,
2830 )
2831 })
2832 .await
2833 .unwrap();
2834 search_bar.update_in(cx, |search_bar, window, cx| {
2835 search_bar.replacement_editor.update(cx, |editor, cx| {
2836 editor.set_text("${1}number", window, cx);
2837 });
2838 search_bar.replace_all(&ReplaceAll, window, cx)
2839 });
2840 assert_eq!(
2841 editor.read_with(cx, |this, cx| { this.text(cx) }),
2842 r#"
2843 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2844 rational expr$12number3number) is a sequence of characters that specifies a search
2845 pattern in text. Usually such patterns are used by string-searching algorithms
2846 for "find" or "find and replace" operations on strings, or for input validation.
2847 "#
2848 .unindent()
2849 );
2850 // Now with a whole-word twist.
2851 search_bar
2852 .update_in(cx, |search_bar, window, cx| {
2853 search_bar.search(
2854 "a\\w+s",
2855 Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2856 true,
2857 window,
2858 cx,
2859 )
2860 })
2861 .await
2862 .unwrap();
2863 search_bar.update_in(cx, |search_bar, window, cx| {
2864 search_bar.replacement_editor.update(cx, |editor, cx| {
2865 editor.set_text("things", window, cx);
2866 });
2867 search_bar.replace_all(&ReplaceAll, window, cx)
2868 });
2869 // The only word affected by this edit should be `algorithms`, even though there's a bunch
2870 // of words in this text that would match this regex if not for WHOLE_WORD.
2871 assert_eq!(
2872 editor.read_with(cx, |this, cx| { this.text(cx) }),
2873 r#"
2874 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2875 rational expr$12number3number) is a sequence of characters that specifies a search
2876 pattern in text. Usually such patterns are used by string-searching things
2877 for "find" or "find and replace" operations on strings, or for input validation.
2878 "#
2879 .unindent()
2880 );
2881 }
2882
2883 #[gpui::test]
2884 async fn test_replace_focus(cx: &mut TestAppContext) {
2885 let (editor, search_bar, cx) = init_test(cx);
2886
2887 editor.update_in(cx, |editor, window, cx| {
2888 editor.set_text("What a bad day!", window, cx)
2889 });
2890
2891 search_bar
2892 .update_in(cx, |search_bar, window, cx| {
2893 search_bar.search("bad", None, true, window, cx)
2894 })
2895 .await
2896 .unwrap();
2897
2898 // Calling `toggle_replace` in the search bar ensures that the "Replace
2899 // *" buttons are rendered, so we can then simulate clicking the
2900 // buttons.
2901 search_bar.update_in(cx, |search_bar, window, cx| {
2902 search_bar.toggle_replace(&ToggleReplace, window, cx)
2903 });
2904
2905 search_bar.update_in(cx, |search_bar, window, cx| {
2906 search_bar.replacement_editor.update(cx, |editor, cx| {
2907 editor.set_text("great", window, cx);
2908 });
2909 });
2910
2911 // Focus on the editor instead of the search bar, as we want to ensure
2912 // that pressing the "Replace Next Match" button will work, even if the
2913 // search bar is not focused.
2914 cx.focus(&editor);
2915
2916 // We'll not simulate clicking the "Replace Next Match " button, asserting that
2917 // the replacement was done.
2918 let button_bounds = cx
2919 .debug_bounds("ICON-ReplaceNext")
2920 .expect("'Replace Next Match' button should be visible");
2921 cx.simulate_click(button_bounds.center(), gpui::Modifiers::none());
2922
2923 assert_eq!(
2924 editor.read_with(cx, |editor, cx| editor.text(cx)),
2925 "What a great day!"
2926 );
2927 }
2928
2929 struct ReplacementTestParams<'a> {
2930 editor: &'a Entity<Editor>,
2931 search_bar: &'a Entity<BufferSearchBar>,
2932 cx: &'a mut VisualTestContext,
2933 search_text: &'static str,
2934 search_options: Option<SearchOptions>,
2935 replacement_text: &'static str,
2936 replace_all: bool,
2937 expected_text: String,
2938 }
2939
2940 async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2941 options
2942 .search_bar
2943 .update_in(options.cx, |search_bar, window, cx| {
2944 if let Some(options) = options.search_options {
2945 search_bar.set_search_options(options, cx);
2946 }
2947 search_bar.search(
2948 options.search_text,
2949 options.search_options,
2950 true,
2951 window,
2952 cx,
2953 )
2954 })
2955 .await
2956 .unwrap();
2957
2958 options
2959 .search_bar
2960 .update_in(options.cx, |search_bar, window, cx| {
2961 search_bar.replacement_editor.update(cx, |editor, cx| {
2962 editor.set_text(options.replacement_text, window, cx);
2963 });
2964
2965 if options.replace_all {
2966 search_bar.replace_all(&ReplaceAll, window, cx)
2967 } else {
2968 search_bar.replace_next(&ReplaceNext, window, cx)
2969 }
2970 });
2971
2972 assert_eq!(
2973 options
2974 .editor
2975 .read_with(options.cx, |this, cx| { this.text(cx) }),
2976 options.expected_text
2977 );
2978 }
2979
2980 #[perf]
2981 #[gpui::test]
2982 async fn test_replace_special_characters(cx: &mut TestAppContext) {
2983 let (editor, search_bar, cx) = init_test(cx);
2984
2985 run_replacement_test(ReplacementTestParams {
2986 editor: &editor,
2987 search_bar: &search_bar,
2988 cx,
2989 search_text: "expression",
2990 search_options: None,
2991 replacement_text: r"\n",
2992 replace_all: true,
2993 expected_text: r#"
2994 A regular \n (shortened as regex or regexp;[1] also referred to as
2995 rational \n[2][3]) is a sequence of characters that specifies a search
2996 pattern in text. Usually such patterns are used by string-searching algorithms
2997 for "find" or "find and replace" operations on strings, or for input validation.
2998 "#
2999 .unindent(),
3000 })
3001 .await;
3002
3003 run_replacement_test(ReplacementTestParams {
3004 editor: &editor,
3005 search_bar: &search_bar,
3006 cx,
3007 search_text: "or",
3008 search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
3009 replacement_text: r"\\\n\\\\",
3010 replace_all: false,
3011 expected_text: r#"
3012 A regular \n (shortened as regex \
3013 \\ regexp;[1] also referred to as
3014 rational \n[2][3]) is a sequence of characters that specifies a search
3015 pattern in text. Usually such patterns are used by string-searching algorithms
3016 for "find" or "find and replace" operations on strings, or for input validation.
3017 "#
3018 .unindent(),
3019 })
3020 .await;
3021
3022 run_replacement_test(ReplacementTestParams {
3023 editor: &editor,
3024 search_bar: &search_bar,
3025 cx,
3026 search_text: r"(that|used) ",
3027 search_options: Some(SearchOptions::REGEX),
3028 replacement_text: r"$1\n",
3029 replace_all: true,
3030 expected_text: r#"
3031 A regular \n (shortened as regex \
3032 \\ regexp;[1] also referred to as
3033 rational \n[2][3]) is a sequence of characters that
3034 specifies a search
3035 pattern in text. Usually such patterns are used
3036 by string-searching algorithms
3037 for "find" or "find and replace" operations on strings, or for input validation.
3038 "#
3039 .unindent(),
3040 })
3041 .await;
3042 }
3043
3044 #[perf]
3045 #[gpui::test]
3046 async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
3047 cx: &mut TestAppContext,
3048 ) {
3049 init_globals(cx);
3050 let buffer = cx.new(|cx| {
3051 Buffer::local(
3052 r#"
3053 aaa bbb aaa ccc
3054 aaa bbb aaa ccc
3055 aaa bbb aaa ccc
3056 aaa bbb aaa ccc
3057 aaa bbb aaa ccc
3058 aaa bbb aaa ccc
3059 "#
3060 .unindent(),
3061 cx,
3062 )
3063 });
3064 let cx = cx.add_empty_window();
3065 let editor =
3066 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
3067
3068 let search_bar = cx.new_window_entity(|window, cx| {
3069 let mut search_bar = BufferSearchBar::new(None, window, cx);
3070 search_bar.set_active_pane_item(Some(&editor), window, cx);
3071 search_bar.show(window, cx);
3072 search_bar
3073 });
3074
3075 editor.update_in(cx, |editor, window, cx| {
3076 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3077 s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
3078 })
3079 });
3080
3081 search_bar.update_in(cx, |search_bar, window, cx| {
3082 let deploy = Deploy {
3083 focus: true,
3084 replace_enabled: false,
3085 selection_search_enabled: true,
3086 };
3087 search_bar.deploy(&deploy, window, cx);
3088 });
3089
3090 cx.run_until_parked();
3091
3092 search_bar
3093 .update_in(cx, |search_bar, window, cx| {
3094 search_bar.search("aaa", None, true, window, cx)
3095 })
3096 .await
3097 .unwrap();
3098
3099 editor.update(cx, |editor, cx| {
3100 assert_eq!(
3101 editor.search_background_highlights(cx),
3102 &[
3103 Point::new(1, 0)..Point::new(1, 3),
3104 Point::new(1, 8)..Point::new(1, 11),
3105 Point::new(2, 0)..Point::new(2, 3),
3106 ]
3107 );
3108 });
3109 }
3110
3111 #[perf]
3112 #[gpui::test]
3113 async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
3114 cx: &mut TestAppContext,
3115 ) {
3116 init_globals(cx);
3117 let text = r#"
3118 aaa bbb aaa ccc
3119 aaa bbb aaa ccc
3120 aaa bbb aaa ccc
3121 aaa bbb aaa ccc
3122 aaa bbb aaa ccc
3123 aaa bbb aaa ccc
3124
3125 aaa bbb aaa ccc
3126 aaa bbb aaa ccc
3127 aaa bbb aaa ccc
3128 aaa bbb aaa ccc
3129 aaa bbb aaa ccc
3130 aaa bbb aaa ccc
3131 "#
3132 .unindent();
3133
3134 let cx = cx.add_empty_window();
3135 let editor = cx.new_window_entity(|window, cx| {
3136 let multibuffer = MultiBuffer::build_multi(
3137 [
3138 (
3139 &text,
3140 vec![
3141 Point::new(0, 0)..Point::new(2, 0),
3142 Point::new(4, 0)..Point::new(5, 0),
3143 ],
3144 ),
3145 (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
3146 ],
3147 cx,
3148 );
3149 Editor::for_multibuffer(multibuffer, None, window, cx)
3150 });
3151
3152 let search_bar = cx.new_window_entity(|window, cx| {
3153 let mut search_bar = BufferSearchBar::new(None, window, cx);
3154 search_bar.set_active_pane_item(Some(&editor), window, cx);
3155 search_bar.show(window, cx);
3156 search_bar
3157 });
3158
3159 editor.update_in(cx, |editor, window, cx| {
3160 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3161 s.select_ranges(vec![
3162 Point::new(1, 0)..Point::new(1, 4),
3163 Point::new(5, 3)..Point::new(6, 4),
3164 ])
3165 })
3166 });
3167
3168 search_bar.update_in(cx, |search_bar, window, cx| {
3169 let deploy = Deploy {
3170 focus: true,
3171 replace_enabled: false,
3172 selection_search_enabled: true,
3173 };
3174 search_bar.deploy(&deploy, window, cx);
3175 });
3176
3177 cx.run_until_parked();
3178
3179 search_bar
3180 .update_in(cx, |search_bar, window, cx| {
3181 search_bar.search("aaa", None, true, window, cx)
3182 })
3183 .await
3184 .unwrap();
3185
3186 editor.update(cx, |editor, cx| {
3187 assert_eq!(
3188 editor.search_background_highlights(cx),
3189 &[
3190 Point::new(1, 0)..Point::new(1, 3),
3191 Point::new(5, 8)..Point::new(5, 11),
3192 Point::new(6, 0)..Point::new(6, 3),
3193 ]
3194 );
3195 });
3196 }
3197
3198 #[perf]
3199 #[gpui::test]
3200 async fn test_hides_and_uses_secondary_when_in_singleton_buffer(cx: &mut TestAppContext) {
3201 let (editor, search_bar, cx) = init_test(cx);
3202
3203 let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3204 search_bar.set_active_pane_item(Some(&editor), window, cx)
3205 });
3206
3207 assert_eq!(initial_location, ToolbarItemLocation::Secondary);
3208
3209 let mut events = cx.events(&search_bar);
3210
3211 search_bar.update_in(cx, |search_bar, window, cx| {
3212 search_bar.dismiss(&Dismiss, window, cx);
3213 });
3214
3215 assert_eq!(
3216 events.try_next().unwrap(),
3217 Some(ToolbarItemEvent::ChangeLocation(
3218 ToolbarItemLocation::Hidden
3219 ))
3220 );
3221
3222 search_bar.update_in(cx, |search_bar, window, cx| {
3223 search_bar.show(window, cx);
3224 });
3225
3226 assert_eq!(
3227 events.try_next().unwrap(),
3228 Some(ToolbarItemEvent::ChangeLocation(
3229 ToolbarItemLocation::Secondary
3230 ))
3231 );
3232 }
3233
3234 #[perf]
3235 #[gpui::test]
3236 async fn test_uses_primary_left_when_in_multi_buffer(cx: &mut TestAppContext) {
3237 let (editor, search_bar, cx) = init_multibuffer_test(cx);
3238
3239 let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3240 search_bar.set_active_pane_item(Some(&editor), window, cx)
3241 });
3242
3243 assert_eq!(initial_location, ToolbarItemLocation::PrimaryLeft);
3244
3245 let mut events = cx.events(&search_bar);
3246
3247 search_bar.update_in(cx, |search_bar, window, cx| {
3248 search_bar.dismiss(&Dismiss, window, cx);
3249 });
3250
3251 assert_eq!(
3252 events.try_next().unwrap(),
3253 Some(ToolbarItemEvent::ChangeLocation(
3254 ToolbarItemLocation::PrimaryLeft
3255 ))
3256 );
3257
3258 search_bar.update_in(cx, |search_bar, window, cx| {
3259 search_bar.show(window, cx);
3260 });
3261
3262 assert_eq!(
3263 events.try_next().unwrap(),
3264 Some(ToolbarItemEvent::ChangeLocation(
3265 ToolbarItemLocation::PrimaryLeft
3266 ))
3267 );
3268 }
3269
3270 #[perf]
3271 #[gpui::test]
3272 async fn test_hides_and_uses_secondary_when_part_of_project_search(cx: &mut TestAppContext) {
3273 let (editor, search_bar, cx) = init_multibuffer_test(cx);
3274
3275 editor.update(cx, |editor, _| {
3276 editor.set_in_project_search(true);
3277 });
3278
3279 let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3280 search_bar.set_active_pane_item(Some(&editor), window, cx)
3281 });
3282
3283 assert_eq!(initial_location, ToolbarItemLocation::Hidden);
3284
3285 let mut events = cx.events(&search_bar);
3286
3287 search_bar.update_in(cx, |search_bar, window, cx| {
3288 search_bar.dismiss(&Dismiss, window, cx);
3289 });
3290
3291 assert_eq!(
3292 events.try_next().unwrap(),
3293 Some(ToolbarItemEvent::ChangeLocation(
3294 ToolbarItemLocation::Hidden
3295 ))
3296 );
3297
3298 search_bar.update_in(cx, |search_bar, window, cx| {
3299 search_bar.show(window, cx);
3300 });
3301
3302 assert_eq!(
3303 events.try_next().unwrap(),
3304 Some(ToolbarItemEvent::ChangeLocation(
3305 ToolbarItemLocation::Secondary
3306 ))
3307 );
3308 }
3309
3310 #[perf]
3311 #[gpui::test]
3312 async fn test_sets_collapsed_when_editor_fold_events_emitted(cx: &mut TestAppContext) {
3313 let (editor, search_bar, cx) = init_multibuffer_test(cx);
3314
3315 search_bar.update_in(cx, |search_bar, window, cx| {
3316 search_bar.set_active_pane_item(Some(&editor), window, cx);
3317 });
3318
3319 editor.update_in(cx, |editor, window, cx| {
3320 editor.fold_all(&FoldAll, window, cx);
3321 });
3322
3323 let is_collapsed = search_bar.read_with(cx, |search_bar, _| search_bar.is_collapsed);
3324
3325 assert!(is_collapsed);
3326
3327 editor.update_in(cx, |editor, window, cx| {
3328 editor.unfold_all(&UnfoldAll, window, cx);
3329 });
3330
3331 let is_collapsed = search_bar.read_with(cx, |search_bar, _| search_bar.is_collapsed);
3332
3333 assert!(!is_collapsed);
3334 }
3335
3336 #[perf]
3337 #[gpui::test]
3338 async fn test_search_options_changes(cx: &mut TestAppContext) {
3339 let (_editor, search_bar, cx) = init_test(cx);
3340 update_search_settings(
3341 SearchSettings {
3342 button: true,
3343 whole_word: false,
3344 case_sensitive: false,
3345 include_ignored: false,
3346 regex: false,
3347 center_on_match: false,
3348 },
3349 cx,
3350 );
3351
3352 let deploy = Deploy {
3353 focus: true,
3354 replace_enabled: false,
3355 selection_search_enabled: true,
3356 };
3357
3358 search_bar.update_in(cx, |search_bar, window, cx| {
3359 assert_eq!(
3360 search_bar.search_options,
3361 SearchOptions::NONE,
3362 "Should have no search options enabled by default"
3363 );
3364 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3365 assert_eq!(
3366 search_bar.search_options,
3367 SearchOptions::WHOLE_WORD,
3368 "Should enable the option toggled"
3369 );
3370 assert!(
3371 !search_bar.dismissed,
3372 "Search bar should be present and visible"
3373 );
3374 search_bar.deploy(&deploy, window, cx);
3375 assert_eq!(
3376 search_bar.search_options,
3377 SearchOptions::WHOLE_WORD,
3378 "After (re)deploying, the option should still be enabled"
3379 );
3380
3381 search_bar.dismiss(&Dismiss, window, cx);
3382 search_bar.deploy(&deploy, window, cx);
3383 assert_eq!(
3384 search_bar.search_options,
3385 SearchOptions::WHOLE_WORD,
3386 "After hiding and showing the search bar, search options should be preserved"
3387 );
3388
3389 search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
3390 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3391 assert_eq!(
3392 search_bar.search_options,
3393 SearchOptions::REGEX,
3394 "Should enable the options toggled"
3395 );
3396 assert!(
3397 !search_bar.dismissed,
3398 "Search bar should be present and visible"
3399 );
3400 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3401 });
3402
3403 update_search_settings(
3404 SearchSettings {
3405 button: true,
3406 whole_word: false,
3407 case_sensitive: true,
3408 include_ignored: false,
3409 regex: false,
3410 center_on_match: false,
3411 },
3412 cx,
3413 );
3414 search_bar.update_in(cx, |search_bar, window, cx| {
3415 assert_eq!(
3416 search_bar.search_options,
3417 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3418 "Should have no search options enabled by default"
3419 );
3420
3421 search_bar.deploy(&deploy, window, cx);
3422 assert_eq!(
3423 search_bar.search_options,
3424 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3425 "Toggling a non-dismissed search bar with custom options should not change the default options"
3426 );
3427 search_bar.dismiss(&Dismiss, window, cx);
3428 search_bar.deploy(&deploy, window, cx);
3429 assert_eq!(
3430 search_bar.configured_options,
3431 SearchOptions::CASE_SENSITIVE,
3432 "After a settings update and toggling the search bar, configured options should be updated"
3433 );
3434 assert_eq!(
3435 search_bar.search_options,
3436 SearchOptions::CASE_SENSITIVE,
3437 "After a settings update and toggling the search bar, configured options should be used"
3438 );
3439 });
3440
3441 update_search_settings(
3442 SearchSettings {
3443 button: true,
3444 whole_word: true,
3445 case_sensitive: true,
3446 include_ignored: false,
3447 regex: false,
3448 center_on_match: false,
3449 },
3450 cx,
3451 );
3452
3453 search_bar.update_in(cx, |search_bar, window, cx| {
3454 search_bar.deploy(&deploy, window, cx);
3455 search_bar.dismiss(&Dismiss, window, cx);
3456 search_bar.show(window, cx);
3457 assert_eq!(
3458 search_bar.search_options,
3459 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
3460 "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
3461 );
3462 });
3463 }
3464
3465 #[gpui::test]
3466 async fn test_select_occurrence_case_sensitivity(cx: &mut TestAppContext) {
3467 let (editor, search_bar, cx) = init_test(cx);
3468 let mut editor_cx = EditorTestContext::for_editor_in(editor, cx).await;
3469
3470 // Start with case sensitive search settings.
3471 let mut search_settings = SearchSettings::default();
3472 search_settings.case_sensitive = true;
3473 update_search_settings(search_settings, cx);
3474 search_bar.update(cx, |search_bar, cx| {
3475 let mut search_options = search_bar.search_options;
3476 search_options.insert(SearchOptions::CASE_SENSITIVE);
3477 search_bar.set_search_options(search_options, cx);
3478 });
3479
3480 editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3481 editor_cx.update_editor(|e, window, cx| {
3482 e.select_next(&Default::default(), window, cx).unwrap();
3483 });
3484 editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
3485
3486 // Update the search bar's case sensitivite toggle, so we can later
3487 // confirm that `select_next` will now be case-insensitive.
3488 editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3489 search_bar.update_in(cx, |search_bar, window, cx| {
3490 search_bar.toggle_case_sensitive(&Default::default(), window, cx);
3491 });
3492 editor_cx.update_editor(|e, window, cx| {
3493 e.select_next(&Default::default(), window, cx).unwrap();
3494 });
3495 editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
3496
3497 // Confirm that, after dismissing the search bar, only the editor's
3498 // search settings actually affect the behavior of `select_next`.
3499 search_bar.update_in(cx, |search_bar, window, cx| {
3500 search_bar.dismiss(&Default::default(), window, cx);
3501 });
3502 editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3503 editor_cx.update_editor(|e, window, cx| {
3504 e.select_next(&Default::default(), window, cx).unwrap();
3505 });
3506 editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
3507
3508 // Update the editor's search settings, disabling case sensitivity, to
3509 // check that the value is respected.
3510 let mut search_settings = SearchSettings::default();
3511 search_settings.case_sensitive = false;
3512 update_search_settings(search_settings, cx);
3513 editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3514 editor_cx.update_editor(|e, window, cx| {
3515 e.select_next(&Default::default(), window, cx).unwrap();
3516 });
3517 editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
3518 }
3519
3520 fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
3521 cx.update(|cx| {
3522 SettingsStore::update_global(cx, |store, cx| {
3523 store.update_user_settings(cx, |settings| {
3524 settings.editor.search = Some(SearchSettingsContent {
3525 button: Some(search_settings.button),
3526 whole_word: Some(search_settings.whole_word),
3527 case_sensitive: Some(search_settings.case_sensitive),
3528 include_ignored: Some(search_settings.include_ignored),
3529 regex: Some(search_settings.regex),
3530 center_on_match: Some(search_settings.center_on_match),
3531 });
3532 });
3533 });
3534 });
3535 }
3536}