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