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