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