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