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