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