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