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