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