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