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,
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, Action)]
50#[action(namespace = buffer_search)]
51#[serde(deny_unknown_fields)]
52pub struct Deploy {
53 #[serde(default = "util::serde::default_true")]
54 pub focus: bool,
55 #[serde(default)]
56 pub replace_enabled: bool,
57 #[serde(default)]
58 pub selection_search_enabled: bool,
59}
60
61actions!(buffer_search, [DeployReplace, Dismiss, FocusEditor]);
62
63impl Deploy {
64 pub fn find() -> Self {
65 Self {
66 focus: true,
67 replace_enabled: false,
68 selection_search_enabled: false,
69 }
70 }
71
72 pub fn replace() -> Self {
73 Self {
74 focus: true,
75 replace_enabled: true,
76 selection_search_enabled: false,
77 }
78 }
79}
80
81pub enum Event {
82 UpdateLocation,
83}
84
85pub fn init(cx: &mut App) {
86 cx.observe_new(|workspace: &mut Workspace, _, _| BufferSearchBar::register(workspace))
87 .detach();
88}
89
90pub struct BufferSearchBar {
91 query_editor: Entity<Editor>,
92 query_editor_focused: bool,
93 replacement_editor: Entity<Editor>,
94 replacement_editor_focused: bool,
95 active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
96 active_match_index: Option<usize>,
97 active_searchable_item_subscription: Option<Subscription>,
98 active_search: Option<Arc<SearchQuery>>,
99 searchable_items_with_matches: HashMap<Box<dyn WeakSearchableItemHandle>, AnyVec<dyn Send>>,
100 pending_search: Option<Task<()>>,
101 search_options: SearchOptions,
102 default_options: SearchOptions,
103 configured_options: SearchOptions,
104 query_contains_error: bool,
105 dismissed: bool,
106 search_history: SearchHistory,
107 search_history_cursor: SearchHistoryCursor,
108 replace_enabled: bool,
109 selection_search_enabled: bool,
110 scroll_handle: ScrollHandle,
111 editor_scroll_handle: ScrollHandle,
112 editor_needed_width: Pixels,
113 regex_language: Option<Arc<Language>>,
114}
115
116impl BufferSearchBar {
117 fn render_text_input(
118 &self,
119 editor: &Entity<Editor>,
120 color_override: Option<Color>,
121 cx: &mut Context<Self>,
122 ) -> impl IntoElement {
123 let (color, use_syntax) = if editor.read(cx).read_only(cx) {
124 (cx.theme().colors().text_disabled, false)
125 } else {
126 match color_override {
127 Some(color_override) => (color_override.color(cx), false),
128 None => (cx.theme().colors().text, true),
129 }
130 };
131
132 let settings = ThemeSettings::get_global(cx);
133 let text_style = TextStyle {
134 color,
135 font_family: settings.buffer_font.family.clone(),
136 font_features: settings.buffer_font.features.clone(),
137 font_fallbacks: settings.buffer_font.fallbacks.clone(),
138 font_size: rems(0.875).into(),
139 font_weight: settings.buffer_font.weight,
140 line_height: relative(1.3),
141 ..TextStyle::default()
142 };
143
144 let mut editor_style = EditorStyle {
145 background: cx.theme().colors().toolbar_background,
146 local_player: cx.theme().players().local(),
147 text: text_style,
148 ..EditorStyle::default()
149 };
150 if use_syntax {
151 editor_style.syntax = cx.theme().syntax().clone();
152 }
153
154 EditorElement::new(editor, editor_style)
155 }
156
157 pub fn query_editor_focused(&self) -> bool {
158 self.query_editor_focused
159 }
160}
161
162impl EventEmitter<Event> for BufferSearchBar {}
163impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
164impl Render for BufferSearchBar {
165 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
166 if self.dismissed {
167 return div().id("search_bar");
168 }
169
170 let focus_handle = self.focus_handle(cx);
171
172 let narrow_mode =
173 self.scroll_handle.bounds().size.width / window.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
174 let hide_inline_icons = self.editor_needed_width
175 > self.editor_scroll_handle.bounds().size.width - window.rem_size() * 6.;
176
177 let supported_options = self.supported_options(cx);
178
179 if self.query_editor.update(cx, |query_editor, _cx| {
180 query_editor.placeholder_text().is_none()
181 }) {
182 self.query_editor.update(cx, |editor, cx| {
183 editor.set_placeholder_text("Search…", cx);
184 });
185 }
186
187 self.replacement_editor.update(cx, |editor, cx| {
188 editor.set_placeholder_text("Replace with…", cx);
189 });
190
191 let mut color_override = None;
192 let match_text = self
193 .active_searchable_item
194 .as_ref()
195 .and_then(|searchable_item| {
196 if self.query(cx).is_empty() {
197 return None;
198 }
199 let matches_count = self
200 .searchable_items_with_matches
201 .get(&searchable_item.downgrade())
202 .map(AnyVec::len)
203 .unwrap_or(0);
204 if let Some(match_ix) = self.active_match_index {
205 Some(format!("{}/{}", match_ix + 1, matches_count))
206 } else {
207 color_override = Some(Color::Error); // No matches found
208 None
209 }
210 })
211 .unwrap_or_else(|| "0/0".to_string());
212 let should_show_replace_input = self.replace_enabled && supported_options.replacement;
213 let in_replace = self.replacement_editor.focus_handle(cx).is_focused(window);
214
215 let mut key_context = KeyContext::new_with_defaults();
216 key_context.add("BufferSearchBar");
217 if in_replace {
218 key_context.add("in_replace");
219 }
220 let editor_border = if self.query_contains_error {
221 Color::Error.color(cx)
222 } else {
223 cx.theme().colors().border
224 };
225
226 let container_width = window.viewport_size().width;
227 let input_width = SearchInputWidth::calc_width(container_width);
228
229 let input_base_styles = || {
230 h_flex()
231 .min_w_32()
232 .w(input_width)
233 .h_8()
234 .pl_2()
235 .pr_1()
236 .py_1()
237 .border_1()
238 .border_color(editor_border)
239 .rounded_lg()
240 };
241
242 let search_line = h_flex()
243 .gap_2()
244 .when(supported_options.find_in_results, |el| {
245 el.child(Label::new("Find in results").color(Color::Hint))
246 })
247 .child(
248 input_base_styles()
249 .id("editor-scroll")
250 .track_scroll(&self.editor_scroll_handle)
251 .child(self.render_text_input(&self.query_editor, color_override, cx))
252 .when(!hide_inline_icons, |div| {
253 div.child(
254 h_flex()
255 .gap_1()
256 .children(supported_options.case.then(|| {
257 self.render_search_option_button(
258 SearchOptions::CASE_SENSITIVE,
259 focus_handle.clone(),
260 cx.listener(|this, _, window, cx| {
261 this.toggle_case_sensitive(
262 &ToggleCaseSensitive,
263 window,
264 cx,
265 )
266 }),
267 )
268 }))
269 .children(supported_options.word.then(|| {
270 self.render_search_option_button(
271 SearchOptions::WHOLE_WORD,
272 focus_handle.clone(),
273 cx.listener(|this, _, window, cx| {
274 this.toggle_whole_word(&ToggleWholeWord, window, cx)
275 }),
276 )
277 }))
278 .children(supported_options.regex.then(|| {
279 self.render_search_option_button(
280 SearchOptions::REGEX,
281 focus_handle.clone(),
282 cx.listener(|this, _, window, cx| {
283 this.toggle_regex(&ToggleRegex, window, cx)
284 }),
285 )
286 })),
287 )
288 }),
289 )
290 .child(
291 h_flex()
292 .gap_1()
293 .min_w_64()
294 .when(supported_options.replacement, |this| {
295 this.child(
296 IconButton::new(
297 "buffer-search-bar-toggle-replace-button",
298 IconName::Replace,
299 )
300 .style(ButtonStyle::Subtle)
301 .shape(IconButtonShape::Square)
302 .when(self.replace_enabled, |button| {
303 button.style(ButtonStyle::Filled)
304 })
305 .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
306 this.toggle_replace(&ToggleReplace, window, cx);
307 }))
308 .toggle_state(self.replace_enabled)
309 .tooltip({
310 let focus_handle = focus_handle.clone();
311 move |window, cx| {
312 Tooltip::for_action_in(
313 "Toggle Replace",
314 &ToggleReplace,
315 &focus_handle,
316 window,
317 cx,
318 )
319 }
320 }),
321 )
322 })
323 .when(supported_options.selection, |this| {
324 this.child(
325 IconButton::new(
326 "buffer-search-bar-toggle-search-selection-button",
327 IconName::SearchSelection,
328 )
329 .style(ButtonStyle::Subtle)
330 .shape(IconButtonShape::Square)
331 .when(self.selection_search_enabled, |button| {
332 button.style(ButtonStyle::Filled)
333 })
334 .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
335 this.toggle_selection(&ToggleSelection, window, cx);
336 }))
337 .toggle_state(self.selection_search_enabled)
338 .tooltip({
339 let focus_handle = focus_handle.clone();
340 move |window, cx| {
341 Tooltip::for_action_in(
342 "Toggle Search Selection",
343 &ToggleSelection,
344 &focus_handle,
345 window,
346 cx,
347 )
348 }
349 }),
350 )
351 })
352 .when(!supported_options.find_in_results, |el| {
353 el.child(
354 IconButton::new("select-all", ui::IconName::SelectAll)
355 .on_click(|_, window, cx| {
356 window.dispatch_action(SelectAllMatches.boxed_clone(), cx)
357 })
358 .shape(IconButtonShape::Square)
359 .tooltip({
360 let focus_handle = focus_handle.clone();
361 move |window, cx| {
362 Tooltip::for_action_in(
363 "Select All Matches",
364 &SelectAllMatches,
365 &focus_handle,
366 window,
367 cx,
368 )
369 }
370 }),
371 )
372 .child(
373 h_flex()
374 .pl_2()
375 .ml_1()
376 .border_l_1()
377 .border_color(cx.theme().colors().border_variant)
378 .child(render_nav_button(
379 ui::IconName::ChevronLeft,
380 self.active_match_index.is_some(),
381 "Select Previous Match",
382 &SelectPreviousMatch,
383 focus_handle.clone(),
384 ))
385 .child(render_nav_button(
386 ui::IconName::ChevronRight,
387 self.active_match_index.is_some(),
388 "Select Next Match",
389 &SelectNextMatch,
390 focus_handle.clone(),
391 )),
392 )
393 .when(!narrow_mode, |this| {
394 this.child(h_flex().ml_2().min_w(rems_from_px(40.)).child(
395 Label::new(match_text).size(LabelSize::Small).color(
396 if self.active_match_index.is_some() {
397 Color::Default
398 } else {
399 Color::Disabled
400 },
401 ),
402 ))
403 })
404 })
405 .when(supported_options.find_in_results, |el| {
406 el.child(
407 IconButton::new(SharedString::from("Close"), IconName::Close)
408 .shape(IconButtonShape::Square)
409 .tooltip(move |window, cx| {
410 Tooltip::for_action("Close Search Bar", &Dismiss, window, cx)
411 })
412 .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
413 this.dismiss(&Dismiss, window, cx)
414 })),
415 )
416 }),
417 );
418
419 let replace_line = should_show_replace_input.then(|| {
420 h_flex()
421 .gap_2()
422 .child(input_base_styles().child(self.render_text_input(
423 &self.replacement_editor,
424 None,
425 cx,
426 )))
427 .child(
428 h_flex()
429 .min_w_64()
430 .gap_1()
431 .child(
432 IconButton::new("search-replace-next", ui::IconName::ReplaceNext)
433 .shape(IconButtonShape::Square)
434 .tooltip({
435 let focus_handle = focus_handle.clone();
436 move |window, cx| {
437 Tooltip::for_action_in(
438 "Replace Next Match",
439 &ReplaceNext,
440 &focus_handle,
441 window,
442 cx,
443 )
444 }
445 })
446 .on_click(cx.listener(|this, _, window, cx| {
447 this.replace_next(&ReplaceNext, window, cx)
448 })),
449 )
450 .child(
451 IconButton::new("search-replace-all", ui::IconName::ReplaceAll)
452 .shape(IconButtonShape::Square)
453 .tooltip({
454 let focus_handle = focus_handle.clone();
455 move |window, cx| {
456 Tooltip::for_action_in(
457 "Replace All Matches",
458 &ReplaceAll,
459 &focus_handle,
460 window,
461 cx,
462 )
463 }
464 })
465 .on_click(cx.listener(|this, _, window, cx| {
466 this.replace_all(&ReplaceAll, window, cx)
467 })),
468 ),
469 )
470 });
471
472 v_flex()
473 .id("buffer_search")
474 .gap_2()
475 .py(px(1.0))
476 .track_scroll(&self.scroll_handle)
477 .key_context(key_context)
478 .capture_action(cx.listener(Self::tab))
479 .capture_action(cx.listener(Self::backtab))
480 .on_action(cx.listener(Self::previous_history_query))
481 .on_action(cx.listener(Self::next_history_query))
482 .on_action(cx.listener(Self::dismiss))
483 .on_action(cx.listener(Self::select_next_match))
484 .on_action(cx.listener(Self::select_prev_match))
485 .on_action(cx.listener(|this, _: &ToggleOutline, window, cx| {
486 if let Some(active_searchable_item) = &mut this.active_searchable_item {
487 active_searchable_item.relay_action(Box::new(ToggleOutline), window, cx);
488 }
489 }))
490 .when(self.supported_options(cx).replacement, |this| {
491 this.on_action(cx.listener(Self::toggle_replace))
492 .when(in_replace, |this| {
493 this.on_action(cx.listener(Self::replace_next))
494 .on_action(cx.listener(Self::replace_all))
495 })
496 })
497 .when(self.supported_options(cx).case, |this| {
498 this.on_action(cx.listener(Self::toggle_case_sensitive))
499 })
500 .when(self.supported_options(cx).word, |this| {
501 this.on_action(cx.listener(Self::toggle_whole_word))
502 })
503 .when(self.supported_options(cx).regex, |this| {
504 this.on_action(cx.listener(Self::toggle_regex))
505 })
506 .when(self.supported_options(cx).selection, |this| {
507 this.on_action(cx.listener(Self::toggle_selection))
508 })
509 .child(h_flex().relative().child(search_line.w_full()).when(
510 !narrow_mode && !supported_options.find_in_results,
511 |div| {
512 div.child(
513 h_flex().absolute().right_0().child(
514 IconButton::new(SharedString::from("Close"), IconName::Close)
515 .shape(IconButtonShape::Square)
516 .tooltip(move |window, cx| {
517 Tooltip::for_action("Close Search Bar", &Dismiss, window, cx)
518 })
519 .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
520 this.dismiss(&Dismiss, window, cx)
521 })),
522 ),
523 )
524 .w_full()
525 },
526 ))
527 .children(replace_line)
528 }
529}
530
531impl Focusable for BufferSearchBar {
532 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
533 self.query_editor.focus_handle(cx)
534 }
535}
536
537impl ToolbarItemView for BufferSearchBar {
538 fn set_active_pane_item(
539 &mut self,
540 item: Option<&dyn ItemHandle>,
541 window: &mut Window,
542 cx: &mut Context<Self>,
543 ) -> ToolbarItemLocation {
544 cx.notify();
545 self.active_searchable_item_subscription.take();
546 self.active_searchable_item.take();
547
548 self.pending_search.take();
549
550 if let Some(searchable_item_handle) =
551 item.and_then(|item| item.to_searchable_item_handle(cx))
552 {
553 let this = cx.entity().downgrade();
554
555 self.active_searchable_item_subscription =
556 Some(searchable_item_handle.subscribe_to_search_events(
557 window,
558 cx,
559 Box::new(move |search_event, window, cx| {
560 if let Some(this) = this.upgrade() {
561 this.update(cx, |this, cx| {
562 this.on_active_searchable_item_event(search_event, window, cx)
563 });
564 }
565 }),
566 ));
567
568 let is_project_search = searchable_item_handle.supported_options(cx).find_in_results;
569 self.active_searchable_item = Some(searchable_item_handle);
570 drop(self.update_matches(true, window, cx));
571 if !self.dismissed {
572 if is_project_search {
573 self.dismiss(&Default::default(), window, cx);
574 } else {
575 return ToolbarItemLocation::Secondary;
576 }
577 }
578 }
579 ToolbarItemLocation::Hidden
580 }
581}
582
583impl BufferSearchBar {
584 pub fn register(registrar: &mut impl SearchActionsRegistrar) {
585 registrar.register_handler(ForDeployed(|this, _: &FocusSearch, window, cx| {
586 this.query_editor.focus_handle(cx).focus(window);
587 this.select_query(window, cx);
588 }));
589 registrar.register_handler(ForDeployed(
590 |this, action: &ToggleCaseSensitive, window, cx| {
591 if this.supported_options(cx).case {
592 this.toggle_case_sensitive(action, window, cx);
593 }
594 },
595 ));
596 registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, window, cx| {
597 if this.supported_options(cx).word {
598 this.toggle_whole_word(action, window, cx);
599 }
600 }));
601 registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, window, cx| {
602 if this.supported_options(cx).regex {
603 this.toggle_regex(action, window, cx);
604 }
605 }));
606 registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, window, cx| {
607 if this.supported_options(cx).selection {
608 this.toggle_selection(action, window, cx);
609 } else {
610 cx.propagate();
611 }
612 }));
613 registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, window, cx| {
614 if this.supported_options(cx).replacement {
615 this.toggle_replace(action, window, cx);
616 } else {
617 cx.propagate();
618 }
619 }));
620 registrar.register_handler(WithResults(|this, action: &SelectNextMatch, window, cx| {
621 if this.supported_options(cx).find_in_results {
622 cx.propagate();
623 } else {
624 this.select_next_match(action, window, cx);
625 }
626 }));
627 registrar.register_handler(WithResults(
628 |this, action: &SelectPreviousMatch, window, cx| {
629 if this.supported_options(cx).find_in_results {
630 cx.propagate();
631 } else {
632 this.select_prev_match(action, window, cx);
633 }
634 },
635 ));
636 registrar.register_handler(WithResults(
637 |this, action: &SelectAllMatches, window, cx| {
638 if this.supported_options(cx).find_in_results {
639 cx.propagate();
640 } else {
641 this.select_all_matches(action, window, cx);
642 }
643 },
644 ));
645 registrar.register_handler(ForDeployed(
646 |this, _: &editor::actions::Cancel, window, cx| {
647 this.dismiss(&Dismiss, window, cx);
648 },
649 ));
650 registrar.register_handler(ForDeployed(|this, _: &Dismiss, window, cx| {
651 this.dismiss(&Dismiss, window, cx);
652 }));
653
654 // register deploy buffer search for both search bar states, since we want to focus into the search bar
655 // when the deploy action is triggered in the buffer.
656 registrar.register_handler(ForDeployed(|this, deploy, window, cx| {
657 this.deploy(deploy, window, cx);
658 }));
659 registrar.register_handler(ForDismissed(|this, deploy, window, cx| {
660 this.deploy(deploy, window, cx);
661 }));
662 registrar.register_handler(ForDeployed(|this, _: &DeployReplace, window, cx| {
663 if this.supported_options(cx).find_in_results {
664 cx.propagate();
665 } else {
666 this.deploy(&Deploy::replace(), window, cx);
667 }
668 }));
669 registrar.register_handler(ForDismissed(|this, _: &DeployReplace, window, cx| {
670 if this.supported_options(cx).find_in_results {
671 cx.propagate();
672 } else {
673 this.deploy(&Deploy::replace(), window, cx);
674 }
675 }));
676 }
677
678 pub fn new(
679 languages: Option<Arc<LanguageRegistry>>,
680 window: &mut Window,
681 cx: &mut Context<Self>,
682 ) -> Self {
683 let query_editor = cx.new(|cx| Editor::single_line(window, cx));
684 cx.subscribe_in(&query_editor, window, Self::on_query_editor_event)
685 .detach();
686 let replacement_editor = cx.new(|cx| Editor::single_line(window, cx));
687 cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
688 .detach();
689
690 let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
691 if let Some(languages) = languages {
692 let query_buffer = query_editor
693 .read(cx)
694 .buffer()
695 .read(cx)
696 .as_singleton()
697 .expect("query editor should be backed by a singleton buffer");
698 query_buffer
699 .read(cx)
700 .set_language_registry(languages.clone());
701
702 cx.spawn(async move |buffer_search_bar, cx| {
703 let regex_language = languages
704 .language_for_name("regex")
705 .await
706 .context("loading regex language")?;
707 buffer_search_bar
708 .update(cx, |buffer_search_bar, cx| {
709 buffer_search_bar.regex_language = Some(regex_language);
710 buffer_search_bar.adjust_query_regex_language(cx);
711 })
712 .ok();
713 anyhow::Ok(())
714 })
715 .detach_and_log_err(cx);
716 }
717
718 Self {
719 query_editor,
720 query_editor_focused: false,
721 replacement_editor,
722 replacement_editor_focused: false,
723 active_searchable_item: None,
724 active_searchable_item_subscription: None,
725 active_match_index: None,
726 searchable_items_with_matches: Default::default(),
727 default_options: search_options,
728 configured_options: search_options,
729 search_options,
730 pending_search: None,
731 query_contains_error: false,
732 dismissed: true,
733 search_history: SearchHistory::new(
734 Some(MAX_BUFFER_SEARCH_HISTORY_SIZE),
735 project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
736 ),
737 search_history_cursor: Default::default(),
738 active_search: None,
739 replace_enabled: false,
740 selection_search_enabled: false,
741 scroll_handle: ScrollHandle::new(),
742 editor_scroll_handle: ScrollHandle::new(),
743 editor_needed_width: px(0.),
744 regex_language: None,
745 }
746 }
747
748 pub fn is_dismissed(&self) -> bool {
749 self.dismissed
750 }
751
752 pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
753 self.dismissed = true;
754 for searchable_item in self.searchable_items_with_matches.keys() {
755 if let Some(searchable_item) =
756 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
757 {
758 searchable_item.clear_matches(window, cx);
759 }
760 }
761 if let Some(active_editor) = self.active_searchable_item.as_mut() {
762 self.selection_search_enabled = false;
763 self.replace_enabled = false;
764 active_editor.search_bar_visibility_changed(false, window, cx);
765 active_editor.toggle_filtered_search_ranges(false, window, cx);
766 let handle = active_editor.item_focus_handle(cx);
767 self.focus(&handle, window, cx);
768 }
769 cx.emit(Event::UpdateLocation);
770 cx.emit(ToolbarItemEvent::ChangeLocation(
771 ToolbarItemLocation::Hidden,
772 ));
773 cx.notify();
774 }
775
776 pub fn deploy(&mut self, deploy: &Deploy, window: &mut Window, cx: &mut Context<Self>) -> bool {
777 if self.show(window, cx) {
778 if let Some(active_item) = self.active_searchable_item.as_mut() {
779 active_item.toggle_filtered_search_ranges(
780 deploy.selection_search_enabled,
781 window,
782 cx,
783 );
784 }
785 self.search_suggested(window, cx);
786 self.smartcase(window, cx);
787 self.replace_enabled = deploy.replace_enabled;
788 self.selection_search_enabled = deploy.selection_search_enabled;
789 if deploy.focus {
790 let mut handle = self.query_editor.focus_handle(cx).clone();
791 let mut select_query = true;
792 if deploy.replace_enabled && handle.is_focused(window) {
793 handle = self.replacement_editor.focus_handle(cx).clone();
794 select_query = false;
795 };
796
797 if select_query {
798 self.select_query(window, cx);
799 }
800
801 window.focus(&handle);
802 }
803 return true;
804 }
805
806 cx.propagate();
807 false
808 }
809
810 pub fn toggle(&mut self, action: &Deploy, window: &mut Window, cx: &mut Context<Self>) {
811 if self.is_dismissed() {
812 self.deploy(action, window, cx);
813 } else {
814 self.dismiss(&Dismiss, window, cx);
815 }
816 }
817
818 pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
819 let Some(handle) = self.active_searchable_item.as_ref() else {
820 return false;
821 };
822
823 self.configured_options =
824 SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
825 if self.dismissed
826 && (self.configured_options != self.default_options
827 || self.configured_options != self.search_options)
828 {
829 self.search_options = self.configured_options;
830 self.default_options = self.configured_options;
831 }
832
833 self.dismissed = false;
834 self.adjust_query_regex_language(cx);
835 handle.search_bar_visibility_changed(true, window, cx);
836 cx.notify();
837 cx.emit(Event::UpdateLocation);
838 cx.emit(ToolbarItemEvent::ChangeLocation(
839 ToolbarItemLocation::Secondary,
840 ));
841 true
842 }
843
844 fn supported_options(&self, cx: &mut Context<Self>) -> workspace::searchable::SearchOptions {
845 self.active_searchable_item
846 .as_ref()
847 .map(|item| item.supported_options(cx))
848 .unwrap_or_default()
849 }
850
851 pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context<Self>) {
852 let search = self
853 .query_suggestion(window, cx)
854 .map(|suggestion| self.search(&suggestion, Some(self.default_options), window, cx));
855
856 if let Some(search) = search {
857 cx.spawn_in(window, async move |this, cx| {
858 search.await?;
859 this.update_in(cx, |this, window, cx| {
860 this.activate_current_match(window, cx)
861 })
862 })
863 .detach_and_log_err(cx);
864 }
865 }
866
867 pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
868 if let Some(match_ix) = self.active_match_index {
869 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
870 if let Some(matches) = self
871 .searchable_items_with_matches
872 .get(&active_searchable_item.downgrade())
873 {
874 active_searchable_item.activate_match(match_ix, matches, window, cx)
875 }
876 }
877 }
878 }
879
880 pub fn select_query(&mut self, window: &mut Window, cx: &mut Context<Self>) {
881 self.query_editor.update(cx, |query_editor, cx| {
882 query_editor.select_all(&Default::default(), window, cx);
883 });
884 }
885
886 pub fn query(&self, cx: &App) -> String {
887 self.query_editor.read(cx).text(cx)
888 }
889
890 pub fn replacement(&self, cx: &mut App) -> String {
891 self.replacement_editor.read(cx).text(cx)
892 }
893
894 pub fn query_suggestion(
895 &mut self,
896 window: &mut Window,
897 cx: &mut Context<Self>,
898 ) -> Option<String> {
899 self.active_searchable_item
900 .as_ref()
901 .map(|searchable_item| searchable_item.query_suggestion(window, cx))
902 .filter(|suggestion| !suggestion.is_empty())
903 }
904
905 pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut Context<Self>) {
906 if replacement.is_none() {
907 self.replace_enabled = false;
908 return;
909 }
910 self.replace_enabled = true;
911 self.replacement_editor
912 .update(cx, |replacement_editor, cx| {
913 replacement_editor
914 .buffer()
915 .update(cx, |replacement_buffer, cx| {
916 let len = replacement_buffer.len(cx);
917 replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
918 });
919 });
920 }
921
922 pub fn search(
923 &mut self,
924 query: &str,
925 options: Option<SearchOptions>,
926 window: &mut Window,
927 cx: &mut Context<Self>,
928 ) -> oneshot::Receiver<()> {
929 let options = options.unwrap_or(self.default_options);
930 let updated = query != self.query(cx) || self.search_options != options;
931 if updated {
932 self.query_editor.update(cx, |query_editor, cx| {
933 query_editor.buffer().update(cx, |query_buffer, cx| {
934 let len = query_buffer.len(cx);
935 query_buffer.edit([(0..len, query)], None, cx);
936 });
937 });
938 self.set_search_options(options, cx);
939 self.clear_matches(window, cx);
940 cx.notify();
941 }
942 self.update_matches(!updated, window, cx)
943 }
944
945 fn render_search_option_button<Action: Fn(&ClickEvent, &mut Window, &mut App) + 'static>(
946 &self,
947 option: SearchOptions,
948 focus_handle: FocusHandle,
949 action: Action,
950 ) -> impl IntoElement + use<Action> {
951 let is_active = self.search_options.contains(option);
952 option.as_button(is_active, focus_handle, action)
953 }
954
955 pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
956 if let Some(active_editor) = self.active_searchable_item.as_ref() {
957 let handle = active_editor.item_focus_handle(cx);
958 window.focus(&handle);
959 }
960 }
961
962 pub fn toggle_search_option(
963 &mut self,
964 search_option: SearchOptions,
965 window: &mut Window,
966 cx: &mut Context<Self>,
967 ) {
968 self.search_options.toggle(search_option);
969 self.default_options = self.search_options;
970 drop(self.update_matches(false, window, cx));
971 self.adjust_query_regex_language(cx);
972 cx.notify();
973 }
974
975 pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
976 self.search_options.contains(search_option)
977 }
978
979 pub fn enable_search_option(
980 &mut self,
981 search_option: SearchOptions,
982 window: &mut Window,
983 cx: &mut Context<Self>,
984 ) {
985 if !self.search_options.contains(search_option) {
986 self.toggle_search_option(search_option, window, cx)
987 }
988 }
989
990 pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context<Self>) {
991 self.search_options = search_options;
992 self.adjust_query_regex_language(cx);
993 cx.notify();
994 }
995
996 pub fn clear_search_within_ranges(
997 &mut self,
998 search_options: SearchOptions,
999 cx: &mut Context<Self>,
1000 ) {
1001 self.search_options = search_options;
1002 self.adjust_query_regex_language(cx);
1003 cx.notify();
1004 }
1005
1006 fn select_next_match(
1007 &mut self,
1008 _: &SelectNextMatch,
1009 window: &mut Window,
1010 cx: &mut Context<Self>,
1011 ) {
1012 self.select_match(Direction::Next, 1, window, cx);
1013 }
1014
1015 fn select_prev_match(
1016 &mut self,
1017 _: &SelectPreviousMatch,
1018 window: &mut Window,
1019 cx: &mut Context<Self>,
1020 ) {
1021 self.select_match(Direction::Prev, 1, window, cx);
1022 }
1023
1024 fn select_all_matches(
1025 &mut self,
1026 _: &SelectAllMatches,
1027 window: &mut Window,
1028 cx: &mut Context<Self>,
1029 ) {
1030 if !self.dismissed && self.active_match_index.is_some() {
1031 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1032 if let Some(matches) = self
1033 .searchable_items_with_matches
1034 .get(&searchable_item.downgrade())
1035 {
1036 searchable_item.select_matches(matches, window, cx);
1037 self.focus_editor(&FocusEditor, window, cx);
1038 }
1039 }
1040 }
1041 }
1042
1043 pub fn select_match(
1044 &mut self,
1045 direction: Direction,
1046 count: usize,
1047 window: &mut Window,
1048 cx: &mut Context<Self>,
1049 ) {
1050 if let Some(index) = self.active_match_index {
1051 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1052 if let Some(matches) = self
1053 .searchable_items_with_matches
1054 .get(&searchable_item.downgrade())
1055 .filter(|matches| !matches.is_empty())
1056 {
1057 // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
1058 if !EditorSettings::get_global(cx).search_wrap
1059 && ((direction == Direction::Next && index + count >= matches.len())
1060 || (direction == Direction::Prev && index < count))
1061 {
1062 crate::show_no_more_matches(window, cx);
1063 return;
1064 }
1065 let new_match_index = searchable_item
1066 .match_index_for_direction(matches, index, direction, count, window, cx);
1067
1068 searchable_item.update_matches(matches, window, cx);
1069 searchable_item.activate_match(new_match_index, matches, window, cx);
1070 }
1071 }
1072 }
1073 }
1074
1075 pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1076 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1077 if let Some(matches) = self
1078 .searchable_items_with_matches
1079 .get(&searchable_item.downgrade())
1080 {
1081 if matches.is_empty() {
1082 return;
1083 }
1084 let new_match_index = matches.len() - 1;
1085 searchable_item.update_matches(matches, window, cx);
1086 searchable_item.activate_match(new_match_index, matches, window, cx);
1087 }
1088 }
1089 }
1090
1091 fn on_query_editor_event(
1092 &mut self,
1093 editor: &Entity<Editor>,
1094 event: &editor::EditorEvent,
1095 window: &mut Window,
1096 cx: &mut Context<Self>,
1097 ) {
1098 match event {
1099 editor::EditorEvent::Focused => self.query_editor_focused = true,
1100 editor::EditorEvent::Blurred => self.query_editor_focused = false,
1101 editor::EditorEvent::Edited { .. } => {
1102 self.smartcase(window, cx);
1103 self.clear_matches(window, cx);
1104 let search = self.update_matches(false, window, cx);
1105
1106 let width = editor.update(cx, |editor, cx| {
1107 let text_layout_details = editor.text_layout_details(window);
1108 let snapshot = editor.snapshot(window, cx).display_snapshot;
1109
1110 snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
1111 - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
1112 });
1113 self.editor_needed_width = width;
1114 cx.notify();
1115
1116 cx.spawn_in(window, async move |this, cx| {
1117 search.await?;
1118 this.update_in(cx, |this, window, cx| {
1119 this.activate_current_match(window, cx)
1120 })
1121 })
1122 .detach_and_log_err(cx);
1123 }
1124 _ => {}
1125 }
1126 }
1127
1128 fn on_replacement_editor_event(
1129 &mut self,
1130 _: Entity<Editor>,
1131 event: &editor::EditorEvent,
1132 _: &mut Context<Self>,
1133 ) {
1134 match event {
1135 editor::EditorEvent::Focused => self.replacement_editor_focused = true,
1136 editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
1137 _ => {}
1138 }
1139 }
1140
1141 fn on_active_searchable_item_event(
1142 &mut self,
1143 event: &SearchEvent,
1144 window: &mut Window,
1145 cx: &mut Context<Self>,
1146 ) {
1147 match event {
1148 SearchEvent::MatchesInvalidated => {
1149 drop(self.update_matches(false, window, cx));
1150 }
1151 SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx),
1152 }
1153 }
1154
1155 fn toggle_case_sensitive(
1156 &mut self,
1157 _: &ToggleCaseSensitive,
1158 window: &mut Window,
1159 cx: &mut Context<Self>,
1160 ) {
1161 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx)
1162 }
1163
1164 fn toggle_whole_word(
1165 &mut self,
1166 _: &ToggleWholeWord,
1167 window: &mut Window,
1168 cx: &mut Context<Self>,
1169 ) {
1170 self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1171 }
1172
1173 fn toggle_selection(
1174 &mut self,
1175 _: &ToggleSelection,
1176 window: &mut Window,
1177 cx: &mut Context<Self>,
1178 ) {
1179 if let Some(active_item) = self.active_searchable_item.as_mut() {
1180 self.selection_search_enabled = !self.selection_search_enabled;
1181 active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx);
1182 drop(self.update_matches(false, window, cx));
1183 cx.notify();
1184 }
1185 }
1186
1187 fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context<Self>) {
1188 self.toggle_search_option(SearchOptions::REGEX, window, cx)
1189 }
1190
1191 fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) {
1192 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1193 self.active_match_index = None;
1194 self.searchable_items_with_matches
1195 .remove(&active_searchable_item.downgrade());
1196 active_searchable_item.clear_matches(window, cx);
1197 }
1198 }
1199
1200 pub fn has_active_match(&self) -> bool {
1201 self.active_match_index.is_some()
1202 }
1203
1204 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1205 let mut active_item_matches = None;
1206 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
1207 if let Some(searchable_item) =
1208 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
1209 {
1210 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
1211 active_item_matches = Some((searchable_item.downgrade(), matches));
1212 } else {
1213 searchable_item.clear_matches(window, cx);
1214 }
1215 }
1216 }
1217
1218 self.searchable_items_with_matches
1219 .extend(active_item_matches);
1220 }
1221
1222 fn update_matches(
1223 &mut self,
1224 reuse_existing_query: bool,
1225 window: &mut Window,
1226 cx: &mut Context<Self>,
1227 ) -> oneshot::Receiver<()> {
1228 let (done_tx, done_rx) = oneshot::channel();
1229 let query = self.query(cx);
1230 self.pending_search.take();
1231
1232 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1233 self.query_contains_error = false;
1234 if query.is_empty() {
1235 self.clear_active_searchable_item_matches(window, cx);
1236 let _ = done_tx.send(());
1237 cx.notify();
1238 } else {
1239 let query: Arc<_> = if let Some(search) =
1240 self.active_search.take().filter(|_| reuse_existing_query)
1241 {
1242 search
1243 } else {
1244 if self.search_options.contains(SearchOptions::REGEX) {
1245 match SearchQuery::regex(
1246 query,
1247 self.search_options.contains(SearchOptions::WHOLE_WORD),
1248 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1249 false,
1250 self.search_options
1251 .contains(SearchOptions::ONE_MATCH_PER_LINE),
1252 Default::default(),
1253 Default::default(),
1254 false,
1255 None,
1256 ) {
1257 Ok(query) => query.with_replacement(self.replacement(cx)),
1258 Err(_) => {
1259 self.query_contains_error = true;
1260 self.clear_active_searchable_item_matches(window, cx);
1261 cx.notify();
1262 return done_rx;
1263 }
1264 }
1265 } else {
1266 match SearchQuery::text(
1267 query,
1268 self.search_options.contains(SearchOptions::WHOLE_WORD),
1269 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1270 false,
1271 Default::default(),
1272 Default::default(),
1273 false,
1274 None,
1275 ) {
1276 Ok(query) => query.with_replacement(self.replacement(cx)),
1277 Err(_) => {
1278 self.query_contains_error = true;
1279 self.clear_active_searchable_item_matches(window, cx);
1280 cx.notify();
1281 return done_rx;
1282 }
1283 }
1284 }
1285 .into()
1286 };
1287
1288 self.active_search = Some(query.clone());
1289 let query_text = query.as_str().to_string();
1290
1291 let matches = active_searchable_item.find_matches(query, window, cx);
1292
1293 let active_searchable_item = active_searchable_item.downgrade();
1294 self.pending_search = Some(cx.spawn_in(window, async move |this, cx| {
1295 let matches = matches.await;
1296
1297 this.update_in(cx, |this, window, cx| {
1298 if let Some(active_searchable_item) =
1299 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1300 {
1301 this.searchable_items_with_matches
1302 .insert(active_searchable_item.downgrade(), matches);
1303
1304 this.update_match_index(window, cx);
1305 this.search_history
1306 .add(&mut this.search_history_cursor, query_text);
1307 if !this.dismissed {
1308 let matches = this
1309 .searchable_items_with_matches
1310 .get(&active_searchable_item.downgrade())
1311 .unwrap();
1312 if matches.is_empty() {
1313 active_searchable_item.clear_matches(window, cx);
1314 } else {
1315 active_searchable_item.update_matches(matches, window, cx);
1316 }
1317 let _ = done_tx.send(());
1318 }
1319 cx.notify();
1320 }
1321 })
1322 .log_err();
1323 }));
1324 }
1325 }
1326 done_rx
1327 }
1328
1329 fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction {
1330 if self.search_options.contains(SearchOptions::BACKWARDS) {
1331 direction.opposite()
1332 } else {
1333 direction
1334 }
1335 }
1336
1337 pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1338 let direction = self.reverse_direction_if_backwards(Direction::Next);
1339 let new_index = self
1340 .active_searchable_item
1341 .as_ref()
1342 .and_then(|searchable_item| {
1343 let matches = self
1344 .searchable_items_with_matches
1345 .get(&searchable_item.downgrade())?;
1346 searchable_item.active_match_index(direction, matches, window, cx)
1347 });
1348 if new_index != self.active_match_index {
1349 self.active_match_index = new_index;
1350 cx.notify();
1351 }
1352 }
1353
1354 fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1355 // Search -> Replace -> Editor
1356 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1357 self.replacement_editor.focus_handle(cx)
1358 } else if let Some(item) = self.active_searchable_item.as_ref() {
1359 item.item_focus_handle(cx)
1360 } else {
1361 return;
1362 };
1363 self.focus(&focus_handle, window, cx);
1364 cx.stop_propagation();
1365 }
1366
1367 fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1368 // Search -> Replace -> Search
1369 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1370 self.replacement_editor.focus_handle(cx)
1371 } else if self.replacement_editor_focused {
1372 self.query_editor.focus_handle(cx)
1373 } else {
1374 return;
1375 };
1376 self.focus(&focus_handle, window, cx);
1377 cx.stop_propagation();
1378 }
1379
1380 fn next_history_query(
1381 &mut self,
1382 _: &NextHistoryQuery,
1383 window: &mut Window,
1384 cx: &mut Context<Self>,
1385 ) {
1386 if let Some(new_query) = self
1387 .search_history
1388 .next(&mut self.search_history_cursor)
1389 .map(str::to_string)
1390 {
1391 drop(self.search(&new_query, Some(self.search_options), window, cx));
1392 } else {
1393 self.search_history_cursor.reset();
1394 drop(self.search("", Some(self.search_options), window, cx));
1395 }
1396 }
1397
1398 fn previous_history_query(
1399 &mut self,
1400 _: &PreviousHistoryQuery,
1401 window: &mut Window,
1402 cx: &mut Context<Self>,
1403 ) {
1404 if self.query(cx).is_empty() {
1405 if let Some(new_query) = self
1406 .search_history
1407 .current(&mut self.search_history_cursor)
1408 .map(str::to_string)
1409 {
1410 drop(self.search(&new_query, Some(self.search_options), window, cx));
1411 return;
1412 }
1413 }
1414
1415 if let Some(new_query) = self
1416 .search_history
1417 .previous(&mut self.search_history_cursor)
1418 .map(str::to_string)
1419 {
1420 drop(self.search(&new_query, Some(self.search_options), window, cx));
1421 }
1422 }
1423
1424 fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut Context<Self>) {
1425 cx.on_next_frame(window, |_, window, _| {
1426 window.invalidate_character_coordinates();
1427 });
1428 window.focus(handle);
1429 }
1430
1431 fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1432 if self.active_searchable_item.is_some() {
1433 self.replace_enabled = !self.replace_enabled;
1434 let handle = if self.replace_enabled {
1435 self.replacement_editor.focus_handle(cx)
1436 } else {
1437 self.query_editor.focus_handle(cx)
1438 };
1439 self.focus(&handle, window, cx);
1440 cx.notify();
1441 }
1442 }
1443
1444 fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1445 let mut should_propagate = true;
1446 if !self.dismissed && self.active_search.is_some() {
1447 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1448 if let Some(query) = self.active_search.as_ref() {
1449 if let Some(matches) = self
1450 .searchable_items_with_matches
1451 .get(&searchable_item.downgrade())
1452 {
1453 if let Some(active_index) = self.active_match_index {
1454 let query = query
1455 .as_ref()
1456 .clone()
1457 .with_replacement(self.replacement(cx));
1458 searchable_item.replace(matches.at(active_index), &query, window, cx);
1459 self.select_next_match(&SelectNextMatch, window, cx);
1460 }
1461 should_propagate = false;
1462 }
1463 }
1464 }
1465 }
1466 if !should_propagate {
1467 cx.stop_propagation();
1468 }
1469 }
1470
1471 pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1472 if !self.dismissed && self.active_search.is_some() {
1473 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1474 if let Some(query) = self.active_search.as_ref() {
1475 if let Some(matches) = self
1476 .searchable_items_with_matches
1477 .get(&searchable_item.downgrade())
1478 {
1479 let query = query
1480 .as_ref()
1481 .clone()
1482 .with_replacement(self.replacement(cx));
1483 searchable_item.replace_all(&mut matches.iter(), &query, window, cx);
1484 }
1485 }
1486 }
1487 }
1488 }
1489
1490 pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1491 self.update_match_index(window, cx);
1492 self.active_match_index.is_some()
1493 }
1494
1495 pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1496 EditorSettings::get_global(cx).use_smartcase_search
1497 }
1498
1499 pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1500 str.chars().any(|c| c.is_uppercase())
1501 }
1502
1503 fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1504 if self.should_use_smartcase_search(cx) {
1505 let query = self.query(cx);
1506 if !query.is_empty() {
1507 let is_case = self.is_contains_uppercase(&query);
1508 if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1509 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1510 }
1511 }
1512 }
1513 }
1514
1515 fn adjust_query_regex_language(&self, cx: &mut App) {
1516 let enable = self.search_options.contains(SearchOptions::REGEX);
1517 let query_buffer = self
1518 .query_editor
1519 .read(cx)
1520 .buffer()
1521 .read(cx)
1522 .as_singleton()
1523 .expect("query editor should be backed by a singleton buffer");
1524 if enable {
1525 if let Some(regex_language) = self.regex_language.clone() {
1526 query_buffer.update(cx, |query_buffer, cx| {
1527 query_buffer.set_language(Some(regex_language), cx);
1528 })
1529 }
1530 } else {
1531 query_buffer.update(cx, |query_buffer, cx| {
1532 query_buffer.set_language(None, cx);
1533 })
1534 }
1535 }
1536}
1537
1538#[cfg(test)]
1539mod tests {
1540 use std::ops::Range;
1541
1542 use super::*;
1543 use editor::{
1544 DisplayPoint, Editor, MultiBuffer, SearchSettings, SelectionEffects,
1545 display_map::DisplayRow,
1546 };
1547 use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1548 use language::{Buffer, Point};
1549 use project::Project;
1550 use settings::SettingsStore;
1551 use smol::stream::StreamExt as _;
1552 use unindent::Unindent as _;
1553
1554 fn init_globals(cx: &mut TestAppContext) {
1555 cx.update(|cx| {
1556 let store = settings::SettingsStore::test(cx);
1557 cx.set_global(store);
1558 workspace::init_settings(cx);
1559 editor::init(cx);
1560
1561 language::init(cx);
1562 Project::init_settings(cx);
1563 theme::init(theme::LoadThemes::JustBase, cx);
1564 crate::init(cx);
1565 });
1566 }
1567
1568 fn init_test(
1569 cx: &mut TestAppContext,
1570 ) -> (
1571 Entity<Editor>,
1572 Entity<BufferSearchBar>,
1573 &mut VisualTestContext,
1574 ) {
1575 init_globals(cx);
1576 let buffer = cx.new(|cx| {
1577 Buffer::local(
1578 r#"
1579 A regular expression (shortened as regex or regexp;[1] also referred to as
1580 rational expression[2][3]) is a sequence of characters that specifies a search
1581 pattern in text. Usually such patterns are used by string-searching algorithms
1582 for "find" or "find and replace" operations on strings, or for input validation.
1583 "#
1584 .unindent(),
1585 cx,
1586 )
1587 });
1588 let cx = cx.add_empty_window();
1589 let editor =
1590 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
1591
1592 let search_bar = cx.new_window_entity(|window, cx| {
1593 let mut search_bar = BufferSearchBar::new(None, window, cx);
1594 search_bar.set_active_pane_item(Some(&editor), window, cx);
1595 search_bar.show(window, cx);
1596 search_bar
1597 });
1598
1599 (editor, search_bar, cx)
1600 }
1601
1602 #[gpui::test]
1603 async fn test_search_simple(cx: &mut TestAppContext) {
1604 let (editor, search_bar, cx) = init_test(cx);
1605 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1606 background_highlights
1607 .into_iter()
1608 .map(|(range, _)| range)
1609 .collect::<Vec<_>>()
1610 };
1611 // Search for a string that appears with different casing.
1612 // By default, search is case-insensitive.
1613 search_bar
1614 .update_in(cx, |search_bar, window, cx| {
1615 search_bar.search("us", None, window, cx)
1616 })
1617 .await
1618 .unwrap();
1619 editor.update_in(cx, |editor, window, cx| {
1620 assert_eq!(
1621 display_points_of(editor.all_text_background_highlights(window, cx)),
1622 &[
1623 DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1624 DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1625 ]
1626 );
1627 });
1628
1629 // Switch to a case sensitive search.
1630 search_bar.update_in(cx, |search_bar, window, cx| {
1631 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1632 });
1633 let mut editor_notifications = cx.notifications(&editor);
1634 editor_notifications.next().await;
1635 editor.update_in(cx, |editor, window, cx| {
1636 assert_eq!(
1637 display_points_of(editor.all_text_background_highlights(window, cx)),
1638 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1639 );
1640 });
1641
1642 // Search for a string that appears both as a whole word and
1643 // within other words. By default, all results are found.
1644 search_bar
1645 .update_in(cx, |search_bar, window, cx| {
1646 search_bar.search("or", None, window, cx)
1647 })
1648 .await
1649 .unwrap();
1650 editor.update_in(cx, |editor, window, cx| {
1651 assert_eq!(
1652 display_points_of(editor.all_text_background_highlights(window, cx)),
1653 &[
1654 DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1655 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1656 DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1657 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1658 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1659 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1660 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1661 ]
1662 );
1663 });
1664
1665 // Switch to a whole word search.
1666 search_bar.update_in(cx, |search_bar, window, cx| {
1667 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1668 });
1669 let mut editor_notifications = cx.notifications(&editor);
1670 editor_notifications.next().await;
1671 editor.update_in(cx, |editor, window, cx| {
1672 assert_eq!(
1673 display_points_of(editor.all_text_background_highlights(window, cx)),
1674 &[
1675 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1676 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1677 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1678 ]
1679 );
1680 });
1681
1682 editor.update_in(cx, |editor, window, cx| {
1683 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1684 s.select_display_ranges([
1685 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1686 ])
1687 });
1688 });
1689 search_bar.update_in(cx, |search_bar, window, cx| {
1690 assert_eq!(search_bar.active_match_index, Some(0));
1691 search_bar.select_next_match(&SelectNextMatch, window, cx);
1692 assert_eq!(
1693 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1694 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1695 );
1696 });
1697 search_bar.read_with(cx, |search_bar, _| {
1698 assert_eq!(search_bar.active_match_index, Some(0));
1699 });
1700
1701 search_bar.update_in(cx, |search_bar, window, cx| {
1702 search_bar.select_next_match(&SelectNextMatch, window, cx);
1703 assert_eq!(
1704 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1705 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1706 );
1707 });
1708 search_bar.read_with(cx, |search_bar, _| {
1709 assert_eq!(search_bar.active_match_index, Some(1));
1710 });
1711
1712 search_bar.update_in(cx, |search_bar, window, cx| {
1713 search_bar.select_next_match(&SelectNextMatch, window, cx);
1714 assert_eq!(
1715 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1716 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1717 );
1718 });
1719 search_bar.read_with(cx, |search_bar, _| {
1720 assert_eq!(search_bar.active_match_index, Some(2));
1721 });
1722
1723 search_bar.update_in(cx, |search_bar, window, cx| {
1724 search_bar.select_next_match(&SelectNextMatch, window, cx);
1725 assert_eq!(
1726 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1727 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1728 );
1729 });
1730 search_bar.read_with(cx, |search_bar, _| {
1731 assert_eq!(search_bar.active_match_index, Some(0));
1732 });
1733
1734 search_bar.update_in(cx, |search_bar, window, cx| {
1735 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1736 assert_eq!(
1737 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1738 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1739 );
1740 });
1741 search_bar.read_with(cx, |search_bar, _| {
1742 assert_eq!(search_bar.active_match_index, Some(2));
1743 });
1744
1745 search_bar.update_in(cx, |search_bar, window, cx| {
1746 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1747 assert_eq!(
1748 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1749 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1750 );
1751 });
1752 search_bar.read_with(cx, |search_bar, _| {
1753 assert_eq!(search_bar.active_match_index, Some(1));
1754 });
1755
1756 search_bar.update_in(cx, |search_bar, window, cx| {
1757 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1758 assert_eq!(
1759 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1760 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1761 );
1762 });
1763 search_bar.read_with(cx, |search_bar, _| {
1764 assert_eq!(search_bar.active_match_index, Some(0));
1765 });
1766
1767 // Park the cursor in between matches and ensure that going to the previous match selects
1768 // the closest match to the left.
1769 editor.update_in(cx, |editor, window, cx| {
1770 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1771 s.select_display_ranges([
1772 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1773 ])
1774 });
1775 });
1776 search_bar.update_in(cx, |search_bar, window, cx| {
1777 assert_eq!(search_bar.active_match_index, Some(1));
1778 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1779 assert_eq!(
1780 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1781 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1782 );
1783 });
1784 search_bar.read_with(cx, |search_bar, _| {
1785 assert_eq!(search_bar.active_match_index, Some(0));
1786 });
1787
1788 // Park the cursor in between matches and ensure that going to the next match selects the
1789 // closest match to the right.
1790 editor.update_in(cx, |editor, window, cx| {
1791 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1792 s.select_display_ranges([
1793 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1794 ])
1795 });
1796 });
1797 search_bar.update_in(cx, |search_bar, window, cx| {
1798 assert_eq!(search_bar.active_match_index, Some(1));
1799 search_bar.select_next_match(&SelectNextMatch, window, cx);
1800 assert_eq!(
1801 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1802 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1803 );
1804 });
1805 search_bar.read_with(cx, |search_bar, _| {
1806 assert_eq!(search_bar.active_match_index, Some(1));
1807 });
1808
1809 // Park the cursor after the last match and ensure that going to the previous match selects
1810 // the last match.
1811 editor.update_in(cx, |editor, window, cx| {
1812 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1813 s.select_display_ranges([
1814 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1815 ])
1816 });
1817 });
1818 search_bar.update_in(cx, |search_bar, window, cx| {
1819 assert_eq!(search_bar.active_match_index, Some(2));
1820 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1821 assert_eq!(
1822 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1823 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1824 );
1825 });
1826 search_bar.read_with(cx, |search_bar, _| {
1827 assert_eq!(search_bar.active_match_index, Some(2));
1828 });
1829
1830 // Park the cursor after the last match and ensure that going to the next match selects the
1831 // first match.
1832 editor.update_in(cx, |editor, window, cx| {
1833 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1834 s.select_display_ranges([
1835 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1836 ])
1837 });
1838 });
1839 search_bar.update_in(cx, |search_bar, window, cx| {
1840 assert_eq!(search_bar.active_match_index, Some(2));
1841 search_bar.select_next_match(&SelectNextMatch, window, cx);
1842 assert_eq!(
1843 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1844 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1845 );
1846 });
1847 search_bar.read_with(cx, |search_bar, _| {
1848 assert_eq!(search_bar.active_match_index, Some(0));
1849 });
1850
1851 // Park the cursor before the first match and ensure that going to the previous match
1852 // selects the last match.
1853 editor.update_in(cx, |editor, window, cx| {
1854 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1855 s.select_display_ranges([
1856 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1857 ])
1858 });
1859 });
1860 search_bar.update_in(cx, |search_bar, window, cx| {
1861 assert_eq!(search_bar.active_match_index, Some(0));
1862 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1863 assert_eq!(
1864 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1865 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1866 );
1867 });
1868 search_bar.read_with(cx, |search_bar, _| {
1869 assert_eq!(search_bar.active_match_index, Some(2));
1870 });
1871 }
1872
1873 fn display_points_of(
1874 background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1875 ) -> Vec<Range<DisplayPoint>> {
1876 background_highlights
1877 .into_iter()
1878 .map(|(range, _)| range)
1879 .collect::<Vec<_>>()
1880 }
1881
1882 #[gpui::test]
1883 async fn test_search_option_handling(cx: &mut TestAppContext) {
1884 let (editor, search_bar, cx) = init_test(cx);
1885
1886 // show with options should make current search case sensitive
1887 search_bar
1888 .update_in(cx, |search_bar, window, cx| {
1889 search_bar.show(window, cx);
1890 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1891 })
1892 .await
1893 .unwrap();
1894 editor.update_in(cx, |editor, window, cx| {
1895 assert_eq!(
1896 display_points_of(editor.all_text_background_highlights(window, cx)),
1897 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1898 );
1899 });
1900
1901 // search_suggested should restore default options
1902 search_bar.update_in(cx, |search_bar, window, cx| {
1903 search_bar.search_suggested(window, cx);
1904 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1905 });
1906
1907 // toggling a search option should update the defaults
1908 search_bar
1909 .update_in(cx, |search_bar, window, cx| {
1910 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1911 })
1912 .await
1913 .unwrap();
1914 search_bar.update_in(cx, |search_bar, window, cx| {
1915 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1916 });
1917 let mut editor_notifications = cx.notifications(&editor);
1918 editor_notifications.next().await;
1919 editor.update_in(cx, |editor, window, cx| {
1920 assert_eq!(
1921 display_points_of(editor.all_text_background_highlights(window, cx)),
1922 &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1923 );
1924 });
1925
1926 // defaults should still include whole word
1927 search_bar.update_in(cx, |search_bar, window, cx| {
1928 search_bar.search_suggested(window, cx);
1929 assert_eq!(
1930 search_bar.search_options,
1931 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1932 )
1933 });
1934 }
1935
1936 #[gpui::test]
1937 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1938 init_globals(cx);
1939 let buffer_text = r#"
1940 A regular expression (shortened as regex or regexp;[1] also referred to as
1941 rational expression[2][3]) is a sequence of characters that specifies a search
1942 pattern in text. Usually such patterns are used by string-searching algorithms
1943 for "find" or "find and replace" operations on strings, or for input validation.
1944 "#
1945 .unindent();
1946 let expected_query_matches_count = buffer_text
1947 .chars()
1948 .filter(|c| c.eq_ignore_ascii_case(&'a'))
1949 .count();
1950 assert!(
1951 expected_query_matches_count > 1,
1952 "Should pick a query with multiple results"
1953 );
1954 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
1955 let window = cx.add_window(|_, _| gpui::Empty);
1956
1957 let editor = window.build_entity(cx, |window, cx| {
1958 Editor::for_buffer(buffer.clone(), None, window, cx)
1959 });
1960
1961 let search_bar = window.build_entity(cx, |window, cx| {
1962 let mut search_bar = BufferSearchBar::new(None, window, cx);
1963 search_bar.set_active_pane_item(Some(&editor), window, cx);
1964 search_bar.show(window, cx);
1965 search_bar
1966 });
1967
1968 window
1969 .update(cx, |_, window, cx| {
1970 search_bar.update(cx, |search_bar, cx| {
1971 search_bar.search("a", None, window, cx)
1972 })
1973 })
1974 .unwrap()
1975 .await
1976 .unwrap();
1977 let initial_selections = window
1978 .update(cx, |_, window, cx| {
1979 search_bar.update(cx, |search_bar, cx| {
1980 let handle = search_bar.query_editor.focus_handle(cx);
1981 window.focus(&handle);
1982 search_bar.activate_current_match(window, cx);
1983 });
1984 assert!(
1985 !editor.read(cx).is_focused(window),
1986 "Initially, the editor should not be focused"
1987 );
1988 let initial_selections = editor.update(cx, |editor, cx| {
1989 let initial_selections = editor.selections.display_ranges(cx);
1990 assert_eq!(
1991 initial_selections.len(), 1,
1992 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1993 );
1994 initial_selections
1995 });
1996 search_bar.update(cx, |search_bar, cx| {
1997 assert_eq!(search_bar.active_match_index, Some(0));
1998 let handle = search_bar.query_editor.focus_handle(cx);
1999 window.focus(&handle);
2000 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2001 });
2002 assert!(
2003 editor.read(cx).is_focused(window),
2004 "Should focus editor after successful SelectAllMatches"
2005 );
2006 search_bar.update(cx, |search_bar, cx| {
2007 let all_selections =
2008 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2009 assert_eq!(
2010 all_selections.len(),
2011 expected_query_matches_count,
2012 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2013 );
2014 assert_eq!(
2015 search_bar.active_match_index,
2016 Some(0),
2017 "Match index should not change after selecting all matches"
2018 );
2019 });
2020
2021 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
2022 initial_selections
2023 }).unwrap();
2024
2025 window
2026 .update(cx, |_, window, cx| {
2027 assert!(
2028 editor.read(cx).is_focused(window),
2029 "Should still have editor focused after SelectNextMatch"
2030 );
2031 search_bar.update(cx, |search_bar, cx| {
2032 let all_selections =
2033 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2034 assert_eq!(
2035 all_selections.len(),
2036 1,
2037 "On next match, should deselect items and select the next match"
2038 );
2039 assert_ne!(
2040 all_selections, initial_selections,
2041 "Next match should be different from the first selection"
2042 );
2043 assert_eq!(
2044 search_bar.active_match_index,
2045 Some(1),
2046 "Match index should be updated to the next one"
2047 );
2048 let handle = search_bar.query_editor.focus_handle(cx);
2049 window.focus(&handle);
2050 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2051 });
2052 })
2053 .unwrap();
2054 window
2055 .update(cx, |_, window, cx| {
2056 assert!(
2057 editor.read(cx).is_focused(window),
2058 "Should focus editor after successful SelectAllMatches"
2059 );
2060 search_bar.update(cx, |search_bar, cx| {
2061 let all_selections =
2062 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2063 assert_eq!(
2064 all_selections.len(),
2065 expected_query_matches_count,
2066 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2067 );
2068 assert_eq!(
2069 search_bar.active_match_index,
2070 Some(1),
2071 "Match index should not change after selecting all matches"
2072 );
2073 });
2074 search_bar.update(cx, |search_bar, cx| {
2075 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2076 });
2077 })
2078 .unwrap();
2079 let last_match_selections = window
2080 .update(cx, |_, window, cx| {
2081 assert!(
2082 editor.read(cx).is_focused(window),
2083 "Should still have editor focused after SelectPreviousMatch"
2084 );
2085
2086 search_bar.update(cx, |search_bar, cx| {
2087 let all_selections =
2088 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2089 assert_eq!(
2090 all_selections.len(),
2091 1,
2092 "On previous match, should deselect items and select the previous item"
2093 );
2094 assert_eq!(
2095 all_selections, initial_selections,
2096 "Previous match should be the same as the first selection"
2097 );
2098 assert_eq!(
2099 search_bar.active_match_index,
2100 Some(0),
2101 "Match index should be updated to the previous one"
2102 );
2103 all_selections
2104 })
2105 })
2106 .unwrap();
2107
2108 window
2109 .update(cx, |_, window, cx| {
2110 search_bar.update(cx, |search_bar, cx| {
2111 let handle = search_bar.query_editor.focus_handle(cx);
2112 window.focus(&handle);
2113 search_bar.search("abas_nonexistent_match", None, window, cx)
2114 })
2115 })
2116 .unwrap()
2117 .await
2118 .unwrap();
2119 window
2120 .update(cx, |_, window, cx| {
2121 search_bar.update(cx, |search_bar, cx| {
2122 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2123 });
2124 assert!(
2125 editor.update(cx, |this, _cx| !this.is_focused(window)),
2126 "Should not switch focus to editor if SelectAllMatches does not find any matches"
2127 );
2128 search_bar.update(cx, |search_bar, cx| {
2129 let all_selections =
2130 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2131 assert_eq!(
2132 all_selections, last_match_selections,
2133 "Should not select anything new if there are no matches"
2134 );
2135 assert!(
2136 search_bar.active_match_index.is_none(),
2137 "For no matches, there should be no active match index"
2138 );
2139 });
2140 })
2141 .unwrap();
2142 }
2143
2144 #[gpui::test]
2145 async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2146 init_globals(cx);
2147 let buffer_text = r#"
2148 self.buffer.update(cx, |buffer, cx| {
2149 buffer.edit(
2150 edits,
2151 Some(AutoindentMode::Block {
2152 original_indent_columns,
2153 }),
2154 cx,
2155 )
2156 });
2157
2158 this.buffer.update(cx, |buffer, cx| {
2159 buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2160 });
2161 "#
2162 .unindent();
2163 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2164 let cx = cx.add_empty_window();
2165
2166 let editor =
2167 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2168
2169 let search_bar = cx.new_window_entity(|window, cx| {
2170 let mut search_bar = BufferSearchBar::new(None, window, cx);
2171 search_bar.set_active_pane_item(Some(&editor), window, cx);
2172 search_bar.show(window, cx);
2173 search_bar
2174 });
2175
2176 search_bar
2177 .update_in(cx, |search_bar, window, cx| {
2178 search_bar.search(
2179 "edit\\(",
2180 Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2181 window,
2182 cx,
2183 )
2184 })
2185 .await
2186 .unwrap();
2187
2188 search_bar.update_in(cx, |search_bar, window, cx| {
2189 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2190 });
2191 search_bar.update(cx, |_, cx| {
2192 let all_selections =
2193 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2194 assert_eq!(
2195 all_selections.len(),
2196 2,
2197 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2198 );
2199 });
2200
2201 search_bar
2202 .update_in(cx, |search_bar, window, cx| {
2203 search_bar.search(
2204 "edit(",
2205 Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2206 window,
2207 cx,
2208 )
2209 })
2210 .await
2211 .unwrap();
2212
2213 search_bar.update_in(cx, |search_bar, window, cx| {
2214 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2215 });
2216 search_bar.update(cx, |_, cx| {
2217 let all_selections =
2218 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2219 assert_eq!(
2220 all_selections.len(),
2221 2,
2222 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2223 );
2224 });
2225 }
2226
2227 #[gpui::test]
2228 async fn test_search_query_history(cx: &mut TestAppContext) {
2229 init_globals(cx);
2230 let buffer_text = r#"
2231 A regular expression (shortened as regex or regexp;[1] also referred to as
2232 rational expression[2][3]) is a sequence of characters that specifies a search
2233 pattern in text. Usually such patterns are used by string-searching algorithms
2234 for "find" or "find and replace" operations on strings, or for input validation.
2235 "#
2236 .unindent();
2237 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2238 let cx = cx.add_empty_window();
2239
2240 let editor =
2241 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2242
2243 let search_bar = cx.new_window_entity(|window, cx| {
2244 let mut search_bar = BufferSearchBar::new(None, window, cx);
2245 search_bar.set_active_pane_item(Some(&editor), window, cx);
2246 search_bar.show(window, cx);
2247 search_bar
2248 });
2249
2250 // Add 3 search items into the history.
2251 search_bar
2252 .update_in(cx, |search_bar, window, cx| {
2253 search_bar.search("a", None, window, cx)
2254 })
2255 .await
2256 .unwrap();
2257 search_bar
2258 .update_in(cx, |search_bar, window, cx| {
2259 search_bar.search("b", None, window, cx)
2260 })
2261 .await
2262 .unwrap();
2263 search_bar
2264 .update_in(cx, |search_bar, window, cx| {
2265 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), window, cx)
2266 })
2267 .await
2268 .unwrap();
2269 // Ensure that the latest search is active.
2270 search_bar.update(cx, |search_bar, cx| {
2271 assert_eq!(search_bar.query(cx), "c");
2272 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2273 });
2274
2275 // Next history query after the latest should set the query to the empty string.
2276 search_bar.update_in(cx, |search_bar, window, cx| {
2277 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2278 });
2279 search_bar.update(cx, |search_bar, cx| {
2280 assert_eq!(search_bar.query(cx), "");
2281 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2282 });
2283 search_bar.update_in(cx, |search_bar, window, cx| {
2284 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2285 });
2286 search_bar.update(cx, |search_bar, cx| {
2287 assert_eq!(search_bar.query(cx), "");
2288 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2289 });
2290
2291 // First previous query for empty current query should set the query to the latest.
2292 search_bar.update_in(cx, |search_bar, window, cx| {
2293 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2294 });
2295 search_bar.update(cx, |search_bar, cx| {
2296 assert_eq!(search_bar.query(cx), "c");
2297 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2298 });
2299
2300 // Further previous items should go over the history in reverse order.
2301 search_bar.update_in(cx, |search_bar, window, cx| {
2302 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2303 });
2304 search_bar.update(cx, |search_bar, cx| {
2305 assert_eq!(search_bar.query(cx), "b");
2306 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2307 });
2308
2309 // Previous items should never go behind the first history item.
2310 search_bar.update_in(cx, |search_bar, window, cx| {
2311 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2312 });
2313 search_bar.update(cx, |search_bar, cx| {
2314 assert_eq!(search_bar.query(cx), "a");
2315 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2316 });
2317 search_bar.update_in(cx, |search_bar, window, cx| {
2318 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2319 });
2320 search_bar.update(cx, |search_bar, cx| {
2321 assert_eq!(search_bar.query(cx), "a");
2322 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2323 });
2324
2325 // Next items should go over the history in the original order.
2326 search_bar.update_in(cx, |search_bar, window, cx| {
2327 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2328 });
2329 search_bar.update(cx, |search_bar, cx| {
2330 assert_eq!(search_bar.query(cx), "b");
2331 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2332 });
2333
2334 search_bar
2335 .update_in(cx, |search_bar, window, cx| {
2336 search_bar.search("ba", None, window, cx)
2337 })
2338 .await
2339 .unwrap();
2340 search_bar.update(cx, |search_bar, cx| {
2341 assert_eq!(search_bar.query(cx), "ba");
2342 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2343 });
2344
2345 // New search input should add another entry to history and move the selection to the end of the history.
2346 search_bar.update_in(cx, |search_bar, window, cx| {
2347 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2348 });
2349 search_bar.update(cx, |search_bar, cx| {
2350 assert_eq!(search_bar.query(cx), "c");
2351 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2352 });
2353 search_bar.update_in(cx, |search_bar, window, cx| {
2354 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2355 });
2356 search_bar.update(cx, |search_bar, cx| {
2357 assert_eq!(search_bar.query(cx), "b");
2358 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2359 });
2360 search_bar.update_in(cx, |search_bar, window, cx| {
2361 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2362 });
2363 search_bar.update(cx, |search_bar, cx| {
2364 assert_eq!(search_bar.query(cx), "c");
2365 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2366 });
2367 search_bar.update_in(cx, |search_bar, window, cx| {
2368 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2369 });
2370 search_bar.update(cx, |search_bar, cx| {
2371 assert_eq!(search_bar.query(cx), "ba");
2372 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2373 });
2374 search_bar.update_in(cx, |search_bar, window, cx| {
2375 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2376 });
2377 search_bar.update(cx, |search_bar, cx| {
2378 assert_eq!(search_bar.query(cx), "");
2379 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2380 });
2381 }
2382
2383 #[gpui::test]
2384 async fn test_replace_simple(cx: &mut TestAppContext) {
2385 let (editor, search_bar, cx) = init_test(cx);
2386
2387 search_bar
2388 .update_in(cx, |search_bar, window, cx| {
2389 search_bar.search("expression", None, window, cx)
2390 })
2391 .await
2392 .unwrap();
2393
2394 search_bar.update_in(cx, |search_bar, window, cx| {
2395 search_bar.replacement_editor.update(cx, |editor, cx| {
2396 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2397 editor.set_text("expr$1", window, cx);
2398 });
2399 search_bar.replace_all(&ReplaceAll, window, cx)
2400 });
2401 assert_eq!(
2402 editor.read_with(cx, |this, cx| { this.text(cx) }),
2403 r#"
2404 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2405 rational expr$1[2][3]) is a sequence of characters that specifies a search
2406 pattern in text. Usually such patterns are used by string-searching algorithms
2407 for "find" or "find and replace" operations on strings, or for input validation.
2408 "#
2409 .unindent()
2410 );
2411
2412 // Search for word boundaries and replace just a single one.
2413 search_bar
2414 .update_in(cx, |search_bar, window, cx| {
2415 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), window, cx)
2416 })
2417 .await
2418 .unwrap();
2419
2420 search_bar.update_in(cx, |search_bar, window, cx| {
2421 search_bar.replacement_editor.update(cx, |editor, cx| {
2422 editor.set_text("banana", window, cx);
2423 });
2424 search_bar.replace_next(&ReplaceNext, window, cx)
2425 });
2426 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2427 assert_eq!(
2428 editor.read_with(cx, |this, cx| { this.text(cx) }),
2429 r#"
2430 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2431 rational expr$1[2][3]) is a sequence of characters that specifies a search
2432 pattern in text. Usually such patterns are used by string-searching algorithms
2433 for "find" or "find and replace" operations on strings, or for input validation.
2434 "#
2435 .unindent()
2436 );
2437 // Let's turn on regex mode.
2438 search_bar
2439 .update_in(cx, |search_bar, window, cx| {
2440 search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), window, cx)
2441 })
2442 .await
2443 .unwrap();
2444 search_bar.update_in(cx, |search_bar, window, cx| {
2445 search_bar.replacement_editor.update(cx, |editor, cx| {
2446 editor.set_text("${1}number", window, cx);
2447 });
2448 search_bar.replace_all(&ReplaceAll, window, cx)
2449 });
2450 assert_eq!(
2451 editor.read_with(cx, |this, cx| { this.text(cx) }),
2452 r#"
2453 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2454 rational expr$12number3number) is a sequence of characters that specifies a search
2455 pattern in text. Usually such patterns are used by string-searching algorithms
2456 for "find" or "find and replace" operations on strings, or for input validation.
2457 "#
2458 .unindent()
2459 );
2460 // Now with a whole-word twist.
2461 search_bar
2462 .update_in(cx, |search_bar, window, cx| {
2463 search_bar.search(
2464 "a\\w+s",
2465 Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2466 window,
2467 cx,
2468 )
2469 })
2470 .await
2471 .unwrap();
2472 search_bar.update_in(cx, |search_bar, window, cx| {
2473 search_bar.replacement_editor.update(cx, |editor, cx| {
2474 editor.set_text("things", window, cx);
2475 });
2476 search_bar.replace_all(&ReplaceAll, window, cx)
2477 });
2478 // The only word affected by this edit should be `algorithms`, even though there's a bunch
2479 // of words in this text that would match this regex if not for WHOLE_WORD.
2480 assert_eq!(
2481 editor.read_with(cx, |this, cx| { this.text(cx) }),
2482 r#"
2483 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2484 rational expr$12number3number) is a sequence of characters that specifies a search
2485 pattern in text. Usually such patterns are used by string-searching things
2486 for "find" or "find and replace" operations on strings, or for input validation.
2487 "#
2488 .unindent()
2489 );
2490 }
2491
2492 struct ReplacementTestParams<'a> {
2493 editor: &'a Entity<Editor>,
2494 search_bar: &'a Entity<BufferSearchBar>,
2495 cx: &'a mut VisualTestContext,
2496 search_text: &'static str,
2497 search_options: Option<SearchOptions>,
2498 replacement_text: &'static str,
2499 replace_all: bool,
2500 expected_text: String,
2501 }
2502
2503 async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2504 options
2505 .search_bar
2506 .update_in(options.cx, |search_bar, window, cx| {
2507 if let Some(options) = options.search_options {
2508 search_bar.set_search_options(options, cx);
2509 }
2510 search_bar.search(options.search_text, options.search_options, window, cx)
2511 })
2512 .await
2513 .unwrap();
2514
2515 options
2516 .search_bar
2517 .update_in(options.cx, |search_bar, window, cx| {
2518 search_bar.replacement_editor.update(cx, |editor, cx| {
2519 editor.set_text(options.replacement_text, window, cx);
2520 });
2521
2522 if options.replace_all {
2523 search_bar.replace_all(&ReplaceAll, window, cx)
2524 } else {
2525 search_bar.replace_next(&ReplaceNext, window, cx)
2526 }
2527 });
2528
2529 assert_eq!(
2530 options
2531 .editor
2532 .read_with(options.cx, |this, cx| { this.text(cx) }),
2533 options.expected_text
2534 );
2535 }
2536
2537 #[gpui::test]
2538 async fn test_replace_special_characters(cx: &mut TestAppContext) {
2539 let (editor, search_bar, cx) = init_test(cx);
2540
2541 run_replacement_test(ReplacementTestParams {
2542 editor: &editor,
2543 search_bar: &search_bar,
2544 cx,
2545 search_text: "expression",
2546 search_options: None,
2547 replacement_text: r"\n",
2548 replace_all: true,
2549 expected_text: r#"
2550 A regular \n (shortened as regex or regexp;[1] also referred to as
2551 rational \n[2][3]) is a sequence of characters that specifies a search
2552 pattern in text. Usually such patterns are used by string-searching algorithms
2553 for "find" or "find and replace" operations on strings, or for input validation.
2554 "#
2555 .unindent(),
2556 })
2557 .await;
2558
2559 run_replacement_test(ReplacementTestParams {
2560 editor: &editor,
2561 search_bar: &search_bar,
2562 cx,
2563 search_text: "or",
2564 search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2565 replacement_text: r"\\\n\\\\",
2566 replace_all: false,
2567 expected_text: r#"
2568 A regular \n (shortened as regex \
2569 \\ regexp;[1] also referred to as
2570 rational \n[2][3]) is a sequence of characters that specifies a search
2571 pattern in text. Usually such patterns are used by string-searching algorithms
2572 for "find" or "find and replace" operations on strings, or for input validation.
2573 "#
2574 .unindent(),
2575 })
2576 .await;
2577
2578 run_replacement_test(ReplacementTestParams {
2579 editor: &editor,
2580 search_bar: &search_bar,
2581 cx,
2582 search_text: r"(that|used) ",
2583 search_options: Some(SearchOptions::REGEX),
2584 replacement_text: r"$1\n",
2585 replace_all: true,
2586 expected_text: r#"
2587 A regular \n (shortened as regex \
2588 \\ regexp;[1] also referred to as
2589 rational \n[2][3]) is a sequence of characters that
2590 specifies a search
2591 pattern in text. Usually such patterns are used
2592 by string-searching algorithms
2593 for "find" or "find and replace" operations on strings, or for input validation.
2594 "#
2595 .unindent(),
2596 })
2597 .await;
2598 }
2599
2600 #[gpui::test]
2601 async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2602 cx: &mut TestAppContext,
2603 ) {
2604 init_globals(cx);
2605 let buffer = cx.new(|cx| {
2606 Buffer::local(
2607 r#"
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 aaa bbb aaa ccc
2614 "#
2615 .unindent(),
2616 cx,
2617 )
2618 });
2619 let cx = cx.add_empty_window();
2620 let editor =
2621 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2622
2623 let search_bar = cx.new_window_entity(|window, cx| {
2624 let mut search_bar = BufferSearchBar::new(None, window, cx);
2625 search_bar.set_active_pane_item(Some(&editor), window, cx);
2626 search_bar.show(window, cx);
2627 search_bar
2628 });
2629
2630 editor.update_in(cx, |editor, window, cx| {
2631 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2632 s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2633 })
2634 });
2635
2636 search_bar.update_in(cx, |search_bar, window, cx| {
2637 let deploy = Deploy {
2638 focus: true,
2639 replace_enabled: false,
2640 selection_search_enabled: true,
2641 };
2642 search_bar.deploy(&deploy, window, cx);
2643 });
2644
2645 cx.run_until_parked();
2646
2647 search_bar
2648 .update_in(cx, |search_bar, window, cx| {
2649 search_bar.search("aaa", None, window, cx)
2650 })
2651 .await
2652 .unwrap();
2653
2654 editor.update(cx, |editor, cx| {
2655 assert_eq!(
2656 editor.search_background_highlights(cx),
2657 &[
2658 Point::new(1, 0)..Point::new(1, 3),
2659 Point::new(1, 8)..Point::new(1, 11),
2660 Point::new(2, 0)..Point::new(2, 3),
2661 ]
2662 );
2663 });
2664 }
2665
2666 #[gpui::test]
2667 async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2668 cx: &mut TestAppContext,
2669 ) {
2670 init_globals(cx);
2671 let text = r#"
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 aaa bbb aaa ccc
2678
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 aaa bbb aaa ccc
2685 "#
2686 .unindent();
2687
2688 let cx = cx.add_empty_window();
2689 let editor = cx.new_window_entity(|window, cx| {
2690 let multibuffer = MultiBuffer::build_multi(
2691 [
2692 (
2693 &text,
2694 vec![
2695 Point::new(0, 0)..Point::new(2, 0),
2696 Point::new(4, 0)..Point::new(5, 0),
2697 ],
2698 ),
2699 (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2700 ],
2701 cx,
2702 );
2703 Editor::for_multibuffer(multibuffer, None, window, cx)
2704 });
2705
2706 let search_bar = cx.new_window_entity(|window, cx| {
2707 let mut search_bar = BufferSearchBar::new(None, window, cx);
2708 search_bar.set_active_pane_item(Some(&editor), window, cx);
2709 search_bar.show(window, cx);
2710 search_bar
2711 });
2712
2713 editor.update_in(cx, |editor, window, cx| {
2714 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2715 s.select_ranges(vec![
2716 Point::new(1, 0)..Point::new(1, 4),
2717 Point::new(5, 3)..Point::new(6, 4),
2718 ])
2719 })
2720 });
2721
2722 search_bar.update_in(cx, |search_bar, window, cx| {
2723 let deploy = Deploy {
2724 focus: true,
2725 replace_enabled: false,
2726 selection_search_enabled: true,
2727 };
2728 search_bar.deploy(&deploy, window, cx);
2729 });
2730
2731 cx.run_until_parked();
2732
2733 search_bar
2734 .update_in(cx, |search_bar, window, cx| {
2735 search_bar.search("aaa", None, window, cx)
2736 })
2737 .await
2738 .unwrap();
2739
2740 editor.update(cx, |editor, cx| {
2741 assert_eq!(
2742 editor.search_background_highlights(cx),
2743 &[
2744 Point::new(1, 0)..Point::new(1, 3),
2745 Point::new(5, 8)..Point::new(5, 11),
2746 Point::new(6, 0)..Point::new(6, 3),
2747 ]
2748 );
2749 });
2750 }
2751
2752 #[gpui::test]
2753 async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2754 let (editor, search_bar, cx) = init_test(cx);
2755 // Search using valid regexp
2756 search_bar
2757 .update_in(cx, |search_bar, window, cx| {
2758 search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
2759 search_bar.search("expression", None, window, cx)
2760 })
2761 .await
2762 .unwrap();
2763 editor.update_in(cx, |editor, window, cx| {
2764 assert_eq!(
2765 display_points_of(editor.all_text_background_highlights(window, cx)),
2766 &[
2767 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2768 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2769 ],
2770 );
2771 });
2772
2773 // Now, the expression is invalid
2774 search_bar
2775 .update_in(cx, |search_bar, window, cx| {
2776 search_bar.search("expression (", None, window, cx)
2777 })
2778 .await
2779 .unwrap_err();
2780 editor.update_in(cx, |editor, window, cx| {
2781 assert!(
2782 display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
2783 );
2784 });
2785 }
2786
2787 #[gpui::test]
2788 async fn test_search_options_changes(cx: &mut TestAppContext) {
2789 let (_editor, search_bar, cx) = init_test(cx);
2790 update_search_settings(
2791 SearchSettings {
2792 button: true,
2793 whole_word: false,
2794 case_sensitive: false,
2795 include_ignored: false,
2796 regex: false,
2797 },
2798 cx,
2799 );
2800
2801 let deploy = Deploy {
2802 focus: true,
2803 replace_enabled: false,
2804 selection_search_enabled: true,
2805 };
2806
2807 search_bar.update_in(cx, |search_bar, window, cx| {
2808 assert_eq!(
2809 search_bar.search_options,
2810 SearchOptions::NONE,
2811 "Should have no search options enabled by default"
2812 );
2813 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2814 assert_eq!(
2815 search_bar.search_options,
2816 SearchOptions::WHOLE_WORD,
2817 "Should enable the option toggled"
2818 );
2819 assert!(
2820 !search_bar.dismissed,
2821 "Search bar should be present and visible"
2822 );
2823 search_bar.deploy(&deploy, window, cx);
2824 assert_eq!(
2825 search_bar.configured_options,
2826 SearchOptions::NONE,
2827 "Should have configured search options matching the settings"
2828 );
2829 assert_eq!(
2830 search_bar.search_options,
2831 SearchOptions::WHOLE_WORD,
2832 "After (re)deploying, the option should still be enabled"
2833 );
2834
2835 search_bar.dismiss(&Dismiss, window, cx);
2836 search_bar.deploy(&deploy, window, cx);
2837 assert_eq!(
2838 search_bar.search_options,
2839 SearchOptions::NONE,
2840 "After hiding and showing the search bar, default options should be used"
2841 );
2842
2843 search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
2844 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2845 assert_eq!(
2846 search_bar.search_options,
2847 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2848 "Should enable the options toggled"
2849 );
2850 assert!(
2851 !search_bar.dismissed,
2852 "Search bar should be present and visible"
2853 );
2854 });
2855
2856 update_search_settings(
2857 SearchSettings {
2858 button: true,
2859 whole_word: false,
2860 case_sensitive: true,
2861 include_ignored: false,
2862 regex: false,
2863 },
2864 cx,
2865 );
2866 search_bar.update_in(cx, |search_bar, window, cx| {
2867 assert_eq!(
2868 search_bar.search_options,
2869 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2870 "Should have no search options enabled by default"
2871 );
2872
2873 search_bar.deploy(&deploy, window, cx);
2874 assert_eq!(
2875 search_bar.configured_options,
2876 SearchOptions::CASE_SENSITIVE,
2877 "Should have configured search options matching the settings"
2878 );
2879 assert_eq!(
2880 search_bar.search_options,
2881 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2882 "Toggling a non-dismissed search bar with custom options should not change the default options"
2883 );
2884 search_bar.dismiss(&Dismiss, window, cx);
2885 search_bar.deploy(&deploy, window, cx);
2886 assert_eq!(
2887 search_bar.search_options,
2888 SearchOptions::CASE_SENSITIVE,
2889 "After hiding and showing the search bar, default options should be used"
2890 );
2891 });
2892 }
2893
2894 fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2895 cx.update(|cx| {
2896 SettingsStore::update_global(cx, |store, cx| {
2897 store.update_user_settings::<EditorSettings>(cx, |settings| {
2898 settings.search = Some(search_settings);
2899 });
2900 });
2901 });
2902 }
2903}