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