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