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