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