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