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