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