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