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