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