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