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