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