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