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 project::Project;
1536 use settings::{SearchSettingsContent, SettingsStore};
1537 use smol::stream::StreamExt as _;
1538 use unindent::Unindent as _;
1539 use util_macros::perf;
1540
1541 fn init_globals(cx: &mut TestAppContext) {
1542 cx.update(|cx| {
1543 let store = settings::SettingsStore::test(cx);
1544 cx.set_global(store);
1545 workspace::init_settings(cx);
1546 editor::init(cx);
1547
1548 language::init(cx);
1549 Project::init_settings(cx);
1550 theme::init(theme::LoadThemes::JustBase, cx);
1551 crate::init(cx);
1552 });
1553 }
1554
1555 fn init_test(
1556 cx: &mut TestAppContext,
1557 ) -> (
1558 Entity<Editor>,
1559 Entity<BufferSearchBar>,
1560 &mut VisualTestContext,
1561 ) {
1562 init_globals(cx);
1563 let buffer = cx.new(|cx| {
1564 Buffer::local(
1565 r#"
1566 A regular expression (shortened as regex or regexp;[1] also referred to as
1567 rational expression[2][3]) is a sequence of characters that specifies a search
1568 pattern in text. Usually such patterns are used by string-searching algorithms
1569 for "find" or "find and replace" operations on strings, or for input validation.
1570 "#
1571 .unindent(),
1572 cx,
1573 )
1574 });
1575 let mut editor = None;
1576 let window = cx.add_window(|window, cx| {
1577 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
1578 "keymaps/default-macos.json",
1579 cx,
1580 )
1581 .unwrap();
1582 cx.bind_keys(default_key_bindings);
1583 editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
1584 let mut search_bar = BufferSearchBar::new(None, window, cx);
1585 search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
1586 search_bar.show(window, cx);
1587 search_bar
1588 });
1589 let search_bar = window.root(cx).unwrap();
1590
1591 let cx = VisualTestContext::from_window(*window, cx).into_mut();
1592
1593 (editor.unwrap(), search_bar, cx)
1594 }
1595
1596 #[perf]
1597 #[gpui::test]
1598 async fn test_search_simple(cx: &mut TestAppContext) {
1599 let (editor, search_bar, cx) = init_test(cx);
1600 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1601 background_highlights
1602 .into_iter()
1603 .map(|(range, _)| range)
1604 .collect::<Vec<_>>()
1605 };
1606 // Search for a string that appears with different casing.
1607 // By default, search is case-insensitive.
1608 search_bar
1609 .update_in(cx, |search_bar, window, cx| {
1610 search_bar.search("us", None, true, window, cx)
1611 })
1612 .await
1613 .unwrap();
1614 editor.update_in(cx, |editor, window, cx| {
1615 assert_eq!(
1616 display_points_of(editor.all_text_background_highlights(window, cx)),
1617 &[
1618 DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1619 DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1620 ]
1621 );
1622 });
1623
1624 // Switch to a case sensitive search.
1625 search_bar.update_in(cx, |search_bar, window, cx| {
1626 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1627 });
1628 let mut editor_notifications = cx.notifications(&editor);
1629 editor_notifications.next().await;
1630 editor.update_in(cx, |editor, window, cx| {
1631 assert_eq!(
1632 display_points_of(editor.all_text_background_highlights(window, cx)),
1633 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1634 );
1635 });
1636
1637 // Search for a string that appears both as a whole word and
1638 // within other words. By default, all results are found.
1639 search_bar
1640 .update_in(cx, |search_bar, window, cx| {
1641 search_bar.search("or", None, true, window, cx)
1642 })
1643 .await
1644 .unwrap();
1645 editor.update_in(cx, |editor, window, cx| {
1646 assert_eq!(
1647 display_points_of(editor.all_text_background_highlights(window, cx)),
1648 &[
1649 DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1650 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1651 DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1652 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1653 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1654 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1655 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1656 ]
1657 );
1658 });
1659
1660 // Switch to a whole word search.
1661 search_bar.update_in(cx, |search_bar, window, cx| {
1662 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1663 });
1664 let mut editor_notifications = cx.notifications(&editor);
1665 editor_notifications.next().await;
1666 editor.update_in(cx, |editor, window, cx| {
1667 assert_eq!(
1668 display_points_of(editor.all_text_background_highlights(window, cx)),
1669 &[
1670 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1671 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1672 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1673 ]
1674 );
1675 });
1676
1677 editor.update_in(cx, |editor, window, cx| {
1678 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1679 s.select_display_ranges([
1680 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1681 ])
1682 });
1683 });
1684 search_bar.update_in(cx, |search_bar, window, cx| {
1685 assert_eq!(search_bar.active_match_index, Some(0));
1686 search_bar.select_next_match(&SelectNextMatch, window, cx);
1687 assert_eq!(
1688 editor.update(cx, |editor, cx| editor
1689 .selections
1690 .display_ranges(&editor.display_snapshot(cx))),
1691 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1692 );
1693 });
1694 search_bar.read_with(cx, |search_bar, _| {
1695 assert_eq!(search_bar.active_match_index, Some(0));
1696 });
1697
1698 search_bar.update_in(cx, |search_bar, window, cx| {
1699 search_bar.select_next_match(&SelectNextMatch, window, cx);
1700 assert_eq!(
1701 editor.update(cx, |editor, cx| editor
1702 .selections
1703 .display_ranges(&editor.display_snapshot(cx))),
1704 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1705 );
1706 });
1707 search_bar.read_with(cx, |search_bar, _| {
1708 assert_eq!(search_bar.active_match_index, Some(1));
1709 });
1710
1711 search_bar.update_in(cx, |search_bar, window, cx| {
1712 search_bar.select_next_match(&SelectNextMatch, window, cx);
1713 assert_eq!(
1714 editor.update(cx, |editor, cx| editor
1715 .selections
1716 .display_ranges(&editor.display_snapshot(cx))),
1717 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1718 );
1719 });
1720 search_bar.read_with(cx, |search_bar, _| {
1721 assert_eq!(search_bar.active_match_index, Some(2));
1722 });
1723
1724 search_bar.update_in(cx, |search_bar, window, cx| {
1725 search_bar.select_next_match(&SelectNextMatch, window, cx);
1726 assert_eq!(
1727 editor.update(cx, |editor, cx| editor
1728 .selections
1729 .display_ranges(&editor.display_snapshot(cx))),
1730 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1731 );
1732 });
1733 search_bar.read_with(cx, |search_bar, _| {
1734 assert_eq!(search_bar.active_match_index, Some(0));
1735 });
1736
1737 search_bar.update_in(cx, |search_bar, window, cx| {
1738 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1739 assert_eq!(
1740 editor.update(cx, |editor, cx| editor
1741 .selections
1742 .display_ranges(&editor.display_snapshot(cx))),
1743 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1744 );
1745 });
1746 search_bar.read_with(cx, |search_bar, _| {
1747 assert_eq!(search_bar.active_match_index, Some(2));
1748 });
1749
1750 search_bar.update_in(cx, |search_bar, window, cx| {
1751 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1752 assert_eq!(
1753 editor.update(cx, |editor, cx| editor
1754 .selections
1755 .display_ranges(&editor.display_snapshot(cx))),
1756 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1757 );
1758 });
1759 search_bar.read_with(cx, |search_bar, _| {
1760 assert_eq!(search_bar.active_match_index, Some(1));
1761 });
1762
1763 search_bar.update_in(cx, |search_bar, window, cx| {
1764 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1765 assert_eq!(
1766 editor.update(cx, |editor, cx| editor
1767 .selections
1768 .display_ranges(&editor.display_snapshot(cx))),
1769 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1770 );
1771 });
1772 search_bar.read_with(cx, |search_bar, _| {
1773 assert_eq!(search_bar.active_match_index, Some(0));
1774 });
1775
1776 // Park the cursor in between matches and ensure that going to the previous match selects
1777 // the closest match to the left.
1778 editor.update_in(cx, |editor, window, cx| {
1779 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1780 s.select_display_ranges([
1781 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1782 ])
1783 });
1784 });
1785 search_bar.update_in(cx, |search_bar, window, cx| {
1786 assert_eq!(search_bar.active_match_index, Some(1));
1787 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1788 assert_eq!(
1789 editor.update(cx, |editor, cx| editor
1790 .selections
1791 .display_ranges(&editor.display_snapshot(cx))),
1792 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1793 );
1794 });
1795 search_bar.read_with(cx, |search_bar, _| {
1796 assert_eq!(search_bar.active_match_index, Some(0));
1797 });
1798
1799 // Park the cursor in between matches and ensure that going to the next match selects the
1800 // closest match to the right.
1801 editor.update_in(cx, |editor, window, cx| {
1802 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1803 s.select_display_ranges([
1804 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1805 ])
1806 });
1807 });
1808 search_bar.update_in(cx, |search_bar, window, cx| {
1809 assert_eq!(search_bar.active_match_index, Some(1));
1810 search_bar.select_next_match(&SelectNextMatch, window, cx);
1811 assert_eq!(
1812 editor.update(cx, |editor, cx| editor
1813 .selections
1814 .display_ranges(&editor.display_snapshot(cx))),
1815 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1816 );
1817 });
1818 search_bar.read_with(cx, |search_bar, _| {
1819 assert_eq!(search_bar.active_match_index, Some(1));
1820 });
1821
1822 // Park the cursor after the last match and ensure that going to the previous match selects
1823 // the last match.
1824 editor.update_in(cx, |editor, window, cx| {
1825 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1826 s.select_display_ranges([
1827 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1828 ])
1829 });
1830 });
1831 search_bar.update_in(cx, |search_bar, window, cx| {
1832 assert_eq!(search_bar.active_match_index, Some(2));
1833 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1834 assert_eq!(
1835 editor.update(cx, |editor, cx| editor
1836 .selections
1837 .display_ranges(&editor.display_snapshot(cx))),
1838 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1839 );
1840 });
1841 search_bar.read_with(cx, |search_bar, _| {
1842 assert_eq!(search_bar.active_match_index, Some(2));
1843 });
1844
1845 // Park the cursor after the last match and ensure that going to the next match selects the
1846 // first match.
1847 editor.update_in(cx, |editor, window, cx| {
1848 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1849 s.select_display_ranges([
1850 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1851 ])
1852 });
1853 });
1854 search_bar.update_in(cx, |search_bar, window, cx| {
1855 assert_eq!(search_bar.active_match_index, Some(2));
1856 search_bar.select_next_match(&SelectNextMatch, window, cx);
1857 assert_eq!(
1858 editor.update(cx, |editor, cx| editor
1859 .selections
1860 .display_ranges(&editor.display_snapshot(cx))),
1861 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1862 );
1863 });
1864 search_bar.read_with(cx, |search_bar, _| {
1865 assert_eq!(search_bar.active_match_index, Some(0));
1866 });
1867
1868 // Park the cursor before the first match and ensure that going to the previous match
1869 // selects the last match.
1870 editor.update_in(cx, |editor, window, cx| {
1871 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1872 s.select_display_ranges([
1873 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1874 ])
1875 });
1876 });
1877 search_bar.update_in(cx, |search_bar, window, cx| {
1878 assert_eq!(search_bar.active_match_index, Some(0));
1879 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1880 assert_eq!(
1881 editor.update(cx, |editor, cx| editor
1882 .selections
1883 .display_ranges(&editor.display_snapshot(cx))),
1884 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1885 );
1886 });
1887 search_bar.read_with(cx, |search_bar, _| {
1888 assert_eq!(search_bar.active_match_index, Some(2));
1889 });
1890 }
1891
1892 fn display_points_of(
1893 background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1894 ) -> Vec<Range<DisplayPoint>> {
1895 background_highlights
1896 .into_iter()
1897 .map(|(range, _)| range)
1898 .collect::<Vec<_>>()
1899 }
1900
1901 #[perf]
1902 #[gpui::test]
1903 async fn test_search_option_handling(cx: &mut TestAppContext) {
1904 let (editor, search_bar, cx) = init_test(cx);
1905
1906 // show with options should make current search case sensitive
1907 search_bar
1908 .update_in(cx, |search_bar, window, cx| {
1909 search_bar.show(window, cx);
1910 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
1911 })
1912 .await
1913 .unwrap();
1914 editor.update_in(cx, |editor, window, cx| {
1915 assert_eq!(
1916 display_points_of(editor.all_text_background_highlights(window, cx)),
1917 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1918 );
1919 });
1920
1921 // search_suggested should restore default options
1922 search_bar.update_in(cx, |search_bar, window, cx| {
1923 search_bar.search_suggested(window, cx);
1924 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1925 });
1926
1927 // toggling a search option should update the defaults
1928 search_bar
1929 .update_in(cx, |search_bar, window, cx| {
1930 search_bar.search(
1931 "regex",
1932 Some(SearchOptions::CASE_SENSITIVE),
1933 true,
1934 window,
1935 cx,
1936 )
1937 })
1938 .await
1939 .unwrap();
1940 search_bar.update_in(cx, |search_bar, window, cx| {
1941 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1942 });
1943 let mut editor_notifications = cx.notifications(&editor);
1944 editor_notifications.next().await;
1945 editor.update_in(cx, |editor, window, cx| {
1946 assert_eq!(
1947 display_points_of(editor.all_text_background_highlights(window, cx)),
1948 &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1949 );
1950 });
1951
1952 // defaults should still include whole word
1953 search_bar.update_in(cx, |search_bar, window, cx| {
1954 search_bar.search_suggested(window, cx);
1955 assert_eq!(
1956 search_bar.search_options,
1957 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1958 )
1959 });
1960 }
1961
1962 #[perf]
1963 #[gpui::test]
1964 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1965 init_globals(cx);
1966 let buffer_text = r#"
1967 A regular expression (shortened as regex or regexp;[1] also referred to as
1968 rational expression[2][3]) is a sequence of characters that specifies a search
1969 pattern in text. Usually such patterns are used by string-searching algorithms
1970 for "find" or "find and replace" operations on strings, or for input validation.
1971 "#
1972 .unindent();
1973 let expected_query_matches_count = buffer_text
1974 .chars()
1975 .filter(|c| c.eq_ignore_ascii_case(&'a'))
1976 .count();
1977 assert!(
1978 expected_query_matches_count > 1,
1979 "Should pick a query with multiple results"
1980 );
1981 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
1982 let window = cx.add_window(|_, _| gpui::Empty);
1983
1984 let editor = window.build_entity(cx, |window, cx| {
1985 Editor::for_buffer(buffer.clone(), None, window, cx)
1986 });
1987
1988 let search_bar = window.build_entity(cx, |window, cx| {
1989 let mut search_bar = BufferSearchBar::new(None, window, cx);
1990 search_bar.set_active_pane_item(Some(&editor), window, cx);
1991 search_bar.show(window, cx);
1992 search_bar
1993 });
1994
1995 window
1996 .update(cx, |_, window, cx| {
1997 search_bar.update(cx, |search_bar, cx| {
1998 search_bar.search("a", None, true, window, cx)
1999 })
2000 })
2001 .unwrap()
2002 .await
2003 .unwrap();
2004 let initial_selections = window
2005 .update(cx, |_, window, cx| {
2006 search_bar.update(cx, |search_bar, cx| {
2007 let handle = search_bar.query_editor.focus_handle(cx);
2008 window.focus(&handle);
2009 search_bar.activate_current_match(window, cx);
2010 });
2011 assert!(
2012 !editor.read(cx).is_focused(window),
2013 "Initially, the editor should not be focused"
2014 );
2015 let initial_selections = editor.update(cx, |editor, cx| {
2016 let initial_selections = editor.selections.display_ranges(&editor.display_snapshot(cx));
2017 assert_eq!(
2018 initial_selections.len(), 1,
2019 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
2020 );
2021 initial_selections
2022 });
2023 search_bar.update(cx, |search_bar, cx| {
2024 assert_eq!(search_bar.active_match_index, Some(0));
2025 let handle = search_bar.query_editor.focus_handle(cx);
2026 window.focus(&handle);
2027 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2028 });
2029 assert!(
2030 editor.read(cx).is_focused(window),
2031 "Should focus editor after successful SelectAllMatches"
2032 );
2033 search_bar.update(cx, |search_bar, cx| {
2034 let all_selections =
2035 editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2036 assert_eq!(
2037 all_selections.len(),
2038 expected_query_matches_count,
2039 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2040 );
2041 assert_eq!(
2042 search_bar.active_match_index,
2043 Some(0),
2044 "Match index should not change after selecting all matches"
2045 );
2046 });
2047
2048 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
2049 initial_selections
2050 }).unwrap();
2051
2052 window
2053 .update(cx, |_, window, cx| {
2054 assert!(
2055 editor.read(cx).is_focused(window),
2056 "Should still have editor focused after SelectNextMatch"
2057 );
2058 search_bar.update(cx, |search_bar, cx| {
2059 let all_selections = editor.update(cx, |editor, cx| {
2060 editor
2061 .selections
2062 .display_ranges(&editor.display_snapshot(cx))
2063 });
2064 assert_eq!(
2065 all_selections.len(),
2066 1,
2067 "On next match, should deselect items and select the next match"
2068 );
2069 assert_ne!(
2070 all_selections, initial_selections,
2071 "Next match should be different from the first selection"
2072 );
2073 assert_eq!(
2074 search_bar.active_match_index,
2075 Some(1),
2076 "Match index should be updated to the next one"
2077 );
2078 let handle = search_bar.query_editor.focus_handle(cx);
2079 window.focus(&handle);
2080 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2081 });
2082 })
2083 .unwrap();
2084 window
2085 .update(cx, |_, window, cx| {
2086 assert!(
2087 editor.read(cx).is_focused(window),
2088 "Should focus editor after successful SelectAllMatches"
2089 );
2090 search_bar.update(cx, |search_bar, cx| {
2091 let all_selections =
2092 editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2093 assert_eq!(
2094 all_selections.len(),
2095 expected_query_matches_count,
2096 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2097 );
2098 assert_eq!(
2099 search_bar.active_match_index,
2100 Some(1),
2101 "Match index should not change after selecting all matches"
2102 );
2103 });
2104 search_bar.update(cx, |search_bar, cx| {
2105 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2106 });
2107 })
2108 .unwrap();
2109 let last_match_selections = window
2110 .update(cx, |_, window, cx| {
2111 assert!(
2112 editor.read(cx).is_focused(window),
2113 "Should still have editor focused after SelectPreviousMatch"
2114 );
2115
2116 search_bar.update(cx, |search_bar, cx| {
2117 let all_selections = editor.update(cx, |editor, cx| {
2118 editor
2119 .selections
2120 .display_ranges(&editor.display_snapshot(cx))
2121 });
2122 assert_eq!(
2123 all_selections.len(),
2124 1,
2125 "On previous match, should deselect items and select the previous item"
2126 );
2127 assert_eq!(
2128 all_selections, initial_selections,
2129 "Previous match should be the same as the first selection"
2130 );
2131 assert_eq!(
2132 search_bar.active_match_index,
2133 Some(0),
2134 "Match index should be updated to the previous one"
2135 );
2136 all_selections
2137 })
2138 })
2139 .unwrap();
2140
2141 window
2142 .update(cx, |_, window, cx| {
2143 search_bar.update(cx, |search_bar, cx| {
2144 let handle = search_bar.query_editor.focus_handle(cx);
2145 window.focus(&handle);
2146 search_bar.search("abas_nonexistent_match", None, true, window, cx)
2147 })
2148 })
2149 .unwrap()
2150 .await
2151 .unwrap();
2152 window
2153 .update(cx, |_, window, cx| {
2154 search_bar.update(cx, |search_bar, cx| {
2155 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2156 });
2157 assert!(
2158 editor.update(cx, |this, _cx| !this.is_focused(window)),
2159 "Should not switch focus to editor if SelectAllMatches does not find any matches"
2160 );
2161 search_bar.update(cx, |search_bar, cx| {
2162 let all_selections =
2163 editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2164 assert_eq!(
2165 all_selections, last_match_selections,
2166 "Should not select anything new if there are no matches"
2167 );
2168 assert!(
2169 search_bar.active_match_index.is_none(),
2170 "For no matches, there should be no active match index"
2171 );
2172 });
2173 })
2174 .unwrap();
2175 }
2176
2177 #[perf]
2178 #[gpui::test]
2179 async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2180 init_globals(cx);
2181 let buffer_text = r#"
2182 self.buffer.update(cx, |buffer, cx| {
2183 buffer.edit(
2184 edits,
2185 Some(AutoindentMode::Block {
2186 original_indent_columns,
2187 }),
2188 cx,
2189 )
2190 });
2191
2192 this.buffer.update(cx, |buffer, cx| {
2193 buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2194 });
2195 "#
2196 .unindent();
2197 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2198 let cx = cx.add_empty_window();
2199
2200 let editor =
2201 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2202
2203 let search_bar = cx.new_window_entity(|window, cx| {
2204 let mut search_bar = BufferSearchBar::new(None, window, cx);
2205 search_bar.set_active_pane_item(Some(&editor), window, cx);
2206 search_bar.show(window, cx);
2207 search_bar
2208 });
2209
2210 search_bar
2211 .update_in(cx, |search_bar, window, cx| {
2212 search_bar.search(
2213 "edit\\(",
2214 Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2215 true,
2216 window,
2217 cx,
2218 )
2219 })
2220 .await
2221 .unwrap();
2222
2223 search_bar.update_in(cx, |search_bar, window, cx| {
2224 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2225 });
2226 search_bar.update(cx, |_, cx| {
2227 let all_selections = editor.update(cx, |editor, cx| {
2228 editor
2229 .selections
2230 .display_ranges(&editor.display_snapshot(cx))
2231 });
2232 assert_eq!(
2233 all_selections.len(),
2234 2,
2235 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2236 );
2237 });
2238
2239 search_bar
2240 .update_in(cx, |search_bar, window, cx| {
2241 search_bar.search(
2242 "edit(",
2243 Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2244 true,
2245 window,
2246 cx,
2247 )
2248 })
2249 .await
2250 .unwrap();
2251
2252 search_bar.update_in(cx, |search_bar, window, cx| {
2253 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2254 });
2255 search_bar.update(cx, |_, cx| {
2256 let all_selections = editor.update(cx, |editor, cx| {
2257 editor
2258 .selections
2259 .display_ranges(&editor.display_snapshot(cx))
2260 });
2261 assert_eq!(
2262 all_selections.len(),
2263 2,
2264 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2265 );
2266 });
2267 }
2268
2269 #[perf]
2270 #[gpui::test]
2271 async fn test_search_query_history(cx: &mut TestAppContext) {
2272 let (_editor, search_bar, cx) = init_test(cx);
2273
2274 // Add 3 search items into the history.
2275 search_bar
2276 .update_in(cx, |search_bar, window, cx| {
2277 search_bar.search("a", None, true, window, cx)
2278 })
2279 .await
2280 .unwrap();
2281 search_bar
2282 .update_in(cx, |search_bar, window, cx| {
2283 search_bar.search("b", None, true, window, cx)
2284 })
2285 .await
2286 .unwrap();
2287 search_bar
2288 .update_in(cx, |search_bar, window, cx| {
2289 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2290 })
2291 .await
2292 .unwrap();
2293 // Ensure that the latest search is active.
2294 search_bar.update(cx, |search_bar, cx| {
2295 assert_eq!(search_bar.query(cx), "c");
2296 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2297 });
2298
2299 // Next history query after the latest should set the query to the empty string.
2300 search_bar.update_in(cx, |search_bar, window, cx| {
2301 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2302 });
2303 cx.background_executor.run_until_parked();
2304 search_bar.update(cx, |search_bar, cx| {
2305 assert_eq!(search_bar.query(cx), "");
2306 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2307 });
2308 search_bar.update_in(cx, |search_bar, window, cx| {
2309 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2310 });
2311 cx.background_executor.run_until_parked();
2312 search_bar.update(cx, |search_bar, cx| {
2313 assert_eq!(search_bar.query(cx), "");
2314 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2315 });
2316
2317 // First previous query for empty current query should set the query to the latest.
2318 search_bar.update_in(cx, |search_bar, window, cx| {
2319 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2320 });
2321 cx.background_executor.run_until_parked();
2322 search_bar.update(cx, |search_bar, cx| {
2323 assert_eq!(search_bar.query(cx), "c");
2324 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2325 });
2326
2327 // Further previous items should go over the history in reverse order.
2328 search_bar.update_in(cx, |search_bar, window, cx| {
2329 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2330 });
2331 cx.background_executor.run_until_parked();
2332 search_bar.update(cx, |search_bar, cx| {
2333 assert_eq!(search_bar.query(cx), "b");
2334 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2335 });
2336
2337 // Previous items should never go behind the first history item.
2338 search_bar.update_in(cx, |search_bar, window, cx| {
2339 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2340 });
2341 cx.background_executor.run_until_parked();
2342 search_bar.update(cx, |search_bar, cx| {
2343 assert_eq!(search_bar.query(cx), "a");
2344 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2345 });
2346 search_bar.update_in(cx, |search_bar, window, cx| {
2347 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2348 });
2349 cx.background_executor.run_until_parked();
2350 search_bar.update(cx, |search_bar, cx| {
2351 assert_eq!(search_bar.query(cx), "a");
2352 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2353 });
2354
2355 // Next items should go over the history in the original order.
2356 search_bar.update_in(cx, |search_bar, window, cx| {
2357 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2358 });
2359 cx.background_executor.run_until_parked();
2360 search_bar.update(cx, |search_bar, cx| {
2361 assert_eq!(search_bar.query(cx), "b");
2362 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2363 });
2364
2365 search_bar
2366 .update_in(cx, |search_bar, window, cx| {
2367 search_bar.search("ba", None, true, window, cx)
2368 })
2369 .await
2370 .unwrap();
2371 search_bar.update(cx, |search_bar, cx| {
2372 assert_eq!(search_bar.query(cx), "ba");
2373 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2374 });
2375
2376 // New search input should add another entry to history and move the selection to the end of the history.
2377 search_bar.update_in(cx, |search_bar, window, cx| {
2378 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2379 });
2380 cx.background_executor.run_until_parked();
2381 search_bar.update(cx, |search_bar, cx| {
2382 assert_eq!(search_bar.query(cx), "c");
2383 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2384 });
2385 search_bar.update_in(cx, |search_bar, window, cx| {
2386 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2387 });
2388 cx.background_executor.run_until_parked();
2389 search_bar.update(cx, |search_bar, cx| {
2390 assert_eq!(search_bar.query(cx), "b");
2391 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2392 });
2393 search_bar.update_in(cx, |search_bar, window, cx| {
2394 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2395 });
2396 cx.background_executor.run_until_parked();
2397 search_bar.update(cx, |search_bar, cx| {
2398 assert_eq!(search_bar.query(cx), "c");
2399 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2400 });
2401 search_bar.update_in(cx, |search_bar, window, cx| {
2402 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2403 });
2404 cx.background_executor.run_until_parked();
2405 search_bar.update(cx, |search_bar, cx| {
2406 assert_eq!(search_bar.query(cx), "ba");
2407 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2408 });
2409 search_bar.update_in(cx, |search_bar, window, cx| {
2410 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2411 });
2412 cx.background_executor.run_until_parked();
2413 search_bar.update(cx, |search_bar, cx| {
2414 assert_eq!(search_bar.query(cx), "");
2415 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2416 });
2417 }
2418
2419 #[perf]
2420 #[gpui::test]
2421 async fn test_replace_simple(cx: &mut TestAppContext) {
2422 let (editor, search_bar, cx) = init_test(cx);
2423
2424 search_bar
2425 .update_in(cx, |search_bar, window, cx| {
2426 search_bar.search("expression", None, true, window, cx)
2427 })
2428 .await
2429 .unwrap();
2430
2431 search_bar.update_in(cx, |search_bar, window, cx| {
2432 search_bar.replacement_editor.update(cx, |editor, cx| {
2433 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2434 editor.set_text("expr$1", window, cx);
2435 });
2436 search_bar.replace_all(&ReplaceAll, window, cx)
2437 });
2438 assert_eq!(
2439 editor.read_with(cx, |this, cx| { this.text(cx) }),
2440 r#"
2441 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2442 rational expr$1[2][3]) is a sequence of characters that specifies a search
2443 pattern in text. Usually such patterns are used by string-searching algorithms
2444 for "find" or "find and replace" operations on strings, or for input validation.
2445 "#
2446 .unindent()
2447 );
2448
2449 // Search for word boundaries and replace just a single one.
2450 search_bar
2451 .update_in(cx, |search_bar, window, cx| {
2452 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx)
2453 })
2454 .await
2455 .unwrap();
2456
2457 search_bar.update_in(cx, |search_bar, window, cx| {
2458 search_bar.replacement_editor.update(cx, |editor, cx| {
2459 editor.set_text("banana", window, cx);
2460 });
2461 search_bar.replace_next(&ReplaceNext, window, cx)
2462 });
2463 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2464 assert_eq!(
2465 editor.read_with(cx, |this, cx| { this.text(cx) }),
2466 r#"
2467 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2468 rational expr$1[2][3]) is a sequence of characters that specifies a search
2469 pattern in text. Usually such patterns are used by string-searching algorithms
2470 for "find" or "find and replace" operations on strings, or for input validation.
2471 "#
2472 .unindent()
2473 );
2474 // Let's turn on regex mode.
2475 search_bar
2476 .update_in(cx, |search_bar, window, cx| {
2477 search_bar.search(
2478 "\\[([^\\]]+)\\]",
2479 Some(SearchOptions::REGEX),
2480 true,
2481 window,
2482 cx,
2483 )
2484 })
2485 .await
2486 .unwrap();
2487 search_bar.update_in(cx, |search_bar, window, cx| {
2488 search_bar.replacement_editor.update(cx, |editor, cx| {
2489 editor.set_text("${1}number", window, cx);
2490 });
2491 search_bar.replace_all(&ReplaceAll, window, cx)
2492 });
2493 assert_eq!(
2494 editor.read_with(cx, |this, cx| { this.text(cx) }),
2495 r#"
2496 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2497 rational expr$12number3number) is a sequence of characters that specifies a search
2498 pattern in text. Usually such patterns are used by string-searching algorithms
2499 for "find" or "find and replace" operations on strings, or for input validation.
2500 "#
2501 .unindent()
2502 );
2503 // Now with a whole-word twist.
2504 search_bar
2505 .update_in(cx, |search_bar, window, cx| {
2506 search_bar.search(
2507 "a\\w+s",
2508 Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2509 true,
2510 window,
2511 cx,
2512 )
2513 })
2514 .await
2515 .unwrap();
2516 search_bar.update_in(cx, |search_bar, window, cx| {
2517 search_bar.replacement_editor.update(cx, |editor, cx| {
2518 editor.set_text("things", window, cx);
2519 });
2520 search_bar.replace_all(&ReplaceAll, window, cx)
2521 });
2522 // The only word affected by this edit should be `algorithms`, even though there's a bunch
2523 // of words in this text that would match this regex if not for WHOLE_WORD.
2524 assert_eq!(
2525 editor.read_with(cx, |this, cx| { this.text(cx) }),
2526 r#"
2527 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2528 rational expr$12number3number) is a sequence of characters that specifies a search
2529 pattern in text. Usually such patterns are used by string-searching things
2530 for "find" or "find and replace" operations on strings, or for input validation.
2531 "#
2532 .unindent()
2533 );
2534 }
2535
2536 struct ReplacementTestParams<'a> {
2537 editor: &'a Entity<Editor>,
2538 search_bar: &'a Entity<BufferSearchBar>,
2539 cx: &'a mut VisualTestContext,
2540 search_text: &'static str,
2541 search_options: Option<SearchOptions>,
2542 replacement_text: &'static str,
2543 replace_all: bool,
2544 expected_text: String,
2545 }
2546
2547 async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2548 options
2549 .search_bar
2550 .update_in(options.cx, |search_bar, window, cx| {
2551 if let Some(options) = options.search_options {
2552 search_bar.set_search_options(options, cx);
2553 }
2554 search_bar.search(
2555 options.search_text,
2556 options.search_options,
2557 true,
2558 window,
2559 cx,
2560 )
2561 })
2562 .await
2563 .unwrap();
2564
2565 options
2566 .search_bar
2567 .update_in(options.cx, |search_bar, window, cx| {
2568 search_bar.replacement_editor.update(cx, |editor, cx| {
2569 editor.set_text(options.replacement_text, window, cx);
2570 });
2571
2572 if options.replace_all {
2573 search_bar.replace_all(&ReplaceAll, window, cx)
2574 } else {
2575 search_bar.replace_next(&ReplaceNext, window, cx)
2576 }
2577 });
2578
2579 assert_eq!(
2580 options
2581 .editor
2582 .read_with(options.cx, |this, cx| { this.text(cx) }),
2583 options.expected_text
2584 );
2585 }
2586
2587 #[perf]
2588 #[gpui::test]
2589 async fn test_replace_special_characters(cx: &mut TestAppContext) {
2590 let (editor, search_bar, cx) = init_test(cx);
2591
2592 run_replacement_test(ReplacementTestParams {
2593 editor: &editor,
2594 search_bar: &search_bar,
2595 cx,
2596 search_text: "expression",
2597 search_options: None,
2598 replacement_text: r"\n",
2599 replace_all: true,
2600 expected_text: r#"
2601 A regular \n (shortened as regex or regexp;[1] also referred to as
2602 rational \n[2][3]) is a sequence of characters that specifies a search
2603 pattern in text. Usually such patterns are used by string-searching algorithms
2604 for "find" or "find and replace" operations on strings, or for input validation.
2605 "#
2606 .unindent(),
2607 })
2608 .await;
2609
2610 run_replacement_test(ReplacementTestParams {
2611 editor: &editor,
2612 search_bar: &search_bar,
2613 cx,
2614 search_text: "or",
2615 search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2616 replacement_text: r"\\\n\\\\",
2617 replace_all: false,
2618 expected_text: r#"
2619 A regular \n (shortened as regex \
2620 \\ regexp;[1] also referred to as
2621 rational \n[2][3]) is a sequence of characters that specifies a search
2622 pattern in text. Usually such patterns are used by string-searching algorithms
2623 for "find" or "find and replace" operations on strings, or for input validation.
2624 "#
2625 .unindent(),
2626 })
2627 .await;
2628
2629 run_replacement_test(ReplacementTestParams {
2630 editor: &editor,
2631 search_bar: &search_bar,
2632 cx,
2633 search_text: r"(that|used) ",
2634 search_options: Some(SearchOptions::REGEX),
2635 replacement_text: r"$1\n",
2636 replace_all: true,
2637 expected_text: r#"
2638 A regular \n (shortened as regex \
2639 \\ regexp;[1] also referred to as
2640 rational \n[2][3]) is a sequence of characters that
2641 specifies a search
2642 pattern in text. Usually such patterns are used
2643 by string-searching algorithms
2644 for "find" or "find and replace" operations on strings, or for input validation.
2645 "#
2646 .unindent(),
2647 })
2648 .await;
2649 }
2650
2651 #[perf]
2652 #[gpui::test]
2653 async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2654 cx: &mut TestAppContext,
2655 ) {
2656 init_globals(cx);
2657 let buffer = cx.new(|cx| {
2658 Buffer::local(
2659 r#"
2660 aaa bbb aaa ccc
2661 aaa bbb aaa ccc
2662 aaa bbb aaa ccc
2663 aaa bbb aaa ccc
2664 aaa bbb aaa ccc
2665 aaa bbb aaa ccc
2666 "#
2667 .unindent(),
2668 cx,
2669 )
2670 });
2671 let cx = cx.add_empty_window();
2672 let editor =
2673 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2674
2675 let search_bar = cx.new_window_entity(|window, cx| {
2676 let mut search_bar = BufferSearchBar::new(None, window, cx);
2677 search_bar.set_active_pane_item(Some(&editor), window, cx);
2678 search_bar.show(window, cx);
2679 search_bar
2680 });
2681
2682 editor.update_in(cx, |editor, window, cx| {
2683 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2684 s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2685 })
2686 });
2687
2688 search_bar.update_in(cx, |search_bar, window, cx| {
2689 let deploy = Deploy {
2690 focus: true,
2691 replace_enabled: false,
2692 selection_search_enabled: true,
2693 };
2694 search_bar.deploy(&deploy, window, cx);
2695 });
2696
2697 cx.run_until_parked();
2698
2699 search_bar
2700 .update_in(cx, |search_bar, window, cx| {
2701 search_bar.search("aaa", None, true, window, cx)
2702 })
2703 .await
2704 .unwrap();
2705
2706 editor.update(cx, |editor, cx| {
2707 assert_eq!(
2708 editor.search_background_highlights(cx),
2709 &[
2710 Point::new(1, 0)..Point::new(1, 3),
2711 Point::new(1, 8)..Point::new(1, 11),
2712 Point::new(2, 0)..Point::new(2, 3),
2713 ]
2714 );
2715 });
2716 }
2717
2718 #[perf]
2719 #[gpui::test]
2720 async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2721 cx: &mut TestAppContext,
2722 ) {
2723 init_globals(cx);
2724 let text = r#"
2725 aaa bbb aaa ccc
2726 aaa bbb aaa ccc
2727 aaa bbb aaa ccc
2728 aaa bbb aaa ccc
2729 aaa bbb aaa ccc
2730 aaa bbb aaa ccc
2731
2732 aaa bbb aaa ccc
2733 aaa bbb aaa ccc
2734 aaa bbb aaa ccc
2735 aaa bbb aaa ccc
2736 aaa bbb aaa ccc
2737 aaa bbb aaa ccc
2738 "#
2739 .unindent();
2740
2741 let cx = cx.add_empty_window();
2742 let editor = cx.new_window_entity(|window, cx| {
2743 let multibuffer = MultiBuffer::build_multi(
2744 [
2745 (
2746 &text,
2747 vec![
2748 Point::new(0, 0)..Point::new(2, 0),
2749 Point::new(4, 0)..Point::new(5, 0),
2750 ],
2751 ),
2752 (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2753 ],
2754 cx,
2755 );
2756 Editor::for_multibuffer(multibuffer, None, window, cx)
2757 });
2758
2759 let search_bar = cx.new_window_entity(|window, cx| {
2760 let mut search_bar = BufferSearchBar::new(None, window, cx);
2761 search_bar.set_active_pane_item(Some(&editor), window, cx);
2762 search_bar.show(window, cx);
2763 search_bar
2764 });
2765
2766 editor.update_in(cx, |editor, window, cx| {
2767 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2768 s.select_ranges(vec![
2769 Point::new(1, 0)..Point::new(1, 4),
2770 Point::new(5, 3)..Point::new(6, 4),
2771 ])
2772 })
2773 });
2774
2775 search_bar.update_in(cx, |search_bar, window, cx| {
2776 let deploy = Deploy {
2777 focus: true,
2778 replace_enabled: false,
2779 selection_search_enabled: true,
2780 };
2781 search_bar.deploy(&deploy, window, cx);
2782 });
2783
2784 cx.run_until_parked();
2785
2786 search_bar
2787 .update_in(cx, |search_bar, window, cx| {
2788 search_bar.search("aaa", None, true, window, cx)
2789 })
2790 .await
2791 .unwrap();
2792
2793 editor.update(cx, |editor, cx| {
2794 assert_eq!(
2795 editor.search_background_highlights(cx),
2796 &[
2797 Point::new(1, 0)..Point::new(1, 3),
2798 Point::new(5, 8)..Point::new(5, 11),
2799 Point::new(6, 0)..Point::new(6, 3),
2800 ]
2801 );
2802 });
2803 }
2804
2805 #[perf]
2806 #[gpui::test]
2807 async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2808 let (editor, search_bar, cx) = init_test(cx);
2809 // Search using valid regexp
2810 search_bar
2811 .update_in(cx, |search_bar, window, cx| {
2812 search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
2813 search_bar.search("expression", None, true, window, cx)
2814 })
2815 .await
2816 .unwrap();
2817 editor.update_in(cx, |editor, window, cx| {
2818 assert_eq!(
2819 display_points_of(editor.all_text_background_highlights(window, cx)),
2820 &[
2821 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2822 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2823 ],
2824 );
2825 });
2826
2827 // Now, the expression is invalid
2828 search_bar
2829 .update_in(cx, |search_bar, window, cx| {
2830 search_bar.search("expression (", None, true, window, cx)
2831 })
2832 .await
2833 .unwrap_err();
2834 editor.update_in(cx, |editor, window, cx| {
2835 assert!(
2836 display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
2837 );
2838 });
2839 }
2840
2841 #[perf]
2842 #[gpui::test]
2843 async fn test_search_options_changes(cx: &mut TestAppContext) {
2844 let (_editor, search_bar, cx) = init_test(cx);
2845 update_search_settings(
2846 SearchSettings {
2847 button: true,
2848 whole_word: false,
2849 case_sensitive: false,
2850 include_ignored: false,
2851 regex: false,
2852 center_on_match: false,
2853 },
2854 cx,
2855 );
2856
2857 let deploy = Deploy {
2858 focus: true,
2859 replace_enabled: false,
2860 selection_search_enabled: true,
2861 };
2862
2863 search_bar.update_in(cx, |search_bar, window, cx| {
2864 assert_eq!(
2865 search_bar.search_options,
2866 SearchOptions::NONE,
2867 "Should have no search options enabled by default"
2868 );
2869 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2870 assert_eq!(
2871 search_bar.search_options,
2872 SearchOptions::WHOLE_WORD,
2873 "Should enable the option toggled"
2874 );
2875 assert!(
2876 !search_bar.dismissed,
2877 "Search bar should be present and visible"
2878 );
2879 search_bar.deploy(&deploy, window, cx);
2880 assert_eq!(
2881 search_bar.search_options,
2882 SearchOptions::WHOLE_WORD,
2883 "After (re)deploying, the option should still be enabled"
2884 );
2885
2886 search_bar.dismiss(&Dismiss, window, cx);
2887 search_bar.deploy(&deploy, window, cx);
2888 assert_eq!(
2889 search_bar.search_options,
2890 SearchOptions::WHOLE_WORD,
2891 "After hiding and showing the search bar, search options should be preserved"
2892 );
2893
2894 search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
2895 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2896 assert_eq!(
2897 search_bar.search_options,
2898 SearchOptions::REGEX,
2899 "Should enable the options toggled"
2900 );
2901 assert!(
2902 !search_bar.dismissed,
2903 "Search bar should be present and visible"
2904 );
2905 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2906 });
2907
2908 update_search_settings(
2909 SearchSettings {
2910 button: true,
2911 whole_word: false,
2912 case_sensitive: true,
2913 include_ignored: false,
2914 regex: false,
2915 center_on_match: false,
2916 },
2917 cx,
2918 );
2919 search_bar.update_in(cx, |search_bar, window, cx| {
2920 assert_eq!(
2921 search_bar.search_options,
2922 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2923 "Should have no search options enabled by default"
2924 );
2925
2926 search_bar.deploy(&deploy, window, cx);
2927 assert_eq!(
2928 search_bar.search_options,
2929 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2930 "Toggling a non-dismissed search bar with custom options should not change the default options"
2931 );
2932 search_bar.dismiss(&Dismiss, window, cx);
2933 search_bar.deploy(&deploy, window, cx);
2934 assert_eq!(
2935 search_bar.configured_options,
2936 SearchOptions::CASE_SENSITIVE,
2937 "After a settings update and toggling the search bar, configured options should be updated"
2938 );
2939 assert_eq!(
2940 search_bar.search_options,
2941 SearchOptions::CASE_SENSITIVE,
2942 "After a settings update and toggling the search bar, configured options should be used"
2943 );
2944 });
2945
2946 update_search_settings(
2947 SearchSettings {
2948 button: true,
2949 whole_word: true,
2950 case_sensitive: true,
2951 include_ignored: false,
2952 regex: false,
2953 center_on_match: false,
2954 },
2955 cx,
2956 );
2957
2958 search_bar.update_in(cx, |search_bar, window, cx| {
2959 search_bar.deploy(&deploy, window, cx);
2960 search_bar.dismiss(&Dismiss, window, cx);
2961 search_bar.show(window, cx);
2962 assert_eq!(
2963 search_bar.search_options,
2964 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
2965 "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
2966 );
2967 });
2968 }
2969
2970 fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2971 cx.update(|cx| {
2972 SettingsStore::update_global(cx, |store, cx| {
2973 store.update_user_settings(cx, |settings| {
2974 settings.editor.search = Some(SearchSettingsContent {
2975 button: Some(search_settings.button),
2976 whole_word: Some(search_settings.whole_word),
2977 case_sensitive: Some(search_settings.case_sensitive),
2978 include_ignored: Some(search_settings.include_ignored),
2979 regex: Some(search_settings.regex),
2980 center_on_match: Some(search_settings.center_on_match),
2981 });
2982 });
2983 });
2984 });
2985 }
2986}