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