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