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