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