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 self.adjust_query_regex_language(cx);
824 handle.search_bar_visibility_changed(true, window, cx);
825 cx.notify();
826 cx.emit(Event::UpdateLocation);
827 cx.emit(ToolbarItemEvent::ChangeLocation(
828 ToolbarItemLocation::Secondary,
829 ));
830 true
831 }
832
833 fn supported_options(&self, cx: &mut Context<Self>) -> workspace::searchable::SearchOptions {
834 self.active_searchable_item
835 .as_ref()
836 .map(|item| item.supported_options(cx))
837 .unwrap_or_default()
838 }
839
840 pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context<Self>) {
841 let search = self
842 .query_suggestion(window, cx)
843 .map(|suggestion| self.search(&suggestion, Some(self.default_options), window, cx));
844
845 if let Some(search) = search {
846 cx.spawn_in(window, |this, mut cx| async move {
847 search.await?;
848 this.update_in(&mut cx, |this, window, cx| {
849 this.activate_current_match(window, cx)
850 })
851 })
852 .detach_and_log_err(cx);
853 }
854 }
855
856 pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
857 if let Some(match_ix) = self.active_match_index {
858 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
859 if let Some(matches) = self
860 .searchable_items_with_matches
861 .get(&active_searchable_item.downgrade())
862 {
863 active_searchable_item.activate_match(match_ix, matches, window, cx)
864 }
865 }
866 }
867 }
868
869 pub fn select_query(&mut self, window: &mut Window, cx: &mut Context<Self>) {
870 self.query_editor.update(cx, |query_editor, cx| {
871 query_editor.select_all(&Default::default(), window, cx);
872 });
873 }
874
875 pub fn query(&self, cx: &App) -> String {
876 self.query_editor.read(cx).text(cx)
877 }
878
879 pub fn replacement(&self, cx: &mut App) -> String {
880 self.replacement_editor.read(cx).text(cx)
881 }
882
883 pub fn query_suggestion(
884 &mut self,
885 window: &mut Window,
886 cx: &mut Context<Self>,
887 ) -> Option<String> {
888 self.active_searchable_item
889 .as_ref()
890 .map(|searchable_item| searchable_item.query_suggestion(window, cx))
891 .filter(|suggestion| !suggestion.is_empty())
892 }
893
894 pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut Context<Self>) {
895 if replacement.is_none() {
896 self.replace_enabled = false;
897 return;
898 }
899 self.replace_enabled = true;
900 self.replacement_editor
901 .update(cx, |replacement_editor, cx| {
902 replacement_editor
903 .buffer()
904 .update(cx, |replacement_buffer, cx| {
905 let len = replacement_buffer.len(cx);
906 replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
907 });
908 });
909 }
910
911 pub fn search(
912 &mut self,
913 query: &str,
914 options: Option<SearchOptions>,
915 window: &mut Window,
916 cx: &mut Context<Self>,
917 ) -> oneshot::Receiver<()> {
918 let options = options.unwrap_or(self.default_options);
919 let updated = query != self.query(cx) || self.search_options != options;
920 if updated {
921 self.query_editor.update(cx, |query_editor, cx| {
922 query_editor.buffer().update(cx, |query_buffer, cx| {
923 let len = query_buffer.len(cx);
924 query_buffer.edit([(0..len, query)], None, cx);
925 });
926 });
927 self.search_options = options;
928 self.clear_matches(window, cx);
929 cx.notify();
930 }
931 self.update_matches(!updated, window, cx)
932 }
933
934 fn render_search_option_button(
935 &self,
936 option: SearchOptions,
937 focus_handle: FocusHandle,
938 action: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
939 ) -> impl IntoElement {
940 let is_active = self.search_options.contains(option);
941 option.as_button(is_active, focus_handle, action)
942 }
943
944 pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
945 if let Some(active_editor) = self.active_searchable_item.as_ref() {
946 let handle = active_editor.item_focus_handle(cx);
947 window.focus(&handle);
948 }
949 }
950
951 pub fn toggle_search_option(
952 &mut self,
953 search_option: SearchOptions,
954 window: &mut Window,
955 cx: &mut Context<Self>,
956 ) {
957 self.search_options.toggle(search_option);
958 self.default_options = self.search_options;
959 drop(self.update_matches(false, window, cx));
960 self.adjust_query_regex_language(cx);
961 cx.notify();
962 }
963
964 pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
965 self.search_options.contains(search_option)
966 }
967
968 pub fn enable_search_option(
969 &mut self,
970 search_option: SearchOptions,
971 window: &mut Window,
972 cx: &mut Context<Self>,
973 ) {
974 if !self.search_options.contains(search_option) {
975 self.toggle_search_option(search_option, window, cx)
976 }
977 }
978
979 pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context<Self>) {
980 self.search_options = search_options;
981 cx.notify();
982 }
983
984 fn select_next_match(
985 &mut self,
986 _: &SelectNextMatch,
987 window: &mut Window,
988 cx: &mut Context<Self>,
989 ) {
990 self.select_match(Direction::Next, 1, window, cx);
991 }
992
993 fn select_prev_match(
994 &mut self,
995 _: &SelectPrevMatch,
996 window: &mut Window,
997 cx: &mut Context<Self>,
998 ) {
999 self.select_match(Direction::Prev, 1, window, cx);
1000 }
1001
1002 fn select_all_matches(
1003 &mut self,
1004 _: &SelectAllMatches,
1005 window: &mut Window,
1006 cx: &mut Context<Self>,
1007 ) {
1008 if !self.dismissed && self.active_match_index.is_some() {
1009 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1010 if let Some(matches) = self
1011 .searchable_items_with_matches
1012 .get(&searchable_item.downgrade())
1013 {
1014 searchable_item.select_matches(matches, window, cx);
1015 self.focus_editor(&FocusEditor, window, cx);
1016 }
1017 }
1018 }
1019 }
1020
1021 pub fn select_match(
1022 &mut self,
1023 direction: Direction,
1024 count: usize,
1025 window: &mut Window,
1026 cx: &mut Context<Self>,
1027 ) {
1028 if let Some(index) = self.active_match_index {
1029 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1030 if let Some(matches) = self
1031 .searchable_items_with_matches
1032 .get(&searchable_item.downgrade())
1033 .filter(|matches| !matches.is_empty())
1034 {
1035 // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
1036 if !EditorSettings::get_global(cx).search_wrap
1037 && ((direction == Direction::Next && index + count >= matches.len())
1038 || (direction == Direction::Prev && index < count))
1039 {
1040 crate::show_no_more_matches(window, cx);
1041 return;
1042 }
1043 let new_match_index = searchable_item
1044 .match_index_for_direction(matches, index, direction, count, window, cx);
1045
1046 searchable_item.update_matches(matches, window, cx);
1047 searchable_item.activate_match(new_match_index, matches, window, cx);
1048 }
1049 }
1050 }
1051 }
1052
1053 pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1054 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1055 if let Some(matches) = self
1056 .searchable_items_with_matches
1057 .get(&searchable_item.downgrade())
1058 {
1059 if matches.is_empty() {
1060 return;
1061 }
1062 let new_match_index = matches.len() - 1;
1063 searchable_item.update_matches(matches, window, cx);
1064 searchable_item.activate_match(new_match_index, matches, window, cx);
1065 }
1066 }
1067 }
1068
1069 fn on_query_editor_event(
1070 &mut self,
1071 editor: &Entity<Editor>,
1072 event: &editor::EditorEvent,
1073 window: &mut Window,
1074 cx: &mut Context<Self>,
1075 ) {
1076 match event {
1077 editor::EditorEvent::Focused => self.query_editor_focused = true,
1078 editor::EditorEvent::Blurred => self.query_editor_focused = false,
1079 editor::EditorEvent::Edited { .. } => {
1080 self.smartcase(window, cx);
1081 self.clear_matches(window, cx);
1082 let search = self.update_matches(false, window, cx);
1083
1084 let width = editor.update(cx, |editor, cx| {
1085 let text_layout_details = editor.text_layout_details(window);
1086 let snapshot = editor.snapshot(window, cx).display_snapshot;
1087
1088 snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
1089 - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
1090 });
1091 self.editor_needed_width = width;
1092 cx.notify();
1093
1094 cx.spawn_in(window, |this, mut cx| async move {
1095 search.await?;
1096 this.update_in(&mut cx, |this, window, cx| {
1097 this.activate_current_match(window, cx)
1098 })
1099 })
1100 .detach_and_log_err(cx);
1101 }
1102 _ => {}
1103 }
1104 }
1105
1106 fn on_replacement_editor_event(
1107 &mut self,
1108 _: Entity<Editor>,
1109 event: &editor::EditorEvent,
1110 _: &mut Context<Self>,
1111 ) {
1112 match event {
1113 editor::EditorEvent::Focused => self.replacement_editor_focused = true,
1114 editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
1115 _ => {}
1116 }
1117 }
1118
1119 fn on_active_searchable_item_event(
1120 &mut self,
1121 event: &SearchEvent,
1122 window: &mut Window,
1123 cx: &mut Context<Self>,
1124 ) {
1125 match event {
1126 SearchEvent::MatchesInvalidated => {
1127 drop(self.update_matches(false, window, cx));
1128 }
1129 SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx),
1130 }
1131 }
1132
1133 fn toggle_case_sensitive(
1134 &mut self,
1135 _: &ToggleCaseSensitive,
1136 window: &mut Window,
1137 cx: &mut Context<Self>,
1138 ) {
1139 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx)
1140 }
1141
1142 fn toggle_whole_word(
1143 &mut self,
1144 _: &ToggleWholeWord,
1145 window: &mut Window,
1146 cx: &mut Context<Self>,
1147 ) {
1148 self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1149 }
1150
1151 fn toggle_selection(
1152 &mut self,
1153 _: &ToggleSelection,
1154 window: &mut Window,
1155 cx: &mut Context<Self>,
1156 ) {
1157 if let Some(active_item) = self.active_searchable_item.as_mut() {
1158 self.selection_search_enabled = !self.selection_search_enabled;
1159 active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx);
1160 drop(self.update_matches(false, window, cx));
1161 cx.notify();
1162 }
1163 }
1164
1165 fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context<Self>) {
1166 self.toggle_search_option(SearchOptions::REGEX, window, cx)
1167 }
1168
1169 fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) {
1170 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1171 self.active_match_index = None;
1172 self.searchable_items_with_matches
1173 .remove(&active_searchable_item.downgrade());
1174 active_searchable_item.clear_matches(window, cx);
1175 }
1176 }
1177
1178 pub fn has_active_match(&self) -> bool {
1179 self.active_match_index.is_some()
1180 }
1181
1182 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1183 let mut active_item_matches = None;
1184 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
1185 if let Some(searchable_item) =
1186 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
1187 {
1188 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
1189 active_item_matches = Some((searchable_item.downgrade(), matches));
1190 } else {
1191 searchable_item.clear_matches(window, cx);
1192 }
1193 }
1194 }
1195
1196 self.searchable_items_with_matches
1197 .extend(active_item_matches);
1198 }
1199
1200 fn update_matches(
1201 &mut self,
1202 reuse_existing_query: bool,
1203 window: &mut Window,
1204 cx: &mut Context<Self>,
1205 ) -> oneshot::Receiver<()> {
1206 let (done_tx, done_rx) = oneshot::channel();
1207 let query = self.query(cx);
1208 self.pending_search.take();
1209
1210 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1211 self.query_contains_error = false;
1212 if query.is_empty() {
1213 self.clear_active_searchable_item_matches(window, cx);
1214 let _ = done_tx.send(());
1215 cx.notify();
1216 } else {
1217 let query: Arc<_> = if let Some(search) =
1218 self.active_search.take().filter(|_| reuse_existing_query)
1219 {
1220 search
1221 } else {
1222 if self.search_options.contains(SearchOptions::REGEX) {
1223 match SearchQuery::regex(
1224 query,
1225 self.search_options.contains(SearchOptions::WHOLE_WORD),
1226 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1227 false,
1228 Default::default(),
1229 Default::default(),
1230 None,
1231 ) {
1232 Ok(query) => query.with_replacement(self.replacement(cx)),
1233 Err(_) => {
1234 self.query_contains_error = true;
1235 self.clear_active_searchable_item_matches(window, cx);
1236 cx.notify();
1237 return done_rx;
1238 }
1239 }
1240 } else {
1241 match SearchQuery::text(
1242 query,
1243 self.search_options.contains(SearchOptions::WHOLE_WORD),
1244 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1245 false,
1246 Default::default(),
1247 Default::default(),
1248 None,
1249 ) {
1250 Ok(query) => query.with_replacement(self.replacement(cx)),
1251 Err(_) => {
1252 self.query_contains_error = true;
1253 self.clear_active_searchable_item_matches(window, cx);
1254 cx.notify();
1255 return done_rx;
1256 }
1257 }
1258 }
1259 .into()
1260 };
1261
1262 self.active_search = Some(query.clone());
1263 let query_text = query.as_str().to_string();
1264
1265 let matches = active_searchable_item.find_matches(query, window, cx);
1266
1267 let active_searchable_item = active_searchable_item.downgrade();
1268 self.pending_search = Some(cx.spawn_in(window, |this, mut cx| async move {
1269 let matches = matches.await;
1270
1271 this.update_in(&mut cx, |this, window, cx| {
1272 if let Some(active_searchable_item) =
1273 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1274 {
1275 this.searchable_items_with_matches
1276 .insert(active_searchable_item.downgrade(), matches);
1277
1278 this.update_match_index(window, cx);
1279 this.search_history
1280 .add(&mut this.search_history_cursor, query_text);
1281 if !this.dismissed {
1282 let matches = this
1283 .searchable_items_with_matches
1284 .get(&active_searchable_item.downgrade())
1285 .unwrap();
1286 if matches.is_empty() {
1287 active_searchable_item.clear_matches(window, cx);
1288 } else {
1289 active_searchable_item.update_matches(matches, window, cx);
1290 }
1291 let _ = done_tx.send(());
1292 }
1293 cx.notify();
1294 }
1295 })
1296 .log_err();
1297 }));
1298 }
1299 }
1300 done_rx
1301 }
1302
1303 pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1304 let new_index = self
1305 .active_searchable_item
1306 .as_ref()
1307 .and_then(|searchable_item| {
1308 let matches = self
1309 .searchable_items_with_matches
1310 .get(&searchable_item.downgrade())?;
1311 searchable_item.active_match_index(matches, window, cx)
1312 });
1313 if new_index != self.active_match_index {
1314 self.active_match_index = new_index;
1315 cx.notify();
1316 }
1317 }
1318
1319 fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1320 // Search -> Replace -> Editor
1321 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1322 self.replacement_editor.focus_handle(cx)
1323 } else if let Some(item) = self.active_searchable_item.as_ref() {
1324 item.item_focus_handle(cx)
1325 } else {
1326 return;
1327 };
1328 self.focus(&focus_handle, window, cx);
1329 cx.stop_propagation();
1330 }
1331
1332 fn tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
1333 // Search -> Replace -> Search
1334 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1335 self.replacement_editor.focus_handle(cx)
1336 } else if self.replacement_editor_focused {
1337 self.query_editor.focus_handle(cx)
1338 } else {
1339 return;
1340 };
1341 self.focus(&focus_handle, window, cx);
1342 cx.stop_propagation();
1343 }
1344
1345 fn next_history_query(
1346 &mut self,
1347 _: &NextHistoryQuery,
1348 window: &mut Window,
1349 cx: &mut Context<Self>,
1350 ) {
1351 if let Some(new_query) = self
1352 .search_history
1353 .next(&mut self.search_history_cursor)
1354 .map(str::to_string)
1355 {
1356 drop(self.search(&new_query, Some(self.search_options), window, cx));
1357 } else {
1358 self.search_history_cursor.reset();
1359 drop(self.search("", Some(self.search_options), window, cx));
1360 }
1361 }
1362
1363 fn previous_history_query(
1364 &mut self,
1365 _: &PreviousHistoryQuery,
1366 window: &mut Window,
1367 cx: &mut Context<Self>,
1368 ) {
1369 if self.query(cx).is_empty() {
1370 if let Some(new_query) = self
1371 .search_history
1372 .current(&mut self.search_history_cursor)
1373 .map(str::to_string)
1374 {
1375 drop(self.search(&new_query, Some(self.search_options), window, cx));
1376 return;
1377 }
1378 }
1379
1380 if let Some(new_query) = self
1381 .search_history
1382 .previous(&mut self.search_history_cursor)
1383 .map(str::to_string)
1384 {
1385 drop(self.search(&new_query, Some(self.search_options), window, cx));
1386 }
1387 }
1388
1389 fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut Context<Self>) {
1390 cx.on_next_frame(window, |_, window, _| {
1391 window.invalidate_character_coordinates();
1392 });
1393 window.focus(handle);
1394 }
1395
1396 fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1397 if self.active_searchable_item.is_some() {
1398 self.replace_enabled = !self.replace_enabled;
1399 let handle = if self.replace_enabled {
1400 self.replacement_editor.focus_handle(cx)
1401 } else {
1402 self.query_editor.focus_handle(cx)
1403 };
1404 self.focus(&handle, window, cx);
1405 cx.notify();
1406 }
1407 }
1408
1409 fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1410 let mut should_propagate = true;
1411 if !self.dismissed && self.active_search.is_some() {
1412 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1413 if let Some(query) = self.active_search.as_ref() {
1414 if let Some(matches) = self
1415 .searchable_items_with_matches
1416 .get(&searchable_item.downgrade())
1417 {
1418 if let Some(active_index) = self.active_match_index {
1419 let query = query
1420 .as_ref()
1421 .clone()
1422 .with_replacement(self.replacement(cx));
1423 searchable_item.replace(matches.at(active_index), &query, window, cx);
1424 self.select_next_match(&SelectNextMatch, window, cx);
1425 }
1426 should_propagate = false;
1427 self.focus_editor(&FocusEditor, window, cx);
1428 }
1429 }
1430 }
1431 }
1432 if !should_propagate {
1433 cx.stop_propagation();
1434 }
1435 }
1436
1437 pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1438 if !self.dismissed && self.active_search.is_some() {
1439 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1440 if let Some(query) = self.active_search.as_ref() {
1441 if let Some(matches) = self
1442 .searchable_items_with_matches
1443 .get(&searchable_item.downgrade())
1444 {
1445 let query = query
1446 .as_ref()
1447 .clone()
1448 .with_replacement(self.replacement(cx));
1449 searchable_item.replace_all(&mut matches.iter(), &query, window, cx);
1450 }
1451 }
1452 }
1453 }
1454 }
1455
1456 pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1457 self.update_match_index(window, cx);
1458 self.active_match_index.is_some()
1459 }
1460
1461 pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1462 EditorSettings::get_global(cx).use_smartcase_search
1463 }
1464
1465 pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1466 str.chars().any(|c| c.is_uppercase())
1467 }
1468
1469 fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1470 if self.should_use_smartcase_search(cx) {
1471 let query = self.query(cx);
1472 if !query.is_empty() {
1473 let is_case = self.is_contains_uppercase(&query);
1474 if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1475 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1476 }
1477 }
1478 }
1479 }
1480
1481 fn adjust_query_regex_language(&self, cx: &mut App) {
1482 let enable = self.search_options.contains(SearchOptions::REGEX);
1483 let query_buffer = self
1484 .query_editor
1485 .read(cx)
1486 .buffer()
1487 .read(cx)
1488 .as_singleton()
1489 .expect("query editor should be backed by a singleton buffer");
1490 if enable {
1491 if let Some(regex_language) = self.regex_language.clone() {
1492 query_buffer.update(cx, |query_buffer, cx| {
1493 query_buffer.set_language(Some(regex_language), cx);
1494 })
1495 }
1496 } else {
1497 query_buffer.update(cx, |query_buffer, cx| {
1498 query_buffer.set_language(None, cx);
1499 })
1500 }
1501 }
1502}
1503
1504#[cfg(test)]
1505mod tests {
1506 use std::ops::Range;
1507
1508 use super::*;
1509 use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer, SearchSettings};
1510 use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1511 use language::{Buffer, Point};
1512 use project::Project;
1513 use settings::SettingsStore;
1514 use smol::stream::StreamExt as _;
1515 use unindent::Unindent as _;
1516
1517 fn init_globals(cx: &mut TestAppContext) {
1518 cx.update(|cx| {
1519 let store = settings::SettingsStore::test(cx);
1520 cx.set_global(store);
1521 workspace::init_settings(cx);
1522 editor::init(cx);
1523
1524 language::init(cx);
1525 Project::init_settings(cx);
1526 theme::init(theme::LoadThemes::JustBase, cx);
1527 crate::init(cx);
1528 });
1529 }
1530
1531 fn init_test(
1532 cx: &mut TestAppContext,
1533 ) -> (
1534 Entity<Editor>,
1535 Entity<BufferSearchBar>,
1536 &mut VisualTestContext,
1537 ) {
1538 init_globals(cx);
1539 let buffer = cx.new(|cx| {
1540 Buffer::local(
1541 r#"
1542 A regular expression (shortened as regex or regexp;[1] also referred to as
1543 rational expression[2][3]) is a sequence of characters that specifies a search
1544 pattern in text. Usually such patterns are used by string-searching algorithms
1545 for "find" or "find and replace" operations on strings, or for input validation.
1546 "#
1547 .unindent(),
1548 cx,
1549 )
1550 });
1551 let cx = cx.add_empty_window();
1552 let editor =
1553 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
1554
1555 let search_bar = cx.new_window_entity(|window, cx| {
1556 let mut search_bar = BufferSearchBar::new(None, window, cx);
1557 search_bar.set_active_pane_item(Some(&editor), window, cx);
1558 search_bar.show(window, cx);
1559 search_bar
1560 });
1561
1562 (editor, search_bar, cx)
1563 }
1564
1565 #[gpui::test]
1566 async fn test_search_simple(cx: &mut TestAppContext) {
1567 let (editor, search_bar, cx) = init_test(cx);
1568 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1569 background_highlights
1570 .into_iter()
1571 .map(|(range, _)| range)
1572 .collect::<Vec<_>>()
1573 };
1574 // Search for a string that appears with different casing.
1575 // By default, search is case-insensitive.
1576 search_bar
1577 .update_in(cx, |search_bar, window, cx| {
1578 search_bar.search("us", None, window, cx)
1579 })
1580 .await
1581 .unwrap();
1582 editor.update_in(cx, |editor, window, cx| {
1583 assert_eq!(
1584 display_points_of(editor.all_text_background_highlights(window, cx)),
1585 &[
1586 DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1587 DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1588 ]
1589 );
1590 });
1591
1592 // Switch to a case sensitive search.
1593 search_bar.update_in(cx, |search_bar, window, cx| {
1594 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1595 });
1596 let mut editor_notifications = cx.notifications(&editor);
1597 editor_notifications.next().await;
1598 editor.update_in(cx, |editor, window, cx| {
1599 assert_eq!(
1600 display_points_of(editor.all_text_background_highlights(window, cx)),
1601 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1602 );
1603 });
1604
1605 // Search for a string that appears both as a whole word and
1606 // within other words. By default, all results are found.
1607 search_bar
1608 .update_in(cx, |search_bar, window, cx| {
1609 search_bar.search("or", None, window, cx)
1610 })
1611 .await
1612 .unwrap();
1613 editor.update_in(cx, |editor, window, cx| {
1614 assert_eq!(
1615 display_points_of(editor.all_text_background_highlights(window, cx)),
1616 &[
1617 DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1618 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1619 DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1620 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1621 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1622 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1623 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1624 ]
1625 );
1626 });
1627
1628 // Switch to a whole word search.
1629 search_bar.update_in(cx, |search_bar, window, cx| {
1630 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1631 });
1632 let mut editor_notifications = cx.notifications(&editor);
1633 editor_notifications.next().await;
1634 editor.update_in(cx, |editor, window, cx| {
1635 assert_eq!(
1636 display_points_of(editor.all_text_background_highlights(window, cx)),
1637 &[
1638 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1639 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1640 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1641 ]
1642 );
1643 });
1644
1645 editor.update_in(cx, |editor, window, cx| {
1646 editor.change_selections(None, window, cx, |s| {
1647 s.select_display_ranges([
1648 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1649 ])
1650 });
1651 });
1652 search_bar.update_in(cx, |search_bar, window, cx| {
1653 assert_eq!(search_bar.active_match_index, Some(0));
1654 search_bar.select_next_match(&SelectNextMatch, window, cx);
1655 assert_eq!(
1656 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1657 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1658 );
1659 });
1660 search_bar.update(cx, |search_bar, _| {
1661 assert_eq!(search_bar.active_match_index, Some(0));
1662 });
1663
1664 search_bar.update_in(cx, |search_bar, window, cx| {
1665 search_bar.select_next_match(&SelectNextMatch, window, cx);
1666 assert_eq!(
1667 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1668 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1669 );
1670 });
1671 search_bar.update(cx, |search_bar, _| {
1672 assert_eq!(search_bar.active_match_index, Some(1));
1673 });
1674
1675 search_bar.update_in(cx, |search_bar, window, cx| {
1676 search_bar.select_next_match(&SelectNextMatch, window, cx);
1677 assert_eq!(
1678 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1679 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1680 );
1681 });
1682 search_bar.update(cx, |search_bar, _| {
1683 assert_eq!(search_bar.active_match_index, Some(2));
1684 });
1685
1686 search_bar.update_in(cx, |search_bar, window, cx| {
1687 search_bar.select_next_match(&SelectNextMatch, window, cx);
1688 assert_eq!(
1689 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1690 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1691 );
1692 });
1693 search_bar.update(cx, |search_bar, _| {
1694 assert_eq!(search_bar.active_match_index, Some(0));
1695 });
1696
1697 search_bar.update_in(cx, |search_bar, window, cx| {
1698 search_bar.select_prev_match(&SelectPrevMatch, window, cx);
1699 assert_eq!(
1700 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1701 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1702 );
1703 });
1704 search_bar.update(cx, |search_bar, _| {
1705 assert_eq!(search_bar.active_match_index, Some(2));
1706 });
1707
1708 search_bar.update_in(cx, |search_bar, window, cx| {
1709 search_bar.select_prev_match(&SelectPrevMatch, window, cx);
1710 assert_eq!(
1711 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1712 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1713 );
1714 });
1715 search_bar.update(cx, |search_bar, _| {
1716 assert_eq!(search_bar.active_match_index, Some(1));
1717 });
1718
1719 search_bar.update_in(cx, |search_bar, window, cx| {
1720 search_bar.select_prev_match(&SelectPrevMatch, window, cx);
1721 assert_eq!(
1722 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1723 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1724 );
1725 });
1726 search_bar.update(cx, |search_bar, _| {
1727 assert_eq!(search_bar.active_match_index, Some(0));
1728 });
1729
1730 // Park the cursor in between matches and ensure that going to the previous match selects
1731 // the closest match to the left.
1732 editor.update_in(cx, |editor, window, cx| {
1733 editor.change_selections(None, window, cx, |s| {
1734 s.select_display_ranges([
1735 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1736 ])
1737 });
1738 });
1739 search_bar.update_in(cx, |search_bar, window, cx| {
1740 assert_eq!(search_bar.active_match_index, Some(1));
1741 search_bar.select_prev_match(&SelectPrevMatch, window, cx);
1742 assert_eq!(
1743 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1744 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1745 );
1746 });
1747 search_bar.update(cx, |search_bar, _| {
1748 assert_eq!(search_bar.active_match_index, Some(0));
1749 });
1750
1751 // Park the cursor in between matches and ensure that going to the next match selects the
1752 // closest match to the right.
1753 editor.update_in(cx, |editor, window, cx| {
1754 editor.change_selections(None, window, cx, |s| {
1755 s.select_display_ranges([
1756 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1757 ])
1758 });
1759 });
1760 search_bar.update_in(cx, |search_bar, window, cx| {
1761 assert_eq!(search_bar.active_match_index, Some(1));
1762 search_bar.select_next_match(&SelectNextMatch, window, cx);
1763 assert_eq!(
1764 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1765 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1766 );
1767 });
1768 search_bar.update(cx, |search_bar, _| {
1769 assert_eq!(search_bar.active_match_index, Some(1));
1770 });
1771
1772 // Park the cursor after the last match and ensure that going to the previous match selects
1773 // the last match.
1774 editor.update_in(cx, |editor, window, cx| {
1775 editor.change_selections(None, window, cx, |s| {
1776 s.select_display_ranges([
1777 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1778 ])
1779 });
1780 });
1781 search_bar.update_in(cx, |search_bar, window, cx| {
1782 assert_eq!(search_bar.active_match_index, Some(2));
1783 search_bar.select_prev_match(&SelectPrevMatch, window, cx);
1784 assert_eq!(
1785 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1786 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1787 );
1788 });
1789 search_bar.update(cx, |search_bar, _| {
1790 assert_eq!(search_bar.active_match_index, Some(2));
1791 });
1792
1793 // Park the cursor after the last match and ensure that going to the next match selects the
1794 // first match.
1795 editor.update_in(cx, |editor, window, cx| {
1796 editor.change_selections(None, window, cx, |s| {
1797 s.select_display_ranges([
1798 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1799 ])
1800 });
1801 });
1802 search_bar.update_in(cx, |search_bar, window, cx| {
1803 assert_eq!(search_bar.active_match_index, Some(2));
1804 search_bar.select_next_match(&SelectNextMatch, window, cx);
1805 assert_eq!(
1806 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1807 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1808 );
1809 });
1810 search_bar.update(cx, |search_bar, _| {
1811 assert_eq!(search_bar.active_match_index, Some(0));
1812 });
1813
1814 // Park the cursor before the first match and ensure that going to the previous match
1815 // selects the last match.
1816 editor.update_in(cx, |editor, window, cx| {
1817 editor.change_selections(None, window, cx, |s| {
1818 s.select_display_ranges([
1819 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1820 ])
1821 });
1822 });
1823 search_bar.update_in(cx, |search_bar, window, cx| {
1824 assert_eq!(search_bar.active_match_index, Some(0));
1825 search_bar.select_prev_match(&SelectPrevMatch, window, cx);
1826 assert_eq!(
1827 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1828 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1829 );
1830 });
1831 search_bar.update(cx, |search_bar, _| {
1832 assert_eq!(search_bar.active_match_index, Some(2));
1833 });
1834 }
1835
1836 fn display_points_of(
1837 background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1838 ) -> Vec<Range<DisplayPoint>> {
1839 background_highlights
1840 .into_iter()
1841 .map(|(range, _)| range)
1842 .collect::<Vec<_>>()
1843 }
1844
1845 #[gpui::test]
1846 async fn test_search_option_handling(cx: &mut TestAppContext) {
1847 let (editor, search_bar, cx) = init_test(cx);
1848
1849 // show with options should make current search case sensitive
1850 search_bar
1851 .update_in(cx, |search_bar, window, cx| {
1852 search_bar.show(window, cx);
1853 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1854 })
1855 .await
1856 .unwrap();
1857 editor.update_in(cx, |editor, window, cx| {
1858 assert_eq!(
1859 display_points_of(editor.all_text_background_highlights(window, cx)),
1860 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1861 );
1862 });
1863
1864 // search_suggested should restore default options
1865 search_bar.update_in(cx, |search_bar, window, cx| {
1866 search_bar.search_suggested(window, cx);
1867 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1868 });
1869
1870 // toggling a search option should update the defaults
1871 search_bar
1872 .update_in(cx, |search_bar, window, cx| {
1873 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1874 })
1875 .await
1876 .unwrap();
1877 search_bar.update_in(cx, |search_bar, window, cx| {
1878 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1879 });
1880 let mut editor_notifications = cx.notifications(&editor);
1881 editor_notifications.next().await;
1882 editor.update_in(cx, |editor, window, cx| {
1883 assert_eq!(
1884 display_points_of(editor.all_text_background_highlights(window, cx)),
1885 &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1886 );
1887 });
1888
1889 // defaults should still include whole word
1890 search_bar.update_in(cx, |search_bar, window, cx| {
1891 search_bar.search_suggested(window, cx);
1892 assert_eq!(
1893 search_bar.search_options,
1894 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1895 )
1896 });
1897 }
1898
1899 #[gpui::test]
1900 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1901 init_globals(cx);
1902 let buffer_text = r#"
1903 A regular expression (shortened as regex or regexp;[1] also referred to as
1904 rational expression[2][3]) is a sequence of characters that specifies a search
1905 pattern in text. Usually such patterns are used by string-searching algorithms
1906 for "find" or "find and replace" operations on strings, or for input validation.
1907 "#
1908 .unindent();
1909 let expected_query_matches_count = buffer_text
1910 .chars()
1911 .filter(|c| c.to_ascii_lowercase() == 'a')
1912 .count();
1913 assert!(
1914 expected_query_matches_count > 1,
1915 "Should pick a query with multiple results"
1916 );
1917 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
1918 let window = cx.add_window(|_, _| gpui::Empty);
1919
1920 let editor = window.build_entity(cx, |window, cx| {
1921 Editor::for_buffer(buffer.clone(), None, window, cx)
1922 });
1923
1924 let search_bar = window.build_entity(cx, |window, cx| {
1925 let mut search_bar = BufferSearchBar::new(None, window, cx);
1926 search_bar.set_active_pane_item(Some(&editor), window, cx);
1927 search_bar.show(window, cx);
1928 search_bar
1929 });
1930
1931 window
1932 .update(cx, |_, window, cx| {
1933 search_bar.update(cx, |search_bar, cx| {
1934 search_bar.search("a", None, window, cx)
1935 })
1936 })
1937 .unwrap()
1938 .await
1939 .unwrap();
1940 let initial_selections = window
1941 .update(cx, |_, window, cx| {
1942 search_bar.update(cx, |search_bar, cx| {
1943 let handle = search_bar.query_editor.focus_handle(cx);
1944 window.focus(&handle);
1945 search_bar.activate_current_match(window, cx);
1946 });
1947 assert!(
1948 !editor.read(cx).is_focused(window),
1949 "Initially, the editor should not be focused"
1950 );
1951 let initial_selections = editor.update(cx, |editor, cx| {
1952 let initial_selections = editor.selections.display_ranges(cx);
1953 assert_eq!(
1954 initial_selections.len(), 1,
1955 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1956 );
1957 initial_selections
1958 });
1959 search_bar.update(cx, |search_bar, cx| {
1960 assert_eq!(search_bar.active_match_index, Some(0));
1961 let handle = search_bar.query_editor.focus_handle(cx);
1962 window.focus(&handle);
1963 search_bar.select_all_matches(&SelectAllMatches, window, cx);
1964 });
1965 assert!(
1966 editor.read(cx).is_focused(window),
1967 "Should focus editor after successful SelectAllMatches"
1968 );
1969 search_bar.update(cx, |search_bar, cx| {
1970 let all_selections =
1971 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1972 assert_eq!(
1973 all_selections.len(),
1974 expected_query_matches_count,
1975 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1976 );
1977 assert_eq!(
1978 search_bar.active_match_index,
1979 Some(0),
1980 "Match index should not change after selecting all matches"
1981 );
1982 });
1983
1984 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
1985 initial_selections
1986 }).unwrap();
1987
1988 window
1989 .update(cx, |_, window, cx| {
1990 assert!(
1991 editor.read(cx).is_focused(window),
1992 "Should still have editor focused after SelectNextMatch"
1993 );
1994 search_bar.update(cx, |search_bar, cx| {
1995 let all_selections =
1996 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1997 assert_eq!(
1998 all_selections.len(),
1999 1,
2000 "On next match, should deselect items and select the next match"
2001 );
2002 assert_ne!(
2003 all_selections, initial_selections,
2004 "Next match should be different from the first selection"
2005 );
2006 assert_eq!(
2007 search_bar.active_match_index,
2008 Some(1),
2009 "Match index should be updated to the next one"
2010 );
2011 let handle = search_bar.query_editor.focus_handle(cx);
2012 window.focus(&handle);
2013 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2014 });
2015 })
2016 .unwrap();
2017 window
2018 .update(cx, |_, window, cx| {
2019 assert!(
2020 editor.read(cx).is_focused(window),
2021 "Should focus editor after successful SelectAllMatches"
2022 );
2023 search_bar.update(cx, |search_bar, cx| {
2024 let all_selections =
2025 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2026 assert_eq!(
2027 all_selections.len(),
2028 expected_query_matches_count,
2029 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2030 );
2031 assert_eq!(
2032 search_bar.active_match_index,
2033 Some(1),
2034 "Match index should not change after selecting all matches"
2035 );
2036 });
2037 search_bar.update(cx, |search_bar, cx| {
2038 search_bar.select_prev_match(&SelectPrevMatch, window, cx);
2039 });
2040 })
2041 .unwrap();
2042 let last_match_selections = window
2043 .update(cx, |_, window, cx| {
2044 assert!(
2045 editor.read(cx).is_focused(window),
2046 "Should still have editor focused after SelectPrevMatch"
2047 );
2048
2049 search_bar.update(cx, |search_bar, cx| {
2050 let all_selections =
2051 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2052 assert_eq!(
2053 all_selections.len(),
2054 1,
2055 "On previous match, should deselect items and select the previous item"
2056 );
2057 assert_eq!(
2058 all_selections, initial_selections,
2059 "Previous match should be the same as the first selection"
2060 );
2061 assert_eq!(
2062 search_bar.active_match_index,
2063 Some(0),
2064 "Match index should be updated to the previous one"
2065 );
2066 all_selections
2067 })
2068 })
2069 .unwrap();
2070
2071 window
2072 .update(cx, |_, window, cx| {
2073 search_bar.update(cx, |search_bar, cx| {
2074 let handle = search_bar.query_editor.focus_handle(cx);
2075 window.focus(&handle);
2076 search_bar.search("abas_nonexistent_match", None, window, cx)
2077 })
2078 })
2079 .unwrap()
2080 .await
2081 .unwrap();
2082 window
2083 .update(cx, |_, window, cx| {
2084 search_bar.update(cx, |search_bar, cx| {
2085 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2086 });
2087 assert!(
2088 editor.update(cx, |this, _cx| !this.is_focused(window)),
2089 "Should not switch focus to editor if SelectAllMatches does not find any matches"
2090 );
2091 search_bar.update(cx, |search_bar, cx| {
2092 let all_selections =
2093 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2094 assert_eq!(
2095 all_selections, last_match_selections,
2096 "Should not select anything new if there are no matches"
2097 );
2098 assert!(
2099 search_bar.active_match_index.is_none(),
2100 "For no matches, there should be no active match index"
2101 );
2102 });
2103 })
2104 .unwrap();
2105 }
2106
2107 #[gpui::test]
2108 async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2109 init_globals(cx);
2110 let buffer_text = r#"
2111 self.buffer.update(cx, |buffer, cx| {
2112 buffer.edit(
2113 edits,
2114 Some(AutoindentMode::Block {
2115 original_indent_columns,
2116 }),
2117 cx,
2118 )
2119 });
2120
2121 this.buffer.update(cx, |buffer, cx| {
2122 buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2123 });
2124 "#
2125 .unindent();
2126 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2127 let cx = cx.add_empty_window();
2128
2129 let editor =
2130 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2131
2132 let search_bar = cx.new_window_entity(|window, cx| {
2133 let mut search_bar = BufferSearchBar::new(None, window, cx);
2134 search_bar.set_active_pane_item(Some(&editor), window, cx);
2135 search_bar.show(window, cx);
2136 search_bar
2137 });
2138
2139 search_bar
2140 .update_in(cx, |search_bar, window, cx| {
2141 search_bar.search(
2142 "edit\\(",
2143 Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2144 window,
2145 cx,
2146 )
2147 })
2148 .await
2149 .unwrap();
2150
2151 search_bar.update_in(cx, |search_bar, window, cx| {
2152 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2153 });
2154 search_bar.update(cx, |_, cx| {
2155 let all_selections =
2156 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2157 assert_eq!(
2158 all_selections.len(),
2159 2,
2160 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2161 );
2162 });
2163
2164 search_bar
2165 .update_in(cx, |search_bar, window, cx| {
2166 search_bar.search(
2167 "edit(",
2168 Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2169 window,
2170 cx,
2171 )
2172 })
2173 .await
2174 .unwrap();
2175
2176 search_bar.update_in(cx, |search_bar, window, cx| {
2177 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2178 });
2179 search_bar.update(cx, |_, cx| {
2180 let all_selections =
2181 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2182 assert_eq!(
2183 all_selections.len(),
2184 2,
2185 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2186 );
2187 });
2188 }
2189
2190 #[gpui::test]
2191 async fn test_search_query_history(cx: &mut TestAppContext) {
2192 init_globals(cx);
2193 let buffer_text = r#"
2194 A regular expression (shortened as regex or regexp;[1] also referred to as
2195 rational expression[2][3]) is a sequence of characters that specifies a search
2196 pattern in text. Usually such patterns are used by string-searching algorithms
2197 for "find" or "find and replace" operations on strings, or for input validation.
2198 "#
2199 .unindent();
2200 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2201 let cx = cx.add_empty_window();
2202
2203 let editor =
2204 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2205
2206 let search_bar = cx.new_window_entity(|window, cx| {
2207 let mut search_bar = BufferSearchBar::new(None, window, cx);
2208 search_bar.set_active_pane_item(Some(&editor), window, cx);
2209 search_bar.show(window, cx);
2210 search_bar
2211 });
2212
2213 // Add 3 search items into the history.
2214 search_bar
2215 .update_in(cx, |search_bar, window, cx| {
2216 search_bar.search("a", None, window, cx)
2217 })
2218 .await
2219 .unwrap();
2220 search_bar
2221 .update_in(cx, |search_bar, window, cx| {
2222 search_bar.search("b", None, window, cx)
2223 })
2224 .await
2225 .unwrap();
2226 search_bar
2227 .update_in(cx, |search_bar, window, cx| {
2228 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), window, cx)
2229 })
2230 .await
2231 .unwrap();
2232 // Ensure that the latest search is active.
2233 search_bar.update(cx, |search_bar, cx| {
2234 assert_eq!(search_bar.query(cx), "c");
2235 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2236 });
2237
2238 // Next history query after the latest should set the query to the empty string.
2239 search_bar.update_in(cx, |search_bar, window, cx| {
2240 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2241 });
2242 search_bar.update(cx, |search_bar, cx| {
2243 assert_eq!(search_bar.query(cx), "");
2244 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2245 });
2246 search_bar.update_in(cx, |search_bar, window, cx| {
2247 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2248 });
2249 search_bar.update(cx, |search_bar, cx| {
2250 assert_eq!(search_bar.query(cx), "");
2251 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2252 });
2253
2254 // First previous query for empty current query should set the query to the latest.
2255 search_bar.update_in(cx, |search_bar, window, cx| {
2256 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2257 });
2258 search_bar.update(cx, |search_bar, cx| {
2259 assert_eq!(search_bar.query(cx), "c");
2260 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2261 });
2262
2263 // Further previous items should go over the history in reverse order.
2264 search_bar.update_in(cx, |search_bar, window, cx| {
2265 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2266 });
2267 search_bar.update(cx, |search_bar, cx| {
2268 assert_eq!(search_bar.query(cx), "b");
2269 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2270 });
2271
2272 // Previous items should never go behind the first history item.
2273 search_bar.update_in(cx, |search_bar, window, cx| {
2274 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2275 });
2276 search_bar.update(cx, |search_bar, cx| {
2277 assert_eq!(search_bar.query(cx), "a");
2278 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2279 });
2280 search_bar.update_in(cx, |search_bar, window, cx| {
2281 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2282 });
2283 search_bar.update(cx, |search_bar, cx| {
2284 assert_eq!(search_bar.query(cx), "a");
2285 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2286 });
2287
2288 // Next items should go over the history in the original order.
2289 search_bar.update_in(cx, |search_bar, window, cx| {
2290 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2291 });
2292 search_bar.update(cx, |search_bar, cx| {
2293 assert_eq!(search_bar.query(cx), "b");
2294 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2295 });
2296
2297 search_bar
2298 .update_in(cx, |search_bar, window, cx| {
2299 search_bar.search("ba", None, window, cx)
2300 })
2301 .await
2302 .unwrap();
2303 search_bar.update(cx, |search_bar, cx| {
2304 assert_eq!(search_bar.query(cx), "ba");
2305 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2306 });
2307
2308 // New search input should add another entry to history and move the selection to the end of the history.
2309 search_bar.update_in(cx, |search_bar, window, cx| {
2310 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2311 });
2312 search_bar.update(cx, |search_bar, cx| {
2313 assert_eq!(search_bar.query(cx), "c");
2314 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2315 });
2316 search_bar.update_in(cx, |search_bar, window, cx| {
2317 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2318 });
2319 search_bar.update(cx, |search_bar, cx| {
2320 assert_eq!(search_bar.query(cx), "b");
2321 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2322 });
2323 search_bar.update_in(cx, |search_bar, window, cx| {
2324 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2325 });
2326 search_bar.update(cx, |search_bar, cx| {
2327 assert_eq!(search_bar.query(cx), "c");
2328 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2329 });
2330 search_bar.update_in(cx, |search_bar, window, cx| {
2331 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2332 });
2333 search_bar.update(cx, |search_bar, cx| {
2334 assert_eq!(search_bar.query(cx), "ba");
2335 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2336 });
2337 search_bar.update_in(cx, |search_bar, window, cx| {
2338 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2339 });
2340 search_bar.update(cx, |search_bar, cx| {
2341 assert_eq!(search_bar.query(cx), "");
2342 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2343 });
2344 }
2345
2346 #[gpui::test]
2347 async fn test_replace_simple(cx: &mut TestAppContext) {
2348 let (editor, search_bar, cx) = init_test(cx);
2349
2350 search_bar
2351 .update_in(cx, |search_bar, window, cx| {
2352 search_bar.search("expression", None, window, cx)
2353 })
2354 .await
2355 .unwrap();
2356
2357 search_bar.update_in(cx, |search_bar, window, cx| {
2358 search_bar.replacement_editor.update(cx, |editor, cx| {
2359 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2360 editor.set_text("expr$1", window, cx);
2361 });
2362 search_bar.replace_all(&ReplaceAll, window, cx)
2363 });
2364 assert_eq!(
2365 editor.update(cx, |this, cx| { this.text(cx) }),
2366 r#"
2367 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2368 rational expr$1[2][3]) is a sequence of characters that specifies a search
2369 pattern in text. Usually such patterns are used by string-searching algorithms
2370 for "find" or "find and replace" operations on strings, or for input validation.
2371 "#
2372 .unindent()
2373 );
2374
2375 // Search for word boundaries and replace just a single one.
2376 search_bar
2377 .update_in(cx, |search_bar, window, cx| {
2378 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), window, cx)
2379 })
2380 .await
2381 .unwrap();
2382
2383 search_bar.update_in(cx, |search_bar, window, cx| {
2384 search_bar.replacement_editor.update(cx, |editor, cx| {
2385 editor.set_text("banana", window, cx);
2386 });
2387 search_bar.replace_next(&ReplaceNext, window, cx)
2388 });
2389 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2390 assert_eq!(
2391 editor.update(cx, |this, cx| { this.text(cx) }),
2392 r#"
2393 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2394 rational expr$1[2][3]) is a sequence of characters that specifies a search
2395 pattern in text. Usually such patterns are used by string-searching algorithms
2396 for "find" or "find and replace" operations on strings, or for input validation.
2397 "#
2398 .unindent()
2399 );
2400 // Let's turn on regex mode.
2401 search_bar
2402 .update_in(cx, |search_bar, window, cx| {
2403 search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), window, cx)
2404 })
2405 .await
2406 .unwrap();
2407 search_bar.update_in(cx, |search_bar, window, cx| {
2408 search_bar.replacement_editor.update(cx, |editor, cx| {
2409 editor.set_text("${1}number", window, cx);
2410 });
2411 search_bar.replace_all(&ReplaceAll, window, cx)
2412 });
2413 assert_eq!(
2414 editor.update(cx, |this, cx| { this.text(cx) }),
2415 r#"
2416 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2417 rational expr$12number3number) is a sequence of characters that specifies a search
2418 pattern in text. Usually such patterns are used by string-searching algorithms
2419 for "find" or "find and replace" operations on strings, or for input validation.
2420 "#
2421 .unindent()
2422 );
2423 // Now with a whole-word twist.
2424 search_bar
2425 .update_in(cx, |search_bar, window, cx| {
2426 search_bar.search(
2427 "a\\w+s",
2428 Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2429 window,
2430 cx,
2431 )
2432 })
2433 .await
2434 .unwrap();
2435 search_bar.update_in(cx, |search_bar, window, cx| {
2436 search_bar.replacement_editor.update(cx, |editor, cx| {
2437 editor.set_text("things", window, cx);
2438 });
2439 search_bar.replace_all(&ReplaceAll, window, cx)
2440 });
2441 // The only word affected by this edit should be `algorithms`, even though there's a bunch
2442 // of words in this text that would match this regex if not for WHOLE_WORD.
2443 assert_eq!(
2444 editor.update(cx, |this, cx| { this.text(cx) }),
2445 r#"
2446 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2447 rational expr$12number3number) is a sequence of characters that specifies a search
2448 pattern in text. Usually such patterns are used by string-searching things
2449 for "find" or "find and replace" operations on strings, or for input validation.
2450 "#
2451 .unindent()
2452 );
2453 }
2454
2455 struct ReplacementTestParams<'a> {
2456 editor: &'a Entity<Editor>,
2457 search_bar: &'a Entity<BufferSearchBar>,
2458 cx: &'a mut VisualTestContext,
2459 search_text: &'static str,
2460 search_options: Option<SearchOptions>,
2461 replacement_text: &'static str,
2462 replace_all: bool,
2463 expected_text: String,
2464 }
2465
2466 async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2467 options
2468 .search_bar
2469 .update_in(options.cx, |search_bar, window, cx| {
2470 if let Some(options) = options.search_options {
2471 search_bar.set_search_options(options, cx);
2472 }
2473 search_bar.search(options.search_text, options.search_options, window, cx)
2474 })
2475 .await
2476 .unwrap();
2477
2478 options
2479 .search_bar
2480 .update_in(options.cx, |search_bar, window, cx| {
2481 search_bar.replacement_editor.update(cx, |editor, cx| {
2482 editor.set_text(options.replacement_text, window, cx);
2483 });
2484
2485 if options.replace_all {
2486 search_bar.replace_all(&ReplaceAll, window, cx)
2487 } else {
2488 search_bar.replace_next(&ReplaceNext, window, cx)
2489 }
2490 });
2491
2492 assert_eq!(
2493 options
2494 .editor
2495 .update(options.cx, |this, cx| { this.text(cx) }),
2496 options.expected_text
2497 );
2498 }
2499
2500 #[gpui::test]
2501 async fn test_replace_special_characters(cx: &mut TestAppContext) {
2502 let (editor, search_bar, cx) = init_test(cx);
2503
2504 run_replacement_test(ReplacementTestParams {
2505 editor: &editor,
2506 search_bar: &search_bar,
2507 cx,
2508 search_text: "expression",
2509 search_options: None,
2510 replacement_text: r"\n",
2511 replace_all: true,
2512 expected_text: r#"
2513 A regular \n (shortened as regex or regexp;[1] also referred to as
2514 rational \n[2][3]) is a sequence of characters that specifies a search
2515 pattern in text. Usually such patterns are used by string-searching algorithms
2516 for "find" or "find and replace" operations on strings, or for input validation.
2517 "#
2518 .unindent(),
2519 })
2520 .await;
2521
2522 run_replacement_test(ReplacementTestParams {
2523 editor: &editor,
2524 search_bar: &search_bar,
2525 cx,
2526 search_text: "or",
2527 search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2528 replacement_text: r"\\\n\\\\",
2529 replace_all: false,
2530 expected_text: r#"
2531 A regular \n (shortened as regex \
2532 \\ regexp;[1] also referred to as
2533 rational \n[2][3]) is a sequence of characters that specifies a search
2534 pattern in text. Usually such patterns are used by string-searching algorithms
2535 for "find" or "find and replace" operations on strings, or for input validation.
2536 "#
2537 .unindent(),
2538 })
2539 .await;
2540
2541 run_replacement_test(ReplacementTestParams {
2542 editor: &editor,
2543 search_bar: &search_bar,
2544 cx,
2545 search_text: r"(that|used) ",
2546 search_options: Some(SearchOptions::REGEX),
2547 replacement_text: r"$1\n",
2548 replace_all: true,
2549 expected_text: r#"
2550 A regular \n (shortened as regex \
2551 \\ regexp;[1] also referred to as
2552 rational \n[2][3]) is a sequence of characters that
2553 specifies a search
2554 pattern in text. Usually such patterns are used
2555 by string-searching algorithms
2556 for "find" or "find and replace" operations on strings, or for input validation.
2557 "#
2558 .unindent(),
2559 })
2560 .await;
2561 }
2562
2563 #[gpui::test]
2564 async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2565 cx: &mut TestAppContext,
2566 ) {
2567 init_globals(cx);
2568 let buffer = cx.new(|cx| {
2569 Buffer::local(
2570 r#"
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 aaa bbb aaa ccc
2577 "#
2578 .unindent(),
2579 cx,
2580 )
2581 });
2582 let cx = cx.add_empty_window();
2583 let editor =
2584 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2585
2586 let search_bar = cx.new_window_entity(|window, cx| {
2587 let mut search_bar = BufferSearchBar::new(None, window, cx);
2588 search_bar.set_active_pane_item(Some(&editor), window, cx);
2589 search_bar.show(window, cx);
2590 search_bar
2591 });
2592
2593 editor.update_in(cx, |editor, window, cx| {
2594 editor.change_selections(None, window, cx, |s| {
2595 s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2596 })
2597 });
2598
2599 search_bar.update_in(cx, |search_bar, window, cx| {
2600 let deploy = Deploy {
2601 focus: true,
2602 replace_enabled: false,
2603 selection_search_enabled: true,
2604 };
2605 search_bar.deploy(&deploy, window, cx);
2606 });
2607
2608 cx.run_until_parked();
2609
2610 search_bar
2611 .update_in(cx, |search_bar, window, cx| {
2612 search_bar.search("aaa", None, window, cx)
2613 })
2614 .await
2615 .unwrap();
2616
2617 editor.update(cx, |editor, cx| {
2618 assert_eq!(
2619 editor.search_background_highlights(cx),
2620 &[
2621 Point::new(1, 0)..Point::new(1, 3),
2622 Point::new(1, 8)..Point::new(1, 11),
2623 Point::new(2, 0)..Point::new(2, 3),
2624 ]
2625 );
2626 });
2627 }
2628
2629 #[gpui::test]
2630 async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2631 cx: &mut TestAppContext,
2632 ) {
2633 init_globals(cx);
2634 let text = r#"
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 aaa bbb aaa ccc
2641
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 aaa bbb aaa ccc
2648 "#
2649 .unindent();
2650
2651 let cx = cx.add_empty_window();
2652 let editor = cx.new_window_entity(|window, cx| {
2653 let multibuffer = MultiBuffer::build_multi(
2654 [
2655 (
2656 &text,
2657 vec![
2658 Point::new(0, 0)..Point::new(2, 0),
2659 Point::new(4, 0)..Point::new(5, 0),
2660 ],
2661 ),
2662 (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2663 ],
2664 cx,
2665 );
2666 Editor::for_multibuffer(multibuffer, None, false, window, cx)
2667 });
2668
2669 let search_bar = cx.new_window_entity(|window, cx| {
2670 let mut search_bar = BufferSearchBar::new(None, window, cx);
2671 search_bar.set_active_pane_item(Some(&editor), window, cx);
2672 search_bar.show(window, cx);
2673 search_bar
2674 });
2675
2676 editor.update_in(cx, |editor, window, cx| {
2677 editor.change_selections(None, window, cx, |s| {
2678 s.select_ranges(vec![
2679 Point::new(1, 0)..Point::new(1, 4),
2680 Point::new(5, 3)..Point::new(6, 4),
2681 ])
2682 })
2683 });
2684
2685 search_bar.update_in(cx, |search_bar, window, cx| {
2686 let deploy = Deploy {
2687 focus: true,
2688 replace_enabled: false,
2689 selection_search_enabled: true,
2690 };
2691 search_bar.deploy(&deploy, window, cx);
2692 });
2693
2694 cx.run_until_parked();
2695
2696 search_bar
2697 .update_in(cx, |search_bar, window, cx| {
2698 search_bar.search("aaa", None, window, cx)
2699 })
2700 .await
2701 .unwrap();
2702
2703 editor.update(cx, |editor, cx| {
2704 assert_eq!(
2705 editor.search_background_highlights(cx),
2706 &[
2707 Point::new(1, 0)..Point::new(1, 3),
2708 Point::new(5, 8)..Point::new(5, 11),
2709 Point::new(6, 0)..Point::new(6, 3),
2710 ]
2711 );
2712 });
2713 }
2714
2715 #[gpui::test]
2716 async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2717 let (editor, search_bar, cx) = init_test(cx);
2718 // Search using valid regexp
2719 search_bar
2720 .update_in(cx, |search_bar, window, cx| {
2721 search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
2722 search_bar.search("expression", None, window, cx)
2723 })
2724 .await
2725 .unwrap();
2726 editor.update_in(cx, |editor, window, cx| {
2727 assert_eq!(
2728 display_points_of(editor.all_text_background_highlights(window, cx)),
2729 &[
2730 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2731 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2732 ],
2733 );
2734 });
2735
2736 // Now, the expression is invalid
2737 search_bar
2738 .update_in(cx, |search_bar, window, cx| {
2739 search_bar.search("expression (", None, window, cx)
2740 })
2741 .await
2742 .unwrap_err();
2743 editor.update_in(cx, |editor, window, cx| {
2744 assert!(
2745 display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
2746 );
2747 });
2748 }
2749
2750 #[gpui::test]
2751 async fn test_search_options_changes(cx: &mut TestAppContext) {
2752 let (_editor, search_bar, cx) = init_test(cx);
2753 update_search_settings(
2754 SearchSettings {
2755 whole_word: false,
2756 case_sensitive: false,
2757 include_ignored: false,
2758 regex: false,
2759 },
2760 cx,
2761 );
2762
2763 let deploy = Deploy {
2764 focus: true,
2765 replace_enabled: false,
2766 selection_search_enabled: true,
2767 };
2768
2769 search_bar.update_in(cx, |search_bar, window, cx| {
2770 assert_eq!(
2771 search_bar.search_options,
2772 SearchOptions::NONE,
2773 "Should have no search options enabled by default"
2774 );
2775 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2776 assert_eq!(
2777 search_bar.search_options,
2778 SearchOptions::WHOLE_WORD,
2779 "Should enable the option toggled"
2780 );
2781 assert!(
2782 !search_bar.dismissed,
2783 "Search bar should be present and visible"
2784 );
2785 search_bar.deploy(&deploy, window, cx);
2786 assert_eq!(
2787 search_bar.configured_options,
2788 SearchOptions::NONE,
2789 "Should have configured search options matching the settings"
2790 );
2791 assert_eq!(
2792 search_bar.search_options,
2793 SearchOptions::WHOLE_WORD,
2794 "After (re)deploying, the option should still be enabled"
2795 );
2796
2797 search_bar.dismiss(&Dismiss, window, cx);
2798 search_bar.deploy(&deploy, window, cx);
2799 assert_eq!(
2800 search_bar.search_options,
2801 SearchOptions::NONE,
2802 "After hiding and showing the search bar, default options should be used"
2803 );
2804
2805 search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
2806 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2807 assert_eq!(
2808 search_bar.search_options,
2809 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2810 "Should enable the options toggled"
2811 );
2812 assert!(
2813 !search_bar.dismissed,
2814 "Search bar should be present and visible"
2815 );
2816 });
2817
2818 update_search_settings(
2819 SearchSettings {
2820 whole_word: false,
2821 case_sensitive: true,
2822 include_ignored: false,
2823 regex: false,
2824 },
2825 cx,
2826 );
2827 search_bar.update_in(cx, |search_bar, window, cx| {
2828 assert_eq!(
2829 search_bar.search_options,
2830 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2831 "Should have no search options enabled by default"
2832 );
2833
2834 search_bar.deploy(&deploy, window, cx);
2835 assert_eq!(
2836 search_bar.configured_options,
2837 SearchOptions::CASE_SENSITIVE,
2838 "Should have configured search options matching the settings"
2839 );
2840 assert_eq!(
2841 search_bar.search_options,
2842 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2843 "Toggling a non-dismissed search bar with custom options should not change the default options"
2844 );
2845 search_bar.dismiss(&Dismiss, window, cx);
2846 search_bar.deploy(&deploy, window, cx);
2847 assert_eq!(
2848 search_bar.search_options,
2849 SearchOptions::CASE_SENSITIVE,
2850 "After hiding and showing the search bar, default options should be used"
2851 );
2852 });
2853 }
2854
2855 fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2856 cx.update(|cx| {
2857 SettingsStore::update_global(cx, |store, cx| {
2858 store.update_user_settings::<EditorSettings>(cx, |settings| {
2859 settings.search = Some(search_settings);
2860 });
2861 });
2862 });
2863 }
2864}