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;
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 if self.search_options.contains(SearchOptions::REGEX) {
1218 match SearchQuery::regex(
1219 query,
1220 self.search_options.contains(SearchOptions::WHOLE_WORD),
1221 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1222 false,
1223 self.search_options
1224 .contains(SearchOptions::ONE_MATCH_PER_LINE),
1225 Default::default(),
1226 Default::default(),
1227 false,
1228 None,
1229 ) {
1230 Ok(query) => query.with_replacement(self.replacement(cx)),
1231 Err(e) => {
1232 self.query_error = Some(e.to_string());
1233 self.clear_active_searchable_item_matches(window, cx);
1234 cx.notify();
1235 return done_rx;
1236 }
1237 }
1238 } else {
1239 match SearchQuery::text(
1240 query,
1241 self.search_options.contains(SearchOptions::WHOLE_WORD),
1242 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1243 false,
1244 Default::default(),
1245 Default::default(),
1246 false,
1247 None,
1248 ) {
1249 Ok(query) => query.with_replacement(self.replacement(cx)),
1250 Err(e) => {
1251 self.query_error = Some(e.to_string());
1252 self.clear_active_searchable_item_matches(window, cx);
1253 cx.notify();
1254 return done_rx;
1255 }
1256 }
1257 }
1258 .into()
1259 };
1260
1261 self.active_search = Some(query.clone());
1262 let query_text = query.as_str().to_string();
1263
1264 let matches = active_searchable_item.find_matches(query, window, cx);
1265
1266 let active_searchable_item = active_searchable_item.downgrade();
1267 self.pending_search = Some(cx.spawn_in(window, async move |this, cx| {
1268 let matches = matches.await;
1269
1270 this.update_in(cx, |this, window, cx| {
1271 if let Some(active_searchable_item) =
1272 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1273 {
1274 this.searchable_items_with_matches
1275 .insert(active_searchable_item.downgrade(), matches);
1276
1277 this.update_match_index(window, cx);
1278 if add_to_history {
1279 this.search_history
1280 .add(&mut this.search_history_cursor, query_text);
1281 }
1282 if !this.dismissed {
1283 let matches = this
1284 .searchable_items_with_matches
1285 .get(&active_searchable_item.downgrade())
1286 .unwrap();
1287 if matches.is_empty() {
1288 active_searchable_item.clear_matches(window, cx);
1289 } else {
1290 active_searchable_item.update_matches(matches, window, cx);
1291 }
1292 let _ = done_tx.send(());
1293 }
1294 cx.notify();
1295 }
1296 })
1297 .log_err();
1298 }));
1299 }
1300 }
1301 done_rx
1302 }
1303
1304 fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction {
1305 if self.search_options.contains(SearchOptions::BACKWARDS) {
1306 direction.opposite()
1307 } else {
1308 direction
1309 }
1310 }
1311
1312 pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1313 let direction = self.reverse_direction_if_backwards(Direction::Next);
1314 let new_index = self
1315 .active_searchable_item
1316 .as_ref()
1317 .and_then(|searchable_item| {
1318 let matches = self
1319 .searchable_items_with_matches
1320 .get(&searchable_item.downgrade())?;
1321 searchable_item.active_match_index(direction, matches, window, cx)
1322 });
1323 if new_index != self.active_match_index {
1324 self.active_match_index = new_index;
1325 cx.notify();
1326 }
1327 }
1328
1329 fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1330 self.cycle_field(Direction::Next, window, cx);
1331 }
1332
1333 fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1334 self.cycle_field(Direction::Prev, window, cx);
1335 }
1336 fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1337 let mut handles = vec![self.query_editor.focus_handle(cx)];
1338 if self.replace_enabled {
1339 handles.push(self.replacement_editor.focus_handle(cx));
1340 }
1341 if let Some(item) = self.active_searchable_item.as_ref() {
1342 handles.push(item.item_focus_handle(cx));
1343 }
1344 let current_index = match handles.iter().position(|focus| focus.is_focused(window)) {
1345 Some(index) => index,
1346 None => return,
1347 };
1348
1349 let new_index = match direction {
1350 Direction::Next => (current_index + 1) % handles.len(),
1351 Direction::Prev if current_index == 0 => handles.len() - 1,
1352 Direction::Prev => (current_index - 1) % handles.len(),
1353 };
1354 let next_focus_handle = &handles[new_index];
1355 self.focus(next_focus_handle, window);
1356 cx.stop_propagation();
1357 }
1358
1359 fn next_history_query(
1360 &mut self,
1361 _: &NextHistoryQuery,
1362 window: &mut Window,
1363 cx: &mut Context<Self>,
1364 ) {
1365 if let Some(new_query) = self
1366 .search_history
1367 .next(&mut self.search_history_cursor)
1368 .map(str::to_string)
1369 {
1370 drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1371 } else {
1372 self.search_history_cursor.reset();
1373 drop(self.search("", Some(self.search_options), false, window, cx));
1374 }
1375 }
1376
1377 fn previous_history_query(
1378 &mut self,
1379 _: &PreviousHistoryQuery,
1380 window: &mut Window,
1381 cx: &mut Context<Self>,
1382 ) {
1383 if self.query(cx).is_empty()
1384 && let Some(new_query) = self
1385 .search_history
1386 .current(&self.search_history_cursor)
1387 .map(str::to_string)
1388 {
1389 drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1390 return;
1391 }
1392
1393 if let Some(new_query) = self
1394 .search_history
1395 .previous(&mut self.search_history_cursor)
1396 .map(str::to_string)
1397 {
1398 drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1399 }
1400 }
1401
1402 fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window) {
1403 window.invalidate_character_coordinates();
1404 window.focus(handle);
1405 }
1406
1407 fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1408 if self.active_searchable_item.is_some() {
1409 self.replace_enabled = !self.replace_enabled;
1410 let handle = if self.replace_enabled {
1411 self.replacement_editor.focus_handle(cx)
1412 } else {
1413 self.query_editor.focus_handle(cx)
1414 };
1415 self.focus(&handle, window);
1416 cx.notify();
1417 }
1418 }
1419
1420 fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1421 let mut should_propagate = true;
1422 if !self.dismissed
1423 && self.active_search.is_some()
1424 && let Some(searchable_item) = self.active_searchable_item.as_ref()
1425 && let Some(query) = self.active_search.as_ref()
1426 && let Some(matches) = self
1427 .searchable_items_with_matches
1428 .get(&searchable_item.downgrade())
1429 {
1430 if let Some(active_index) = self.active_match_index {
1431 let query = query
1432 .as_ref()
1433 .clone()
1434 .with_replacement(self.replacement(cx));
1435 searchable_item.replace(matches.at(active_index), &query, window, cx);
1436 self.select_next_match(&SelectNextMatch, window, cx);
1437 }
1438 should_propagate = false;
1439 }
1440 if !should_propagate {
1441 cx.stop_propagation();
1442 }
1443 }
1444
1445 pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1446 if !self.dismissed
1447 && self.active_search.is_some()
1448 && let Some(searchable_item) = self.active_searchable_item.as_ref()
1449 && let Some(query) = self.active_search.as_ref()
1450 && let Some(matches) = self
1451 .searchable_items_with_matches
1452 .get(&searchable_item.downgrade())
1453 {
1454 let query = query
1455 .as_ref()
1456 .clone()
1457 .with_replacement(self.replacement(cx));
1458 searchable_item.replace_all(&mut matches.iter(), &query, window, cx);
1459 }
1460 }
1461
1462 pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1463 self.update_match_index(window, cx);
1464 self.active_match_index.is_some()
1465 }
1466
1467 pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1468 EditorSettings::get_global(cx).use_smartcase_search
1469 }
1470
1471 pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1472 str.chars().any(|c| c.is_uppercase())
1473 }
1474
1475 fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1476 if self.should_use_smartcase_search(cx) {
1477 let query = self.query(cx);
1478 if !query.is_empty() {
1479 let is_case = self.is_contains_uppercase(&query);
1480 if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1481 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1482 }
1483 }
1484 }
1485 }
1486
1487 fn adjust_query_regex_language(&self, cx: &mut App) {
1488 let enable = self.search_options.contains(SearchOptions::REGEX);
1489 let query_buffer = self
1490 .query_editor
1491 .read(cx)
1492 .buffer()
1493 .read(cx)
1494 .as_singleton()
1495 .expect("query editor should be backed by a singleton buffer");
1496 if enable {
1497 if let Some(regex_language) = self.regex_language.clone() {
1498 query_buffer.update(cx, |query_buffer, cx| {
1499 query_buffer.set_language(Some(regex_language), cx);
1500 })
1501 }
1502 } else {
1503 query_buffer.update(cx, |query_buffer, cx| {
1504 query_buffer.set_language(None, cx);
1505 })
1506 }
1507 }
1508}
1509
1510#[cfg(test)]
1511mod tests {
1512 use std::ops::Range;
1513
1514 use super::*;
1515 use editor::{
1516 DisplayPoint, Editor, MultiBuffer, SearchSettings, SelectionEffects,
1517 display_map::DisplayRow,
1518 };
1519 use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1520 use language::{Buffer, Point};
1521 use project::Project;
1522 use settings::{SearchSettingsContent, SettingsStore};
1523 use smol::stream::StreamExt as _;
1524 use unindent::Unindent as _;
1525
1526 fn init_globals(cx: &mut TestAppContext) {
1527 cx.update(|cx| {
1528 let store = settings::SettingsStore::test(cx);
1529 cx.set_global(store);
1530 workspace::init_settings(cx);
1531 editor::init(cx);
1532
1533 language::init(cx);
1534 Project::init_settings(cx);
1535 theme::init(theme::LoadThemes::JustBase, cx);
1536 crate::init(cx);
1537 });
1538 }
1539
1540 fn init_test(
1541 cx: &mut TestAppContext,
1542 ) -> (
1543 Entity<Editor>,
1544 Entity<BufferSearchBar>,
1545 &mut VisualTestContext,
1546 ) {
1547 init_globals(cx);
1548 let buffer = cx.new(|cx| {
1549 Buffer::local(
1550 r#"
1551 A regular expression (shortened as regex or regexp;[1] also referred to as
1552 rational expression[2][3]) is a sequence of characters that specifies a search
1553 pattern in text. Usually such patterns are used by string-searching algorithms
1554 for "find" or "find and replace" operations on strings, or for input validation.
1555 "#
1556 .unindent(),
1557 cx,
1558 )
1559 });
1560 let mut editor = None;
1561 let window = cx.add_window(|window, cx| {
1562 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
1563 "keymaps/default-macos.json",
1564 cx,
1565 )
1566 .unwrap();
1567 cx.bind_keys(default_key_bindings);
1568 editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
1569 let mut search_bar = BufferSearchBar::new(None, window, cx);
1570 search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
1571 search_bar.show(window, cx);
1572 search_bar
1573 });
1574 let search_bar = window.root(cx).unwrap();
1575
1576 let cx = VisualTestContext::from_window(*window, cx).into_mut();
1577
1578 (editor.unwrap(), search_bar, cx)
1579 }
1580
1581 #[gpui::test]
1582 async fn test_search_simple(cx: &mut TestAppContext) {
1583 let (editor, search_bar, cx) = init_test(cx);
1584 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1585 background_highlights
1586 .into_iter()
1587 .map(|(range, _)| range)
1588 .collect::<Vec<_>>()
1589 };
1590 // Search for a string that appears with different casing.
1591 // By default, search is case-insensitive.
1592 search_bar
1593 .update_in(cx, |search_bar, window, cx| {
1594 search_bar.search("us", None, true, window, cx)
1595 })
1596 .await
1597 .unwrap();
1598 editor.update_in(cx, |editor, window, cx| {
1599 assert_eq!(
1600 display_points_of(editor.all_text_background_highlights(window, cx)),
1601 &[
1602 DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1603 DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1604 ]
1605 );
1606 });
1607
1608 // Switch to a case sensitive search.
1609 search_bar.update_in(cx, |search_bar, window, cx| {
1610 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1611 });
1612 let mut editor_notifications = cx.notifications(&editor);
1613 editor_notifications.next().await;
1614 editor.update_in(cx, |editor, window, cx| {
1615 assert_eq!(
1616 display_points_of(editor.all_text_background_highlights(window, cx)),
1617 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1618 );
1619 });
1620
1621 // Search for a string that appears both as a whole word and
1622 // within other words. By default, all results are found.
1623 search_bar
1624 .update_in(cx, |search_bar, window, cx| {
1625 search_bar.search("or", None, true, window, cx)
1626 })
1627 .await
1628 .unwrap();
1629 editor.update_in(cx, |editor, window, cx| {
1630 assert_eq!(
1631 display_points_of(editor.all_text_background_highlights(window, cx)),
1632 &[
1633 DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1634 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1635 DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1636 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1637 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1638 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1639 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1640 ]
1641 );
1642 });
1643
1644 // Switch to a whole word search.
1645 search_bar.update_in(cx, |search_bar, window, cx| {
1646 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1647 });
1648 let mut editor_notifications = cx.notifications(&editor);
1649 editor_notifications.next().await;
1650 editor.update_in(cx, |editor, window, cx| {
1651 assert_eq!(
1652 display_points_of(editor.all_text_background_highlights(window, cx)),
1653 &[
1654 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1655 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1656 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1657 ]
1658 );
1659 });
1660
1661 editor.update_in(cx, |editor, window, cx| {
1662 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1663 s.select_display_ranges([
1664 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1665 ])
1666 });
1667 });
1668 search_bar.update_in(cx, |search_bar, window, cx| {
1669 assert_eq!(search_bar.active_match_index, Some(0));
1670 search_bar.select_next_match(&SelectNextMatch, window, cx);
1671 assert_eq!(
1672 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1673 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1674 );
1675 });
1676 search_bar.read_with(cx, |search_bar, _| {
1677 assert_eq!(search_bar.active_match_index, Some(0));
1678 });
1679
1680 search_bar.update_in(cx, |search_bar, window, cx| {
1681 search_bar.select_next_match(&SelectNextMatch, window, cx);
1682 assert_eq!(
1683 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1684 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1685 );
1686 });
1687 search_bar.read_with(cx, |search_bar, _| {
1688 assert_eq!(search_bar.active_match_index, Some(1));
1689 });
1690
1691 search_bar.update_in(cx, |search_bar, window, cx| {
1692 search_bar.select_next_match(&SelectNextMatch, window, cx);
1693 assert_eq!(
1694 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1695 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1696 );
1697 });
1698 search_bar.read_with(cx, |search_bar, _| {
1699 assert_eq!(search_bar.active_match_index, Some(2));
1700 });
1701
1702 search_bar.update_in(cx, |search_bar, window, cx| {
1703 search_bar.select_next_match(&SelectNextMatch, window, cx);
1704 assert_eq!(
1705 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1706 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1707 );
1708 });
1709 search_bar.read_with(cx, |search_bar, _| {
1710 assert_eq!(search_bar.active_match_index, Some(0));
1711 });
1712
1713 search_bar.update_in(cx, |search_bar, window, cx| {
1714 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1715 assert_eq!(
1716 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1717 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1718 );
1719 });
1720 search_bar.read_with(cx, |search_bar, _| {
1721 assert_eq!(search_bar.active_match_index, Some(2));
1722 });
1723
1724 search_bar.update_in(cx, |search_bar, window, cx| {
1725 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1726 assert_eq!(
1727 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1728 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1729 );
1730 });
1731 search_bar.read_with(cx, |search_bar, _| {
1732 assert_eq!(search_bar.active_match_index, Some(1));
1733 });
1734
1735 search_bar.update_in(cx, |search_bar, window, cx| {
1736 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1737 assert_eq!(
1738 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1739 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1740 );
1741 });
1742 search_bar.read_with(cx, |search_bar, _| {
1743 assert_eq!(search_bar.active_match_index, Some(0));
1744 });
1745
1746 // Park the cursor in between matches and ensure that going to the previous match selects
1747 // the closest match to the left.
1748 editor.update_in(cx, |editor, window, cx| {
1749 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1750 s.select_display_ranges([
1751 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1752 ])
1753 });
1754 });
1755 search_bar.update_in(cx, |search_bar, window, cx| {
1756 assert_eq!(search_bar.active_match_index, Some(1));
1757 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1758 assert_eq!(
1759 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1760 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1761 );
1762 });
1763 search_bar.read_with(cx, |search_bar, _| {
1764 assert_eq!(search_bar.active_match_index, Some(0));
1765 });
1766
1767 // Park the cursor in between matches and ensure that going to the next match selects the
1768 // closest match to the right.
1769 editor.update_in(cx, |editor, window, cx| {
1770 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1771 s.select_display_ranges([
1772 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1773 ])
1774 });
1775 });
1776 search_bar.update_in(cx, |search_bar, window, cx| {
1777 assert_eq!(search_bar.active_match_index, Some(1));
1778 search_bar.select_next_match(&SelectNextMatch, window, cx);
1779 assert_eq!(
1780 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1781 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1782 );
1783 });
1784 search_bar.read_with(cx, |search_bar, _| {
1785 assert_eq!(search_bar.active_match_index, Some(1));
1786 });
1787
1788 // Park the cursor after the last match and ensure that going to the previous match selects
1789 // the last match.
1790 editor.update_in(cx, |editor, window, cx| {
1791 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1792 s.select_display_ranges([
1793 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1794 ])
1795 });
1796 });
1797 search_bar.update_in(cx, |search_bar, window, cx| {
1798 assert_eq!(search_bar.active_match_index, Some(2));
1799 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1800 assert_eq!(
1801 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1802 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1803 );
1804 });
1805 search_bar.read_with(cx, |search_bar, _| {
1806 assert_eq!(search_bar.active_match_index, Some(2));
1807 });
1808
1809 // Park the cursor after the last match and ensure that going to the next match selects the
1810 // first match.
1811 editor.update_in(cx, |editor, window, cx| {
1812 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1813 s.select_display_ranges([
1814 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1815 ])
1816 });
1817 });
1818 search_bar.update_in(cx, |search_bar, window, cx| {
1819 assert_eq!(search_bar.active_match_index, Some(2));
1820 search_bar.select_next_match(&SelectNextMatch, window, cx);
1821 assert_eq!(
1822 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1823 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1824 );
1825 });
1826 search_bar.read_with(cx, |search_bar, _| {
1827 assert_eq!(search_bar.active_match_index, Some(0));
1828 });
1829
1830 // Park the cursor before the first match and ensure that going to the previous match
1831 // selects the last match.
1832 editor.update_in(cx, |editor, window, cx| {
1833 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1834 s.select_display_ranges([
1835 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1836 ])
1837 });
1838 });
1839 search_bar.update_in(cx, |search_bar, window, cx| {
1840 assert_eq!(search_bar.active_match_index, Some(0));
1841 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1842 assert_eq!(
1843 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1844 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1845 );
1846 });
1847 search_bar.read_with(cx, |search_bar, _| {
1848 assert_eq!(search_bar.active_match_index, Some(2));
1849 });
1850 }
1851
1852 fn display_points_of(
1853 background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1854 ) -> Vec<Range<DisplayPoint>> {
1855 background_highlights
1856 .into_iter()
1857 .map(|(range, _)| range)
1858 .collect::<Vec<_>>()
1859 }
1860
1861 #[gpui::test]
1862 async fn test_search_option_handling(cx: &mut TestAppContext) {
1863 let (editor, search_bar, cx) = init_test(cx);
1864
1865 // show with options should make current search case sensitive
1866 search_bar
1867 .update_in(cx, |search_bar, window, cx| {
1868 search_bar.show(window, cx);
1869 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
1870 })
1871 .await
1872 .unwrap();
1873 editor.update_in(cx, |editor, window, cx| {
1874 assert_eq!(
1875 display_points_of(editor.all_text_background_highlights(window, cx)),
1876 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1877 );
1878 });
1879
1880 // search_suggested should restore default options
1881 search_bar.update_in(cx, |search_bar, window, cx| {
1882 search_bar.search_suggested(window, cx);
1883 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1884 });
1885
1886 // toggling a search option should update the defaults
1887 search_bar
1888 .update_in(cx, |search_bar, window, cx| {
1889 search_bar.search(
1890 "regex",
1891 Some(SearchOptions::CASE_SENSITIVE),
1892 true,
1893 window,
1894 cx,
1895 )
1896 })
1897 .await
1898 .unwrap();
1899 search_bar.update_in(cx, |search_bar, window, cx| {
1900 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1901 });
1902 let mut editor_notifications = cx.notifications(&editor);
1903 editor_notifications.next().await;
1904 editor.update_in(cx, |editor, window, cx| {
1905 assert_eq!(
1906 display_points_of(editor.all_text_background_highlights(window, cx)),
1907 &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1908 );
1909 });
1910
1911 // defaults should still include whole word
1912 search_bar.update_in(cx, |search_bar, window, cx| {
1913 search_bar.search_suggested(window, cx);
1914 assert_eq!(
1915 search_bar.search_options,
1916 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1917 )
1918 });
1919 }
1920
1921 #[gpui::test]
1922 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1923 init_globals(cx);
1924 let buffer_text = r#"
1925 A regular expression (shortened as regex or regexp;[1] also referred to as
1926 rational expression[2][3]) is a sequence of characters that specifies a search
1927 pattern in text. Usually such patterns are used by string-searching algorithms
1928 for "find" or "find and replace" operations on strings, or for input validation.
1929 "#
1930 .unindent();
1931 let expected_query_matches_count = buffer_text
1932 .chars()
1933 .filter(|c| c.eq_ignore_ascii_case(&'a'))
1934 .count();
1935 assert!(
1936 expected_query_matches_count > 1,
1937 "Should pick a query with multiple results"
1938 );
1939 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
1940 let window = cx.add_window(|_, _| gpui::Empty);
1941
1942 let editor = window.build_entity(cx, |window, cx| {
1943 Editor::for_buffer(buffer.clone(), None, window, cx)
1944 });
1945
1946 let search_bar = window.build_entity(cx, |window, cx| {
1947 let mut search_bar = BufferSearchBar::new(None, window, cx);
1948 search_bar.set_active_pane_item(Some(&editor), window, cx);
1949 search_bar.show(window, cx);
1950 search_bar
1951 });
1952
1953 window
1954 .update(cx, |_, window, cx| {
1955 search_bar.update(cx, |search_bar, cx| {
1956 search_bar.search("a", None, true, window, cx)
1957 })
1958 })
1959 .unwrap()
1960 .await
1961 .unwrap();
1962 let initial_selections = window
1963 .update(cx, |_, window, cx| {
1964 search_bar.update(cx, |search_bar, cx| {
1965 let handle = search_bar.query_editor.focus_handle(cx);
1966 window.focus(&handle);
1967 search_bar.activate_current_match(window, cx);
1968 });
1969 assert!(
1970 !editor.read(cx).is_focused(window),
1971 "Initially, the editor should not be focused"
1972 );
1973 let initial_selections = editor.update(cx, |editor, cx| {
1974 let initial_selections = editor.selections.display_ranges(cx);
1975 assert_eq!(
1976 initial_selections.len(), 1,
1977 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1978 );
1979 initial_selections
1980 });
1981 search_bar.update(cx, |search_bar, cx| {
1982 assert_eq!(search_bar.active_match_index, Some(0));
1983 let handle = search_bar.query_editor.focus_handle(cx);
1984 window.focus(&handle);
1985 search_bar.select_all_matches(&SelectAllMatches, window, cx);
1986 });
1987 assert!(
1988 editor.read(cx).is_focused(window),
1989 "Should focus editor after successful SelectAllMatches"
1990 );
1991 search_bar.update(cx, |search_bar, cx| {
1992 let all_selections =
1993 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1994 assert_eq!(
1995 all_selections.len(),
1996 expected_query_matches_count,
1997 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1998 );
1999 assert_eq!(
2000 search_bar.active_match_index,
2001 Some(0),
2002 "Match index should not change after selecting all matches"
2003 );
2004 });
2005
2006 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
2007 initial_selections
2008 }).unwrap();
2009
2010 window
2011 .update(cx, |_, window, cx| {
2012 assert!(
2013 editor.read(cx).is_focused(window),
2014 "Should still have editor focused after SelectNextMatch"
2015 );
2016 search_bar.update(cx, |search_bar, cx| {
2017 let all_selections =
2018 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2019 assert_eq!(
2020 all_selections.len(),
2021 1,
2022 "On next match, should deselect items and select the next match"
2023 );
2024 assert_ne!(
2025 all_selections, initial_selections,
2026 "Next match should be different from the first selection"
2027 );
2028 assert_eq!(
2029 search_bar.active_match_index,
2030 Some(1),
2031 "Match index should be updated to the next one"
2032 );
2033 let handle = search_bar.query_editor.focus_handle(cx);
2034 window.focus(&handle);
2035 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2036 });
2037 })
2038 .unwrap();
2039 window
2040 .update(cx, |_, window, cx| {
2041 assert!(
2042 editor.read(cx).is_focused(window),
2043 "Should focus editor after successful SelectAllMatches"
2044 );
2045 search_bar.update(cx, |search_bar, cx| {
2046 let all_selections =
2047 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2048 assert_eq!(
2049 all_selections.len(),
2050 expected_query_matches_count,
2051 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2052 );
2053 assert_eq!(
2054 search_bar.active_match_index,
2055 Some(1),
2056 "Match index should not change after selecting all matches"
2057 );
2058 });
2059 search_bar.update(cx, |search_bar, cx| {
2060 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2061 });
2062 })
2063 .unwrap();
2064 let last_match_selections = window
2065 .update(cx, |_, window, cx| {
2066 assert!(
2067 editor.read(cx).is_focused(window),
2068 "Should still have editor focused after SelectPreviousMatch"
2069 );
2070
2071 search_bar.update(cx, |search_bar, cx| {
2072 let all_selections =
2073 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2074 assert_eq!(
2075 all_selections.len(),
2076 1,
2077 "On previous match, should deselect items and select the previous item"
2078 );
2079 assert_eq!(
2080 all_selections, initial_selections,
2081 "Previous match should be the same as the first selection"
2082 );
2083 assert_eq!(
2084 search_bar.active_match_index,
2085 Some(0),
2086 "Match index should be updated to the previous one"
2087 );
2088 all_selections
2089 })
2090 })
2091 .unwrap();
2092
2093 window
2094 .update(cx, |_, window, cx| {
2095 search_bar.update(cx, |search_bar, cx| {
2096 let handle = search_bar.query_editor.focus_handle(cx);
2097 window.focus(&handle);
2098 search_bar.search("abas_nonexistent_match", None, true, window, cx)
2099 })
2100 })
2101 .unwrap()
2102 .await
2103 .unwrap();
2104 window
2105 .update(cx, |_, window, cx| {
2106 search_bar.update(cx, |search_bar, cx| {
2107 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2108 });
2109 assert!(
2110 editor.update(cx, |this, _cx| !this.is_focused(window)),
2111 "Should not switch focus to editor if SelectAllMatches does not find any matches"
2112 );
2113 search_bar.update(cx, |search_bar, cx| {
2114 let all_selections =
2115 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2116 assert_eq!(
2117 all_selections, last_match_selections,
2118 "Should not select anything new if there are no matches"
2119 );
2120 assert!(
2121 search_bar.active_match_index.is_none(),
2122 "For no matches, there should be no active match index"
2123 );
2124 });
2125 })
2126 .unwrap();
2127 }
2128
2129 #[gpui::test]
2130 async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2131 init_globals(cx);
2132 let buffer_text = r#"
2133 self.buffer.update(cx, |buffer, cx| {
2134 buffer.edit(
2135 edits,
2136 Some(AutoindentMode::Block {
2137 original_indent_columns,
2138 }),
2139 cx,
2140 )
2141 });
2142
2143 this.buffer.update(cx, |buffer, cx| {
2144 buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2145 });
2146 "#
2147 .unindent();
2148 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2149 let cx = cx.add_empty_window();
2150
2151 let editor =
2152 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2153
2154 let search_bar = cx.new_window_entity(|window, cx| {
2155 let mut search_bar = BufferSearchBar::new(None, window, cx);
2156 search_bar.set_active_pane_item(Some(&editor), window, cx);
2157 search_bar.show(window, cx);
2158 search_bar
2159 });
2160
2161 search_bar
2162 .update_in(cx, |search_bar, window, cx| {
2163 search_bar.search(
2164 "edit\\(",
2165 Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2166 true,
2167 window,
2168 cx,
2169 )
2170 })
2171 .await
2172 .unwrap();
2173
2174 search_bar.update_in(cx, |search_bar, window, cx| {
2175 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2176 });
2177 search_bar.update(cx, |_, cx| {
2178 let all_selections =
2179 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2180 assert_eq!(
2181 all_selections.len(),
2182 2,
2183 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2184 );
2185 });
2186
2187 search_bar
2188 .update_in(cx, |search_bar, window, cx| {
2189 search_bar.search(
2190 "edit(",
2191 Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2192 true,
2193 window,
2194 cx,
2195 )
2196 })
2197 .await
2198 .unwrap();
2199
2200 search_bar.update_in(cx, |search_bar, window, cx| {
2201 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2202 });
2203 search_bar.update(cx, |_, cx| {
2204 let all_selections =
2205 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2206 assert_eq!(
2207 all_selections.len(),
2208 2,
2209 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2210 );
2211 });
2212 }
2213
2214 #[gpui::test]
2215 async fn test_search_query_history(cx: &mut TestAppContext) {
2216 let (_editor, search_bar, cx) = init_test(cx);
2217
2218 // Add 3 search items into the history.
2219 search_bar
2220 .update_in(cx, |search_bar, window, cx| {
2221 search_bar.search("a", None, true, window, cx)
2222 })
2223 .await
2224 .unwrap();
2225 search_bar
2226 .update_in(cx, |search_bar, window, cx| {
2227 search_bar.search("b", None, true, window, cx)
2228 })
2229 .await
2230 .unwrap();
2231 search_bar
2232 .update_in(cx, |search_bar, window, cx| {
2233 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2234 })
2235 .await
2236 .unwrap();
2237 // Ensure that the latest search is active.
2238 search_bar.update(cx, |search_bar, cx| {
2239 assert_eq!(search_bar.query(cx), "c");
2240 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2241 });
2242
2243 // Next history query after the latest should set the query to the empty string.
2244 search_bar.update_in(cx, |search_bar, window, cx| {
2245 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2246 });
2247 cx.background_executor.run_until_parked();
2248 search_bar.update(cx, |search_bar, cx| {
2249 assert_eq!(search_bar.query(cx), "");
2250 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2251 });
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
2261 // First previous query for empty current query should set the query to the latest.
2262 search_bar.update_in(cx, |search_bar, window, cx| {
2263 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2264 });
2265 cx.background_executor.run_until_parked();
2266 search_bar.update(cx, |search_bar, cx| {
2267 assert_eq!(search_bar.query(cx), "c");
2268 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2269 });
2270
2271 // Further previous items should go over the history in reverse order.
2272 search_bar.update_in(cx, |search_bar, window, cx| {
2273 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2274 });
2275 cx.background_executor.run_until_parked();
2276 search_bar.update(cx, |search_bar, cx| {
2277 assert_eq!(search_bar.query(cx), "b");
2278 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2279 });
2280
2281 // Previous items should never go behind the first history item.
2282 search_bar.update_in(cx, |search_bar, window, cx| {
2283 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2284 });
2285 cx.background_executor.run_until_parked();
2286 search_bar.update(cx, |search_bar, cx| {
2287 assert_eq!(search_bar.query(cx), "a");
2288 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2289 });
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
2299 // Next items should go over the history in the original order.
2300 search_bar.update_in(cx, |search_bar, window, cx| {
2301 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2302 });
2303 cx.background_executor.run_until_parked();
2304 search_bar.update(cx, |search_bar, cx| {
2305 assert_eq!(search_bar.query(cx), "b");
2306 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2307 });
2308
2309 search_bar
2310 .update_in(cx, |search_bar, window, cx| {
2311 search_bar.search("ba", None, true, window, cx)
2312 })
2313 .await
2314 .unwrap();
2315 search_bar.update(cx, |search_bar, cx| {
2316 assert_eq!(search_bar.query(cx), "ba");
2317 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2318 });
2319
2320 // New search input should add another entry to history and move the selection to the end of the history.
2321 search_bar.update_in(cx, |search_bar, window, cx| {
2322 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2323 });
2324 cx.background_executor.run_until_parked();
2325 search_bar.update(cx, |search_bar, cx| {
2326 assert_eq!(search_bar.query(cx), "c");
2327 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2328 });
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), "b");
2335 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2336 });
2337 search_bar.update_in(cx, |search_bar, window, cx| {
2338 search_bar.next_history_query(&NextHistoryQuery, 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), "c");
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), "ba");
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), "");
2359 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2360 });
2361 }
2362
2363 #[gpui::test]
2364 async fn test_replace_simple(cx: &mut TestAppContext) {
2365 let (editor, search_bar, cx) = init_test(cx);
2366
2367 search_bar
2368 .update_in(cx, |search_bar, window, cx| {
2369 search_bar.search("expression", None, true, window, cx)
2370 })
2371 .await
2372 .unwrap();
2373
2374 search_bar.update_in(cx, |search_bar, window, cx| {
2375 search_bar.replacement_editor.update(cx, |editor, cx| {
2376 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2377 editor.set_text("expr$1", window, cx);
2378 });
2379 search_bar.replace_all(&ReplaceAll, window, cx)
2380 });
2381 assert_eq!(
2382 editor.read_with(cx, |this, cx| { this.text(cx) }),
2383 r#"
2384 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2385 rational expr$1[2][3]) is a sequence of characters that specifies a search
2386 pattern in text. Usually such patterns are used by string-searching algorithms
2387 for "find" or "find and replace" operations on strings, or for input validation.
2388 "#
2389 .unindent()
2390 );
2391
2392 // Search for word boundaries and replace just a single one.
2393 search_bar
2394 .update_in(cx, |search_bar, window, cx| {
2395 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx)
2396 })
2397 .await
2398 .unwrap();
2399
2400 search_bar.update_in(cx, |search_bar, window, cx| {
2401 search_bar.replacement_editor.update(cx, |editor, cx| {
2402 editor.set_text("banana", window, cx);
2403 });
2404 search_bar.replace_next(&ReplaceNext, window, cx)
2405 });
2406 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2407 assert_eq!(
2408 editor.read_with(cx, |this, cx| { this.text(cx) }),
2409 r#"
2410 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2411 rational expr$1[2][3]) is a sequence of characters that specifies a search
2412 pattern in text. Usually such patterns are used by string-searching algorithms
2413 for "find" or "find and replace" operations on strings, or for input validation.
2414 "#
2415 .unindent()
2416 );
2417 // Let's turn on regex mode.
2418 search_bar
2419 .update_in(cx, |search_bar, window, cx| {
2420 search_bar.search(
2421 "\\[([^\\]]+)\\]",
2422 Some(SearchOptions::REGEX),
2423 true,
2424 window,
2425 cx,
2426 )
2427 })
2428 .await
2429 .unwrap();
2430 search_bar.update_in(cx, |search_bar, window, cx| {
2431 search_bar.replacement_editor.update(cx, |editor, cx| {
2432 editor.set_text("${1}number", window, cx);
2433 });
2434 search_bar.replace_all(&ReplaceAll, window, cx)
2435 });
2436 assert_eq!(
2437 editor.read_with(cx, |this, cx| { this.text(cx) }),
2438 r#"
2439 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2440 rational expr$12number3number) is a sequence of characters that specifies a search
2441 pattern in text. Usually such patterns are used by string-searching algorithms
2442 for "find" or "find and replace" operations on strings, or for input validation.
2443 "#
2444 .unindent()
2445 );
2446 // Now with a whole-word twist.
2447 search_bar
2448 .update_in(cx, |search_bar, window, cx| {
2449 search_bar.search(
2450 "a\\w+s",
2451 Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2452 true,
2453 window,
2454 cx,
2455 )
2456 })
2457 .await
2458 .unwrap();
2459 search_bar.update_in(cx, |search_bar, window, cx| {
2460 search_bar.replacement_editor.update(cx, |editor, cx| {
2461 editor.set_text("things", window, cx);
2462 });
2463 search_bar.replace_all(&ReplaceAll, window, cx)
2464 });
2465 // The only word affected by this edit should be `algorithms`, even though there's a bunch
2466 // of words in this text that would match this regex if not for WHOLE_WORD.
2467 assert_eq!(
2468 editor.read_with(cx, |this, cx| { this.text(cx) }),
2469 r#"
2470 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2471 rational expr$12number3number) is a sequence of characters that specifies a search
2472 pattern in text. Usually such patterns are used by string-searching things
2473 for "find" or "find and replace" operations on strings, or for input validation.
2474 "#
2475 .unindent()
2476 );
2477 }
2478
2479 struct ReplacementTestParams<'a> {
2480 editor: &'a Entity<Editor>,
2481 search_bar: &'a Entity<BufferSearchBar>,
2482 cx: &'a mut VisualTestContext,
2483 search_text: &'static str,
2484 search_options: Option<SearchOptions>,
2485 replacement_text: &'static str,
2486 replace_all: bool,
2487 expected_text: String,
2488 }
2489
2490 async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2491 options
2492 .search_bar
2493 .update_in(options.cx, |search_bar, window, cx| {
2494 if let Some(options) = options.search_options {
2495 search_bar.set_search_options(options, cx);
2496 }
2497 search_bar.search(
2498 options.search_text,
2499 options.search_options,
2500 true,
2501 window,
2502 cx,
2503 )
2504 })
2505 .await
2506 .unwrap();
2507
2508 options
2509 .search_bar
2510 .update_in(options.cx, |search_bar, window, cx| {
2511 search_bar.replacement_editor.update(cx, |editor, cx| {
2512 editor.set_text(options.replacement_text, window, cx);
2513 });
2514
2515 if options.replace_all {
2516 search_bar.replace_all(&ReplaceAll, window, cx)
2517 } else {
2518 search_bar.replace_next(&ReplaceNext, window, cx)
2519 }
2520 });
2521
2522 assert_eq!(
2523 options
2524 .editor
2525 .read_with(options.cx, |this, cx| { this.text(cx) }),
2526 options.expected_text
2527 );
2528 }
2529
2530 #[gpui::test]
2531 async fn test_replace_special_characters(cx: &mut TestAppContext) {
2532 let (editor, search_bar, cx) = init_test(cx);
2533
2534 run_replacement_test(ReplacementTestParams {
2535 editor: &editor,
2536 search_bar: &search_bar,
2537 cx,
2538 search_text: "expression",
2539 search_options: None,
2540 replacement_text: r"\n",
2541 replace_all: true,
2542 expected_text: r#"
2543 A regular \n (shortened as regex or regexp;[1] also referred to as
2544 rational \n[2][3]) is a sequence of characters that specifies a search
2545 pattern in text. Usually such patterns are used by string-searching algorithms
2546 for "find" or "find and replace" operations on strings, or for input validation.
2547 "#
2548 .unindent(),
2549 })
2550 .await;
2551
2552 run_replacement_test(ReplacementTestParams {
2553 editor: &editor,
2554 search_bar: &search_bar,
2555 cx,
2556 search_text: "or",
2557 search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2558 replacement_text: r"\\\n\\\\",
2559 replace_all: false,
2560 expected_text: r#"
2561 A regular \n (shortened as regex \
2562 \\ regexp;[1] also referred to as
2563 rational \n[2][3]) is a sequence of characters that specifies a search
2564 pattern in text. Usually such patterns are used by string-searching algorithms
2565 for "find" or "find and replace" operations on strings, or for input validation.
2566 "#
2567 .unindent(),
2568 })
2569 .await;
2570
2571 run_replacement_test(ReplacementTestParams {
2572 editor: &editor,
2573 search_bar: &search_bar,
2574 cx,
2575 search_text: r"(that|used) ",
2576 search_options: Some(SearchOptions::REGEX),
2577 replacement_text: r"$1\n",
2578 replace_all: true,
2579 expected_text: r#"
2580 A regular \n (shortened as regex \
2581 \\ regexp;[1] also referred to as
2582 rational \n[2][3]) is a sequence of characters that
2583 specifies a search
2584 pattern in text. Usually such patterns are used
2585 by string-searching algorithms
2586 for "find" or "find and replace" operations on strings, or for input validation.
2587 "#
2588 .unindent(),
2589 })
2590 .await;
2591 }
2592
2593 #[gpui::test]
2594 async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2595 cx: &mut TestAppContext,
2596 ) {
2597 init_globals(cx);
2598 let buffer = cx.new(|cx| {
2599 Buffer::local(
2600 r#"
2601 aaa bbb aaa ccc
2602 aaa bbb aaa ccc
2603 aaa bbb aaa ccc
2604 aaa bbb aaa ccc
2605 aaa bbb aaa ccc
2606 aaa bbb aaa ccc
2607 "#
2608 .unindent(),
2609 cx,
2610 )
2611 });
2612 let cx = cx.add_empty_window();
2613 let editor =
2614 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2615
2616 let search_bar = cx.new_window_entity(|window, cx| {
2617 let mut search_bar = BufferSearchBar::new(None, window, cx);
2618 search_bar.set_active_pane_item(Some(&editor), window, cx);
2619 search_bar.show(window, cx);
2620 search_bar
2621 });
2622
2623 editor.update_in(cx, |editor, window, cx| {
2624 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2625 s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2626 })
2627 });
2628
2629 search_bar.update_in(cx, |search_bar, window, cx| {
2630 let deploy = Deploy {
2631 focus: true,
2632 replace_enabled: false,
2633 selection_search_enabled: true,
2634 };
2635 search_bar.deploy(&deploy, window, cx);
2636 });
2637
2638 cx.run_until_parked();
2639
2640 search_bar
2641 .update_in(cx, |search_bar, window, cx| {
2642 search_bar.search("aaa", None, true, window, cx)
2643 })
2644 .await
2645 .unwrap();
2646
2647 editor.update(cx, |editor, cx| {
2648 assert_eq!(
2649 editor.search_background_highlights(cx),
2650 &[
2651 Point::new(1, 0)..Point::new(1, 3),
2652 Point::new(1, 8)..Point::new(1, 11),
2653 Point::new(2, 0)..Point::new(2, 3),
2654 ]
2655 );
2656 });
2657 }
2658
2659 #[gpui::test]
2660 async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2661 cx: &mut TestAppContext,
2662 ) {
2663 init_globals(cx);
2664 let text = r#"
2665 aaa bbb aaa ccc
2666 aaa bbb aaa ccc
2667 aaa bbb aaa ccc
2668 aaa bbb aaa ccc
2669 aaa bbb aaa ccc
2670 aaa bbb aaa ccc
2671
2672 aaa bbb aaa ccc
2673 aaa bbb aaa ccc
2674 aaa bbb aaa ccc
2675 aaa bbb aaa ccc
2676 aaa bbb aaa ccc
2677 aaa bbb aaa ccc
2678 "#
2679 .unindent();
2680
2681 let cx = cx.add_empty_window();
2682 let editor = cx.new_window_entity(|window, cx| {
2683 let multibuffer = MultiBuffer::build_multi(
2684 [
2685 (
2686 &text,
2687 vec![
2688 Point::new(0, 0)..Point::new(2, 0),
2689 Point::new(4, 0)..Point::new(5, 0),
2690 ],
2691 ),
2692 (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2693 ],
2694 cx,
2695 );
2696 Editor::for_multibuffer(multibuffer, None, window, cx)
2697 });
2698
2699 let search_bar = cx.new_window_entity(|window, cx| {
2700 let mut search_bar = BufferSearchBar::new(None, window, cx);
2701 search_bar.set_active_pane_item(Some(&editor), window, cx);
2702 search_bar.show(window, cx);
2703 search_bar
2704 });
2705
2706 editor.update_in(cx, |editor, window, cx| {
2707 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2708 s.select_ranges(vec![
2709 Point::new(1, 0)..Point::new(1, 4),
2710 Point::new(5, 3)..Point::new(6, 4),
2711 ])
2712 })
2713 });
2714
2715 search_bar.update_in(cx, |search_bar, window, cx| {
2716 let deploy = Deploy {
2717 focus: true,
2718 replace_enabled: false,
2719 selection_search_enabled: true,
2720 };
2721 search_bar.deploy(&deploy, window, cx);
2722 });
2723
2724 cx.run_until_parked();
2725
2726 search_bar
2727 .update_in(cx, |search_bar, window, cx| {
2728 search_bar.search("aaa", None, true, window, cx)
2729 })
2730 .await
2731 .unwrap();
2732
2733 editor.update(cx, |editor, cx| {
2734 assert_eq!(
2735 editor.search_background_highlights(cx),
2736 &[
2737 Point::new(1, 0)..Point::new(1, 3),
2738 Point::new(5, 8)..Point::new(5, 11),
2739 Point::new(6, 0)..Point::new(6, 3),
2740 ]
2741 );
2742 });
2743 }
2744
2745 #[gpui::test]
2746 async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2747 let (editor, search_bar, cx) = init_test(cx);
2748 // Search using valid regexp
2749 search_bar
2750 .update_in(cx, |search_bar, window, cx| {
2751 search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
2752 search_bar.search("expression", None, true, window, cx)
2753 })
2754 .await
2755 .unwrap();
2756 editor.update_in(cx, |editor, window, cx| {
2757 assert_eq!(
2758 display_points_of(editor.all_text_background_highlights(window, cx)),
2759 &[
2760 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2761 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2762 ],
2763 );
2764 });
2765
2766 // Now, the expression is invalid
2767 search_bar
2768 .update_in(cx, |search_bar, window, cx| {
2769 search_bar.search("expression (", None, true, window, cx)
2770 })
2771 .await
2772 .unwrap_err();
2773 editor.update_in(cx, |editor, window, cx| {
2774 assert!(
2775 display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
2776 );
2777 });
2778 }
2779
2780 #[gpui::test]
2781 async fn test_search_options_changes(cx: &mut TestAppContext) {
2782 let (_editor, search_bar, cx) = init_test(cx);
2783 update_search_settings(
2784 SearchSettings {
2785 button: true,
2786 whole_word: false,
2787 case_sensitive: false,
2788 include_ignored: false,
2789 regex: false,
2790 },
2791 cx,
2792 );
2793
2794 let deploy = Deploy {
2795 focus: true,
2796 replace_enabled: false,
2797 selection_search_enabled: true,
2798 };
2799
2800 search_bar.update_in(cx, |search_bar, window, cx| {
2801 assert_eq!(
2802 search_bar.search_options,
2803 SearchOptions::NONE,
2804 "Should have no search options enabled by default"
2805 );
2806 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2807 assert_eq!(
2808 search_bar.search_options,
2809 SearchOptions::WHOLE_WORD,
2810 "Should enable the option toggled"
2811 );
2812 assert!(
2813 !search_bar.dismissed,
2814 "Search bar should be present and visible"
2815 );
2816 search_bar.deploy(&deploy, window, cx);
2817 assert_eq!(
2818 search_bar.search_options,
2819 SearchOptions::WHOLE_WORD,
2820 "After (re)deploying, the option should still be enabled"
2821 );
2822
2823 search_bar.dismiss(&Dismiss, window, cx);
2824 search_bar.deploy(&deploy, window, cx);
2825 assert_eq!(
2826 search_bar.search_options,
2827 SearchOptions::WHOLE_WORD,
2828 "After hiding and showing the search bar, search options should be preserved"
2829 );
2830
2831 search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
2832 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2833 assert_eq!(
2834 search_bar.search_options,
2835 SearchOptions::REGEX,
2836 "Should enable the options toggled"
2837 );
2838 assert!(
2839 !search_bar.dismissed,
2840 "Search bar should be present and visible"
2841 );
2842 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2843 });
2844
2845 update_search_settings(
2846 SearchSettings {
2847 button: true,
2848 whole_word: false,
2849 case_sensitive: true,
2850 include_ignored: false,
2851 regex: false,
2852 },
2853 cx,
2854 );
2855 search_bar.update_in(cx, |search_bar, window, cx| {
2856 assert_eq!(
2857 search_bar.search_options,
2858 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2859 "Should have no search options enabled by default"
2860 );
2861
2862 search_bar.deploy(&deploy, window, cx);
2863 assert_eq!(
2864 search_bar.search_options,
2865 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2866 "Toggling a non-dismissed search bar with custom options should not change the default options"
2867 );
2868 search_bar.dismiss(&Dismiss, window, cx);
2869 search_bar.deploy(&deploy, window, cx);
2870 assert_eq!(
2871 search_bar.configured_options,
2872 SearchOptions::CASE_SENSITIVE,
2873 "After a settings update and toggling the search bar, configured options should be updated"
2874 );
2875 assert_eq!(
2876 search_bar.search_options,
2877 SearchOptions::CASE_SENSITIVE,
2878 "After a settings update and toggling the search bar, configured options should be used"
2879 );
2880 });
2881
2882 update_search_settings(
2883 SearchSettings {
2884 button: true,
2885 whole_word: true,
2886 case_sensitive: true,
2887 include_ignored: false,
2888 regex: false,
2889 },
2890 cx,
2891 );
2892
2893 search_bar.update_in(cx, |search_bar, window, cx| {
2894 search_bar.deploy(&deploy, window, cx);
2895 search_bar.dismiss(&Dismiss, window, cx);
2896 search_bar.show(window, cx);
2897 assert_eq!(
2898 search_bar.search_options,
2899 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
2900 "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
2901 );
2902 });
2903 }
2904
2905 fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2906 cx.update(|cx| {
2907 SettingsStore::update_global(cx, |store, cx| {
2908 store.update_user_settings(cx, |settings| {
2909 settings.editor.search = Some(SearchSettingsContent {
2910 button: Some(search_settings.button),
2911 whole_word: Some(search_settings.whole_word),
2912 case_sensitive: Some(search_settings.case_sensitive),
2913 include_ignored: Some(search_settings.include_ignored),
2914 regex: Some(search_settings.regex),
2915 });
2916 });
2917 });
2918 });
2919 }
2920}