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