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