1mod registrar;
2
3use crate::{
4 search_bar::render_nav_button, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll,
5 ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch,
6 ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
7};
8use any_vec::AnyVec;
9use anyhow::Context as _;
10use collections::HashMap;
11use editor::{
12 actions::{Tab, TabPrev},
13 DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle,
14};
15use futures::channel::oneshot;
16use gpui::{
17 actions, div, impl_actions, Action, App, ClickEvent, Context, Entity, EventEmitter,
18 FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyContext, ParentElement as _,
19 Render, ScrollHandle, Styled, Subscription, Task, TextStyle, Window,
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::sync::Arc;
30use theme::ThemeSettings;
31use zed_actions::outline::ToggleOutline;
32
33use ui::{
34 h_flex, prelude::*, utils::SearchInputWidth, IconButton, IconButtonShape, IconName, Tooltip,
35 BASE_REM_SIZE_IN_PX,
36};
37use util::ResultExt;
38use workspace::{
39 item::ItemHandle,
40 searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
41 ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
42};
43
44pub use registrar::DivRegistrar;
45use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults};
46
47const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
48
49#[derive(PartialEq, Clone, Deserialize, JsonSchema)]
50#[serde(deny_unknown_fields)]
51pub struct Deploy {
52 #[serde(default = "util::serde::default_true")]
53 pub focus: bool,
54 #[serde(default)]
55 pub replace_enabled: bool,
56 #[serde(default)]
57 pub selection_search_enabled: bool,
58}
59
60impl_actions!(buffer_search, [Deploy]);
61
62actions!(buffer_search, [DeployReplace, Dismiss, FocusEditor]);
63
64impl Deploy {
65 pub fn find() -> Self {
66 Self {
67 focus: true,
68 replace_enabled: false,
69 selection_search_enabled: false,
70 }
71 }
72
73 pub fn replace() -> Self {
74 Self {
75 focus: true,
76 replace_enabled: true,
77 selection_search_enabled: false,
78 }
79 }
80}
81
82pub enum Event {
83 UpdateLocation,
84}
85
86pub fn init(cx: &mut App) {
87 cx.observe_new(|workspace: &mut Workspace, _, _| BufferSearchBar::register(workspace))
88 .detach();
89}
90
91pub struct BufferSearchBar {
92 query_editor: Entity<Editor>,
93 query_editor_focused: bool,
94 replacement_editor: Entity<Editor>,
95 replacement_editor_focused: bool,
96 active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
97 active_match_index: Option<usize>,
98 active_searchable_item_subscription: Option<Subscription>,
99 active_search: Option<Arc<SearchQuery>>,
100 searchable_items_with_matches: HashMap<Box<dyn WeakSearchableItemHandle>, AnyVec<dyn Send>>,
101 pending_search: Option<Task<()>>,
102 search_options: SearchOptions,
103 default_options: SearchOptions,
104 configured_options: SearchOptions,
105 query_contains_error: bool,
106 dismissed: bool,
107 search_history: SearchHistory,
108 search_history_cursor: SearchHistoryCursor,
109 replace_enabled: bool,
110 selection_search_enabled: bool,
111 scroll_handle: ScrollHandle,
112 editor_scroll_handle: ScrollHandle,
113 editor_needed_width: Pixels,
114 regex_language: Option<Arc<Language>>,
115}
116
117impl BufferSearchBar {
118 fn render_text_input(
119 &self,
120 editor: &Entity<Editor>,
121 color_override: Option<Color>,
122 cx: &mut Context<Self>,
123 ) -> impl IntoElement {
124 let (color, use_syntax) = if editor.read(cx).read_only(cx) {
125 (cx.theme().colors().text_disabled, false)
126 } else {
127 match color_override {
128 Some(color_override) => (color_override.color(cx), false),
129 None => (cx.theme().colors().text, true),
130 }
131 };
132
133 let settings = ThemeSettings::get_global(cx);
134 let text_style = TextStyle {
135 color,
136 font_family: settings.buffer_font.family.clone(),
137 font_features: settings.buffer_font.features.clone(),
138 font_fallbacks: settings.buffer_font.fallbacks.clone(),
139 font_size: rems(0.875).into(),
140 font_weight: settings.buffer_font.weight,
141 line_height: relative(1.3),
142 ..TextStyle::default()
143 };
144
145 let mut editor_style = EditorStyle {
146 background: cx.theme().colors().editor_background,
147 local_player: cx.theme().players().local(),
148 text: text_style,
149 ..EditorStyle::default()
150 };
151 if use_syntax {
152 editor_style.syntax = cx.theme().syntax().clone();
153 }
154
155 EditorElement::new(editor, editor_style)
156 }
157
158 pub fn query_editor_focused(&self) -> bool {
159 self.query_editor_focused
160 }
161}
162
163impl EventEmitter<Event> for BufferSearchBar {}
164impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
165impl Render for BufferSearchBar {
166 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
167 if self.dismissed {
168 return div().id("search_bar");
169 }
170
171 let focus_handle = self.focus_handle(cx);
172
173 let narrow_mode =
174 self.scroll_handle.bounds().size.width / window.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
175 let hide_inline_icons = self.editor_needed_width
176 > self.editor_scroll_handle.bounds().size.width - window.rem_size() * 6.;
177
178 let supported_options = self.supported_options(cx);
179
180 if self.query_editor.update(cx, |query_editor, _cx| {
181 query_editor.placeholder_text().is_none()
182 }) {
183 self.query_editor.update(cx, |editor, cx| {
184 editor.set_placeholder_text("Search…", cx);
185 });
186 }
187
188 self.replacement_editor.update(cx, |editor, cx| {
189 editor.set_placeholder_text("Replace with…", cx);
190 });
191
192 let mut color_override = None;
193 let match_text = self
194 .active_searchable_item
195 .as_ref()
196 .and_then(|searchable_item| {
197 if self.query(cx).is_empty() {
198 return None;
199 }
200 let matches_count = self
201 .searchable_items_with_matches
202 .get(&searchable_item.downgrade())
203 .map(AnyVec::len)
204 .unwrap_or(0);
205 if let Some(match_ix) = self.active_match_index {
206 Some(format!("{}/{}", match_ix + 1, matches_count))
207 } else {
208 color_override = Some(Color::Error); // No matches found
209 None
210 }
211 })
212 .unwrap_or_else(|| "0/0".to_string());
213 let should_show_replace_input = self.replace_enabled && supported_options.replacement;
214 let in_replace = self.replacement_editor.focus_handle(cx).is_focused(window);
215
216 let mut key_context = KeyContext::new_with_defaults();
217 key_context.add("BufferSearchBar");
218 if in_replace {
219 key_context.add("in_replace");
220 }
221 let editor_border = if self.query_contains_error {
222 Color::Error.color(cx)
223 } else {
224 cx.theme().colors().border
225 };
226
227 let container_width = window.viewport_size().width;
228 let input_width = SearchInputWidth::calc_width(container_width);
229
230 let input_base_styles = || {
231 h_flex()
232 .min_w_32()
233 .w(input_width)
234 .h_8()
235 .pl_2()
236 .pr_1()
237 .py_1()
238 .border_1()
239 .border_color(editor_border)
240 .rounded_lg()
241 };
242
243 let search_line = h_flex()
244 .gap_2()
245 .when(supported_options.find_in_results, |el| {
246 el.child(Label::new("Find in results").color(Color::Hint))
247 })
248 .child(
249 input_base_styles()
250 .id("editor-scroll")
251 .track_scroll(&self.editor_scroll_handle)
252 .child(self.render_text_input(&self.query_editor, color_override, cx))
253 .when(!hide_inline_icons, |div| {
254 div.child(
255 h_flex()
256 .gap_1()
257 .children(supported_options.case.then(|| {
258 self.render_search_option_button(
259 SearchOptions::CASE_SENSITIVE,
260 focus_handle.clone(),
261 cx.listener(|this, _, window, cx| {
262 this.toggle_case_sensitive(
263 &ToggleCaseSensitive,
264 window,
265 cx,
266 )
267 }),
268 )
269 }))
270 .children(supported_options.word.then(|| {
271 self.render_search_option_button(
272 SearchOptions::WHOLE_WORD,
273 focus_handle.clone(),
274 cx.listener(|this, _, window, cx| {
275 this.toggle_whole_word(&ToggleWholeWord, window, cx)
276 }),
277 )
278 }))
279 .children(supported_options.regex.then(|| {
280 self.render_search_option_button(
281 SearchOptions::REGEX,
282 focus_handle.clone(),
283 cx.listener(|this, _, window, cx| {
284 this.toggle_regex(&ToggleRegex, window, cx)
285 }),
286 )
287 })),
288 )
289 }),
290 )
291 .child(
292 h_flex()
293 .gap_1()
294 .min_w_64()
295 .when(supported_options.replacement, |this| {
296 this.child(
297 IconButton::new(
298 "buffer-search-bar-toggle-replace-button",
299 IconName::Replace,
300 )
301 .style(ButtonStyle::Subtle)
302 .shape(IconButtonShape::Square)
303 .when(self.replace_enabled, |button| {
304 button.style(ButtonStyle::Filled)
305 })
306 .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
307 this.toggle_replace(&ToggleReplace, window, cx);
308 }))
309 .toggle_state(self.replace_enabled)
310 .tooltip({
311 let focus_handle = focus_handle.clone();
312 move |window, cx| {
313 Tooltip::for_action_in(
314 "Toggle Replace",
315 &ToggleReplace,
316 &focus_handle,
317 window,
318 cx,
319 )
320 }
321 }),
322 )
323 })
324 .when(supported_options.selection, |this| {
325 this.child(
326 IconButton::new(
327 "buffer-search-bar-toggle-search-selection-button",
328 IconName::SearchSelection,
329 )
330 .style(ButtonStyle::Subtle)
331 .shape(IconButtonShape::Square)
332 .when(self.selection_search_enabled, |button| {
333 button.style(ButtonStyle::Filled)
334 })
335 .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
336 this.toggle_selection(&ToggleSelection, window, cx);
337 }))
338 .toggle_state(self.selection_search_enabled)
339 .tooltip({
340 let focus_handle = focus_handle.clone();
341 move |window, cx| {
342 Tooltip::for_action_in(
343 "Toggle Search Selection",
344 &ToggleSelection,
345 &focus_handle,
346 window,
347 cx,
348 )
349 }
350 }),
351 )
352 })
353 .when(!supported_options.find_in_results, |el| {
354 el.child(
355 IconButton::new("select-all", ui::IconName::SelectAll)
356 .on_click(|_, window, cx| {
357 window.dispatch_action(SelectAllMatches.boxed_clone(), cx)
358 })
359 .shape(IconButtonShape::Square)
360 .tooltip({
361 let focus_handle = focus_handle.clone();
362 move |window, cx| {
363 Tooltip::for_action_in(
364 "Select All Matches",
365 &SelectAllMatches,
366 &focus_handle,
367 window,
368 cx,
369 )
370 }
371 }),
372 )
373 .child(
374 h_flex()
375 .pl_2()
376 .ml_1()
377 .border_l_1()
378 .border_color(cx.theme().colors().border_variant)
379 .child(render_nav_button(
380 ui::IconName::ChevronLeft,
381 self.active_match_index.is_some(),
382 "Select Previous Match",
383 &SelectPrevMatch,
384 focus_handle.clone(),
385 ))
386 .child(render_nav_button(
387 ui::IconName::ChevronRight,
388 self.active_match_index.is_some(),
389 "Select Next Match",
390 &SelectNextMatch,
391 focus_handle.clone(),
392 )),
393 )
394 .when(!narrow_mode, |this| {
395 this.child(h_flex().ml_2().min_w(rems_from_px(40.)).child(
396 Label::new(match_text).size(LabelSize::Small).color(
397 if self.active_match_index.is_some() {
398 Color::Default
399 } else {
400 Color::Disabled
401 },
402 ),
403 ))
404 })
405 })
406 .when(supported_options.find_in_results, |el| {
407 el.child(
408 IconButton::new(SharedString::from("Close"), IconName::Close)
409 .shape(IconButtonShape::Square)
410 .tooltip(move |window, cx| {
411 Tooltip::for_action("Close Search Bar", &Dismiss, window, cx)
412 })
413 .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
414 this.dismiss(&Dismiss, window, cx)
415 })),
416 )
417 }),
418 );
419
420 let replace_line = should_show_replace_input.then(|| {
421 h_flex()
422 .gap_2()
423 .child(input_base_styles().child(self.render_text_input(
424 &self.replacement_editor,
425 None,
426 cx,
427 )))
428 .child(
429 h_flex()
430 .min_w_64()
431 .gap_1()
432 .child(
433 IconButton::new("search-replace-next", ui::IconName::ReplaceNext)
434 .shape(IconButtonShape::Square)
435 .tooltip({
436 let focus_handle = focus_handle.clone();
437 move |window, cx| {
438 Tooltip::for_action_in(
439 "Replace Next Match",
440 &ReplaceNext,
441 &focus_handle,
442 window,
443 cx,
444 )
445 }
446 })
447 .on_click(cx.listener(|this, _, window, cx| {
448 this.replace_next(&ReplaceNext, window, cx)
449 })),
450 )
451 .child(
452 IconButton::new("search-replace-all", ui::IconName::ReplaceAll)
453 .shape(IconButtonShape::Square)
454 .tooltip({
455 let focus_handle = focus_handle.clone();
456 move |window, cx| {
457 Tooltip::for_action_in(
458 "Replace All Matches",
459 &ReplaceAll,
460 &focus_handle,
461 window,
462 cx,
463 )
464 }
465 })
466 .on_click(cx.listener(|this, _, window, cx| {
467 this.replace_all(&ReplaceAll, window, cx)
468 })),
469 ),
470 )
471 });
472
473 v_flex()
474 .id("buffer_search")
475 .gap_2()
476 .py(px(1.0))
477 .track_scroll(&self.scroll_handle)
478 .key_context(key_context)
479 .capture_action(cx.listener(Self::tab))
480 .capture_action(cx.listener(Self::tab_prev))
481 .on_action(cx.listener(Self::previous_history_query))
482 .on_action(cx.listener(Self::next_history_query))
483 .on_action(cx.listener(Self::dismiss))
484 .on_action(cx.listener(Self::select_next_match))
485 .on_action(cx.listener(Self::select_prev_match))
486 .on_action(cx.listener(|this, _: &ToggleOutline, window, cx| {
487 if let Some(active_searchable_item) = &mut this.active_searchable_item {
488 active_searchable_item.relay_action(Box::new(ToggleOutline), window, cx);
489 }
490 }))
491 .when(self.supported_options(cx).replacement, |this| {
492 this.on_action(cx.listener(Self::toggle_replace))
493 .when(in_replace, |this| {
494 this.on_action(cx.listener(Self::replace_next))
495 .on_action(cx.listener(Self::replace_all))
496 })
497 })
498 .when(self.supported_options(cx).case, |this| {
499 this.on_action(cx.listener(Self::toggle_case_sensitive))
500 })
501 .when(self.supported_options(cx).word, |this| {
502 this.on_action(cx.listener(Self::toggle_whole_word))
503 })
504 .when(self.supported_options(cx).regex, |this| {
505 this.on_action(cx.listener(Self::toggle_regex))
506 })
507 .when(self.supported_options(cx).selection, |this| {
508 this.on_action(cx.listener(Self::toggle_selection))
509 })
510 .child(h_flex().relative().child(search_line.w_full()).when(
511 !narrow_mode && !supported_options.find_in_results,
512 |div| {
513 div.child(
514 h_flex().absolute().right_0().child(
515 IconButton::new(SharedString::from("Close"), IconName::Close)
516 .shape(IconButtonShape::Square)
517 .tooltip(move |window, cx| {
518 Tooltip::for_action("Close Search Bar", &Dismiss, window, cx)
519 })
520 .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
521 this.dismiss(&Dismiss, window, cx)
522 })),
523 ),
524 )
525 .w_full()
526 },
527 ))
528 .children(replace_line)
529 }
530}
531
532impl Focusable for BufferSearchBar {
533 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
534 self.query_editor.focus_handle(cx)
535 }
536}
537
538impl ToolbarItemView for BufferSearchBar {
539 fn set_active_pane_item(
540 &mut self,
541 item: Option<&dyn ItemHandle>,
542 window: &mut Window,
543 cx: &mut Context<Self>,
544 ) -> ToolbarItemLocation {
545 cx.notify();
546 self.active_searchable_item_subscription.take();
547 self.active_searchable_item.take();
548
549 self.pending_search.take();
550
551 if let Some(searchable_item_handle) =
552 item.and_then(|item| item.to_searchable_item_handle(cx))
553 {
554 let this = cx.entity().downgrade();
555
556 self.active_searchable_item_subscription =
557 Some(searchable_item_handle.subscribe_to_search_events(
558 window,
559 cx,
560 Box::new(move |search_event, window, cx| {
561 if let Some(this) = this.upgrade() {
562 this.update(cx, |this, cx| {
563 this.on_active_searchable_item_event(search_event, window, cx)
564 });
565 }
566 }),
567 ));
568
569 let is_project_search = searchable_item_handle.supported_options(cx).find_in_results;
570 self.active_searchable_item = Some(searchable_item_handle);
571 drop(self.update_matches(true, window, cx));
572 if !self.dismissed {
573 if is_project_search {
574 self.dismiss(&Default::default(), window, cx);
575 } else {
576 return ToolbarItemLocation::Secondary;
577 }
578 }
579 }
580 ToolbarItemLocation::Hidden
581 }
582}
583
584impl BufferSearchBar {
585 pub fn register(registrar: &mut impl SearchActionsRegistrar) {
586 registrar.register_handler(ForDeployed(|this, _: &FocusSearch, window, cx| {
587 this.query_editor.focus_handle(cx).focus(window);
588 this.select_query(window, cx);
589 }));
590 registrar.register_handler(ForDeployed(
591 |this, action: &ToggleCaseSensitive, window, cx| {
592 if this.supported_options(cx).case {
593 this.toggle_case_sensitive(action, window, cx);
594 }
595 },
596 ));
597 registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, window, cx| {
598 if this.supported_options(cx).word {
599 this.toggle_whole_word(action, window, cx);
600 }
601 }));
602 registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, window, cx| {
603 if this.supported_options(cx).regex {
604 this.toggle_regex(action, window, cx);
605 }
606 }));
607 registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, window, cx| {
608 if this.supported_options(cx).selection {
609 this.toggle_selection(action, window, cx);
610 } else {
611 cx.propagate();
612 }
613 }));
614 registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, window, cx| {
615 if this.supported_options(cx).replacement {
616 this.toggle_replace(action, window, cx);
617 } else {
618 cx.propagate();
619 }
620 }));
621 registrar.register_handler(WithResults(|this, action: &SelectNextMatch, window, cx| {
622 if this.supported_options(cx).find_in_results {
623 cx.propagate();
624 } else {
625 this.select_next_match(action, window, cx);
626 }
627 }));
628 registrar.register_handler(WithResults(|this, action: &SelectPrevMatch, window, cx| {
629 if this.supported_options(cx).find_in_results {
630 cx.propagate();
631 } else {
632 this.select_prev_match(action, window, cx);
633 }
634 }));
635 registrar.register_handler(WithResults(
636 |this, action: &SelectAllMatches, window, cx| {
637 if this.supported_options(cx).find_in_results {
638 cx.propagate();
639 } else {
640 this.select_all_matches(action, window, cx);
641 }
642 },
643 ));
644 registrar.register_handler(ForDeployed(
645 |this, _: &editor::actions::Cancel, window, cx| {
646 this.dismiss(&Dismiss, window, cx);
647 },
648 ));
649 registrar.register_handler(ForDeployed(|this, _: &Dismiss, window, cx| {
650 this.dismiss(&Dismiss, window, cx);
651 }));
652
653 // register deploy buffer search for both search bar states, since we want to focus into the search bar
654 // when the deploy action is triggered in the buffer.
655 registrar.register_handler(ForDeployed(|this, deploy, window, cx| {
656 this.deploy(deploy, window, cx);
657 }));
658 registrar.register_handler(ForDismissed(|this, _: &DeployReplace, window, cx| {
659 if this.supported_options(cx).find_in_results {
660 cx.propagate();
661 } else {
662 this.deploy(&Deploy::replace(), window, cx);
663 }
664 }));
665 registrar.register_handler(ForDismissed(|this, deploy, window, cx| {
666 this.deploy(deploy, window, cx);
667 }))
668 }
669
670 pub fn new(
671 languages: Option<Arc<LanguageRegistry>>,
672 window: &mut Window,
673 cx: &mut Context<Self>,
674 ) -> Self {
675 let query_editor = cx.new(|cx| Editor::single_line(window, cx));
676 cx.subscribe_in(&query_editor, window, Self::on_query_editor_event)
677 .detach();
678 let replacement_editor = cx.new(|cx| Editor::single_line(window, cx));
679 cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
680 .detach();
681
682 let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
683 if let Some(languages) = languages {
684 let query_buffer = query_editor
685 .read(cx)
686 .buffer()
687 .read(cx)
688 .as_singleton()
689 .expect("query editor should be backed by a singleton buffer");
690 query_buffer.update(cx, |query_buffer, _| {
691 query_buffer.set_language_registry(languages.clone());
692 });
693
694 cx.spawn(|buffer_search_bar, mut cx| async move {
695 let regex_language = languages
696 .language_for_name("regex")
697 .await
698 .context("loading regex language")?;
699 buffer_search_bar
700 .update(&mut cx, |buffer_search_bar, cx| {
701 buffer_search_bar.regex_language = Some(regex_language);
702 buffer_search_bar.adjust_query_regex_language(cx);
703 })
704 .ok();
705 anyhow::Ok(())
706 })
707 .detach_and_log_err(cx);
708 }
709
710 Self {
711 query_editor,
712 query_editor_focused: false,
713 replacement_editor,
714 replacement_editor_focused: false,
715 active_searchable_item: None,
716 active_searchable_item_subscription: None,
717 active_match_index: None,
718 searchable_items_with_matches: Default::default(),
719 default_options: search_options,
720 configured_options: search_options,
721 search_options,
722 pending_search: None,
723 query_contains_error: false,
724 dismissed: true,
725 search_history: SearchHistory::new(
726 Some(MAX_BUFFER_SEARCH_HISTORY_SIZE),
727 project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
728 ),
729 search_history_cursor: Default::default(),
730 active_search: None,
731 replace_enabled: false,
732 selection_search_enabled: false,
733 scroll_handle: ScrollHandle::new(),
734 editor_scroll_handle: ScrollHandle::new(),
735 editor_needed_width: px(0.),
736 regex_language: None,
737 }
738 }
739
740 pub fn is_dismissed(&self) -> bool {
741 self.dismissed
742 }
743
744 pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
745 self.dismissed = true;
746 for searchable_item in self.searchable_items_with_matches.keys() {
747 if let Some(searchable_item) =
748 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
749 {
750 searchable_item.clear_matches(window, cx);
751 }
752 }
753 if let Some(active_editor) = self.active_searchable_item.as_mut() {
754 self.selection_search_enabled = false;
755 self.replace_enabled = false;
756 active_editor.search_bar_visibility_changed(false, window, cx);
757 active_editor.toggle_filtered_search_ranges(false, window, cx);
758 let handle = active_editor.item_focus_handle(cx);
759 self.focus(&handle, window, cx);
760 }
761 cx.emit(Event::UpdateLocation);
762 cx.emit(ToolbarItemEvent::ChangeLocation(
763 ToolbarItemLocation::Hidden,
764 ));
765 cx.notify();
766 }
767
768 pub fn deploy(&mut self, deploy: &Deploy, window: &mut Window, cx: &mut Context<Self>) -> bool {
769 if self.show(window, cx) {
770 if let Some(active_item) = self.active_searchable_item.as_mut() {
771 active_item.toggle_filtered_search_ranges(
772 deploy.selection_search_enabled,
773 window,
774 cx,
775 );
776 }
777 self.search_suggested(window, cx);
778 self.smartcase(window, cx);
779 self.replace_enabled = deploy.replace_enabled;
780 self.selection_search_enabled = deploy.selection_search_enabled;
781 if deploy.focus {
782 let mut handle = self.query_editor.focus_handle(cx).clone();
783 let mut select_query = true;
784 if deploy.replace_enabled && handle.is_focused(window) {
785 handle = self.replacement_editor.focus_handle(cx).clone();
786 select_query = false;
787 };
788
789 if select_query {
790 self.select_query(window, cx);
791 }
792
793 window.focus(&handle);
794 }
795 return true;
796 }
797
798 cx.propagate();
799 false
800 }
801
802 pub fn toggle(&mut self, action: &Deploy, window: &mut Window, cx: &mut Context<Self>) {
803 if self.is_dismissed() {
804 self.deploy(action, window, cx);
805 } else {
806 self.dismiss(&Dismiss, window, cx);
807 }
808 }
809
810 pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
811 let Some(handle) = self.active_searchable_item.as_ref() else {
812 return false;
813 };
814
815 self.configured_options =
816 SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
817 if self.dismissed && self.configured_options != self.default_options {
818 self.search_options = self.configured_options;
819 self.default_options = self.configured_options;
820 }
821
822 self.dismissed = false;
823 handle.search_bar_visibility_changed(true, window, cx);
824 cx.notify();
825 cx.emit(Event::UpdateLocation);
826 cx.emit(ToolbarItemEvent::ChangeLocation(
827 ToolbarItemLocation::Secondary,
828 ));
829 true
830 }
831
832 fn supported_options(&self, cx: &mut Context<Self>) -> workspace::searchable::SearchOptions {
833 self.active_searchable_item
834 .as_ref()
835 .map(|item| item.supported_options(cx))
836 .unwrap_or_default()
837 }
838
839 pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context<Self>) {
840 let search = self
841 .query_suggestion(window, cx)
842 .map(|suggestion| self.search(&suggestion, Some(self.default_options), window, cx));
843
844 if let Some(search) = search {
845 cx.spawn_in(window, |this, mut cx| async move {
846 search.await?;
847 this.update_in(&mut cx, |this, window, cx| {
848 this.activate_current_match(window, cx)
849 })
850 })
851 .detach_and_log_err(cx);
852 }
853 }
854
855 pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
856 if let Some(match_ix) = self.active_match_index {
857 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
858 if let Some(matches) = self
859 .searchable_items_with_matches
860 .get(&active_searchable_item.downgrade())
861 {
862 active_searchable_item.activate_match(match_ix, matches, window, cx)
863 }
864 }
865 }
866 }
867
868 pub fn select_query(&mut self, window: &mut Window, cx: &mut Context<Self>) {
869 self.query_editor.update(cx, |query_editor, cx| {
870 query_editor.select_all(&Default::default(), window, cx);
871 });
872 }
873
874 pub fn query(&self, cx: &App) -> String {
875 self.query_editor.read(cx).text(cx)
876 }
877
878 pub fn replacement(&self, cx: &mut App) -> String {
879 self.replacement_editor.read(cx).text(cx)
880 }
881
882 pub fn query_suggestion(
883 &mut self,
884 window: &mut Window,
885 cx: &mut Context<Self>,
886 ) -> Option<String> {
887 self.active_searchable_item
888 .as_ref()
889 .map(|searchable_item| searchable_item.query_suggestion(window, cx))
890 .filter(|suggestion| !suggestion.is_empty())
891 }
892
893 pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut Context<Self>) {
894 if replacement.is_none() {
895 self.replace_enabled = false;
896 return;
897 }
898 self.replace_enabled = true;
899 self.replacement_editor
900 .update(cx, |replacement_editor, cx| {
901 replacement_editor
902 .buffer()
903 .update(cx, |replacement_buffer, cx| {
904 let len = replacement_buffer.len(cx);
905 replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
906 });
907 });
908 }
909
910 pub fn search(
911 &mut self,
912 query: &str,
913 options: Option<SearchOptions>,
914 window: &mut Window,
915 cx: &mut Context<Self>,
916 ) -> oneshot::Receiver<()> {
917 let options = options.unwrap_or(self.default_options);
918 let updated = query != self.query(cx) || self.search_options != options;
919 if updated {
920 self.query_editor.update(cx, |query_editor, cx| {
921 query_editor.buffer().update(cx, |query_buffer, cx| {
922 let len = query_buffer.len(cx);
923 query_buffer.edit([(0..len, query)], None, cx);
924 });
925 });
926 self.search_options = options;
927 self.clear_matches(window, cx);
928 cx.notify();
929 }
930 self.update_matches(!updated, window, cx)
931 }
932
933 fn render_search_option_button(
934 &self,
935 option: SearchOptions,
936 focus_handle: FocusHandle,
937 action: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
938 ) -> impl IntoElement {
939 let is_active = self.search_options.contains(option);
940 option.as_button(is_active, focus_handle, action)
941 }
942
943 pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
944 if let Some(active_editor) = self.active_searchable_item.as_ref() {
945 let handle = active_editor.item_focus_handle(cx);
946 window.focus(&handle);
947 }
948 }
949
950 pub fn toggle_search_option(
951 &mut self,
952 search_option: SearchOptions,
953 window: &mut Window,
954 cx: &mut Context<Self>,
955 ) {
956 self.search_options.toggle(search_option);
957 self.default_options = self.search_options;
958 drop(self.update_matches(false, window, cx));
959 self.adjust_query_regex_language(cx);
960 cx.notify();
961 }
962
963 pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
964 self.search_options.contains(search_option)
965 }
966
967 pub fn enable_search_option(
968 &mut self,
969 search_option: SearchOptions,
970 window: &mut Window,
971 cx: &mut Context<Self>,
972 ) {
973 if !self.search_options.contains(search_option) {
974 self.toggle_search_option(search_option, window, cx)
975 }
976 }
977
978 pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context<Self>) {
979 self.search_options = search_options;
980 cx.notify();
981 }
982
983 fn select_next_match(
984 &mut self,
985 _: &SelectNextMatch,
986 window: &mut Window,
987 cx: &mut Context<Self>,
988 ) {
989 self.select_match(Direction::Next, 1, window, cx);
990 }
991
992 fn select_prev_match(
993 &mut self,
994 _: &SelectPrevMatch,
995 window: &mut Window,
996 cx: &mut Context<Self>,
997 ) {
998 self.select_match(Direction::Prev, 1, window, cx);
999 }
1000
1001 fn select_all_matches(
1002 &mut self,
1003 _: &SelectAllMatches,
1004 window: &mut Window,
1005 cx: &mut Context<Self>,
1006 ) {
1007 if !self.dismissed && self.active_match_index.is_some() {
1008 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1009 if let Some(matches) = self
1010 .searchable_items_with_matches
1011 .get(&searchable_item.downgrade())
1012 {
1013 searchable_item.select_matches(matches, window, cx);
1014 self.focus_editor(&FocusEditor, window, cx);
1015 }
1016 }
1017 }
1018 }
1019
1020 pub fn select_match(
1021 &mut self,
1022 direction: Direction,
1023 count: usize,
1024 window: &mut Window,
1025 cx: &mut Context<Self>,
1026 ) {
1027 if let Some(index) = self.active_match_index {
1028 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1029 if let Some(matches) = self
1030 .searchable_items_with_matches
1031 .get(&searchable_item.downgrade())
1032 .filter(|matches| !matches.is_empty())
1033 {
1034 // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
1035 if !EditorSettings::get_global(cx).search_wrap
1036 && ((direction == Direction::Next && index + count >= matches.len())
1037 || (direction == Direction::Prev && index < count))
1038 {
1039 crate::show_no_more_matches(window, cx);
1040 return;
1041 }
1042 let new_match_index = searchable_item
1043 .match_index_for_direction(matches, index, direction, count, window, cx);
1044
1045 searchable_item.update_matches(matches, window, cx);
1046 searchable_item.activate_match(new_match_index, matches, window, cx);
1047 }
1048 }
1049 }
1050 }
1051
1052 pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1053 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1054 if let Some(matches) = self
1055 .searchable_items_with_matches
1056 .get(&searchable_item.downgrade())
1057 {
1058 if matches.is_empty() {
1059 return;
1060 }
1061 let new_match_index = matches.len() - 1;
1062 searchable_item.update_matches(matches, window, cx);
1063 searchable_item.activate_match(new_match_index, matches, window, cx);
1064 }
1065 }
1066 }
1067
1068 fn on_query_editor_event(
1069 &mut self,
1070 editor: &Entity<Editor>,
1071 event: &editor::EditorEvent,
1072 window: &mut Window,
1073 cx: &mut Context<Self>,
1074 ) {
1075 match event {
1076 editor::EditorEvent::Focused => self.query_editor_focused = true,
1077 editor::EditorEvent::Blurred => self.query_editor_focused = false,
1078 editor::EditorEvent::Edited { .. } => {
1079 self.smartcase(window, cx);
1080 self.clear_matches(window, cx);
1081 let search = self.update_matches(false, window, cx);
1082
1083 let width = editor.update(cx, |editor, cx| {
1084 let text_layout_details = editor.text_layout_details(window);
1085 let snapshot = editor.snapshot(window, cx).display_snapshot;
1086
1087 snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
1088 - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
1089 });
1090 self.editor_needed_width = width;
1091 cx.notify();
1092
1093 cx.spawn_in(window, |this, mut cx| async move {
1094 search.await?;
1095 this.update_in(&mut cx, |this, window, cx| {
1096 this.activate_current_match(window, cx)
1097 })
1098 })
1099 .detach_and_log_err(cx);
1100 }
1101 _ => {}
1102 }
1103 }
1104
1105 fn on_replacement_editor_event(
1106 &mut self,
1107 _: Entity<Editor>,
1108 event: &editor::EditorEvent,
1109 _: &mut Context<Self>,
1110 ) {
1111 match event {
1112 editor::EditorEvent::Focused => self.replacement_editor_focused = true,
1113 editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
1114 _ => {}
1115 }
1116 }
1117
1118 fn on_active_searchable_item_event(
1119 &mut self,
1120 event: &SearchEvent,
1121 window: &mut Window,
1122 cx: &mut Context<Self>,
1123 ) {
1124 match event {
1125 SearchEvent::MatchesInvalidated => {
1126 drop(self.update_matches(false, window, cx));
1127 }
1128 SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx),
1129 }
1130 }
1131
1132 fn toggle_case_sensitive(
1133 &mut self,
1134 _: &ToggleCaseSensitive,
1135 window: &mut Window,
1136 cx: &mut Context<Self>,
1137 ) {
1138 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx)
1139 }
1140
1141 fn toggle_whole_word(
1142 &mut self,
1143 _: &ToggleWholeWord,
1144 window: &mut Window,
1145 cx: &mut Context<Self>,
1146 ) {
1147 self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1148 }
1149
1150 fn toggle_selection(
1151 &mut self,
1152 _: &ToggleSelection,
1153 window: &mut Window,
1154 cx: &mut Context<Self>,
1155 ) {
1156 if let Some(active_item) = self.active_searchable_item.as_mut() {
1157 self.selection_search_enabled = !self.selection_search_enabled;
1158 active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx);
1159 drop(self.update_matches(false, window, cx));
1160 cx.notify();
1161 }
1162 }
1163
1164 fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context<Self>) {
1165 self.toggle_search_option(SearchOptions::REGEX, window, cx)
1166 }
1167
1168 fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) {
1169 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1170 self.active_match_index = None;
1171 self.searchable_items_with_matches
1172 .remove(&active_searchable_item.downgrade());
1173 active_searchable_item.clear_matches(window, cx);
1174 }
1175 }
1176
1177 pub fn has_active_match(&self) -> bool {
1178 self.active_match_index.is_some()
1179 }
1180
1181 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1182 let mut active_item_matches = None;
1183 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
1184 if let Some(searchable_item) =
1185 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
1186 {
1187 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
1188 active_item_matches = Some((searchable_item.downgrade(), matches));
1189 } else {
1190 searchable_item.clear_matches(window, cx);
1191 }
1192 }
1193 }
1194
1195 self.searchable_items_with_matches
1196 .extend(active_item_matches);
1197 }
1198
1199 fn update_matches(
1200 &mut self,
1201 reuse_existing_query: bool,
1202 window: &mut Window,
1203 cx: &mut Context<Self>,
1204 ) -> oneshot::Receiver<()> {
1205 let (done_tx, done_rx) = oneshot::channel();
1206 let query = self.query(cx);
1207 self.pending_search.take();
1208
1209 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1210 self.query_contains_error = false;
1211 if query.is_empty() {
1212 self.clear_active_searchable_item_matches(window, cx);
1213 let _ = done_tx.send(());
1214 cx.notify();
1215 } else {
1216 let query: Arc<_> = if let Some(search) =
1217 self.active_search.take().filter(|_| reuse_existing_query)
1218 {
1219 search
1220 } else {
1221 if self.search_options.contains(SearchOptions::REGEX) {
1222 match SearchQuery::regex(
1223 query,
1224 self.search_options.contains(SearchOptions::WHOLE_WORD),
1225 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1226 false,
1227 Default::default(),
1228 Default::default(),
1229 None,
1230 ) {
1231 Ok(query) => query.with_replacement(self.replacement(cx)),
1232 Err(_) => {
1233 self.query_contains_error = true;
1234 self.clear_active_searchable_item_matches(window, cx);
1235 cx.notify();
1236 return done_rx;
1237 }
1238 }
1239 } else {
1240 match SearchQuery::text(
1241 query,
1242 self.search_options.contains(SearchOptions::WHOLE_WORD),
1243 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1244 false,
1245 Default::default(),
1246 Default::default(),
1247 None,
1248 ) {
1249 Ok(query) => query.with_replacement(self.replacement(cx)),
1250 Err(_) => {
1251 self.query_contains_error = true;
1252 self.clear_active_searchable_item_matches(window, cx);
1253 cx.notify();
1254 return done_rx;
1255 }
1256 }
1257 }
1258 .into()
1259 };
1260
1261 self.active_search = Some(query.clone());
1262 let query_text = query.as_str().to_string();
1263
1264 let matches = active_searchable_item.find_matches(query, window, cx);
1265
1266 let active_searchable_item = active_searchable_item.downgrade();
1267 self.pending_search = Some(cx.spawn_in(window, |this, mut cx| async move {
1268 let matches = matches.await;
1269
1270 this.update_in(&mut cx, |this, window, cx| {
1271 if let Some(active_searchable_item) =
1272 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1273 {
1274 this.searchable_items_with_matches
1275 .insert(active_searchable_item.downgrade(), matches);
1276
1277 this.update_match_index(window, cx);
1278 this.search_history
1279 .add(&mut this.search_history_cursor, query_text);
1280 if !this.dismissed {
1281 let matches = this
1282 .searchable_items_with_matches
1283 .get(&active_searchable_item.downgrade())
1284 .unwrap();
1285 if matches.is_empty() {
1286 active_searchable_item.clear_matches(window, cx);
1287 } else {
1288 active_searchable_item.update_matches(matches, window, cx);
1289 }
1290 let _ = done_tx.send(());
1291 }
1292 cx.notify();
1293 }
1294 })
1295 .log_err();
1296 }));
1297 }
1298 }
1299 done_rx
1300 }
1301
1302 pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1303 let new_index = self
1304 .active_searchable_item
1305 .as_ref()
1306 .and_then(|searchable_item| {
1307 let matches = self
1308 .searchable_items_with_matches
1309 .get(&searchable_item.downgrade())?;
1310 searchable_item.active_match_index(matches, window, cx)
1311 });
1312 if new_index != self.active_match_index {
1313 self.active_match_index = new_index;
1314 cx.notify();
1315 }
1316 }
1317
1318 fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1319 // Search -> Replace -> Editor
1320 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1321 self.replacement_editor.focus_handle(cx)
1322 } else if let Some(item) = self.active_searchable_item.as_ref() {
1323 item.item_focus_handle(cx)
1324 } else {
1325 return;
1326 };
1327 self.focus(&focus_handle, window, cx);
1328 cx.stop_propagation();
1329 }
1330
1331 fn tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
1332 // Search -> Replace -> Search
1333 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1334 self.replacement_editor.focus_handle(cx)
1335 } else if self.replacement_editor_focused {
1336 self.query_editor.focus_handle(cx)
1337 } else {
1338 return;
1339 };
1340 self.focus(&focus_handle, window, cx);
1341 cx.stop_propagation();
1342 }
1343
1344 fn next_history_query(
1345 &mut self,
1346 _: &NextHistoryQuery,
1347 window: &mut Window,
1348 cx: &mut Context<Self>,
1349 ) {
1350 if let Some(new_query) = self
1351 .search_history
1352 .next(&mut self.search_history_cursor)
1353 .map(str::to_string)
1354 {
1355 drop(self.search(&new_query, Some(self.search_options), window, cx));
1356 } else {
1357 self.search_history_cursor.reset();
1358 drop(self.search("", Some(self.search_options), window, cx));
1359 }
1360 }
1361
1362 fn previous_history_query(
1363 &mut self,
1364 _: &PreviousHistoryQuery,
1365 window: &mut Window,
1366 cx: &mut Context<Self>,
1367 ) {
1368 if self.query(cx).is_empty() {
1369 if let Some(new_query) = self
1370 .search_history
1371 .current(&mut self.search_history_cursor)
1372 .map(str::to_string)
1373 {
1374 drop(self.search(&new_query, Some(self.search_options), window, cx));
1375 return;
1376 }
1377 }
1378
1379 if let Some(new_query) = self
1380 .search_history
1381 .previous(&mut self.search_history_cursor)
1382 .map(str::to_string)
1383 {
1384 drop(self.search(&new_query, Some(self.search_options), window, cx));
1385 }
1386 }
1387
1388 fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut Context<Self>) {
1389 cx.on_next_frame(window, |_, window, _| {
1390 window.invalidate_character_coordinates();
1391 });
1392 window.focus(handle);
1393 }
1394
1395 fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1396 if self.active_searchable_item.is_some() {
1397 self.replace_enabled = !self.replace_enabled;
1398 let handle = if self.replace_enabled {
1399 self.replacement_editor.focus_handle(cx)
1400 } else {
1401 self.query_editor.focus_handle(cx)
1402 };
1403 self.focus(&handle, window, cx);
1404 cx.notify();
1405 }
1406 }
1407
1408 fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1409 let mut should_propagate = true;
1410 if !self.dismissed && self.active_search.is_some() {
1411 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1412 if let Some(query) = self.active_search.as_ref() {
1413 if let Some(matches) = self
1414 .searchable_items_with_matches
1415 .get(&searchable_item.downgrade())
1416 {
1417 if let Some(active_index) = self.active_match_index {
1418 let query = query
1419 .as_ref()
1420 .clone()
1421 .with_replacement(self.replacement(cx));
1422 searchable_item.replace(matches.at(active_index), &query, window, cx);
1423 self.select_next_match(&SelectNextMatch, window, cx);
1424 }
1425 should_propagate = false;
1426 self.focus_editor(&FocusEditor, window, cx);
1427 }
1428 }
1429 }
1430 }
1431 if !should_propagate {
1432 cx.stop_propagation();
1433 }
1434 }
1435
1436 pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1437 if !self.dismissed && self.active_search.is_some() {
1438 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1439 if let Some(query) = self.active_search.as_ref() {
1440 if let Some(matches) = self
1441 .searchable_items_with_matches
1442 .get(&searchable_item.downgrade())
1443 {
1444 let query = query
1445 .as_ref()
1446 .clone()
1447 .with_replacement(self.replacement(cx));
1448 searchable_item.replace_all(&mut matches.iter(), &query, window, cx);
1449 }
1450 }
1451 }
1452 }
1453 }
1454
1455 pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1456 self.update_match_index(window, cx);
1457 self.active_match_index.is_some()
1458 }
1459
1460 pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1461 EditorSettings::get_global(cx).use_smartcase_search
1462 }
1463
1464 pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1465 str.chars().any(|c| c.is_uppercase())
1466 }
1467
1468 fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1469 if self.should_use_smartcase_search(cx) {
1470 let query = self.query(cx);
1471 if !query.is_empty() {
1472 let is_case = self.is_contains_uppercase(&query);
1473 if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1474 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1475 }
1476 }
1477 }
1478 }
1479
1480 fn adjust_query_regex_language(&self, cx: &mut App) {
1481 let enable = self.search_options.contains(SearchOptions::REGEX);
1482 let query_buffer = self
1483 .query_editor
1484 .read(cx)
1485 .buffer()
1486 .read(cx)
1487 .as_singleton()
1488 .expect("query editor should be backed by a singleton buffer");
1489 if enable {
1490 if let Some(regex_language) = self.regex_language.clone() {
1491 query_buffer.update(cx, |query_buffer, cx| {
1492 query_buffer.set_language(Some(regex_language), cx);
1493 })
1494 }
1495 } else {
1496 query_buffer.update(cx, |query_buffer, cx| {
1497 query_buffer.set_language(None, cx);
1498 })
1499 }
1500 }
1501}
1502
1503#[cfg(test)]
1504mod tests {
1505 use std::ops::Range;
1506
1507 use super::*;
1508 use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer, SearchSettings};
1509 use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1510 use language::{Buffer, Point};
1511 use project::Project;
1512 use settings::SettingsStore;
1513 use smol::stream::StreamExt as _;
1514 use unindent::Unindent as _;
1515
1516 fn init_globals(cx: &mut TestAppContext) {
1517 cx.update(|cx| {
1518 let store = settings::SettingsStore::test(cx);
1519 cx.set_global(store);
1520 workspace::init_settings(cx);
1521 editor::init(cx);
1522
1523 language::init(cx);
1524 Project::init_settings(cx);
1525 theme::init(theme::LoadThemes::JustBase, cx);
1526 crate::init(cx);
1527 });
1528 }
1529
1530 fn init_test(
1531 cx: &mut TestAppContext,
1532 ) -> (
1533 Entity<Editor>,
1534 Entity<BufferSearchBar>,
1535 &mut VisualTestContext,
1536 ) {
1537 init_globals(cx);
1538 let buffer = cx.new(|cx| {
1539 Buffer::local(
1540 r#"
1541 A regular expression (shortened as regex or regexp;[1] also referred to as
1542 rational expression[2][3]) is a sequence of characters that specifies a search
1543 pattern in text. Usually such patterns are used by string-searching algorithms
1544 for "find" or "find and replace" operations on strings, or for input validation.
1545 "#
1546 .unindent(),
1547 cx,
1548 )
1549 });
1550 let cx = cx.add_empty_window();
1551 let editor =
1552 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
1553
1554 let search_bar = cx.new_window_entity(|window, cx| {
1555 let mut search_bar = BufferSearchBar::new(None, window, cx);
1556 search_bar.set_active_pane_item(Some(&editor), window, cx);
1557 search_bar.show(window, cx);
1558 search_bar
1559 });
1560
1561 (editor, search_bar, cx)
1562 }
1563
1564 #[gpui::test]
1565 async fn test_search_simple(cx: &mut TestAppContext) {
1566 let (editor, search_bar, cx) = init_test(cx);
1567 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1568 background_highlights
1569 .into_iter()
1570 .map(|(range, _)| range)
1571 .collect::<Vec<_>>()
1572 };
1573 // Search for a string that appears with different casing.
1574 // By default, search is case-insensitive.
1575 search_bar
1576 .update_in(cx, |search_bar, window, cx| {
1577 search_bar.search("us", None, window, cx)
1578 })
1579 .await
1580 .unwrap();
1581 editor.update_in(cx, |editor, window, cx| {
1582 assert_eq!(
1583 display_points_of(editor.all_text_background_highlights(window, cx)),
1584 &[
1585 DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1586 DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1587 ]
1588 );
1589 });
1590
1591 // Switch to a case sensitive search.
1592 search_bar.update_in(cx, |search_bar, window, cx| {
1593 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1594 });
1595 let mut editor_notifications = cx.notifications(&editor);
1596 editor_notifications.next().await;
1597 editor.update_in(cx, |editor, window, cx| {
1598 assert_eq!(
1599 display_points_of(editor.all_text_background_highlights(window, cx)),
1600 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1601 );
1602 });
1603
1604 // Search for a string that appears both as a whole word and
1605 // within other words. By default, all results are found.
1606 search_bar
1607 .update_in(cx, |search_bar, window, cx| {
1608 search_bar.search("or", None, window, cx)
1609 })
1610 .await
1611 .unwrap();
1612 editor.update_in(cx, |editor, window, cx| {
1613 assert_eq!(
1614 display_points_of(editor.all_text_background_highlights(window, cx)),
1615 &[
1616 DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1617 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1618 DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1619 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1620 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1621 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1622 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1623 ]
1624 );
1625 });
1626
1627 // Switch to a whole word search.
1628 search_bar.update_in(cx, |search_bar, window, cx| {
1629 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1630 });
1631 let mut editor_notifications = cx.notifications(&editor);
1632 editor_notifications.next().await;
1633 editor.update_in(cx, |editor, window, cx| {
1634 assert_eq!(
1635 display_points_of(editor.all_text_background_highlights(window, cx)),
1636 &[
1637 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1638 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1639 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1640 ]
1641 );
1642 });
1643
1644 editor.update_in(cx, |editor, window, cx| {
1645 editor.change_selections(None, window, cx, |s| {
1646 s.select_display_ranges([
1647 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1648 ])
1649 });
1650 });
1651 search_bar.update_in(cx, |search_bar, window, cx| {
1652 assert_eq!(search_bar.active_match_index, Some(0));
1653 search_bar.select_next_match(&SelectNextMatch, window, cx);
1654 assert_eq!(
1655 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1656 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1657 );
1658 });
1659 search_bar.update(cx, |search_bar, _| {
1660 assert_eq!(search_bar.active_match_index, Some(0));
1661 });
1662
1663 search_bar.update_in(cx, |search_bar, window, cx| {
1664 search_bar.select_next_match(&SelectNextMatch, window, cx);
1665 assert_eq!(
1666 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1667 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1668 );
1669 });
1670 search_bar.update(cx, |search_bar, _| {
1671 assert_eq!(search_bar.active_match_index, Some(1));
1672 });
1673
1674 search_bar.update_in(cx, |search_bar, window, cx| {
1675 search_bar.select_next_match(&SelectNextMatch, window, cx);
1676 assert_eq!(
1677 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1678 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1679 );
1680 });
1681 search_bar.update(cx, |search_bar, _| {
1682 assert_eq!(search_bar.active_match_index, Some(2));
1683 });
1684
1685 search_bar.update_in(cx, |search_bar, window, cx| {
1686 search_bar.select_next_match(&SelectNextMatch, window, cx);
1687 assert_eq!(
1688 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1689 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1690 );
1691 });
1692 search_bar.update(cx, |search_bar, _| {
1693 assert_eq!(search_bar.active_match_index, Some(0));
1694 });
1695
1696 search_bar.update_in(cx, |search_bar, window, cx| {
1697 search_bar.select_prev_match(&SelectPrevMatch, window, cx);
1698 assert_eq!(
1699 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1700 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1701 );
1702 });
1703 search_bar.update(cx, |search_bar, _| {
1704 assert_eq!(search_bar.active_match_index, Some(2));
1705 });
1706
1707 search_bar.update_in(cx, |search_bar, window, cx| {
1708 search_bar.select_prev_match(&SelectPrevMatch, window, cx);
1709 assert_eq!(
1710 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1711 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1712 );
1713 });
1714 search_bar.update(cx, |search_bar, _| {
1715 assert_eq!(search_bar.active_match_index, Some(1));
1716 });
1717
1718 search_bar.update_in(cx, |search_bar, window, cx| {
1719 search_bar.select_prev_match(&SelectPrevMatch, window, cx);
1720 assert_eq!(
1721 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1722 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1723 );
1724 });
1725 search_bar.update(cx, |search_bar, _| {
1726 assert_eq!(search_bar.active_match_index, Some(0));
1727 });
1728
1729 // Park the cursor in between matches and ensure that going to the previous match selects
1730 // the closest match to the left.
1731 editor.update_in(cx, |editor, window, cx| {
1732 editor.change_selections(None, window, cx, |s| {
1733 s.select_display_ranges([
1734 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1735 ])
1736 });
1737 });
1738 search_bar.update_in(cx, |search_bar, window, cx| {
1739 assert_eq!(search_bar.active_match_index, Some(1));
1740 search_bar.select_prev_match(&SelectPrevMatch, window, cx);
1741 assert_eq!(
1742 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1743 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1744 );
1745 });
1746 search_bar.update(cx, |search_bar, _| {
1747 assert_eq!(search_bar.active_match_index, Some(0));
1748 });
1749
1750 // Park the cursor in between matches and ensure that going to the next match selects the
1751 // closest match to the right.
1752 editor.update_in(cx, |editor, window, cx| {
1753 editor.change_selections(None, window, cx, |s| {
1754 s.select_display_ranges([
1755 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1756 ])
1757 });
1758 });
1759 search_bar.update_in(cx, |search_bar, window, cx| {
1760 assert_eq!(search_bar.active_match_index, Some(1));
1761 search_bar.select_next_match(&SelectNextMatch, window, cx);
1762 assert_eq!(
1763 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1764 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1765 );
1766 });
1767 search_bar.update(cx, |search_bar, _| {
1768 assert_eq!(search_bar.active_match_index, Some(1));
1769 });
1770
1771 // Park the cursor after the last match and ensure that going to the previous match selects
1772 // the last match.
1773 editor.update_in(cx, |editor, window, cx| {
1774 editor.change_selections(None, window, cx, |s| {
1775 s.select_display_ranges([
1776 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1777 ])
1778 });
1779 });
1780 search_bar.update_in(cx, |search_bar, window, cx| {
1781 assert_eq!(search_bar.active_match_index, Some(2));
1782 search_bar.select_prev_match(&SelectPrevMatch, window, cx);
1783 assert_eq!(
1784 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1785 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1786 );
1787 });
1788 search_bar.update(cx, |search_bar, _| {
1789 assert_eq!(search_bar.active_match_index, Some(2));
1790 });
1791
1792 // Park the cursor after the last match and ensure that going to the next match selects the
1793 // first match.
1794 editor.update_in(cx, |editor, window, cx| {
1795 editor.change_selections(None, window, cx, |s| {
1796 s.select_display_ranges([
1797 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1798 ])
1799 });
1800 });
1801 search_bar.update_in(cx, |search_bar, window, cx| {
1802 assert_eq!(search_bar.active_match_index, Some(2));
1803 search_bar.select_next_match(&SelectNextMatch, window, cx);
1804 assert_eq!(
1805 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1806 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1807 );
1808 });
1809 search_bar.update(cx, |search_bar, _| {
1810 assert_eq!(search_bar.active_match_index, Some(0));
1811 });
1812
1813 // Park the cursor before the first match and ensure that going to the previous match
1814 // selects the last match.
1815 editor.update_in(cx, |editor, window, cx| {
1816 editor.change_selections(None, window, cx, |s| {
1817 s.select_display_ranges([
1818 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1819 ])
1820 });
1821 });
1822 search_bar.update_in(cx, |search_bar, window, cx| {
1823 assert_eq!(search_bar.active_match_index, Some(0));
1824 search_bar.select_prev_match(&SelectPrevMatch, window, cx);
1825 assert_eq!(
1826 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1827 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1828 );
1829 });
1830 search_bar.update(cx, |search_bar, _| {
1831 assert_eq!(search_bar.active_match_index, Some(2));
1832 });
1833 }
1834
1835 fn display_points_of(
1836 background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1837 ) -> Vec<Range<DisplayPoint>> {
1838 background_highlights
1839 .into_iter()
1840 .map(|(range, _)| range)
1841 .collect::<Vec<_>>()
1842 }
1843
1844 #[gpui::test]
1845 async fn test_search_option_handling(cx: &mut TestAppContext) {
1846 let (editor, search_bar, cx) = init_test(cx);
1847
1848 // show with options should make current search case sensitive
1849 search_bar
1850 .update_in(cx, |search_bar, window, cx| {
1851 search_bar.show(window, cx);
1852 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1853 })
1854 .await
1855 .unwrap();
1856 editor.update_in(cx, |editor, window, cx| {
1857 assert_eq!(
1858 display_points_of(editor.all_text_background_highlights(window, cx)),
1859 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1860 );
1861 });
1862
1863 // search_suggested should restore default options
1864 search_bar.update_in(cx, |search_bar, window, cx| {
1865 search_bar.search_suggested(window, cx);
1866 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1867 });
1868
1869 // toggling a search option should update the defaults
1870 search_bar
1871 .update_in(cx, |search_bar, window, cx| {
1872 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1873 })
1874 .await
1875 .unwrap();
1876 search_bar.update_in(cx, |search_bar, window, cx| {
1877 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1878 });
1879 let mut editor_notifications = cx.notifications(&editor);
1880 editor_notifications.next().await;
1881 editor.update_in(cx, |editor, window, cx| {
1882 assert_eq!(
1883 display_points_of(editor.all_text_background_highlights(window, cx)),
1884 &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1885 );
1886 });
1887
1888 // defaults should still include whole word
1889 search_bar.update_in(cx, |search_bar, window, cx| {
1890 search_bar.search_suggested(window, cx);
1891 assert_eq!(
1892 search_bar.search_options,
1893 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1894 )
1895 });
1896 }
1897
1898 #[gpui::test]
1899 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1900 init_globals(cx);
1901 let buffer_text = r#"
1902 A regular expression (shortened as regex or regexp;[1] also referred to as
1903 rational expression[2][3]) is a sequence of characters that specifies a search
1904 pattern in text. Usually such patterns are used by string-searching algorithms
1905 for "find" or "find and replace" operations on strings, or for input validation.
1906 "#
1907 .unindent();
1908 let expected_query_matches_count = buffer_text
1909 .chars()
1910 .filter(|c| c.to_ascii_lowercase() == 'a')
1911 .count();
1912 assert!(
1913 expected_query_matches_count > 1,
1914 "Should pick a query with multiple results"
1915 );
1916 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
1917 let window = cx.add_window(|_, _| gpui::Empty);
1918
1919 let editor = window.build_entity(cx, |window, cx| {
1920 Editor::for_buffer(buffer.clone(), None, window, cx)
1921 });
1922
1923 let search_bar = window.build_entity(cx, |window, cx| {
1924 let mut search_bar = BufferSearchBar::new(None, window, cx);
1925 search_bar.set_active_pane_item(Some(&editor), window, cx);
1926 search_bar.show(window, cx);
1927 search_bar
1928 });
1929
1930 window
1931 .update(cx, |_, window, cx| {
1932 search_bar.update(cx, |search_bar, cx| {
1933 search_bar.search("a", None, window, cx)
1934 })
1935 })
1936 .unwrap()
1937 .await
1938 .unwrap();
1939 let initial_selections = window
1940 .update(cx, |_, window, cx| {
1941 search_bar.update(cx, |search_bar, cx| {
1942 let handle = search_bar.query_editor.focus_handle(cx);
1943 window.focus(&handle);
1944 search_bar.activate_current_match(window, cx);
1945 });
1946 assert!(
1947 !editor.read(cx).is_focused(window),
1948 "Initially, the editor should not be focused"
1949 );
1950 let initial_selections = editor.update(cx, |editor, cx| {
1951 let initial_selections = editor.selections.display_ranges(cx);
1952 assert_eq!(
1953 initial_selections.len(), 1,
1954 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1955 );
1956 initial_selections
1957 });
1958 search_bar.update(cx, |search_bar, cx| {
1959 assert_eq!(search_bar.active_match_index, Some(0));
1960 let handle = search_bar.query_editor.focus_handle(cx);
1961 window.focus(&handle);
1962 search_bar.select_all_matches(&SelectAllMatches, window, cx);
1963 });
1964 assert!(
1965 editor.read(cx).is_focused(window),
1966 "Should focus editor after successful SelectAllMatches"
1967 );
1968 search_bar.update(cx, |search_bar, cx| {
1969 let all_selections =
1970 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1971 assert_eq!(
1972 all_selections.len(),
1973 expected_query_matches_count,
1974 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1975 );
1976 assert_eq!(
1977 search_bar.active_match_index,
1978 Some(0),
1979 "Match index should not change after selecting all matches"
1980 );
1981 });
1982
1983 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
1984 initial_selections
1985 }).unwrap();
1986
1987 window
1988 .update(cx, |_, window, cx| {
1989 assert!(
1990 editor.read(cx).is_focused(window),
1991 "Should still have editor focused after SelectNextMatch"
1992 );
1993 search_bar.update(cx, |search_bar, cx| {
1994 let all_selections =
1995 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1996 assert_eq!(
1997 all_selections.len(),
1998 1,
1999 "On next match, should deselect items and select the next match"
2000 );
2001 assert_ne!(
2002 all_selections, initial_selections,
2003 "Next match should be different from the first selection"
2004 );
2005 assert_eq!(
2006 search_bar.active_match_index,
2007 Some(1),
2008 "Match index should be updated to the next one"
2009 );
2010 let handle = search_bar.query_editor.focus_handle(cx);
2011 window.focus(&handle);
2012 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2013 });
2014 })
2015 .unwrap();
2016 window
2017 .update(cx, |_, window, cx| {
2018 assert!(
2019 editor.read(cx).is_focused(window),
2020 "Should focus editor after successful SelectAllMatches"
2021 );
2022 search_bar.update(cx, |search_bar, cx| {
2023 let all_selections =
2024 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2025 assert_eq!(
2026 all_selections.len(),
2027 expected_query_matches_count,
2028 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2029 );
2030 assert_eq!(
2031 search_bar.active_match_index,
2032 Some(1),
2033 "Match index should not change after selecting all matches"
2034 );
2035 });
2036 search_bar.update(cx, |search_bar, cx| {
2037 search_bar.select_prev_match(&SelectPrevMatch, window, cx);
2038 });
2039 })
2040 .unwrap();
2041 let last_match_selections = window
2042 .update(cx, |_, window, cx| {
2043 assert!(
2044 editor.read(cx).is_focused(window),
2045 "Should still have editor focused after SelectPrevMatch"
2046 );
2047
2048 search_bar.update(cx, |search_bar, cx| {
2049 let all_selections =
2050 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2051 assert_eq!(
2052 all_selections.len(),
2053 1,
2054 "On previous match, should deselect items and select the previous item"
2055 );
2056 assert_eq!(
2057 all_selections, initial_selections,
2058 "Previous match should be the same as the first selection"
2059 );
2060 assert_eq!(
2061 search_bar.active_match_index,
2062 Some(0),
2063 "Match index should be updated to the previous one"
2064 );
2065 all_selections
2066 })
2067 })
2068 .unwrap();
2069
2070 window
2071 .update(cx, |_, window, cx| {
2072 search_bar.update(cx, |search_bar, cx| {
2073 let handle = search_bar.query_editor.focus_handle(cx);
2074 window.focus(&handle);
2075 search_bar.search("abas_nonexistent_match", None, window, cx)
2076 })
2077 })
2078 .unwrap()
2079 .await
2080 .unwrap();
2081 window
2082 .update(cx, |_, window, cx| {
2083 search_bar.update(cx, |search_bar, cx| {
2084 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2085 });
2086 assert!(
2087 editor.update(cx, |this, _cx| !this.is_focused(window)),
2088 "Should not switch focus to editor if SelectAllMatches does not find any matches"
2089 );
2090 search_bar.update(cx, |search_bar, cx| {
2091 let all_selections =
2092 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2093 assert_eq!(
2094 all_selections, last_match_selections,
2095 "Should not select anything new if there are no matches"
2096 );
2097 assert!(
2098 search_bar.active_match_index.is_none(),
2099 "For no matches, there should be no active match index"
2100 );
2101 });
2102 })
2103 .unwrap();
2104 }
2105
2106 #[gpui::test]
2107 async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2108 init_globals(cx);
2109 let buffer_text = r#"
2110 self.buffer.update(cx, |buffer, cx| {
2111 buffer.edit(
2112 edits,
2113 Some(AutoindentMode::Block {
2114 original_indent_columns,
2115 }),
2116 cx,
2117 )
2118 });
2119
2120 this.buffer.update(cx, |buffer, cx| {
2121 buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2122 });
2123 "#
2124 .unindent();
2125 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2126 let cx = cx.add_empty_window();
2127
2128 let editor =
2129 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2130
2131 let search_bar = cx.new_window_entity(|window, cx| {
2132 let mut search_bar = BufferSearchBar::new(None, window, cx);
2133 search_bar.set_active_pane_item(Some(&editor), window, cx);
2134 search_bar.show(window, cx);
2135 search_bar
2136 });
2137
2138 search_bar
2139 .update_in(cx, |search_bar, window, cx| {
2140 search_bar.search(
2141 "edit\\(",
2142 Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2143 window,
2144 cx,
2145 )
2146 })
2147 .await
2148 .unwrap();
2149
2150 search_bar.update_in(cx, |search_bar, window, cx| {
2151 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2152 });
2153 search_bar.update(cx, |_, cx| {
2154 let all_selections =
2155 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2156 assert_eq!(
2157 all_selections.len(),
2158 2,
2159 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2160 );
2161 });
2162
2163 search_bar
2164 .update_in(cx, |search_bar, window, cx| {
2165 search_bar.search(
2166 "edit(",
2167 Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2168 window,
2169 cx,
2170 )
2171 })
2172 .await
2173 .unwrap();
2174
2175 search_bar.update_in(cx, |search_bar, window, cx| {
2176 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2177 });
2178 search_bar.update(cx, |_, cx| {
2179 let all_selections =
2180 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2181 assert_eq!(
2182 all_selections.len(),
2183 2,
2184 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2185 );
2186 });
2187 }
2188
2189 #[gpui::test]
2190 async fn test_search_query_history(cx: &mut TestAppContext) {
2191 init_globals(cx);
2192 let buffer_text = r#"
2193 A regular expression (shortened as regex or regexp;[1] also referred to as
2194 rational expression[2][3]) is a sequence of characters that specifies a search
2195 pattern in text. Usually such patterns are used by string-searching algorithms
2196 for "find" or "find and replace" operations on strings, or for input validation.
2197 "#
2198 .unindent();
2199 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2200 let cx = cx.add_empty_window();
2201
2202 let editor =
2203 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2204
2205 let search_bar = cx.new_window_entity(|window, cx| {
2206 let mut search_bar = BufferSearchBar::new(None, window, cx);
2207 search_bar.set_active_pane_item(Some(&editor), window, cx);
2208 search_bar.show(window, cx);
2209 search_bar
2210 });
2211
2212 // Add 3 search items into the history.
2213 search_bar
2214 .update_in(cx, |search_bar, window, cx| {
2215 search_bar.search("a", None, window, cx)
2216 })
2217 .await
2218 .unwrap();
2219 search_bar
2220 .update_in(cx, |search_bar, window, cx| {
2221 search_bar.search("b", None, window, cx)
2222 })
2223 .await
2224 .unwrap();
2225 search_bar
2226 .update_in(cx, |search_bar, window, cx| {
2227 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), window, cx)
2228 })
2229 .await
2230 .unwrap();
2231 // Ensure that the latest search is active.
2232 search_bar.update(cx, |search_bar, cx| {
2233 assert_eq!(search_bar.query(cx), "c");
2234 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2235 });
2236
2237 // Next history query after the latest should set the query to the empty string.
2238 search_bar.update_in(cx, |search_bar, window, cx| {
2239 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2240 });
2241 search_bar.update(cx, |search_bar, cx| {
2242 assert_eq!(search_bar.query(cx), "");
2243 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2244 });
2245 search_bar.update_in(cx, |search_bar, window, cx| {
2246 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2247 });
2248 search_bar.update(cx, |search_bar, cx| {
2249 assert_eq!(search_bar.query(cx), "");
2250 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2251 });
2252
2253 // First previous query for empty current query should set the query to the latest.
2254 search_bar.update_in(cx, |search_bar, window, cx| {
2255 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2256 });
2257 search_bar.update(cx, |search_bar, cx| {
2258 assert_eq!(search_bar.query(cx), "c");
2259 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2260 });
2261
2262 // Further previous items should go over the history in reverse order.
2263 search_bar.update_in(cx, |search_bar, window, cx| {
2264 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2265 });
2266 search_bar.update(cx, |search_bar, cx| {
2267 assert_eq!(search_bar.query(cx), "b");
2268 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2269 });
2270
2271 // Previous items should never go behind the first history item.
2272 search_bar.update_in(cx, |search_bar, window, cx| {
2273 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2274 });
2275 search_bar.update(cx, |search_bar, cx| {
2276 assert_eq!(search_bar.query(cx), "a");
2277 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2278 });
2279 search_bar.update_in(cx, |search_bar, window, cx| {
2280 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2281 });
2282 search_bar.update(cx, |search_bar, cx| {
2283 assert_eq!(search_bar.query(cx), "a");
2284 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2285 });
2286
2287 // Next items should go over the history in the original order.
2288 search_bar.update_in(cx, |search_bar, window, cx| {
2289 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2290 });
2291 search_bar.update(cx, |search_bar, cx| {
2292 assert_eq!(search_bar.query(cx), "b");
2293 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2294 });
2295
2296 search_bar
2297 .update_in(cx, |search_bar, window, cx| {
2298 search_bar.search("ba", None, window, cx)
2299 })
2300 .await
2301 .unwrap();
2302 search_bar.update(cx, |search_bar, cx| {
2303 assert_eq!(search_bar.query(cx), "ba");
2304 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2305 });
2306
2307 // New search input should add another entry to history and move the selection to the end of the history.
2308 search_bar.update_in(cx, |search_bar, window, cx| {
2309 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2310 });
2311 search_bar.update(cx, |search_bar, cx| {
2312 assert_eq!(search_bar.query(cx), "c");
2313 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2314 });
2315 search_bar.update_in(cx, |search_bar, window, cx| {
2316 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2317 });
2318 search_bar.update(cx, |search_bar, cx| {
2319 assert_eq!(search_bar.query(cx), "b");
2320 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2321 });
2322 search_bar.update_in(cx, |search_bar, window, cx| {
2323 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2324 });
2325 search_bar.update(cx, |search_bar, cx| {
2326 assert_eq!(search_bar.query(cx), "c");
2327 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2328 });
2329 search_bar.update_in(cx, |search_bar, window, cx| {
2330 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2331 });
2332 search_bar.update(cx, |search_bar, cx| {
2333 assert_eq!(search_bar.query(cx), "ba");
2334 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2335 });
2336 search_bar.update_in(cx, |search_bar, window, cx| {
2337 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2338 });
2339 search_bar.update(cx, |search_bar, cx| {
2340 assert_eq!(search_bar.query(cx), "");
2341 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2342 });
2343 }
2344
2345 #[gpui::test]
2346 async fn test_replace_simple(cx: &mut TestAppContext) {
2347 let (editor, search_bar, cx) = init_test(cx);
2348
2349 search_bar
2350 .update_in(cx, |search_bar, window, cx| {
2351 search_bar.search("expression", None, window, cx)
2352 })
2353 .await
2354 .unwrap();
2355
2356 search_bar.update_in(cx, |search_bar, window, cx| {
2357 search_bar.replacement_editor.update(cx, |editor, cx| {
2358 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2359 editor.set_text("expr$1", window, cx);
2360 });
2361 search_bar.replace_all(&ReplaceAll, window, cx)
2362 });
2363 assert_eq!(
2364 editor.update(cx, |this, cx| { this.text(cx) }),
2365 r#"
2366 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2367 rational expr$1[2][3]) is a sequence of characters that specifies a search
2368 pattern in text. Usually such patterns are used by string-searching algorithms
2369 for "find" or "find and replace" operations on strings, or for input validation.
2370 "#
2371 .unindent()
2372 );
2373
2374 // Search for word boundaries and replace just a single one.
2375 search_bar
2376 .update_in(cx, |search_bar, window, cx| {
2377 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), window, cx)
2378 })
2379 .await
2380 .unwrap();
2381
2382 search_bar.update_in(cx, |search_bar, window, cx| {
2383 search_bar.replacement_editor.update(cx, |editor, cx| {
2384 editor.set_text("banana", window, cx);
2385 });
2386 search_bar.replace_next(&ReplaceNext, window, cx)
2387 });
2388 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2389 assert_eq!(
2390 editor.update(cx, |this, cx| { this.text(cx) }),
2391 r#"
2392 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2393 rational expr$1[2][3]) is a sequence of characters that specifies a search
2394 pattern in text. Usually such patterns are used by string-searching algorithms
2395 for "find" or "find and replace" operations on strings, or for input validation.
2396 "#
2397 .unindent()
2398 );
2399 // Let's turn on regex mode.
2400 search_bar
2401 .update_in(cx, |search_bar, window, cx| {
2402 search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), window, cx)
2403 })
2404 .await
2405 .unwrap();
2406 search_bar.update_in(cx, |search_bar, window, cx| {
2407 search_bar.replacement_editor.update(cx, |editor, cx| {
2408 editor.set_text("${1}number", window, cx);
2409 });
2410 search_bar.replace_all(&ReplaceAll, window, cx)
2411 });
2412 assert_eq!(
2413 editor.update(cx, |this, cx| { this.text(cx) }),
2414 r#"
2415 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2416 rational expr$12number3number) is a sequence of characters that specifies a search
2417 pattern in text. Usually such patterns are used by string-searching algorithms
2418 for "find" or "find and replace" operations on strings, or for input validation.
2419 "#
2420 .unindent()
2421 );
2422 // Now with a whole-word twist.
2423 search_bar
2424 .update_in(cx, |search_bar, window, cx| {
2425 search_bar.search(
2426 "a\\w+s",
2427 Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2428 window,
2429 cx,
2430 )
2431 })
2432 .await
2433 .unwrap();
2434 search_bar.update_in(cx, |search_bar, window, cx| {
2435 search_bar.replacement_editor.update(cx, |editor, cx| {
2436 editor.set_text("things", window, cx);
2437 });
2438 search_bar.replace_all(&ReplaceAll, window, cx)
2439 });
2440 // The only word affected by this edit should be `algorithms`, even though there's a bunch
2441 // of words in this text that would match this regex if not for WHOLE_WORD.
2442 assert_eq!(
2443 editor.update(cx, |this, cx| { this.text(cx) }),
2444 r#"
2445 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2446 rational expr$12number3number) is a sequence of characters that specifies a search
2447 pattern in text. Usually such patterns are used by string-searching things
2448 for "find" or "find and replace" operations on strings, or for input validation.
2449 "#
2450 .unindent()
2451 );
2452 }
2453
2454 struct ReplacementTestParams<'a> {
2455 editor: &'a Entity<Editor>,
2456 search_bar: &'a Entity<BufferSearchBar>,
2457 cx: &'a mut VisualTestContext,
2458 search_text: &'static str,
2459 search_options: Option<SearchOptions>,
2460 replacement_text: &'static str,
2461 replace_all: bool,
2462 expected_text: String,
2463 }
2464
2465 async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2466 options
2467 .search_bar
2468 .update_in(options.cx, |search_bar, window, cx| {
2469 if let Some(options) = options.search_options {
2470 search_bar.set_search_options(options, cx);
2471 }
2472 search_bar.search(options.search_text, options.search_options, window, cx)
2473 })
2474 .await
2475 .unwrap();
2476
2477 options
2478 .search_bar
2479 .update_in(options.cx, |search_bar, window, cx| {
2480 search_bar.replacement_editor.update(cx, |editor, cx| {
2481 editor.set_text(options.replacement_text, window, cx);
2482 });
2483
2484 if options.replace_all {
2485 search_bar.replace_all(&ReplaceAll, window, cx)
2486 } else {
2487 search_bar.replace_next(&ReplaceNext, window, cx)
2488 }
2489 });
2490
2491 assert_eq!(
2492 options
2493 .editor
2494 .update(options.cx, |this, cx| { this.text(cx) }),
2495 options.expected_text
2496 );
2497 }
2498
2499 #[gpui::test]
2500 async fn test_replace_special_characters(cx: &mut TestAppContext) {
2501 let (editor, search_bar, cx) = init_test(cx);
2502
2503 run_replacement_test(ReplacementTestParams {
2504 editor: &editor,
2505 search_bar: &search_bar,
2506 cx,
2507 search_text: "expression",
2508 search_options: None,
2509 replacement_text: r"\n",
2510 replace_all: true,
2511 expected_text: r#"
2512 A regular \n (shortened as regex or regexp;[1] also referred to as
2513 rational \n[2][3]) is a sequence of characters that specifies a search
2514 pattern in text. Usually such patterns are used by string-searching algorithms
2515 for "find" or "find and replace" operations on strings, or for input validation.
2516 "#
2517 .unindent(),
2518 })
2519 .await;
2520
2521 run_replacement_test(ReplacementTestParams {
2522 editor: &editor,
2523 search_bar: &search_bar,
2524 cx,
2525 search_text: "or",
2526 search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2527 replacement_text: r"\\\n\\\\",
2528 replace_all: false,
2529 expected_text: r#"
2530 A regular \n (shortened as regex \
2531 \\ regexp;[1] also referred to as
2532 rational \n[2][3]) is a sequence of characters that specifies a search
2533 pattern in text. Usually such patterns are used by string-searching algorithms
2534 for "find" or "find and replace" operations on strings, or for input validation.
2535 "#
2536 .unindent(),
2537 })
2538 .await;
2539
2540 run_replacement_test(ReplacementTestParams {
2541 editor: &editor,
2542 search_bar: &search_bar,
2543 cx,
2544 search_text: r"(that|used) ",
2545 search_options: Some(SearchOptions::REGEX),
2546 replacement_text: r"$1\n",
2547 replace_all: true,
2548 expected_text: r#"
2549 A regular \n (shortened as regex \
2550 \\ regexp;[1] also referred to as
2551 rational \n[2][3]) is a sequence of characters that
2552 specifies a search
2553 pattern in text. Usually such patterns are used
2554 by string-searching algorithms
2555 for "find" or "find and replace" operations on strings, or for input validation.
2556 "#
2557 .unindent(),
2558 })
2559 .await;
2560 }
2561
2562 #[gpui::test]
2563 async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2564 cx: &mut TestAppContext,
2565 ) {
2566 init_globals(cx);
2567 let buffer = cx.new(|cx| {
2568 Buffer::local(
2569 r#"
2570 aaa bbb aaa ccc
2571 aaa bbb aaa ccc
2572 aaa bbb aaa ccc
2573 aaa bbb aaa ccc
2574 aaa bbb aaa ccc
2575 aaa bbb aaa ccc
2576 "#
2577 .unindent(),
2578 cx,
2579 )
2580 });
2581 let cx = cx.add_empty_window();
2582 let editor =
2583 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2584
2585 let search_bar = cx.new_window_entity(|window, cx| {
2586 let mut search_bar = BufferSearchBar::new(None, window, cx);
2587 search_bar.set_active_pane_item(Some(&editor), window, cx);
2588 search_bar.show(window, cx);
2589 search_bar
2590 });
2591
2592 editor.update_in(cx, |editor, window, cx| {
2593 editor.change_selections(None, window, cx, |s| {
2594 s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2595 })
2596 });
2597
2598 search_bar.update_in(cx, |search_bar, window, cx| {
2599 let deploy = Deploy {
2600 focus: true,
2601 replace_enabled: false,
2602 selection_search_enabled: true,
2603 };
2604 search_bar.deploy(&deploy, window, cx);
2605 });
2606
2607 cx.run_until_parked();
2608
2609 search_bar
2610 .update_in(cx, |search_bar, window, cx| {
2611 search_bar.search("aaa", None, window, cx)
2612 })
2613 .await
2614 .unwrap();
2615
2616 editor.update(cx, |editor, cx| {
2617 assert_eq!(
2618 editor.search_background_highlights(cx),
2619 &[
2620 Point::new(1, 0)..Point::new(1, 3),
2621 Point::new(1, 8)..Point::new(1, 11),
2622 Point::new(2, 0)..Point::new(2, 3),
2623 ]
2624 );
2625 });
2626 }
2627
2628 #[gpui::test]
2629 async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2630 cx: &mut TestAppContext,
2631 ) {
2632 init_globals(cx);
2633 let text = r#"
2634 aaa bbb aaa ccc
2635 aaa bbb aaa ccc
2636 aaa bbb aaa ccc
2637 aaa bbb aaa ccc
2638 aaa bbb aaa ccc
2639 aaa bbb aaa ccc
2640
2641 aaa bbb aaa ccc
2642 aaa bbb aaa ccc
2643 aaa bbb aaa ccc
2644 aaa bbb aaa ccc
2645 aaa bbb aaa ccc
2646 aaa bbb aaa ccc
2647 "#
2648 .unindent();
2649
2650 let cx = cx.add_empty_window();
2651 let editor = cx.new_window_entity(|window, cx| {
2652 let multibuffer = MultiBuffer::build_multi(
2653 [
2654 (
2655 &text,
2656 vec![
2657 Point::new(0, 0)..Point::new(2, 0),
2658 Point::new(4, 0)..Point::new(5, 0),
2659 ],
2660 ),
2661 (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2662 ],
2663 cx,
2664 );
2665 Editor::for_multibuffer(multibuffer, None, false, window, cx)
2666 });
2667
2668 let search_bar = cx.new_window_entity(|window, cx| {
2669 let mut search_bar = BufferSearchBar::new(None, window, cx);
2670 search_bar.set_active_pane_item(Some(&editor), window, cx);
2671 search_bar.show(window, cx);
2672 search_bar
2673 });
2674
2675 editor.update_in(cx, |editor, window, cx| {
2676 editor.change_selections(None, window, cx, |s| {
2677 s.select_ranges(vec![
2678 Point::new(1, 0)..Point::new(1, 4),
2679 Point::new(5, 3)..Point::new(6, 4),
2680 ])
2681 })
2682 });
2683
2684 search_bar.update_in(cx, |search_bar, window, cx| {
2685 let deploy = Deploy {
2686 focus: true,
2687 replace_enabled: false,
2688 selection_search_enabled: true,
2689 };
2690 search_bar.deploy(&deploy, window, cx);
2691 });
2692
2693 cx.run_until_parked();
2694
2695 search_bar
2696 .update_in(cx, |search_bar, window, cx| {
2697 search_bar.search("aaa", None, window, cx)
2698 })
2699 .await
2700 .unwrap();
2701
2702 editor.update(cx, |editor, cx| {
2703 assert_eq!(
2704 editor.search_background_highlights(cx),
2705 &[
2706 Point::new(1, 0)..Point::new(1, 3),
2707 Point::new(5, 8)..Point::new(5, 11),
2708 Point::new(6, 0)..Point::new(6, 3),
2709 ]
2710 );
2711 });
2712 }
2713
2714 #[gpui::test]
2715 async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2716 let (editor, search_bar, cx) = init_test(cx);
2717 // Search using valid regexp
2718 search_bar
2719 .update_in(cx, |search_bar, window, cx| {
2720 search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
2721 search_bar.search("expression", None, window, cx)
2722 })
2723 .await
2724 .unwrap();
2725 editor.update_in(cx, |editor, window, cx| {
2726 assert_eq!(
2727 display_points_of(editor.all_text_background_highlights(window, cx)),
2728 &[
2729 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2730 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2731 ],
2732 );
2733 });
2734
2735 // Now, the expression is invalid
2736 search_bar
2737 .update_in(cx, |search_bar, window, cx| {
2738 search_bar.search("expression (", None, window, cx)
2739 })
2740 .await
2741 .unwrap_err();
2742 editor.update_in(cx, |editor, window, cx| {
2743 assert!(
2744 display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
2745 );
2746 });
2747 }
2748
2749 #[gpui::test]
2750 async fn test_search_options_changes(cx: &mut TestAppContext) {
2751 let (_editor, search_bar, cx) = init_test(cx);
2752 update_search_settings(
2753 SearchSettings {
2754 whole_word: false,
2755 case_sensitive: false,
2756 include_ignored: false,
2757 regex: false,
2758 },
2759 cx,
2760 );
2761
2762 let deploy = Deploy {
2763 focus: true,
2764 replace_enabled: false,
2765 selection_search_enabled: true,
2766 };
2767
2768 search_bar.update_in(cx, |search_bar, window, cx| {
2769 assert_eq!(
2770 search_bar.search_options,
2771 SearchOptions::NONE,
2772 "Should have no search options enabled by default"
2773 );
2774 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2775 assert_eq!(
2776 search_bar.search_options,
2777 SearchOptions::WHOLE_WORD,
2778 "Should enable the option toggled"
2779 );
2780 assert!(
2781 !search_bar.dismissed,
2782 "Search bar should be present and visible"
2783 );
2784 search_bar.deploy(&deploy, window, cx);
2785 assert_eq!(
2786 search_bar.configured_options,
2787 SearchOptions::NONE,
2788 "Should have configured search options matching the settings"
2789 );
2790 assert_eq!(
2791 search_bar.search_options,
2792 SearchOptions::WHOLE_WORD,
2793 "After (re)deploying, the option should still be enabled"
2794 );
2795
2796 search_bar.dismiss(&Dismiss, window, cx);
2797 search_bar.deploy(&deploy, window, cx);
2798 assert_eq!(
2799 search_bar.search_options,
2800 SearchOptions::NONE,
2801 "After hiding and showing the search bar, default options should be used"
2802 );
2803
2804 search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
2805 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2806 assert_eq!(
2807 search_bar.search_options,
2808 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2809 "Should enable the options toggled"
2810 );
2811 assert!(
2812 !search_bar.dismissed,
2813 "Search bar should be present and visible"
2814 );
2815 });
2816
2817 update_search_settings(
2818 SearchSettings {
2819 whole_word: false,
2820 case_sensitive: true,
2821 include_ignored: false,
2822 regex: false,
2823 },
2824 cx,
2825 );
2826 search_bar.update_in(cx, |search_bar, window, cx| {
2827 assert_eq!(
2828 search_bar.search_options,
2829 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2830 "Should have no search options enabled by default"
2831 );
2832
2833 search_bar.deploy(&deploy, window, cx);
2834 assert_eq!(
2835 search_bar.configured_options,
2836 SearchOptions::CASE_SENSITIVE,
2837 "Should have configured search options matching the settings"
2838 );
2839 assert_eq!(
2840 search_bar.search_options,
2841 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2842 "Toggling a non-dismissed search bar with custom options should not change the default options"
2843 );
2844 search_bar.dismiss(&Dismiss, window, cx);
2845 search_bar.deploy(&deploy, window, cx);
2846 assert_eq!(
2847 search_bar.search_options,
2848 SearchOptions::CASE_SENSITIVE,
2849 "After hiding and showing the search bar, default options should be used"
2850 );
2851 });
2852 }
2853
2854 fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2855 cx.update(|cx| {
2856 SettingsStore::update_global(cx, |store, cx| {
2857 store.update_user_settings::<EditorSettings>(cx, |settings| {
2858 settings.search = Some(search_settings);
2859 });
2860 });
2861 });
2862 }
2863}