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