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 search.await?;
798 this.update_in(cx, |this, window, cx| {
799 this.activate_current_match(window, cx)
800 })
801 })
802 .detach_and_log_err(cx);
803 }
804 }
805
806 pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
807 if let Some(match_ix) = self.active_match_index
808 && let Some(active_searchable_item) = self.active_searchable_item.as_ref()
809 && let Some(matches) = self
810 .searchable_items_with_matches
811 .get(&active_searchable_item.downgrade())
812 {
813 active_searchable_item.activate_match(match_ix, matches, window, cx)
814 }
815 }
816
817 pub fn select_query(&mut self, window: &mut Window, cx: &mut Context<Self>) {
818 self.query_editor.update(cx, |query_editor, cx| {
819 query_editor.select_all(&Default::default(), window, cx);
820 });
821 }
822
823 pub fn query(&self, cx: &App) -> String {
824 self.query_editor.read(cx).text(cx)
825 }
826
827 pub fn replacement(&self, cx: &mut App) -> String {
828 self.replacement_editor.read(cx).text(cx)
829 }
830
831 pub fn query_suggestion(
832 &mut self,
833 window: &mut Window,
834 cx: &mut Context<Self>,
835 ) -> Option<String> {
836 self.active_searchable_item
837 .as_ref()
838 .map(|searchable_item| searchable_item.query_suggestion(window, cx))
839 .filter(|suggestion| !suggestion.is_empty())
840 }
841
842 pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut Context<Self>) {
843 if replacement.is_none() {
844 self.replace_enabled = false;
845 return;
846 }
847 self.replace_enabled = true;
848 self.replacement_editor
849 .update(cx, |replacement_editor, cx| {
850 replacement_editor
851 .buffer()
852 .update(cx, |replacement_buffer, cx| {
853 let len = replacement_buffer.len(cx);
854 replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
855 });
856 });
857 }
858
859 pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
860 self.focus(&self.replacement_editor.focus_handle(cx), window);
861 cx.notify();
862 }
863
864 pub fn search(
865 &mut self,
866 query: &str,
867 options: Option<SearchOptions>,
868 add_to_history: bool,
869 window: &mut Window,
870 cx: &mut Context<Self>,
871 ) -> oneshot::Receiver<()> {
872 let options = options.unwrap_or(self.default_options);
873 let updated = query != self.query(cx) || self.search_options != options;
874 if updated {
875 self.query_editor.update(cx, |query_editor, cx| {
876 query_editor.buffer().update(cx, |query_buffer, cx| {
877 let len = query_buffer.len(cx);
878 query_buffer.edit([(0..len, query)], None, cx);
879 });
880 });
881 self.set_search_options(options, cx);
882 self.clear_matches(window, cx);
883 cx.notify();
884 }
885 self.update_matches(!updated, add_to_history, window, cx)
886 }
887
888 pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
889 if let Some(active_editor) = self.active_searchable_item.as_ref() {
890 let handle = active_editor.item_focus_handle(cx);
891 window.focus(&handle);
892 }
893 }
894
895 pub fn toggle_search_option(
896 &mut self,
897 search_option: SearchOptions,
898 window: &mut Window,
899 cx: &mut Context<Self>,
900 ) {
901 self.search_options.toggle(search_option);
902 self.default_options = self.search_options;
903 drop(self.update_matches(false, false, window, cx));
904 self.adjust_query_regex_language(cx);
905 cx.notify();
906 }
907
908 pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
909 self.search_options.contains(search_option)
910 }
911
912 pub fn enable_search_option(
913 &mut self,
914 search_option: SearchOptions,
915 window: &mut Window,
916 cx: &mut Context<Self>,
917 ) {
918 if !self.search_options.contains(search_option) {
919 self.toggle_search_option(search_option, window, cx)
920 }
921 }
922
923 pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context<Self>) {
924 self.search_options = search_options;
925 self.adjust_query_regex_language(cx);
926 cx.notify();
927 }
928
929 pub fn clear_search_within_ranges(
930 &mut self,
931 search_options: SearchOptions,
932 cx: &mut Context<Self>,
933 ) {
934 self.search_options = search_options;
935 self.adjust_query_regex_language(cx);
936 cx.notify();
937 }
938
939 fn select_next_match(
940 &mut self,
941 _: &SelectNextMatch,
942 window: &mut Window,
943 cx: &mut Context<Self>,
944 ) {
945 self.select_match(Direction::Next, 1, window, cx);
946 }
947
948 fn select_prev_match(
949 &mut self,
950 _: &SelectPreviousMatch,
951 window: &mut Window,
952 cx: &mut Context<Self>,
953 ) {
954 self.select_match(Direction::Prev, 1, window, cx);
955 }
956
957 fn select_all_matches(
958 &mut self,
959 _: &SelectAllMatches,
960 window: &mut Window,
961 cx: &mut Context<Self>,
962 ) {
963 if !self.dismissed
964 && self.active_match_index.is_some()
965 && let Some(searchable_item) = self.active_searchable_item.as_ref()
966 && let Some(matches) = self
967 .searchable_items_with_matches
968 .get(&searchable_item.downgrade())
969 {
970 searchable_item.select_matches(matches, window, cx);
971 self.focus_editor(&FocusEditor, window, cx);
972 }
973 }
974
975 pub fn select_match(
976 &mut self,
977 direction: Direction,
978 count: usize,
979 window: &mut Window,
980 cx: &mut Context<Self>,
981 ) {
982 if let Some(index) = self.active_match_index
983 && let Some(searchable_item) = self.active_searchable_item.as_ref()
984 && let Some(matches) = self
985 .searchable_items_with_matches
986 .get(&searchable_item.downgrade())
987 .filter(|matches| !matches.is_empty())
988 {
989 // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
990 if !EditorSettings::get_global(cx).search_wrap
991 && ((direction == Direction::Next && index + count >= matches.len())
992 || (direction == Direction::Prev && index < count))
993 {
994 crate::show_no_more_matches(window, cx);
995 return;
996 }
997 let new_match_index = searchable_item
998 .match_index_for_direction(matches, index, direction, count, window, cx);
999
1000 searchable_item.update_matches(matches, window, cx);
1001 searchable_item.activate_match(new_match_index, matches, window, cx);
1002 }
1003 }
1004
1005 pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1006 if let Some(searchable_item) = self.active_searchable_item.as_ref()
1007 && let Some(matches) = self
1008 .searchable_items_with_matches
1009 .get(&searchable_item.downgrade())
1010 {
1011 if matches.is_empty() {
1012 return;
1013 }
1014 searchable_item.update_matches(matches, window, cx);
1015 searchable_item.activate_match(0, matches, window, cx);
1016 }
1017 }
1018
1019 pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1020 if let Some(searchable_item) = self.active_searchable_item.as_ref()
1021 && let Some(matches) = self
1022 .searchable_items_with_matches
1023 .get(&searchable_item.downgrade())
1024 {
1025 if matches.is_empty() {
1026 return;
1027 }
1028 let new_match_index = matches.len() - 1;
1029 searchable_item.update_matches(matches, window, cx);
1030 searchable_item.activate_match(new_match_index, matches, window, cx);
1031 }
1032 }
1033
1034 fn on_query_editor_event(
1035 &mut self,
1036 editor: &Entity<Editor>,
1037 event: &editor::EditorEvent,
1038 window: &mut Window,
1039 cx: &mut Context<Self>,
1040 ) {
1041 match event {
1042 editor::EditorEvent::Focused => self.query_editor_focused = true,
1043 editor::EditorEvent::Blurred => self.query_editor_focused = false,
1044 editor::EditorEvent::Edited { .. } => {
1045 self.smartcase(window, cx);
1046 self.clear_matches(window, cx);
1047 let search = self.update_matches(false, true, window, cx);
1048
1049 let width = editor.update(cx, |editor, cx| {
1050 let text_layout_details = editor.text_layout_details(window);
1051 let snapshot = editor.snapshot(window, cx).display_snapshot;
1052
1053 snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
1054 - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
1055 });
1056 self.editor_needed_width = width;
1057 cx.notify();
1058
1059 cx.spawn_in(window, async move |this, cx| {
1060 search.await?;
1061 this.update_in(cx, |this, window, cx| {
1062 this.activate_current_match(window, cx)
1063 })
1064 })
1065 .detach_and_log_err(cx);
1066 }
1067 _ => {}
1068 }
1069 }
1070
1071 fn on_replacement_editor_event(
1072 &mut self,
1073 _: Entity<Editor>,
1074 event: &editor::EditorEvent,
1075 _: &mut Context<Self>,
1076 ) {
1077 match event {
1078 editor::EditorEvent::Focused => self.replacement_editor_focused = true,
1079 editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
1080 _ => {}
1081 }
1082 }
1083
1084 fn on_active_searchable_item_event(
1085 &mut self,
1086 event: &SearchEvent,
1087 window: &mut Window,
1088 cx: &mut Context<Self>,
1089 ) {
1090 match event {
1091 SearchEvent::MatchesInvalidated => {
1092 drop(self.update_matches(false, false, window, cx));
1093 }
1094 SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx),
1095 }
1096 }
1097
1098 fn toggle_case_sensitive(
1099 &mut self,
1100 _: &ToggleCaseSensitive,
1101 window: &mut Window,
1102 cx: &mut Context<Self>,
1103 ) {
1104 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx)
1105 }
1106
1107 fn toggle_whole_word(
1108 &mut self,
1109 _: &ToggleWholeWord,
1110 window: &mut Window,
1111 cx: &mut Context<Self>,
1112 ) {
1113 self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1114 }
1115
1116 fn toggle_selection(
1117 &mut self,
1118 _: &ToggleSelection,
1119 window: &mut Window,
1120 cx: &mut Context<Self>,
1121 ) {
1122 if let Some(active_item) = self.active_searchable_item.as_mut() {
1123 self.selection_search_enabled = !self.selection_search_enabled;
1124 active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx);
1125 drop(self.update_matches(false, false, window, cx));
1126 cx.notify();
1127 }
1128 }
1129
1130 fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context<Self>) {
1131 self.toggle_search_option(SearchOptions::REGEX, window, cx)
1132 }
1133
1134 fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) {
1135 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1136 self.active_match_index = None;
1137 self.searchable_items_with_matches
1138 .remove(&active_searchable_item.downgrade());
1139 active_searchable_item.clear_matches(window, cx);
1140 }
1141 }
1142
1143 pub fn has_active_match(&self) -> bool {
1144 self.active_match_index.is_some()
1145 }
1146
1147 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1148 let mut active_item_matches = None;
1149 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
1150 if let Some(searchable_item) =
1151 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
1152 {
1153 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
1154 active_item_matches = Some((searchable_item.downgrade(), matches));
1155 } else {
1156 searchable_item.clear_matches(window, cx);
1157 }
1158 }
1159 }
1160
1161 self.searchable_items_with_matches
1162 .extend(active_item_matches);
1163 }
1164
1165 fn update_matches(
1166 &mut self,
1167 reuse_existing_query: bool,
1168 add_to_history: bool,
1169 window: &mut Window,
1170 cx: &mut Context<Self>,
1171 ) -> oneshot::Receiver<()> {
1172 let (done_tx, done_rx) = oneshot::channel();
1173 let query = self.query(cx);
1174 self.pending_search.take();
1175
1176 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1177 self.query_error = None;
1178 if query.is_empty() {
1179 self.clear_active_searchable_item_matches(window, cx);
1180 let _ = done_tx.send(());
1181 cx.notify();
1182 } else {
1183 let query: Arc<_> = if let Some(search) =
1184 self.active_search.take().filter(|_| reuse_existing_query)
1185 {
1186 search
1187 } else {
1188 if self.search_options.contains(SearchOptions::REGEX) {
1189 match SearchQuery::regex(
1190 query,
1191 self.search_options.contains(SearchOptions::WHOLE_WORD),
1192 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1193 false,
1194 self.search_options
1195 .contains(SearchOptions::ONE_MATCH_PER_LINE),
1196 Default::default(),
1197 Default::default(),
1198 false,
1199 None,
1200 ) {
1201 Ok(query) => query.with_replacement(self.replacement(cx)),
1202 Err(e) => {
1203 self.query_error = Some(e.to_string());
1204 self.clear_active_searchable_item_matches(window, cx);
1205 cx.notify();
1206 return done_rx;
1207 }
1208 }
1209 } else {
1210 match SearchQuery::text(
1211 query,
1212 self.search_options.contains(SearchOptions::WHOLE_WORD),
1213 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1214 false,
1215 Default::default(),
1216 Default::default(),
1217 false,
1218 None,
1219 ) {
1220 Ok(query) => query.with_replacement(self.replacement(cx)),
1221 Err(e) => {
1222 self.query_error = Some(e.to_string());
1223 self.clear_active_searchable_item_matches(window, cx);
1224 cx.notify();
1225 return done_rx;
1226 }
1227 }
1228 }
1229 .into()
1230 };
1231
1232 self.active_search = Some(query.clone());
1233 let query_text = query.as_str().to_string();
1234
1235 let matches = active_searchable_item.find_matches(query, window, cx);
1236
1237 let active_searchable_item = active_searchable_item.downgrade();
1238 self.pending_search = Some(cx.spawn_in(window, async move |this, cx| {
1239 let matches = matches.await;
1240
1241 this.update_in(cx, |this, window, cx| {
1242 if let Some(active_searchable_item) =
1243 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1244 {
1245 this.searchable_items_with_matches
1246 .insert(active_searchable_item.downgrade(), matches);
1247
1248 this.update_match_index(window, cx);
1249 if add_to_history {
1250 this.search_history
1251 .add(&mut this.search_history_cursor, query_text);
1252 }
1253 if !this.dismissed {
1254 let matches = this
1255 .searchable_items_with_matches
1256 .get(&active_searchable_item.downgrade())
1257 .unwrap();
1258 if matches.is_empty() {
1259 active_searchable_item.clear_matches(window, cx);
1260 } else {
1261 active_searchable_item.update_matches(matches, window, cx);
1262 }
1263 let _ = done_tx.send(());
1264 }
1265 cx.notify();
1266 }
1267 })
1268 .log_err();
1269 }));
1270 }
1271 }
1272 done_rx
1273 }
1274
1275 fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction {
1276 if self.search_options.contains(SearchOptions::BACKWARDS) {
1277 direction.opposite()
1278 } else {
1279 direction
1280 }
1281 }
1282
1283 pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1284 let direction = self.reverse_direction_if_backwards(Direction::Next);
1285 let new_index = self
1286 .active_searchable_item
1287 .as_ref()
1288 .and_then(|searchable_item| {
1289 let matches = self
1290 .searchable_items_with_matches
1291 .get(&searchable_item.downgrade())?;
1292 searchable_item.active_match_index(direction, matches, window, cx)
1293 });
1294 if new_index != self.active_match_index {
1295 self.active_match_index = new_index;
1296 cx.notify();
1297 }
1298 }
1299
1300 fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1301 self.cycle_field(Direction::Next, window, cx);
1302 }
1303
1304 fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1305 self.cycle_field(Direction::Prev, window, cx);
1306 }
1307 fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1308 let mut handles = vec![self.query_editor.focus_handle(cx)];
1309 if self.replace_enabled {
1310 handles.push(self.replacement_editor.focus_handle(cx));
1311 }
1312 if let Some(item) = self.active_searchable_item.as_ref() {
1313 handles.push(item.item_focus_handle(cx));
1314 }
1315 let current_index = match handles.iter().position(|focus| focus.is_focused(window)) {
1316 Some(index) => index,
1317 None => return,
1318 };
1319
1320 let new_index = match direction {
1321 Direction::Next => (current_index + 1) % handles.len(),
1322 Direction::Prev if current_index == 0 => handles.len() - 1,
1323 Direction::Prev => (current_index - 1) % handles.len(),
1324 };
1325 let next_focus_handle = &handles[new_index];
1326 self.focus(next_focus_handle, window);
1327 cx.stop_propagation();
1328 }
1329
1330 fn next_history_query(
1331 &mut self,
1332 _: &NextHistoryQuery,
1333 window: &mut Window,
1334 cx: &mut Context<Self>,
1335 ) {
1336 if let Some(new_query) = self
1337 .search_history
1338 .next(&mut self.search_history_cursor)
1339 .map(str::to_string)
1340 {
1341 drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1342 } else {
1343 self.search_history_cursor.reset();
1344 drop(self.search("", Some(self.search_options), false, window, cx));
1345 }
1346 }
1347
1348 fn previous_history_query(
1349 &mut self,
1350 _: &PreviousHistoryQuery,
1351 window: &mut Window,
1352 cx: &mut Context<Self>,
1353 ) {
1354 if self.query(cx).is_empty()
1355 && let Some(new_query) = self
1356 .search_history
1357 .current(&self.search_history_cursor)
1358 .map(str::to_string)
1359 {
1360 drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1361 return;
1362 }
1363
1364 if let Some(new_query) = self
1365 .search_history
1366 .previous(&mut self.search_history_cursor)
1367 .map(str::to_string)
1368 {
1369 drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1370 }
1371 }
1372
1373 fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window) {
1374 window.invalidate_character_coordinates();
1375 window.focus(handle);
1376 }
1377
1378 fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1379 if self.active_searchable_item.is_some() {
1380 self.replace_enabled = !self.replace_enabled;
1381 let handle = if self.replace_enabled {
1382 self.replacement_editor.focus_handle(cx)
1383 } else {
1384 self.query_editor.focus_handle(cx)
1385 };
1386 self.focus(&handle, window);
1387 cx.notify();
1388 }
1389 }
1390
1391 fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1392 let mut should_propagate = true;
1393 if !self.dismissed
1394 && self.active_search.is_some()
1395 && let Some(searchable_item) = self.active_searchable_item.as_ref()
1396 && let Some(query) = self.active_search.as_ref()
1397 && let Some(matches) = self
1398 .searchable_items_with_matches
1399 .get(&searchable_item.downgrade())
1400 {
1401 if let Some(active_index) = self.active_match_index {
1402 let query = query
1403 .as_ref()
1404 .clone()
1405 .with_replacement(self.replacement(cx));
1406 searchable_item.replace(matches.at(active_index), &query, window, cx);
1407 self.select_next_match(&SelectNextMatch, window, cx);
1408 }
1409 should_propagate = false;
1410 }
1411 if !should_propagate {
1412 cx.stop_propagation();
1413 }
1414 }
1415
1416 pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1417 if !self.dismissed
1418 && self.active_search.is_some()
1419 && let Some(searchable_item) = self.active_searchable_item.as_ref()
1420 && let Some(query) = self.active_search.as_ref()
1421 && let Some(matches) = self
1422 .searchable_items_with_matches
1423 .get(&searchable_item.downgrade())
1424 {
1425 let query = query
1426 .as_ref()
1427 .clone()
1428 .with_replacement(self.replacement(cx));
1429 searchable_item.replace_all(&mut matches.iter(), &query, window, cx);
1430 }
1431 }
1432
1433 pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1434 self.update_match_index(window, cx);
1435 self.active_match_index.is_some()
1436 }
1437
1438 pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1439 EditorSettings::get_global(cx).use_smartcase_search
1440 }
1441
1442 pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1443 str.chars().any(|c| c.is_uppercase())
1444 }
1445
1446 fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1447 if self.should_use_smartcase_search(cx) {
1448 let query = self.query(cx);
1449 if !query.is_empty() {
1450 let is_case = self.is_contains_uppercase(&query);
1451 if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1452 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1453 }
1454 }
1455 }
1456 }
1457
1458 fn adjust_query_regex_language(&self, cx: &mut App) {
1459 let enable = self.search_options.contains(SearchOptions::REGEX);
1460 let query_buffer = self
1461 .query_editor
1462 .read(cx)
1463 .buffer()
1464 .read(cx)
1465 .as_singleton()
1466 .expect("query editor should be backed by a singleton buffer");
1467 if enable {
1468 if let Some(regex_language) = self.regex_language.clone() {
1469 query_buffer.update(cx, |query_buffer, cx| {
1470 query_buffer.set_language(Some(regex_language), cx);
1471 })
1472 }
1473 } else {
1474 query_buffer.update(cx, |query_buffer, cx| {
1475 query_buffer.set_language(None, cx);
1476 })
1477 }
1478 }
1479}
1480
1481#[cfg(test)]
1482mod tests {
1483 use std::ops::Range;
1484
1485 use super::*;
1486 use editor::{
1487 DisplayPoint, Editor, MultiBuffer, SearchSettings, SelectionEffects,
1488 display_map::DisplayRow,
1489 };
1490 use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1491 use language::{Buffer, Point};
1492 use project::Project;
1493 use settings::{SearchSettingsContent, SettingsStore};
1494 use smol::stream::StreamExt as _;
1495 use unindent::Unindent as _;
1496
1497 fn init_globals(cx: &mut TestAppContext) {
1498 cx.update(|cx| {
1499 let store = settings::SettingsStore::test(cx);
1500 cx.set_global(store);
1501 workspace::init_settings(cx);
1502 editor::init(cx);
1503
1504 language::init(cx);
1505 Project::init_settings(cx);
1506 theme::init(theme::LoadThemes::JustBase, cx);
1507 crate::init(cx);
1508 });
1509 }
1510
1511 fn init_test(
1512 cx: &mut TestAppContext,
1513 ) -> (
1514 Entity<Editor>,
1515 Entity<BufferSearchBar>,
1516 &mut VisualTestContext,
1517 ) {
1518 init_globals(cx);
1519 let buffer = cx.new(|cx| {
1520 Buffer::local(
1521 r#"
1522 A regular expression (shortened as regex or regexp;[1] also referred to as
1523 rational expression[2][3]) is a sequence of characters that specifies a search
1524 pattern in text. Usually such patterns are used by string-searching algorithms
1525 for "find" or "find and replace" operations on strings, or for input validation.
1526 "#
1527 .unindent(),
1528 cx,
1529 )
1530 });
1531 let mut editor = None;
1532 let window = cx.add_window(|window, cx| {
1533 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
1534 "keymaps/default-macos.json",
1535 cx,
1536 )
1537 .unwrap();
1538 cx.bind_keys(default_key_bindings);
1539 editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
1540 let mut search_bar = BufferSearchBar::new(None, window, cx);
1541 search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
1542 search_bar.show(window, cx);
1543 search_bar
1544 });
1545 let search_bar = window.root(cx).unwrap();
1546
1547 let cx = VisualTestContext::from_window(*window, cx).into_mut();
1548
1549 (editor.unwrap(), search_bar, cx)
1550 }
1551
1552 #[gpui::test]
1553 async fn test_search_simple(cx: &mut TestAppContext) {
1554 let (editor, search_bar, cx) = init_test(cx);
1555 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1556 background_highlights
1557 .into_iter()
1558 .map(|(range, _)| range)
1559 .collect::<Vec<_>>()
1560 };
1561 // Search for a string that appears with different casing.
1562 // By default, search is case-insensitive.
1563 search_bar
1564 .update_in(cx, |search_bar, window, cx| {
1565 search_bar.search("us", None, true, window, cx)
1566 })
1567 .await
1568 .unwrap();
1569 editor.update_in(cx, |editor, window, cx| {
1570 assert_eq!(
1571 display_points_of(editor.all_text_background_highlights(window, cx)),
1572 &[
1573 DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1574 DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1575 ]
1576 );
1577 });
1578
1579 // Switch to a case sensitive search.
1580 search_bar.update_in(cx, |search_bar, window, cx| {
1581 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1582 });
1583 let mut editor_notifications = cx.notifications(&editor);
1584 editor_notifications.next().await;
1585 editor.update_in(cx, |editor, window, cx| {
1586 assert_eq!(
1587 display_points_of(editor.all_text_background_highlights(window, cx)),
1588 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1589 );
1590 });
1591
1592 // Search for a string that appears both as a whole word and
1593 // within other words. By default, all results are found.
1594 search_bar
1595 .update_in(cx, |search_bar, window, cx| {
1596 search_bar.search("or", None, true, window, cx)
1597 })
1598 .await
1599 .unwrap();
1600 editor.update_in(cx, |editor, window, cx| {
1601 assert_eq!(
1602 display_points_of(editor.all_text_background_highlights(window, cx)),
1603 &[
1604 DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1605 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1606 DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1607 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1608 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1609 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1610 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1611 ]
1612 );
1613 });
1614
1615 // Switch to a whole word search.
1616 search_bar.update_in(cx, |search_bar, window, cx| {
1617 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1618 });
1619 let mut editor_notifications = cx.notifications(&editor);
1620 editor_notifications.next().await;
1621 editor.update_in(cx, |editor, window, cx| {
1622 assert_eq!(
1623 display_points_of(editor.all_text_background_highlights(window, cx)),
1624 &[
1625 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1626 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1627 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1628 ]
1629 );
1630 });
1631
1632 editor.update_in(cx, |editor, window, cx| {
1633 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1634 s.select_display_ranges([
1635 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1636 ])
1637 });
1638 });
1639 search_bar.update_in(cx, |search_bar, window, cx| {
1640 assert_eq!(search_bar.active_match_index, Some(0));
1641 search_bar.select_next_match(&SelectNextMatch, window, cx);
1642 assert_eq!(
1643 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1644 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1645 );
1646 });
1647 search_bar.read_with(cx, |search_bar, _| {
1648 assert_eq!(search_bar.active_match_index, Some(0));
1649 });
1650
1651 search_bar.update_in(cx, |search_bar, window, cx| {
1652 search_bar.select_next_match(&SelectNextMatch, window, cx);
1653 assert_eq!(
1654 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1655 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1656 );
1657 });
1658 search_bar.read_with(cx, |search_bar, _| {
1659 assert_eq!(search_bar.active_match_index, Some(1));
1660 });
1661
1662 search_bar.update_in(cx, |search_bar, window, cx| {
1663 search_bar.select_next_match(&SelectNextMatch, window, cx);
1664 assert_eq!(
1665 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1666 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1667 );
1668 });
1669 search_bar.read_with(cx, |search_bar, _| {
1670 assert_eq!(search_bar.active_match_index, Some(2));
1671 });
1672
1673 search_bar.update_in(cx, |search_bar, window, cx| {
1674 search_bar.select_next_match(&SelectNextMatch, window, cx);
1675 assert_eq!(
1676 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1677 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1678 );
1679 });
1680 search_bar.read_with(cx, |search_bar, _| {
1681 assert_eq!(search_bar.active_match_index, Some(0));
1682 });
1683
1684 search_bar.update_in(cx, |search_bar, window, cx| {
1685 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1686 assert_eq!(
1687 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1688 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1689 );
1690 });
1691 search_bar.read_with(cx, |search_bar, _| {
1692 assert_eq!(search_bar.active_match_index, Some(2));
1693 });
1694
1695 search_bar.update_in(cx, |search_bar, window, cx| {
1696 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1697 assert_eq!(
1698 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1699 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1700 );
1701 });
1702 search_bar.read_with(cx, |search_bar, _| {
1703 assert_eq!(search_bar.active_match_index, Some(1));
1704 });
1705
1706 search_bar.update_in(cx, |search_bar, window, cx| {
1707 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1708 assert_eq!(
1709 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1710 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1711 );
1712 });
1713 search_bar.read_with(cx, |search_bar, _| {
1714 assert_eq!(search_bar.active_match_index, Some(0));
1715 });
1716
1717 // Park the cursor in between matches and ensure that going to the previous match selects
1718 // the closest match to the left.
1719 editor.update_in(cx, |editor, window, cx| {
1720 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1721 s.select_display_ranges([
1722 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1723 ])
1724 });
1725 });
1726 search_bar.update_in(cx, |search_bar, window, cx| {
1727 assert_eq!(search_bar.active_match_index, Some(1));
1728 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1729 assert_eq!(
1730 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1731 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1732 );
1733 });
1734 search_bar.read_with(cx, |search_bar, _| {
1735 assert_eq!(search_bar.active_match_index, Some(0));
1736 });
1737
1738 // Park the cursor in between matches and ensure that going to the next match selects the
1739 // closest match to the right.
1740 editor.update_in(cx, |editor, window, cx| {
1741 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1742 s.select_display_ranges([
1743 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1744 ])
1745 });
1746 });
1747 search_bar.update_in(cx, |search_bar, window, cx| {
1748 assert_eq!(search_bar.active_match_index, Some(1));
1749 search_bar.select_next_match(&SelectNextMatch, window, cx);
1750 assert_eq!(
1751 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1752 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1753 );
1754 });
1755 search_bar.read_with(cx, |search_bar, _| {
1756 assert_eq!(search_bar.active_match_index, Some(1));
1757 });
1758
1759 // Park the cursor after the last match and ensure that going to the previous match selects
1760 // the last match.
1761 editor.update_in(cx, |editor, window, cx| {
1762 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1763 s.select_display_ranges([
1764 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1765 ])
1766 });
1767 });
1768 search_bar.update_in(cx, |search_bar, window, cx| {
1769 assert_eq!(search_bar.active_match_index, Some(2));
1770 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1771 assert_eq!(
1772 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1773 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1774 );
1775 });
1776 search_bar.read_with(cx, |search_bar, _| {
1777 assert_eq!(search_bar.active_match_index, Some(2));
1778 });
1779
1780 // Park the cursor after the last match and ensure that going to the next match selects the
1781 // first match.
1782 editor.update_in(cx, |editor, window, cx| {
1783 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1784 s.select_display_ranges([
1785 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1786 ])
1787 });
1788 });
1789 search_bar.update_in(cx, |search_bar, window, cx| {
1790 assert_eq!(search_bar.active_match_index, Some(2));
1791 search_bar.select_next_match(&SelectNextMatch, window, cx);
1792 assert_eq!(
1793 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1794 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1795 );
1796 });
1797 search_bar.read_with(cx, |search_bar, _| {
1798 assert_eq!(search_bar.active_match_index, Some(0));
1799 });
1800
1801 // Park the cursor before the first match and ensure that going to the previous match
1802 // selects the last match.
1803 editor.update_in(cx, |editor, window, cx| {
1804 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1805 s.select_display_ranges([
1806 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1807 ])
1808 });
1809 });
1810 search_bar.update_in(cx, |search_bar, window, cx| {
1811 assert_eq!(search_bar.active_match_index, Some(0));
1812 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1813 assert_eq!(
1814 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1815 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1816 );
1817 });
1818 search_bar.read_with(cx, |search_bar, _| {
1819 assert_eq!(search_bar.active_match_index, Some(2));
1820 });
1821 }
1822
1823 fn display_points_of(
1824 background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1825 ) -> Vec<Range<DisplayPoint>> {
1826 background_highlights
1827 .into_iter()
1828 .map(|(range, _)| range)
1829 .collect::<Vec<_>>()
1830 }
1831
1832 #[gpui::test]
1833 async fn test_search_option_handling(cx: &mut TestAppContext) {
1834 let (editor, search_bar, cx) = init_test(cx);
1835
1836 // show with options should make current search case sensitive
1837 search_bar
1838 .update_in(cx, |search_bar, window, cx| {
1839 search_bar.show(window, cx);
1840 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
1841 })
1842 .await
1843 .unwrap();
1844 editor.update_in(cx, |editor, window, cx| {
1845 assert_eq!(
1846 display_points_of(editor.all_text_background_highlights(window, cx)),
1847 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1848 );
1849 });
1850
1851 // search_suggested should restore default options
1852 search_bar.update_in(cx, |search_bar, window, cx| {
1853 search_bar.search_suggested(window, cx);
1854 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1855 });
1856
1857 // toggling a search option should update the defaults
1858 search_bar
1859 .update_in(cx, |search_bar, window, cx| {
1860 search_bar.search(
1861 "regex",
1862 Some(SearchOptions::CASE_SENSITIVE),
1863 true,
1864 window,
1865 cx,
1866 )
1867 })
1868 .await
1869 .unwrap();
1870 search_bar.update_in(cx, |search_bar, window, cx| {
1871 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1872 });
1873 let mut editor_notifications = cx.notifications(&editor);
1874 editor_notifications.next().await;
1875 editor.update_in(cx, |editor, window, cx| {
1876 assert_eq!(
1877 display_points_of(editor.all_text_background_highlights(window, cx)),
1878 &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1879 );
1880 });
1881
1882 // defaults should still include whole word
1883 search_bar.update_in(cx, |search_bar, window, cx| {
1884 search_bar.search_suggested(window, cx);
1885 assert_eq!(
1886 search_bar.search_options,
1887 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1888 )
1889 });
1890 }
1891
1892 #[gpui::test]
1893 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1894 init_globals(cx);
1895 let buffer_text = r#"
1896 A regular expression (shortened as regex or regexp;[1] also referred to as
1897 rational expression[2][3]) is a sequence of characters that specifies a search
1898 pattern in text. Usually such patterns are used by string-searching algorithms
1899 for "find" or "find and replace" operations on strings, or for input validation.
1900 "#
1901 .unindent();
1902 let expected_query_matches_count = buffer_text
1903 .chars()
1904 .filter(|c| c.eq_ignore_ascii_case(&'a'))
1905 .count();
1906 assert!(
1907 expected_query_matches_count > 1,
1908 "Should pick a query with multiple results"
1909 );
1910 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
1911 let window = cx.add_window(|_, _| gpui::Empty);
1912
1913 let editor = window.build_entity(cx, |window, cx| {
1914 Editor::for_buffer(buffer.clone(), None, window, cx)
1915 });
1916
1917 let search_bar = window.build_entity(cx, |window, cx| {
1918 let mut search_bar = BufferSearchBar::new(None, window, cx);
1919 search_bar.set_active_pane_item(Some(&editor), window, cx);
1920 search_bar.show(window, cx);
1921 search_bar
1922 });
1923
1924 window
1925 .update(cx, |_, window, cx| {
1926 search_bar.update(cx, |search_bar, cx| {
1927 search_bar.search("a", None, true, window, cx)
1928 })
1929 })
1930 .unwrap()
1931 .await
1932 .unwrap();
1933 let initial_selections = window
1934 .update(cx, |_, window, cx| {
1935 search_bar.update(cx, |search_bar, cx| {
1936 let handle = search_bar.query_editor.focus_handle(cx);
1937 window.focus(&handle);
1938 search_bar.activate_current_match(window, cx);
1939 });
1940 assert!(
1941 !editor.read(cx).is_focused(window),
1942 "Initially, the editor should not be focused"
1943 );
1944 let initial_selections = editor.update(cx, |editor, cx| {
1945 let initial_selections = editor.selections.display_ranges(cx);
1946 assert_eq!(
1947 initial_selections.len(), 1,
1948 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1949 );
1950 initial_selections
1951 });
1952 search_bar.update(cx, |search_bar, cx| {
1953 assert_eq!(search_bar.active_match_index, Some(0));
1954 let handle = search_bar.query_editor.focus_handle(cx);
1955 window.focus(&handle);
1956 search_bar.select_all_matches(&SelectAllMatches, window, cx);
1957 });
1958 assert!(
1959 editor.read(cx).is_focused(window),
1960 "Should focus editor after successful SelectAllMatches"
1961 );
1962 search_bar.update(cx, |search_bar, cx| {
1963 let all_selections =
1964 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1965 assert_eq!(
1966 all_selections.len(),
1967 expected_query_matches_count,
1968 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1969 );
1970 assert_eq!(
1971 search_bar.active_match_index,
1972 Some(0),
1973 "Match index should not change after selecting all matches"
1974 );
1975 });
1976
1977 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
1978 initial_selections
1979 }).unwrap();
1980
1981 window
1982 .update(cx, |_, window, cx| {
1983 assert!(
1984 editor.read(cx).is_focused(window),
1985 "Should still have editor focused after SelectNextMatch"
1986 );
1987 search_bar.update(cx, |search_bar, cx| {
1988 let all_selections =
1989 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1990 assert_eq!(
1991 all_selections.len(),
1992 1,
1993 "On next match, should deselect items and select the next match"
1994 );
1995 assert_ne!(
1996 all_selections, initial_selections,
1997 "Next match should be different from the first selection"
1998 );
1999 assert_eq!(
2000 search_bar.active_match_index,
2001 Some(1),
2002 "Match index should be updated to the next one"
2003 );
2004 let handle = search_bar.query_editor.focus_handle(cx);
2005 window.focus(&handle);
2006 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2007 });
2008 })
2009 .unwrap();
2010 window
2011 .update(cx, |_, window, cx| {
2012 assert!(
2013 editor.read(cx).is_focused(window),
2014 "Should focus editor after successful SelectAllMatches"
2015 );
2016 search_bar.update(cx, |search_bar, cx| {
2017 let all_selections =
2018 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2019 assert_eq!(
2020 all_selections.len(),
2021 expected_query_matches_count,
2022 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2023 );
2024 assert_eq!(
2025 search_bar.active_match_index,
2026 Some(1),
2027 "Match index should not change after selecting all matches"
2028 );
2029 });
2030 search_bar.update(cx, |search_bar, cx| {
2031 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2032 });
2033 })
2034 .unwrap();
2035 let last_match_selections = window
2036 .update(cx, |_, window, cx| {
2037 assert!(
2038 editor.read(cx).is_focused(window),
2039 "Should still have editor focused after SelectPreviousMatch"
2040 );
2041
2042 search_bar.update(cx, |search_bar, cx| {
2043 let all_selections =
2044 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2045 assert_eq!(
2046 all_selections.len(),
2047 1,
2048 "On previous match, should deselect items and select the previous item"
2049 );
2050 assert_eq!(
2051 all_selections, initial_selections,
2052 "Previous match should be the same as the first selection"
2053 );
2054 assert_eq!(
2055 search_bar.active_match_index,
2056 Some(0),
2057 "Match index should be updated to the previous one"
2058 );
2059 all_selections
2060 })
2061 })
2062 .unwrap();
2063
2064 window
2065 .update(cx, |_, window, cx| {
2066 search_bar.update(cx, |search_bar, cx| {
2067 let handle = search_bar.query_editor.focus_handle(cx);
2068 window.focus(&handle);
2069 search_bar.search("abas_nonexistent_match", None, true, window, cx)
2070 })
2071 })
2072 .unwrap()
2073 .await
2074 .unwrap();
2075 window
2076 .update(cx, |_, window, cx| {
2077 search_bar.update(cx, |search_bar, cx| {
2078 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2079 });
2080 assert!(
2081 editor.update(cx, |this, _cx| !this.is_focused(window)),
2082 "Should not switch focus to editor if SelectAllMatches does not find any matches"
2083 );
2084 search_bar.update(cx, |search_bar, cx| {
2085 let all_selections =
2086 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2087 assert_eq!(
2088 all_selections, last_match_selections,
2089 "Should not select anything new if there are no matches"
2090 );
2091 assert!(
2092 search_bar.active_match_index.is_none(),
2093 "For no matches, there should be no active match index"
2094 );
2095 });
2096 })
2097 .unwrap();
2098 }
2099
2100 #[gpui::test]
2101 async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2102 init_globals(cx);
2103 let buffer_text = r#"
2104 self.buffer.update(cx, |buffer, cx| {
2105 buffer.edit(
2106 edits,
2107 Some(AutoindentMode::Block {
2108 original_indent_columns,
2109 }),
2110 cx,
2111 )
2112 });
2113
2114 this.buffer.update(cx, |buffer, cx| {
2115 buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2116 });
2117 "#
2118 .unindent();
2119 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2120 let cx = cx.add_empty_window();
2121
2122 let editor =
2123 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2124
2125 let search_bar = cx.new_window_entity(|window, cx| {
2126 let mut search_bar = BufferSearchBar::new(None, window, cx);
2127 search_bar.set_active_pane_item(Some(&editor), window, cx);
2128 search_bar.show(window, cx);
2129 search_bar
2130 });
2131
2132 search_bar
2133 .update_in(cx, |search_bar, window, cx| {
2134 search_bar.search(
2135 "edit\\(",
2136 Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2137 true,
2138 window,
2139 cx,
2140 )
2141 })
2142 .await
2143 .unwrap();
2144
2145 search_bar.update_in(cx, |search_bar, window, cx| {
2146 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2147 });
2148 search_bar.update(cx, |_, cx| {
2149 let all_selections =
2150 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2151 assert_eq!(
2152 all_selections.len(),
2153 2,
2154 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2155 );
2156 });
2157
2158 search_bar
2159 .update_in(cx, |search_bar, window, cx| {
2160 search_bar.search(
2161 "edit(",
2162 Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2163 true,
2164 window,
2165 cx,
2166 )
2167 })
2168 .await
2169 .unwrap();
2170
2171 search_bar.update_in(cx, |search_bar, window, cx| {
2172 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2173 });
2174 search_bar.update(cx, |_, cx| {
2175 let all_selections =
2176 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2177 assert_eq!(
2178 all_selections.len(),
2179 2,
2180 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2181 );
2182 });
2183 }
2184
2185 #[gpui::test]
2186 async fn test_search_query_history(cx: &mut TestAppContext) {
2187 let (_editor, search_bar, cx) = init_test(cx);
2188
2189 // Add 3 search items into the history.
2190 search_bar
2191 .update_in(cx, |search_bar, window, cx| {
2192 search_bar.search("a", None, true, window, cx)
2193 })
2194 .await
2195 .unwrap();
2196 search_bar
2197 .update_in(cx, |search_bar, window, cx| {
2198 search_bar.search("b", None, true, window, cx)
2199 })
2200 .await
2201 .unwrap();
2202 search_bar
2203 .update_in(cx, |search_bar, window, cx| {
2204 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2205 })
2206 .await
2207 .unwrap();
2208 // Ensure that the latest search is active.
2209 search_bar.update(cx, |search_bar, cx| {
2210 assert_eq!(search_bar.query(cx), "c");
2211 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2212 });
2213
2214 // Next history query after the latest should set the query to the empty string.
2215 search_bar.update_in(cx, |search_bar, window, cx| {
2216 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2217 });
2218 cx.background_executor.run_until_parked();
2219 search_bar.update(cx, |search_bar, cx| {
2220 assert_eq!(search_bar.query(cx), "");
2221 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2222 });
2223 search_bar.update_in(cx, |search_bar, window, cx| {
2224 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2225 });
2226 cx.background_executor.run_until_parked();
2227 search_bar.update(cx, |search_bar, cx| {
2228 assert_eq!(search_bar.query(cx), "");
2229 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2230 });
2231
2232 // First previous query for empty current query should set the query to the latest.
2233 search_bar.update_in(cx, |search_bar, window, cx| {
2234 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2235 });
2236 cx.background_executor.run_until_parked();
2237 search_bar.update(cx, |search_bar, cx| {
2238 assert_eq!(search_bar.query(cx), "c");
2239 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2240 });
2241
2242 // Further previous items should go over the history in reverse order.
2243 search_bar.update_in(cx, |search_bar, window, cx| {
2244 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2245 });
2246 cx.background_executor.run_until_parked();
2247 search_bar.update(cx, |search_bar, cx| {
2248 assert_eq!(search_bar.query(cx), "b");
2249 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2250 });
2251
2252 // Previous items should never go behind the first history item.
2253 search_bar.update_in(cx, |search_bar, window, cx| {
2254 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2255 });
2256 cx.background_executor.run_until_parked();
2257 search_bar.update(cx, |search_bar, cx| {
2258 assert_eq!(search_bar.query(cx), "a");
2259 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2260 });
2261 search_bar.update_in(cx, |search_bar, window, cx| {
2262 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2263 });
2264 cx.background_executor.run_until_parked();
2265 search_bar.update(cx, |search_bar, cx| {
2266 assert_eq!(search_bar.query(cx), "a");
2267 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2268 });
2269
2270 // Next items should go over the history in the original order.
2271 search_bar.update_in(cx, |search_bar, window, cx| {
2272 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2273 });
2274 cx.background_executor.run_until_parked();
2275 search_bar.update(cx, |search_bar, cx| {
2276 assert_eq!(search_bar.query(cx), "b");
2277 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2278 });
2279
2280 search_bar
2281 .update_in(cx, |search_bar, window, cx| {
2282 search_bar.search("ba", None, true, window, cx)
2283 })
2284 .await
2285 .unwrap();
2286 search_bar.update(cx, |search_bar, cx| {
2287 assert_eq!(search_bar.query(cx), "ba");
2288 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2289 });
2290
2291 // New search input should add another entry to history and move the selection to the end of the history.
2292 search_bar.update_in(cx, |search_bar, window, cx| {
2293 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2294 });
2295 cx.background_executor.run_until_parked();
2296 search_bar.update(cx, |search_bar, cx| {
2297 assert_eq!(search_bar.query(cx), "c");
2298 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2299 });
2300 search_bar.update_in(cx, |search_bar, window, cx| {
2301 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2302 });
2303 cx.background_executor.run_until_parked();
2304 search_bar.update(cx, |search_bar, cx| {
2305 assert_eq!(search_bar.query(cx), "b");
2306 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2307 });
2308 search_bar.update_in(cx, |search_bar, window, cx| {
2309 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2310 });
2311 cx.background_executor.run_until_parked();
2312 search_bar.update(cx, |search_bar, cx| {
2313 assert_eq!(search_bar.query(cx), "c");
2314 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2315 });
2316 search_bar.update_in(cx, |search_bar, window, cx| {
2317 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2318 });
2319 cx.background_executor.run_until_parked();
2320 search_bar.update(cx, |search_bar, cx| {
2321 assert_eq!(search_bar.query(cx), "ba");
2322 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2323 });
2324 search_bar.update_in(cx, |search_bar, window, cx| {
2325 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2326 });
2327 cx.background_executor.run_until_parked();
2328 search_bar.update(cx, |search_bar, cx| {
2329 assert_eq!(search_bar.query(cx), "");
2330 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2331 });
2332 }
2333
2334 #[gpui::test]
2335 async fn test_replace_simple(cx: &mut TestAppContext) {
2336 let (editor, search_bar, cx) = init_test(cx);
2337
2338 search_bar
2339 .update_in(cx, |search_bar, window, cx| {
2340 search_bar.search("expression", None, true, window, cx)
2341 })
2342 .await
2343 .unwrap();
2344
2345 search_bar.update_in(cx, |search_bar, window, cx| {
2346 search_bar.replacement_editor.update(cx, |editor, cx| {
2347 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2348 editor.set_text("expr$1", window, cx);
2349 });
2350 search_bar.replace_all(&ReplaceAll, window, cx)
2351 });
2352 assert_eq!(
2353 editor.read_with(cx, |this, cx| { this.text(cx) }),
2354 r#"
2355 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2356 rational expr$1[2][3]) is a sequence of characters that specifies a search
2357 pattern in text. Usually such patterns are used by string-searching algorithms
2358 for "find" or "find and replace" operations on strings, or for input validation.
2359 "#
2360 .unindent()
2361 );
2362
2363 // Search for word boundaries and replace just a single one.
2364 search_bar
2365 .update_in(cx, |search_bar, window, cx| {
2366 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx)
2367 })
2368 .await
2369 .unwrap();
2370
2371 search_bar.update_in(cx, |search_bar, window, cx| {
2372 search_bar.replacement_editor.update(cx, |editor, cx| {
2373 editor.set_text("banana", window, cx);
2374 });
2375 search_bar.replace_next(&ReplaceNext, window, cx)
2376 });
2377 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2378 assert_eq!(
2379 editor.read_with(cx, |this, cx| { this.text(cx) }),
2380 r#"
2381 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2382 rational expr$1[2][3]) is a sequence of characters that specifies a search
2383 pattern in text. Usually such patterns are used by string-searching algorithms
2384 for "find" or "find and replace" operations on strings, or for input validation.
2385 "#
2386 .unindent()
2387 );
2388 // Let's turn on regex mode.
2389 search_bar
2390 .update_in(cx, |search_bar, window, cx| {
2391 search_bar.search(
2392 "\\[([^\\]]+)\\]",
2393 Some(SearchOptions::REGEX),
2394 true,
2395 window,
2396 cx,
2397 )
2398 })
2399 .await
2400 .unwrap();
2401 search_bar.update_in(cx, |search_bar, window, cx| {
2402 search_bar.replacement_editor.update(cx, |editor, cx| {
2403 editor.set_text("${1}number", window, cx);
2404 });
2405 search_bar.replace_all(&ReplaceAll, window, cx)
2406 });
2407 assert_eq!(
2408 editor.read_with(cx, |this, cx| { this.text(cx) }),
2409 r#"
2410 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2411 rational expr$12number3number) is a sequence of characters that specifies a search
2412 pattern in text. Usually such patterns are used by string-searching algorithms
2413 for "find" or "find and replace" operations on strings, or for input validation.
2414 "#
2415 .unindent()
2416 );
2417 // Now with a whole-word twist.
2418 search_bar
2419 .update_in(cx, |search_bar, window, cx| {
2420 search_bar.search(
2421 "a\\w+s",
2422 Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2423 true,
2424 window,
2425 cx,
2426 )
2427 })
2428 .await
2429 .unwrap();
2430 search_bar.update_in(cx, |search_bar, window, cx| {
2431 search_bar.replacement_editor.update(cx, |editor, cx| {
2432 editor.set_text("things", window, cx);
2433 });
2434 search_bar.replace_all(&ReplaceAll, window, cx)
2435 });
2436 // The only word affected by this edit should be `algorithms`, even though there's a bunch
2437 // of words in this text that would match this regex if not for WHOLE_WORD.
2438 assert_eq!(
2439 editor.read_with(cx, |this, cx| { this.text(cx) }),
2440 r#"
2441 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2442 rational expr$12number3number) is a sequence of characters that specifies a search
2443 pattern in text. Usually such patterns are used by string-searching things
2444 for "find" or "find and replace" operations on strings, or for input validation.
2445 "#
2446 .unindent()
2447 );
2448 }
2449
2450 struct ReplacementTestParams<'a> {
2451 editor: &'a Entity<Editor>,
2452 search_bar: &'a Entity<BufferSearchBar>,
2453 cx: &'a mut VisualTestContext,
2454 search_text: &'static str,
2455 search_options: Option<SearchOptions>,
2456 replacement_text: &'static str,
2457 replace_all: bool,
2458 expected_text: String,
2459 }
2460
2461 async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2462 options
2463 .search_bar
2464 .update_in(options.cx, |search_bar, window, cx| {
2465 if let Some(options) = options.search_options {
2466 search_bar.set_search_options(options, cx);
2467 }
2468 search_bar.search(
2469 options.search_text,
2470 options.search_options,
2471 true,
2472 window,
2473 cx,
2474 )
2475 })
2476 .await
2477 .unwrap();
2478
2479 options
2480 .search_bar
2481 .update_in(options.cx, |search_bar, window, cx| {
2482 search_bar.replacement_editor.update(cx, |editor, cx| {
2483 editor.set_text(options.replacement_text, window, cx);
2484 });
2485
2486 if options.replace_all {
2487 search_bar.replace_all(&ReplaceAll, window, cx)
2488 } else {
2489 search_bar.replace_next(&ReplaceNext, window, cx)
2490 }
2491 });
2492
2493 assert_eq!(
2494 options
2495 .editor
2496 .read_with(options.cx, |this, cx| { this.text(cx) }),
2497 options.expected_text
2498 );
2499 }
2500
2501 #[gpui::test]
2502 async fn test_replace_special_characters(cx: &mut TestAppContext) {
2503 let (editor, search_bar, cx) = init_test(cx);
2504
2505 run_replacement_test(ReplacementTestParams {
2506 editor: &editor,
2507 search_bar: &search_bar,
2508 cx,
2509 search_text: "expression",
2510 search_options: None,
2511 replacement_text: r"\n",
2512 replace_all: true,
2513 expected_text: r#"
2514 A regular \n (shortened as regex or regexp;[1] also referred to as
2515 rational \n[2][3]) is a sequence of characters that specifies a search
2516 pattern in text. Usually such patterns are used by string-searching algorithms
2517 for "find" or "find and replace" operations on strings, or for input validation.
2518 "#
2519 .unindent(),
2520 })
2521 .await;
2522
2523 run_replacement_test(ReplacementTestParams {
2524 editor: &editor,
2525 search_bar: &search_bar,
2526 cx,
2527 search_text: "or",
2528 search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2529 replacement_text: r"\\\n\\\\",
2530 replace_all: false,
2531 expected_text: r#"
2532 A regular \n (shortened as regex \
2533 \\ regexp;[1] also referred to as
2534 rational \n[2][3]) is a sequence of characters that specifies a search
2535 pattern in text. Usually such patterns are used by string-searching algorithms
2536 for "find" or "find and replace" operations on strings, or for input validation.
2537 "#
2538 .unindent(),
2539 })
2540 .await;
2541
2542 run_replacement_test(ReplacementTestParams {
2543 editor: &editor,
2544 search_bar: &search_bar,
2545 cx,
2546 search_text: r"(that|used) ",
2547 search_options: Some(SearchOptions::REGEX),
2548 replacement_text: r"$1\n",
2549 replace_all: true,
2550 expected_text: r#"
2551 A regular \n (shortened as regex \
2552 \\ regexp;[1] also referred to as
2553 rational \n[2][3]) is a sequence of characters that
2554 specifies a search
2555 pattern in text. Usually such patterns are used
2556 by string-searching algorithms
2557 for "find" or "find and replace" operations on strings, or for input validation.
2558 "#
2559 .unindent(),
2560 })
2561 .await;
2562 }
2563
2564 #[gpui::test]
2565 async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2566 cx: &mut TestAppContext,
2567 ) {
2568 init_globals(cx);
2569 let buffer = cx.new(|cx| {
2570 Buffer::local(
2571 r#"
2572 aaa bbb aaa ccc
2573 aaa bbb aaa ccc
2574 aaa bbb aaa ccc
2575 aaa bbb aaa ccc
2576 aaa bbb aaa ccc
2577 aaa bbb aaa ccc
2578 "#
2579 .unindent(),
2580 cx,
2581 )
2582 });
2583 let cx = cx.add_empty_window();
2584 let editor =
2585 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2586
2587 let search_bar = cx.new_window_entity(|window, cx| {
2588 let mut search_bar = BufferSearchBar::new(None, window, cx);
2589 search_bar.set_active_pane_item(Some(&editor), window, cx);
2590 search_bar.show(window, cx);
2591 search_bar
2592 });
2593
2594 editor.update_in(cx, |editor, window, cx| {
2595 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2596 s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2597 })
2598 });
2599
2600 search_bar.update_in(cx, |search_bar, window, cx| {
2601 let deploy = Deploy {
2602 focus: true,
2603 replace_enabled: false,
2604 selection_search_enabled: true,
2605 };
2606 search_bar.deploy(&deploy, window, cx);
2607 });
2608
2609 cx.run_until_parked();
2610
2611 search_bar
2612 .update_in(cx, |search_bar, window, cx| {
2613 search_bar.search("aaa", None, true, window, cx)
2614 })
2615 .await
2616 .unwrap();
2617
2618 editor.update(cx, |editor, cx| {
2619 assert_eq!(
2620 editor.search_background_highlights(cx),
2621 &[
2622 Point::new(1, 0)..Point::new(1, 3),
2623 Point::new(1, 8)..Point::new(1, 11),
2624 Point::new(2, 0)..Point::new(2, 3),
2625 ]
2626 );
2627 });
2628 }
2629
2630 #[gpui::test]
2631 async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2632 cx: &mut TestAppContext,
2633 ) {
2634 init_globals(cx);
2635 let text = r#"
2636 aaa bbb aaa ccc
2637 aaa bbb aaa ccc
2638 aaa bbb aaa ccc
2639 aaa bbb aaa ccc
2640 aaa bbb aaa ccc
2641 aaa bbb aaa ccc
2642
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 aaa bbb aaa ccc
2649 "#
2650 .unindent();
2651
2652 let cx = cx.add_empty_window();
2653 let editor = cx.new_window_entity(|window, cx| {
2654 let multibuffer = MultiBuffer::build_multi(
2655 [
2656 (
2657 &text,
2658 vec![
2659 Point::new(0, 0)..Point::new(2, 0),
2660 Point::new(4, 0)..Point::new(5, 0),
2661 ],
2662 ),
2663 (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2664 ],
2665 cx,
2666 );
2667 Editor::for_multibuffer(multibuffer, None, window, cx)
2668 });
2669
2670 let search_bar = cx.new_window_entity(|window, cx| {
2671 let mut search_bar = BufferSearchBar::new(None, window, cx);
2672 search_bar.set_active_pane_item(Some(&editor), window, cx);
2673 search_bar.show(window, cx);
2674 search_bar
2675 });
2676
2677 editor.update_in(cx, |editor, window, cx| {
2678 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2679 s.select_ranges(vec![
2680 Point::new(1, 0)..Point::new(1, 4),
2681 Point::new(5, 3)..Point::new(6, 4),
2682 ])
2683 })
2684 });
2685
2686 search_bar.update_in(cx, |search_bar, window, cx| {
2687 let deploy = Deploy {
2688 focus: true,
2689 replace_enabled: false,
2690 selection_search_enabled: true,
2691 };
2692 search_bar.deploy(&deploy, window, cx);
2693 });
2694
2695 cx.run_until_parked();
2696
2697 search_bar
2698 .update_in(cx, |search_bar, window, cx| {
2699 search_bar.search("aaa", None, true, window, cx)
2700 })
2701 .await
2702 .unwrap();
2703
2704 editor.update(cx, |editor, cx| {
2705 assert_eq!(
2706 editor.search_background_highlights(cx),
2707 &[
2708 Point::new(1, 0)..Point::new(1, 3),
2709 Point::new(5, 8)..Point::new(5, 11),
2710 Point::new(6, 0)..Point::new(6, 3),
2711 ]
2712 );
2713 });
2714 }
2715
2716 #[gpui::test]
2717 async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2718 let (editor, search_bar, cx) = init_test(cx);
2719 // Search using valid regexp
2720 search_bar
2721 .update_in(cx, |search_bar, window, cx| {
2722 search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
2723 search_bar.search("expression", None, true, window, cx)
2724 })
2725 .await
2726 .unwrap();
2727 editor.update_in(cx, |editor, window, cx| {
2728 assert_eq!(
2729 display_points_of(editor.all_text_background_highlights(window, cx)),
2730 &[
2731 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2732 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2733 ],
2734 );
2735 });
2736
2737 // Now, the expression is invalid
2738 search_bar
2739 .update_in(cx, |search_bar, window, cx| {
2740 search_bar.search("expression (", None, true, window, cx)
2741 })
2742 .await
2743 .unwrap_err();
2744 editor.update_in(cx, |editor, window, cx| {
2745 assert!(
2746 display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
2747 );
2748 });
2749 }
2750
2751 #[gpui::test]
2752 async fn test_search_options_changes(cx: &mut TestAppContext) {
2753 let (_editor, search_bar, cx) = init_test(cx);
2754 update_search_settings(
2755 SearchSettings {
2756 button: true,
2757 whole_word: false,
2758 case_sensitive: false,
2759 include_ignored: false,
2760 regex: false,
2761 },
2762 cx,
2763 );
2764
2765 let deploy = Deploy {
2766 focus: true,
2767 replace_enabled: false,
2768 selection_search_enabled: true,
2769 };
2770
2771 search_bar.update_in(cx, |search_bar, window, cx| {
2772 assert_eq!(
2773 search_bar.search_options,
2774 SearchOptions::NONE,
2775 "Should have no search options enabled by default"
2776 );
2777 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2778 assert_eq!(
2779 search_bar.search_options,
2780 SearchOptions::WHOLE_WORD,
2781 "Should enable the option toggled"
2782 );
2783 assert!(
2784 !search_bar.dismissed,
2785 "Search bar should be present and visible"
2786 );
2787 search_bar.deploy(&deploy, window, cx);
2788 assert_eq!(
2789 search_bar.search_options,
2790 SearchOptions::WHOLE_WORD,
2791 "After (re)deploying, the option should still be enabled"
2792 );
2793
2794 search_bar.dismiss(&Dismiss, window, cx);
2795 search_bar.deploy(&deploy, window, cx);
2796 assert_eq!(
2797 search_bar.search_options,
2798 SearchOptions::WHOLE_WORD,
2799 "After hiding and showing the search bar, search options should be preserved"
2800 );
2801
2802 search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
2803 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2804 assert_eq!(
2805 search_bar.search_options,
2806 SearchOptions::REGEX,
2807 "Should enable the options toggled"
2808 );
2809 assert!(
2810 !search_bar.dismissed,
2811 "Search bar should be present and visible"
2812 );
2813 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2814 });
2815
2816 update_search_settings(
2817 SearchSettings {
2818 button: true,
2819 whole_word: false,
2820 case_sensitive: true,
2821 include_ignored: false,
2822 regex: false,
2823 },
2824 cx,
2825 );
2826 search_bar.update_in(cx, |search_bar, window, cx| {
2827 assert_eq!(
2828 search_bar.search_options,
2829 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2830 "Should have no search options enabled by default"
2831 );
2832
2833 search_bar.deploy(&deploy, window, cx);
2834 assert_eq!(
2835 search_bar.search_options,
2836 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2837 "Toggling a non-dismissed search bar with custom options should not change the default options"
2838 );
2839 search_bar.dismiss(&Dismiss, window, cx);
2840 search_bar.deploy(&deploy, window, cx);
2841 assert_eq!(
2842 search_bar.configured_options,
2843 SearchOptions::CASE_SENSITIVE,
2844 "After a settings update and toggling the search bar, configured options should be updated"
2845 );
2846 assert_eq!(
2847 search_bar.search_options,
2848 SearchOptions::CASE_SENSITIVE,
2849 "After a settings update and toggling the search bar, configured options should be used"
2850 );
2851 });
2852
2853 update_search_settings(
2854 SearchSettings {
2855 button: true,
2856 whole_word: true,
2857 case_sensitive: true,
2858 include_ignored: false,
2859 regex: false,
2860 },
2861 cx,
2862 );
2863
2864 search_bar.update_in(cx, |search_bar, window, cx| {
2865 search_bar.deploy(&deploy, window, cx);
2866 search_bar.dismiss(&Dismiss, window, cx);
2867 search_bar.show(window, cx);
2868 assert_eq!(
2869 search_bar.search_options,
2870 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
2871 "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
2872 );
2873 });
2874 }
2875
2876 fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2877 cx.update(|cx| {
2878 SettingsStore::update_global(cx, |store, cx| {
2879 store.update_user_settings(cx, |settings| {
2880 settings.editor.search = Some(SearchSettingsContent {
2881 button: Some(search_settings.button),
2882 whole_word: Some(search_settings.whole_word),
2883 case_sensitive: Some(search_settings.case_sensitive),
2884 include_ignored: Some(search_settings.include_ignored),
2885 regex: Some(search_settings.regex),
2886 });
2887 });
2888 });
2889 });
2890 }
2891}