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