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