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