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