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