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