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