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