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