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