1mod registrar;
2
3use crate::{
4 search_bar::render_nav_button, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll,
5 ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch,
6 ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
7};
8use any_vec::AnyVec;
9use collections::HashMap;
10use editor::{
11 actions::{Tab, TabPrev},
12 DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle,
13};
14use futures::channel::oneshot;
15use gpui::{
16 actions, div, impl_actions, Action, AppContext, ClickEvent, EventEmitter, FocusableView, Hsla,
17 InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle,
18 Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext as _, WindowContext,
19};
20use project::{
21 search::SearchQuery,
22 search_history::{SearchHistory, SearchHistoryCursor},
23};
24use serde::Deserialize;
25use settings::Settings;
26use std::sync::Arc;
27use theme::ThemeSettings;
28
29use ui::{h_flex, prelude::*, IconButton, IconName, Tooltip, BASE_REM_SIZE_IN_PX};
30use util::ResultExt;
31use workspace::{
32 item::ItemHandle,
33 searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
34 ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
35};
36
37pub use registrar::DivRegistrar;
38use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults};
39
40const MIN_INPUT_WIDTH_REMS: f32 = 10.;
41const MAX_INPUT_WIDTH_REMS: f32 = 30.;
42const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
43
44#[derive(PartialEq, Clone, Deserialize)]
45pub struct Deploy {
46 #[serde(default = "util::serde::default_true")]
47 pub focus: bool,
48 #[serde(default)]
49 pub replace_enabled: bool,
50 #[serde(default)]
51 pub selection_search_enabled: bool,
52}
53
54impl_actions!(buffer_search, [Deploy]);
55
56actions!(buffer_search, [Dismiss, FocusEditor]);
57
58impl Deploy {
59 pub fn find() -> Self {
60 Self {
61 focus: true,
62 replace_enabled: false,
63 selection_search_enabled: false,
64 }
65 }
66}
67
68pub enum Event {
69 UpdateLocation,
70}
71
72pub fn init(cx: &mut AppContext) {
73 cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace))
74 .detach();
75}
76
77pub struct BufferSearchBar {
78 query_editor: View<Editor>,
79 query_editor_focused: bool,
80 replacement_editor: View<Editor>,
81 replacement_editor_focused: bool,
82 active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
83 active_match_index: Option<usize>,
84 active_searchable_item_subscription: Option<Subscription>,
85 active_search: Option<Arc<SearchQuery>>,
86 searchable_items_with_matches: HashMap<Box<dyn WeakSearchableItemHandle>, AnyVec<dyn Send>>,
87 pending_search: Option<Task<()>>,
88 search_options: SearchOptions,
89 default_options: SearchOptions,
90 configured_options: SearchOptions,
91 query_contains_error: bool,
92 dismissed: bool,
93 search_history: SearchHistory,
94 search_history_cursor: SearchHistoryCursor,
95 replace_enabled: bool,
96 selection_search_enabled: bool,
97 scroll_handle: ScrollHandle,
98 editor_scroll_handle: ScrollHandle,
99 editor_needed_width: Pixels,
100}
101
102impl BufferSearchBar {
103 fn render_text_input(
104 &self,
105 editor: &View<Editor>,
106 color: Hsla,
107 cx: &ViewContext<Self>,
108 ) -> impl IntoElement {
109 let settings = ThemeSettings::get_global(cx);
110 let text_style = TextStyle {
111 color: if editor.read(cx).read_only(cx) {
112 cx.theme().colors().text_disabled
113 } else {
114 color
115 },
116 font_family: settings.buffer_font.family.clone(),
117 font_features: settings.buffer_font.features.clone(),
118 font_fallbacks: settings.buffer_font.fallbacks.clone(),
119 font_size: rems(0.875).into(),
120 font_weight: settings.buffer_font.weight,
121 line_height: relative(1.3),
122 ..Default::default()
123 };
124
125 EditorElement::new(
126 editor,
127 EditorStyle {
128 background: cx.theme().colors().editor_background,
129 local_player: cx.theme().players().local(),
130 text: text_style,
131 ..Default::default()
132 },
133 )
134 }
135}
136
137impl EventEmitter<Event> for BufferSearchBar {}
138impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
139impl Render for BufferSearchBar {
140 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
141 if self.dismissed {
142 return div().id("search_bar");
143 }
144
145 let narrow_mode =
146 self.scroll_handle.bounds().size.width / cx.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
147 let hide_inline_icons = self.editor_needed_width
148 > self.editor_scroll_handle.bounds().size.width - cx.rem_size() * 6.;
149
150 let supported_options = self.supported_options();
151
152 if self.query_editor.update(cx, |query_editor, cx| {
153 query_editor.placeholder_text(cx).is_none()
154 }) {
155 self.query_editor.update(cx, |editor, cx| {
156 editor.set_placeholder_text("Search", cx);
157 });
158 }
159
160 self.replacement_editor.update(cx, |editor, cx| {
161 editor.set_placeholder_text("Replace with...", cx);
162 });
163
164 let mut text_color = Color::Default;
165 let match_text = self
166 .active_searchable_item
167 .as_ref()
168 .and_then(|searchable_item| {
169 if self.query(cx).is_empty() {
170 return None;
171 }
172 let matches_count = self
173 .searchable_items_with_matches
174 .get(&searchable_item.downgrade())
175 .map(AnyVec::len)
176 .unwrap_or(0);
177 if let Some(match_ix) = self.active_match_index {
178 Some(format!("{}/{}", match_ix + 1, matches_count))
179 } else {
180 text_color = Color::Error; // No matches found
181 None
182 }
183 })
184 .unwrap_or_else(|| "0/0".to_string());
185 let should_show_replace_input = self.replace_enabled && supported_options.replacement;
186 let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx);
187
188 let mut key_context = KeyContext::new_with_defaults();
189 key_context.add("BufferSearchBar");
190 if in_replace {
191 key_context.add("in_replace");
192 }
193 let editor_border = if self.query_contains_error {
194 Color::Error.color(cx)
195 } else {
196 cx.theme().colors().border
197 };
198
199 let search_line = h_flex()
200 .mb_1()
201 .child(
202 h_flex()
203 .id("editor-scroll")
204 .track_scroll(&self.editor_scroll_handle)
205 .flex_1()
206 .h_8()
207 .px_2()
208 .mr_2()
209 .py_1()
210 .border_1()
211 .border_color(editor_border)
212 .min_w(rems(MIN_INPUT_WIDTH_REMS))
213 .max_w(rems(MAX_INPUT_WIDTH_REMS))
214 .rounded_lg()
215 .child(self.render_text_input(&self.query_editor, text_color.color(cx), cx))
216 .when(!hide_inline_icons, |div| {
217 div.children(supported_options.case.then(|| {
218 self.render_search_option_button(
219 SearchOptions::CASE_SENSITIVE,
220 cx.listener(|this, _, cx| {
221 this.toggle_case_sensitive(&ToggleCaseSensitive, cx)
222 }),
223 )
224 }))
225 .children(supported_options.word.then(|| {
226 self.render_search_option_button(
227 SearchOptions::WHOLE_WORD,
228 cx.listener(|this, _, cx| {
229 this.toggle_whole_word(&ToggleWholeWord, cx)
230 }),
231 )
232 }))
233 .children(supported_options.regex.then(|| {
234 self.render_search_option_button(
235 SearchOptions::REGEX,
236 cx.listener(|this, _, cx| this.toggle_regex(&ToggleRegex, cx)),
237 )
238 }))
239 }),
240 )
241 .when(supported_options.replacement, |this| {
242 this.child(
243 IconButton::new("buffer-search-bar-toggle-replace-button", IconName::Replace)
244 .style(ButtonStyle::Subtle)
245 .when(self.replace_enabled, |button| {
246 button.style(ButtonStyle::Filled)
247 })
248 .on_click(cx.listener(|this, _: &ClickEvent, cx| {
249 this.toggle_replace(&ToggleReplace, cx);
250 }))
251 .selected(self.replace_enabled)
252 .size(ButtonSize::Compact)
253 .tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)),
254 )
255 })
256 .when(supported_options.selection, |this| {
257 this.child(
258 IconButton::new(
259 "buffer-search-bar-toggle-search-selection-button",
260 IconName::SearchSelection,
261 )
262 .style(ButtonStyle::Subtle)
263 .when(self.selection_search_enabled, |button| {
264 button.style(ButtonStyle::Filled)
265 })
266 .on_click(cx.listener(|this, _: &ClickEvent, cx| {
267 this.toggle_selection(&ToggleSelection, cx);
268 }))
269 .selected(self.selection_search_enabled)
270 .size(ButtonSize::Compact)
271 .tooltip(|cx| {
272 Tooltip::for_action("Toggle search selection", &ToggleSelection, cx)
273 }),
274 )
275 })
276 .child(
277 h_flex()
278 .flex_none()
279 .child(
280 IconButton::new("select-all", ui::IconName::SelectAll)
281 .on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone()))
282 .size(ButtonSize::Compact)
283 .tooltip(|cx| {
284 Tooltip::for_action("Select all matches", &SelectAllMatches, cx)
285 }),
286 )
287 .child(render_nav_button(
288 ui::IconName::ChevronLeft,
289 self.active_match_index.is_some(),
290 "Select previous match",
291 &SelectPrevMatch,
292 ))
293 .child(render_nav_button(
294 ui::IconName::ChevronRight,
295 self.active_match_index.is_some(),
296 "Select next match",
297 &SelectNextMatch,
298 ))
299 .when(!narrow_mode, |this| {
300 this.child(h_flex().ml_2().min_w(rems_from_px(40.)).child(
301 Label::new(match_text).color(if self.active_match_index.is_some() {
302 Color::Default
303 } else {
304 Color::Disabled
305 }),
306 ))
307 }),
308 );
309
310 let replace_line = should_show_replace_input.then(|| {
311 h_flex()
312 .gap_2()
313 .flex_1()
314 .child(
315 h_flex()
316 .flex_1()
317 // We're giving this a fixed height to match the height of the search input,
318 // which has an icon inside that is increasing its height.
319 .h_8()
320 .px_2()
321 .py_1()
322 .border_1()
323 .border_color(cx.theme().colors().border)
324 .rounded_lg()
325 .min_w(rems(MIN_INPUT_WIDTH_REMS))
326 .max_w(rems(MAX_INPUT_WIDTH_REMS))
327 .child(self.render_text_input(
328 &self.replacement_editor,
329 cx.theme().colors().text,
330 cx,
331 )),
332 )
333 .child(
334 h_flex()
335 .flex_none()
336 .child(
337 IconButton::new("search-replace-next", ui::IconName::ReplaceNext)
338 .tooltip(move |cx| {
339 Tooltip::for_action("Replace next", &ReplaceNext, cx)
340 })
341 .on_click(
342 cx.listener(|this, _, cx| this.replace_next(&ReplaceNext, cx)),
343 ),
344 )
345 .child(
346 IconButton::new("search-replace-all", ui::IconName::ReplaceAll)
347 .tooltip(move |cx| {
348 Tooltip::for_action("Replace all", &ReplaceAll, cx)
349 })
350 .on_click(
351 cx.listener(|this, _, cx| this.replace_all(&ReplaceAll, cx)),
352 ),
353 ),
354 )
355 });
356
357 v_flex()
358 .id("buffer_search")
359 .track_scroll(&self.scroll_handle)
360 .key_context(key_context)
361 .capture_action(cx.listener(Self::tab))
362 .capture_action(cx.listener(Self::tab_prev))
363 .on_action(cx.listener(Self::previous_history_query))
364 .on_action(cx.listener(Self::next_history_query))
365 .on_action(cx.listener(Self::dismiss))
366 .on_action(cx.listener(Self::select_next_match))
367 .on_action(cx.listener(Self::select_prev_match))
368 .when(self.supported_options().replacement, |this| {
369 this.on_action(cx.listener(Self::toggle_replace))
370 .when(in_replace, |this| {
371 this.on_action(cx.listener(Self::replace_next))
372 .on_action(cx.listener(Self::replace_all))
373 })
374 })
375 .when(self.supported_options().case, |this| {
376 this.on_action(cx.listener(Self::toggle_case_sensitive))
377 })
378 .when(self.supported_options().word, |this| {
379 this.on_action(cx.listener(Self::toggle_whole_word))
380 })
381 .when(self.supported_options().regex, |this| {
382 this.on_action(cx.listener(Self::toggle_regex))
383 })
384 .when(self.supported_options().selection, |this| {
385 this.on_action(cx.listener(Self::toggle_selection))
386 })
387 .gap_2()
388 .child(
389 h_flex()
390 .child(search_line.w_full())
391 .when(!narrow_mode, |div| {
392 div.child(
393 IconButton::new(SharedString::from("Close"), IconName::Close)
394 .tooltip(move |cx| {
395 Tooltip::for_action("Close search bar", &Dismiss, cx)
396 })
397 .on_click(cx.listener(|this, _: &ClickEvent, cx| {
398 this.dismiss(&Dismiss, cx)
399 })),
400 )
401 }),
402 )
403 .children(replace_line)
404 }
405}
406
407impl FocusableView for BufferSearchBar {
408 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
409 self.query_editor.focus_handle(cx)
410 }
411}
412
413impl ToolbarItemView for BufferSearchBar {
414 fn set_active_pane_item(
415 &mut self,
416 item: Option<&dyn ItemHandle>,
417 cx: &mut ViewContext<Self>,
418 ) -> ToolbarItemLocation {
419 cx.notify();
420 self.active_searchable_item_subscription.take();
421 self.active_searchable_item.take();
422
423 self.pending_search.take();
424
425 if let Some(searchable_item_handle) =
426 item.and_then(|item| item.to_searchable_item_handle(cx))
427 {
428 let this = cx.view().downgrade();
429
430 self.active_searchable_item_subscription =
431 Some(searchable_item_handle.subscribe_to_search_events(
432 cx,
433 Box::new(move |search_event, cx| {
434 if let Some(this) = this.upgrade() {
435 this.update(cx, |this, cx| {
436 this.on_active_searchable_item_event(search_event, cx)
437 });
438 }
439 }),
440 ));
441
442 self.active_searchable_item = Some(searchable_item_handle);
443 drop(self.update_matches(true, cx));
444 if !self.dismissed {
445 return ToolbarItemLocation::Secondary;
446 }
447 }
448 ToolbarItemLocation::Hidden
449 }
450}
451
452impl BufferSearchBar {
453 pub fn register(registrar: &mut impl SearchActionsRegistrar) {
454 registrar.register_handler(ForDeployed(|this, _: &FocusSearch, cx| {
455 this.query_editor.focus_handle(cx).focus(cx);
456 this.select_query(cx);
457 }));
458 registrar.register_handler(ForDeployed(|this, action: &ToggleCaseSensitive, cx| {
459 if this.supported_options().case {
460 this.toggle_case_sensitive(action, cx);
461 }
462 }));
463 registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, cx| {
464 if this.supported_options().word {
465 this.toggle_whole_word(action, cx);
466 }
467 }));
468 registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, cx| {
469 if this.supported_options().selection {
470 this.toggle_selection(action, cx);
471 }
472 }));
473 registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, cx| {
474 if this.supported_options().replacement {
475 this.toggle_replace(action, cx);
476 }
477 }));
478 registrar.register_handler(WithResults(|this, action: &SelectNextMatch, cx| {
479 this.select_next_match(action, cx);
480 }));
481 registrar.register_handler(WithResults(|this, action: &SelectPrevMatch, cx| {
482 this.select_prev_match(action, cx);
483 }));
484 registrar.register_handler(WithResults(|this, action: &SelectAllMatches, cx| {
485 this.select_all_matches(action, cx);
486 }));
487 registrar.register_handler(ForDeployed(|this, _: &editor::actions::Cancel, cx| {
488 this.dismiss(&Dismiss, cx);
489 }));
490
491 // register deploy buffer search for both search bar states, since we want to focus into the search bar
492 // when the deploy action is triggered in the buffer.
493 registrar.register_handler(ForDeployed(|this, deploy, cx| {
494 this.deploy(deploy, cx);
495 }));
496 registrar.register_handler(ForDismissed(|this, deploy, cx| {
497 this.deploy(deploy, cx);
498 }))
499 }
500
501 pub fn new(cx: &mut ViewContext<Self>) -> Self {
502 let query_editor = cx.new_view(Editor::single_line);
503 cx.subscribe(&query_editor, Self::on_query_editor_event)
504 .detach();
505 let replacement_editor = cx.new_view(Editor::single_line);
506 cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
507 .detach();
508
509 let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
510
511 Self {
512 query_editor,
513 query_editor_focused: false,
514 replacement_editor,
515 replacement_editor_focused: false,
516 active_searchable_item: None,
517 active_searchable_item_subscription: None,
518 active_match_index: None,
519 searchable_items_with_matches: Default::default(),
520 default_options: search_options,
521 configured_options: search_options,
522 search_options,
523 pending_search: None,
524 query_contains_error: false,
525 dismissed: true,
526 search_history: SearchHistory::new(
527 Some(MAX_BUFFER_SEARCH_HISTORY_SIZE),
528 project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
529 ),
530 search_history_cursor: Default::default(),
531 active_search: None,
532 replace_enabled: false,
533 selection_search_enabled: false,
534 scroll_handle: ScrollHandle::new(),
535 editor_scroll_handle: ScrollHandle::new(),
536 editor_needed_width: px(0.),
537 }
538 }
539
540 pub fn is_dismissed(&self) -> bool {
541 self.dismissed
542 }
543
544 pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
545 self.dismissed = true;
546 for searchable_item in self.searchable_items_with_matches.keys() {
547 if let Some(searchable_item) =
548 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
549 {
550 searchable_item.clear_matches(cx);
551 }
552 }
553 if let Some(active_editor) = self.active_searchable_item.as_mut() {
554 self.selection_search_enabled = false;
555 self.replace_enabled = false;
556 active_editor.search_bar_visibility_changed(false, cx);
557 active_editor.toggle_filtered_search_ranges(false, cx);
558 let handle = active_editor.focus_handle(cx);
559 self.focus(&handle, cx);
560 }
561 cx.emit(Event::UpdateLocation);
562 cx.emit(ToolbarItemEvent::ChangeLocation(
563 ToolbarItemLocation::Hidden,
564 ));
565 cx.notify();
566 }
567
568 pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
569 if self.show(cx) {
570 if let Some(active_item) = self.active_searchable_item.as_mut() {
571 active_item.toggle_filtered_search_ranges(deploy.selection_search_enabled, cx);
572 }
573 self.search_suggested(cx);
574 self.smartcase(cx);
575 self.replace_enabled = deploy.replace_enabled;
576 self.selection_search_enabled = deploy.selection_search_enabled;
577 if deploy.focus {
578 let mut handle = self.query_editor.focus_handle(cx).clone();
579 let mut select_query = true;
580 if deploy.replace_enabled && handle.is_focused(cx) {
581 handle = self.replacement_editor.focus_handle(cx).clone();
582 select_query = false;
583 };
584
585 if select_query {
586 self.select_query(cx);
587 }
588
589 cx.focus(&handle);
590 }
591 return true;
592 }
593
594 false
595 }
596
597 pub fn toggle(&mut self, action: &Deploy, cx: &mut ViewContext<Self>) {
598 if self.is_dismissed() {
599 self.deploy(action, cx);
600 } else {
601 self.dismiss(&Dismiss, cx);
602 }
603 }
604
605 pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
606 let Some(handle) = self.active_searchable_item.as_ref() else {
607 return false;
608 };
609
610 self.configured_options =
611 SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
612 if self.dismissed && self.configured_options != self.default_options {
613 self.search_options = self.configured_options;
614 self.default_options = self.configured_options;
615 }
616
617 self.dismissed = false;
618 handle.search_bar_visibility_changed(true, cx);
619 cx.notify();
620 cx.emit(Event::UpdateLocation);
621 cx.emit(ToolbarItemEvent::ChangeLocation(
622 ToolbarItemLocation::Secondary,
623 ));
624 true
625 }
626
627 fn supported_options(&self) -> workspace::searchable::SearchOptions {
628 self.active_searchable_item
629 .as_deref()
630 .map(SearchableItemHandle::supported_options)
631 .unwrap_or_default()
632 }
633
634 pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
635 let search = self
636 .query_suggestion(cx)
637 .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
638
639 if let Some(search) = search {
640 cx.spawn(|this, mut cx| async move {
641 search.await?;
642 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
643 })
644 .detach_and_log_err(cx);
645 }
646 }
647
648 pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
649 if let Some(match_ix) = self.active_match_index {
650 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
651 if let Some(matches) = self
652 .searchable_items_with_matches
653 .get(&active_searchable_item.downgrade())
654 {
655 active_searchable_item.activate_match(match_ix, matches, cx)
656 }
657 }
658 }
659 }
660
661 pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
662 self.query_editor.update(cx, |query_editor, cx| {
663 query_editor.select_all(&Default::default(), cx);
664 });
665 }
666
667 pub fn query(&self, cx: &WindowContext) -> String {
668 self.query_editor.read(cx).text(cx)
669 }
670 pub fn replacement(&self, cx: &WindowContext) -> String {
671 self.replacement_editor.read(cx).text(cx)
672 }
673 pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
674 self.active_searchable_item
675 .as_ref()
676 .map(|searchable_item| searchable_item.query_suggestion(cx))
677 .filter(|suggestion| !suggestion.is_empty())
678 }
679
680 pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
681 if replacement.is_none() {
682 self.replace_enabled = false;
683 return;
684 }
685 self.replace_enabled = true;
686 self.replacement_editor
687 .update(cx, |replacement_editor, cx| {
688 replacement_editor
689 .buffer()
690 .update(cx, |replacement_buffer, cx| {
691 let len = replacement_buffer.len(cx);
692 replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
693 });
694 });
695 }
696
697 pub fn search(
698 &mut self,
699 query: &str,
700 options: Option<SearchOptions>,
701 cx: &mut ViewContext<Self>,
702 ) -> oneshot::Receiver<()> {
703 let options = options.unwrap_or(self.default_options);
704 let updated = query != self.query(cx) || self.search_options != options;
705 if updated {
706 self.query_editor.update(cx, |query_editor, cx| {
707 query_editor.buffer().update(cx, |query_buffer, cx| {
708 let len = query_buffer.len(cx);
709 query_buffer.edit([(0..len, query)], None, cx);
710 });
711 });
712 self.search_options = options;
713 self.clear_matches(cx);
714 cx.notify();
715 }
716 self.update_matches(!updated, cx)
717 }
718
719 fn render_search_option_button(
720 &self,
721 option: SearchOptions,
722 action: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
723 ) -> impl IntoElement {
724 let is_active = self.search_options.contains(option);
725 option.as_button(is_active, action)
726 }
727
728 pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
729 if let Some(active_editor) = self.active_searchable_item.as_ref() {
730 let handle = active_editor.focus_handle(cx);
731 cx.focus(&handle);
732 }
733 }
734
735 pub fn toggle_search_option(
736 &mut self,
737 search_option: SearchOptions,
738 cx: &mut ViewContext<Self>,
739 ) {
740 self.search_options.toggle(search_option);
741 self.default_options = self.search_options;
742 drop(self.update_matches(false, cx));
743 cx.notify();
744 }
745
746 pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
747 self.search_options.contains(search_option)
748 }
749
750 pub fn enable_search_option(
751 &mut self,
752 search_option: SearchOptions,
753 cx: &mut ViewContext<Self>,
754 ) {
755 if !self.search_options.contains(search_option) {
756 self.toggle_search_option(search_option, cx)
757 }
758 }
759
760 pub fn set_search_options(
761 &mut self,
762 search_options: SearchOptions,
763 cx: &mut ViewContext<Self>,
764 ) {
765 self.search_options = search_options;
766 cx.notify();
767 }
768
769 fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
770 self.select_match(Direction::Next, 1, cx);
771 }
772
773 fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
774 self.select_match(Direction::Prev, 1, cx);
775 }
776
777 fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
778 if !self.dismissed && self.active_match_index.is_some() {
779 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
780 if let Some(matches) = self
781 .searchable_items_with_matches
782 .get(&searchable_item.downgrade())
783 {
784 searchable_item.select_matches(matches, cx);
785 self.focus_editor(&FocusEditor, cx);
786 }
787 }
788 }
789 }
790
791 pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
792 if let Some(index) = self.active_match_index {
793 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
794 if let Some(matches) = self
795 .searchable_items_with_matches
796 .get(&searchable_item.downgrade())
797 .filter(|matches| !matches.is_empty())
798 {
799 // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
800 if !EditorSettings::get_global(cx).search_wrap
801 && ((direction == Direction::Next && index + count >= matches.len())
802 || (direction == Direction::Prev && index < count))
803 {
804 crate::show_no_more_matches(cx);
805 return;
806 }
807 let new_match_index = searchable_item
808 .match_index_for_direction(matches, index, direction, count, cx);
809
810 searchable_item.update_matches(matches, cx);
811 searchable_item.activate_match(new_match_index, matches, cx);
812 }
813 }
814 }
815 }
816
817 pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
818 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
819 if let Some(matches) = self
820 .searchable_items_with_matches
821 .get(&searchable_item.downgrade())
822 {
823 if matches.is_empty() {
824 return;
825 }
826 let new_match_index = matches.len() - 1;
827 searchable_item.update_matches(matches, cx);
828 searchable_item.activate_match(new_match_index, matches, cx);
829 }
830 }
831 }
832
833 fn on_query_editor_event(
834 &mut self,
835 editor: View<Editor>,
836 event: &editor::EditorEvent,
837 cx: &mut ViewContext<Self>,
838 ) {
839 match event {
840 editor::EditorEvent::Focused => self.query_editor_focused = true,
841 editor::EditorEvent::Blurred => self.query_editor_focused = false,
842 editor::EditorEvent::Edited { .. } => {
843 self.smartcase(cx);
844 self.clear_matches(cx);
845 let search = self.update_matches(false, cx);
846
847 let width = editor.update(cx, |editor, cx| {
848 let text_layout_details = editor.text_layout_details(cx);
849 let snapshot = editor.snapshot(cx).display_snapshot;
850
851 snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
852 - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
853 });
854 self.editor_needed_width = width;
855 cx.notify();
856
857 cx.spawn(|this, mut cx| async move {
858 search.await?;
859 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
860 })
861 .detach_and_log_err(cx);
862 }
863 _ => {}
864 }
865 }
866
867 fn on_replacement_editor_event(
868 &mut self,
869 _: View<Editor>,
870 event: &editor::EditorEvent,
871 _: &mut ViewContext<Self>,
872 ) {
873 match event {
874 editor::EditorEvent::Focused => self.replacement_editor_focused = true,
875 editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
876 _ => {}
877 }
878 }
879
880 fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext<Self>) {
881 match event {
882 SearchEvent::MatchesInvalidated => {
883 drop(self.update_matches(false, cx));
884 }
885 SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
886 }
887 }
888
889 fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext<Self>) {
890 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
891 }
892
893 fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext<Self>) {
894 self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
895 }
896
897 fn toggle_selection(&mut self, _: &ToggleSelection, cx: &mut ViewContext<Self>) {
898 if let Some(active_item) = self.active_searchable_item.as_mut() {
899 self.selection_search_enabled = !self.selection_search_enabled;
900 active_item.toggle_filtered_search_ranges(self.selection_search_enabled, cx);
901 drop(self.update_matches(false, cx));
902 cx.notify();
903 }
904 }
905
906 fn toggle_regex(&mut self, _: &ToggleRegex, cx: &mut ViewContext<Self>) {
907 self.toggle_search_option(SearchOptions::REGEX, cx)
908 }
909
910 fn clear_active_searchable_item_matches(&mut self, cx: &mut WindowContext) {
911 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
912 self.active_match_index = None;
913 self.searchable_items_with_matches
914 .remove(&active_searchable_item.downgrade());
915 active_searchable_item.clear_matches(cx);
916 }
917 }
918
919 pub fn has_active_match(&self) -> bool {
920 self.active_match_index.is_some()
921 }
922
923 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
924 let mut active_item_matches = None;
925 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
926 if let Some(searchable_item) =
927 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
928 {
929 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
930 active_item_matches = Some((searchable_item.downgrade(), matches));
931 } else {
932 searchable_item.clear_matches(cx);
933 }
934 }
935 }
936
937 self.searchable_items_with_matches
938 .extend(active_item_matches);
939 }
940
941 fn update_matches(
942 &mut self,
943 reuse_existing_query: bool,
944 cx: &mut ViewContext<Self>,
945 ) -> oneshot::Receiver<()> {
946 let (done_tx, done_rx) = oneshot::channel();
947 let query = self.query(cx);
948 self.pending_search.take();
949
950 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
951 self.query_contains_error = false;
952 if query.is_empty() {
953 self.clear_active_searchable_item_matches(cx);
954 let _ = done_tx.send(());
955 cx.notify();
956 } else {
957 let query: Arc<_> = if let Some(search) =
958 self.active_search.take().filter(|_| reuse_existing_query)
959 {
960 search
961 } else {
962 if self.search_options.contains(SearchOptions::REGEX) {
963 match SearchQuery::regex(
964 query,
965 self.search_options.contains(SearchOptions::WHOLE_WORD),
966 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
967 false,
968 Default::default(),
969 Default::default(),
970 None,
971 ) {
972 Ok(query) => query.with_replacement(self.replacement(cx)),
973 Err(_) => {
974 self.query_contains_error = true;
975 self.clear_active_searchable_item_matches(cx);
976 cx.notify();
977 return done_rx;
978 }
979 }
980 } else {
981 match SearchQuery::text(
982 query,
983 self.search_options.contains(SearchOptions::WHOLE_WORD),
984 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
985 false,
986 Default::default(),
987 Default::default(),
988 None,
989 ) {
990 Ok(query) => query.with_replacement(self.replacement(cx)),
991 Err(_) => {
992 self.query_contains_error = true;
993 self.clear_active_searchable_item_matches(cx);
994 cx.notify();
995 return done_rx;
996 }
997 }
998 }
999 .into()
1000 };
1001
1002 self.active_search = Some(query.clone());
1003 let query_text = query.as_str().to_string();
1004
1005 let matches = active_searchable_item.find_matches(query, cx);
1006
1007 let active_searchable_item = active_searchable_item.downgrade();
1008 self.pending_search = Some(cx.spawn(|this, mut cx| async move {
1009 let matches = matches.await;
1010
1011 this.update(&mut cx, |this, cx| {
1012 if let Some(active_searchable_item) =
1013 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1014 {
1015 this.searchable_items_with_matches
1016 .insert(active_searchable_item.downgrade(), matches);
1017
1018 this.update_match_index(cx);
1019 this.search_history
1020 .add(&mut this.search_history_cursor, query_text);
1021 if !this.dismissed {
1022 let matches = this
1023 .searchable_items_with_matches
1024 .get(&active_searchable_item.downgrade())
1025 .unwrap();
1026 if matches.is_empty() {
1027 active_searchable_item.clear_matches(cx);
1028 } else {
1029 active_searchable_item.update_matches(matches, cx);
1030 }
1031 let _ = done_tx.send(());
1032 }
1033 cx.notify();
1034 }
1035 })
1036 .log_err();
1037 }));
1038 }
1039 }
1040 done_rx
1041 }
1042
1043 pub fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
1044 let new_index = self
1045 .active_searchable_item
1046 .as_ref()
1047 .and_then(|searchable_item| {
1048 let matches = self
1049 .searchable_items_with_matches
1050 .get(&searchable_item.downgrade())?;
1051 searchable_item.active_match_index(matches, cx)
1052 });
1053 if new_index != self.active_match_index {
1054 self.active_match_index = new_index;
1055 cx.notify();
1056 }
1057 }
1058
1059 fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
1060 // Search -> Replace -> Editor
1061 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1062 self.replacement_editor.focus_handle(cx)
1063 } else if let Some(item) = self.active_searchable_item.as_ref() {
1064 item.focus_handle(cx)
1065 } else {
1066 return;
1067 };
1068 self.focus(&focus_handle, cx);
1069 cx.stop_propagation();
1070 }
1071
1072 fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext<Self>) {
1073 // Search -> Replace -> Search
1074 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1075 self.replacement_editor.focus_handle(cx)
1076 } else if self.replacement_editor_focused {
1077 self.query_editor.focus_handle(cx)
1078 } else {
1079 return;
1080 };
1081 self.focus(&focus_handle, cx);
1082 cx.stop_propagation();
1083 }
1084
1085 fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
1086 if let Some(new_query) = self
1087 .search_history
1088 .next(&mut self.search_history_cursor)
1089 .map(str::to_string)
1090 {
1091 drop(self.search(&new_query, Some(self.search_options), cx));
1092 } else {
1093 self.search_history_cursor.reset();
1094 drop(self.search("", Some(self.search_options), cx));
1095 }
1096 }
1097
1098 fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
1099 if self.query(cx).is_empty() {
1100 if let Some(new_query) = self
1101 .search_history
1102 .current(&mut self.search_history_cursor)
1103 .map(str::to_string)
1104 {
1105 drop(self.search(&new_query, Some(self.search_options), cx));
1106 return;
1107 }
1108 }
1109
1110 if let Some(new_query) = self
1111 .search_history
1112 .previous(&mut self.search_history_cursor)
1113 .map(str::to_string)
1114 {
1115 drop(self.search(&new_query, Some(self.search_options), cx));
1116 }
1117 }
1118
1119 fn focus(&self, handle: &gpui::FocusHandle, cx: &mut ViewContext<Self>) {
1120 cx.on_next_frame(|_, cx| {
1121 cx.invalidate_character_coordinates();
1122 });
1123 cx.focus(handle);
1124 }
1125 fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
1126 if self.active_searchable_item.is_some() {
1127 self.replace_enabled = !self.replace_enabled;
1128 let handle = if self.replace_enabled {
1129 self.replacement_editor.focus_handle(cx)
1130 } else {
1131 self.query_editor.focus_handle(cx)
1132 };
1133 self.focus(&handle, cx);
1134 cx.notify();
1135 }
1136 }
1137 fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
1138 let mut should_propagate = true;
1139 if !self.dismissed && self.active_search.is_some() {
1140 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1141 if let Some(query) = self.active_search.as_ref() {
1142 if let Some(matches) = self
1143 .searchable_items_with_matches
1144 .get(&searchable_item.downgrade())
1145 {
1146 if let Some(active_index) = self.active_match_index {
1147 let query = query
1148 .as_ref()
1149 .clone()
1150 .with_replacement(self.replacement(cx));
1151 searchable_item.replace(matches.at(active_index), &query, cx);
1152 self.select_next_match(&SelectNextMatch, cx);
1153 }
1154 should_propagate = false;
1155 self.focus_editor(&FocusEditor, cx);
1156 }
1157 }
1158 }
1159 }
1160 if !should_propagate {
1161 cx.stop_propagation();
1162 }
1163 }
1164 pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
1165 if !self.dismissed && self.active_search.is_some() {
1166 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1167 if let Some(query) = self.active_search.as_ref() {
1168 if let Some(matches) = self
1169 .searchable_items_with_matches
1170 .get(&searchable_item.downgrade())
1171 {
1172 let query = query
1173 .as_ref()
1174 .clone()
1175 .with_replacement(self.replacement(cx));
1176 searchable_item.replace_all(&mut matches.iter(), &query, cx);
1177 }
1178 }
1179 }
1180 }
1181 }
1182
1183 pub fn match_exists(&mut self, cx: &mut ViewContext<Self>) -> bool {
1184 self.update_match_index(cx);
1185 self.active_match_index.is_some()
1186 }
1187
1188 pub fn should_use_smartcase_search(&mut self, cx: &mut ViewContext<Self>) -> bool {
1189 EditorSettings::get_global(cx).use_smartcase_search
1190 }
1191
1192 pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1193 str.chars().any(|c| c.is_uppercase())
1194 }
1195
1196 fn smartcase(&mut self, cx: &mut ViewContext<Self>) {
1197 if self.should_use_smartcase_search(cx) {
1198 let query = self.query(cx);
1199 if !query.is_empty() {
1200 let is_case = self.is_contains_uppercase(&query);
1201 if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1202 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1203 }
1204 }
1205 }
1206 }
1207}
1208
1209#[cfg(test)]
1210mod tests {
1211 use std::ops::Range;
1212
1213 use super::*;
1214 use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer, SearchSettings};
1215 use gpui::{Context, Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1216 use language::{Buffer, Point};
1217 use project::Project;
1218 use settings::SettingsStore;
1219 use smol::stream::StreamExt as _;
1220 use unindent::Unindent as _;
1221
1222 fn init_globals(cx: &mut TestAppContext) {
1223 cx.update(|cx| {
1224 let store = settings::SettingsStore::test(cx);
1225 cx.set_global(store);
1226 editor::init(cx);
1227
1228 language::init(cx);
1229 Project::init_settings(cx);
1230 theme::init(theme::LoadThemes::JustBase, cx);
1231 crate::init(cx);
1232 });
1233 }
1234
1235 fn init_test(
1236 cx: &mut TestAppContext,
1237 ) -> (View<Editor>, View<BufferSearchBar>, &mut VisualTestContext) {
1238 init_globals(cx);
1239 let buffer = cx.new_model(|cx| {
1240 Buffer::local(
1241 r#"
1242 A regular expression (shortened as regex or regexp;[1] also referred to as
1243 rational expression[2][3]) is a sequence of characters that specifies a search
1244 pattern in text. Usually such patterns are used by string-searching algorithms
1245 for "find" or "find and replace" operations on strings, or for input validation.
1246 "#
1247 .unindent(),
1248 cx,
1249 )
1250 });
1251 let cx = cx.add_empty_window();
1252 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1253
1254 let search_bar = cx.new_view(|cx| {
1255 let mut search_bar = BufferSearchBar::new(cx);
1256 search_bar.set_active_pane_item(Some(&editor), cx);
1257 search_bar.show(cx);
1258 search_bar
1259 });
1260
1261 (editor, search_bar, cx)
1262 }
1263
1264 #[gpui::test]
1265 async fn test_search_simple(cx: &mut TestAppContext) {
1266 let (editor, search_bar, cx) = init_test(cx);
1267 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1268 background_highlights
1269 .into_iter()
1270 .map(|(range, _)| range)
1271 .collect::<Vec<_>>()
1272 };
1273 // Search for a string that appears with different casing.
1274 // By default, search is case-insensitive.
1275 search_bar
1276 .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
1277 .await
1278 .unwrap();
1279 editor.update(cx, |editor, cx| {
1280 assert_eq!(
1281 display_points_of(editor.all_text_background_highlights(cx)),
1282 &[
1283 DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1284 DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1285 ]
1286 );
1287 });
1288
1289 // Switch to a case sensitive search.
1290 search_bar.update(cx, |search_bar, cx| {
1291 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1292 });
1293 let mut editor_notifications = cx.notifications(&editor);
1294 editor_notifications.next().await;
1295 editor.update(cx, |editor, cx| {
1296 assert_eq!(
1297 display_points_of(editor.all_text_background_highlights(cx)),
1298 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1299 );
1300 });
1301
1302 // Search for a string that appears both as a whole word and
1303 // within other words. By default, all results are found.
1304 search_bar
1305 .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1306 .await
1307 .unwrap();
1308 editor.update(cx, |editor, cx| {
1309 assert_eq!(
1310 display_points_of(editor.all_text_background_highlights(cx)),
1311 &[
1312 DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1313 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1314 DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1315 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1316 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1317 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1318 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1319 ]
1320 );
1321 });
1322
1323 // Switch to a whole word search.
1324 search_bar.update(cx, |search_bar, cx| {
1325 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1326 });
1327 let mut editor_notifications = cx.notifications(&editor);
1328 editor_notifications.next().await;
1329 editor.update(cx, |editor, cx| {
1330 assert_eq!(
1331 display_points_of(editor.all_text_background_highlights(cx)),
1332 &[
1333 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1334 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1335 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1336 ]
1337 );
1338 });
1339
1340 editor.update(cx, |editor, cx| {
1341 editor.change_selections(None, cx, |s| {
1342 s.select_display_ranges([
1343 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1344 ])
1345 });
1346 });
1347 search_bar.update(cx, |search_bar, cx| {
1348 assert_eq!(search_bar.active_match_index, Some(0));
1349 search_bar.select_next_match(&SelectNextMatch, cx);
1350 assert_eq!(
1351 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1352 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1353 );
1354 });
1355 search_bar.update(cx, |search_bar, _| {
1356 assert_eq!(search_bar.active_match_index, Some(0));
1357 });
1358
1359 search_bar.update(cx, |search_bar, cx| {
1360 search_bar.select_next_match(&SelectNextMatch, cx);
1361 assert_eq!(
1362 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1363 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1364 );
1365 });
1366 search_bar.update(cx, |search_bar, _| {
1367 assert_eq!(search_bar.active_match_index, Some(1));
1368 });
1369
1370 search_bar.update(cx, |search_bar, cx| {
1371 search_bar.select_next_match(&SelectNextMatch, cx);
1372 assert_eq!(
1373 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1374 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1375 );
1376 });
1377 search_bar.update(cx, |search_bar, _| {
1378 assert_eq!(search_bar.active_match_index, Some(2));
1379 });
1380
1381 search_bar.update(cx, |search_bar, cx| {
1382 search_bar.select_next_match(&SelectNextMatch, cx);
1383 assert_eq!(
1384 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1385 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1386 );
1387 });
1388 search_bar.update(cx, |search_bar, _| {
1389 assert_eq!(search_bar.active_match_index, Some(0));
1390 });
1391
1392 search_bar.update(cx, |search_bar, cx| {
1393 search_bar.select_prev_match(&SelectPrevMatch, cx);
1394 assert_eq!(
1395 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1396 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1397 );
1398 });
1399 search_bar.update(cx, |search_bar, _| {
1400 assert_eq!(search_bar.active_match_index, Some(2));
1401 });
1402
1403 search_bar.update(cx, |search_bar, cx| {
1404 search_bar.select_prev_match(&SelectPrevMatch, cx);
1405 assert_eq!(
1406 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1407 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1408 );
1409 });
1410 search_bar.update(cx, |search_bar, _| {
1411 assert_eq!(search_bar.active_match_index, Some(1));
1412 });
1413
1414 search_bar.update(cx, |search_bar, cx| {
1415 search_bar.select_prev_match(&SelectPrevMatch, cx);
1416 assert_eq!(
1417 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1418 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1419 );
1420 });
1421 search_bar.update(cx, |search_bar, _| {
1422 assert_eq!(search_bar.active_match_index, Some(0));
1423 });
1424
1425 // Park the cursor in between matches and ensure that going to the previous match selects
1426 // the closest match to the left.
1427 editor.update(cx, |editor, cx| {
1428 editor.change_selections(None, cx, |s| {
1429 s.select_display_ranges([
1430 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1431 ])
1432 });
1433 });
1434 search_bar.update(cx, |search_bar, cx| {
1435 assert_eq!(search_bar.active_match_index, Some(1));
1436 search_bar.select_prev_match(&SelectPrevMatch, cx);
1437 assert_eq!(
1438 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1439 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1440 );
1441 });
1442 search_bar.update(cx, |search_bar, _| {
1443 assert_eq!(search_bar.active_match_index, Some(0));
1444 });
1445
1446 // Park the cursor in between matches and ensure that going to the next match selects the
1447 // closest match to the right.
1448 editor.update(cx, |editor, cx| {
1449 editor.change_selections(None, cx, |s| {
1450 s.select_display_ranges([
1451 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1452 ])
1453 });
1454 });
1455 search_bar.update(cx, |search_bar, cx| {
1456 assert_eq!(search_bar.active_match_index, Some(1));
1457 search_bar.select_next_match(&SelectNextMatch, cx);
1458 assert_eq!(
1459 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1460 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1461 );
1462 });
1463 search_bar.update(cx, |search_bar, _| {
1464 assert_eq!(search_bar.active_match_index, Some(1));
1465 });
1466
1467 // Park the cursor after the last match and ensure that going to the previous match selects
1468 // the last match.
1469 editor.update(cx, |editor, cx| {
1470 editor.change_selections(None, cx, |s| {
1471 s.select_display_ranges([
1472 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1473 ])
1474 });
1475 });
1476 search_bar.update(cx, |search_bar, cx| {
1477 assert_eq!(search_bar.active_match_index, Some(2));
1478 search_bar.select_prev_match(&SelectPrevMatch, cx);
1479 assert_eq!(
1480 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1481 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1482 );
1483 });
1484 search_bar.update(cx, |search_bar, _| {
1485 assert_eq!(search_bar.active_match_index, Some(2));
1486 });
1487
1488 // Park the cursor after the last match and ensure that going to the next match selects the
1489 // first match.
1490 editor.update(cx, |editor, cx| {
1491 editor.change_selections(None, cx, |s| {
1492 s.select_display_ranges([
1493 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1494 ])
1495 });
1496 });
1497 search_bar.update(cx, |search_bar, cx| {
1498 assert_eq!(search_bar.active_match_index, Some(2));
1499 search_bar.select_next_match(&SelectNextMatch, cx);
1500 assert_eq!(
1501 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1502 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1503 );
1504 });
1505 search_bar.update(cx, |search_bar, _| {
1506 assert_eq!(search_bar.active_match_index, Some(0));
1507 });
1508
1509 // Park the cursor before the first match and ensure that going to the previous match
1510 // selects the last match.
1511 editor.update(cx, |editor, cx| {
1512 editor.change_selections(None, cx, |s| {
1513 s.select_display_ranges([
1514 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1515 ])
1516 });
1517 });
1518 search_bar.update(cx, |search_bar, cx| {
1519 assert_eq!(search_bar.active_match_index, Some(0));
1520 search_bar.select_prev_match(&SelectPrevMatch, cx);
1521 assert_eq!(
1522 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1523 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1524 );
1525 });
1526 search_bar.update(cx, |search_bar, _| {
1527 assert_eq!(search_bar.active_match_index, Some(2));
1528 });
1529 }
1530
1531 fn display_points_of(
1532 background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1533 ) -> Vec<Range<DisplayPoint>> {
1534 background_highlights
1535 .into_iter()
1536 .map(|(range, _)| range)
1537 .collect::<Vec<_>>()
1538 }
1539
1540 #[gpui::test]
1541 async fn test_search_option_handling(cx: &mut TestAppContext) {
1542 let (editor, search_bar, cx) = init_test(cx);
1543
1544 // show with options should make current search case sensitive
1545 search_bar
1546 .update(cx, |search_bar, cx| {
1547 search_bar.show(cx);
1548 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1549 })
1550 .await
1551 .unwrap();
1552 editor.update(cx, |editor, cx| {
1553 assert_eq!(
1554 display_points_of(editor.all_text_background_highlights(cx)),
1555 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1556 );
1557 });
1558
1559 // search_suggested should restore default options
1560 search_bar.update(cx, |search_bar, cx| {
1561 search_bar.search_suggested(cx);
1562 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1563 });
1564
1565 // toggling a search option should update the defaults
1566 search_bar
1567 .update(cx, |search_bar, cx| {
1568 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1569 })
1570 .await
1571 .unwrap();
1572 search_bar.update(cx, |search_bar, cx| {
1573 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1574 });
1575 let mut editor_notifications = cx.notifications(&editor);
1576 editor_notifications.next().await;
1577 editor.update(cx, |editor, cx| {
1578 assert_eq!(
1579 display_points_of(editor.all_text_background_highlights(cx)),
1580 &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1581 );
1582 });
1583
1584 // defaults should still include whole word
1585 search_bar.update(cx, |search_bar, cx| {
1586 search_bar.search_suggested(cx);
1587 assert_eq!(
1588 search_bar.search_options,
1589 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1590 )
1591 });
1592 }
1593
1594 #[gpui::test]
1595 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1596 init_globals(cx);
1597 let buffer_text = r#"
1598 A regular expression (shortened as regex or regexp;[1] also referred to as
1599 rational expression[2][3]) is a sequence of characters that specifies a search
1600 pattern in text. Usually such patterns are used by string-searching algorithms
1601 for "find" or "find and replace" operations on strings, or for input validation.
1602 "#
1603 .unindent();
1604 let expected_query_matches_count = buffer_text
1605 .chars()
1606 .filter(|c| c.to_ascii_lowercase() == 'a')
1607 .count();
1608 assert!(
1609 expected_query_matches_count > 1,
1610 "Should pick a query with multiple results"
1611 );
1612 let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1613 let window = cx.add_window(|_| gpui::Empty);
1614
1615 let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1616
1617 let search_bar = window.build_view(cx, |cx| {
1618 let mut search_bar = BufferSearchBar::new(cx);
1619 search_bar.set_active_pane_item(Some(&editor), cx);
1620 search_bar.show(cx);
1621 search_bar
1622 });
1623
1624 window
1625 .update(cx, |_, cx| {
1626 search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1627 })
1628 .unwrap()
1629 .await
1630 .unwrap();
1631 let initial_selections = window
1632 .update(cx, |_, cx| {
1633 search_bar.update(cx, |search_bar, cx| {
1634 let handle = search_bar.query_editor.focus_handle(cx);
1635 cx.focus(&handle);
1636 search_bar.activate_current_match(cx);
1637 });
1638 assert!(
1639 !editor.read(cx).is_focused(cx),
1640 "Initially, the editor should not be focused"
1641 );
1642 let initial_selections = editor.update(cx, |editor, cx| {
1643 let initial_selections = editor.selections.display_ranges(cx);
1644 assert_eq!(
1645 initial_selections.len(), 1,
1646 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1647 );
1648 initial_selections
1649 });
1650 search_bar.update(cx, |search_bar, cx| {
1651 assert_eq!(search_bar.active_match_index, Some(0));
1652 let handle = search_bar.query_editor.focus_handle(cx);
1653 cx.focus(&handle);
1654 search_bar.select_all_matches(&SelectAllMatches, cx);
1655 });
1656 assert!(
1657 editor.read(cx).is_focused(cx),
1658 "Should focus editor after successful SelectAllMatches"
1659 );
1660 search_bar.update(cx, |search_bar, cx| {
1661 let all_selections =
1662 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1663 assert_eq!(
1664 all_selections.len(),
1665 expected_query_matches_count,
1666 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1667 );
1668 assert_eq!(
1669 search_bar.active_match_index,
1670 Some(0),
1671 "Match index should not change after selecting all matches"
1672 );
1673 });
1674
1675 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
1676 initial_selections
1677 }).unwrap();
1678
1679 window
1680 .update(cx, |_, cx| {
1681 assert!(
1682 editor.read(cx).is_focused(cx),
1683 "Should still have editor focused after SelectNextMatch"
1684 );
1685 search_bar.update(cx, |search_bar, cx| {
1686 let all_selections =
1687 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1688 assert_eq!(
1689 all_selections.len(),
1690 1,
1691 "On next match, should deselect items and select the next match"
1692 );
1693 assert_ne!(
1694 all_selections, initial_selections,
1695 "Next match should be different from the first selection"
1696 );
1697 assert_eq!(
1698 search_bar.active_match_index,
1699 Some(1),
1700 "Match index should be updated to the next one"
1701 );
1702 let handle = search_bar.query_editor.focus_handle(cx);
1703 cx.focus(&handle);
1704 search_bar.select_all_matches(&SelectAllMatches, cx);
1705 });
1706 })
1707 .unwrap();
1708 window
1709 .update(cx, |_, cx| {
1710 assert!(
1711 editor.read(cx).is_focused(cx),
1712 "Should focus editor after successful SelectAllMatches"
1713 );
1714 search_bar.update(cx, |search_bar, cx| {
1715 let all_selections =
1716 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1717 assert_eq!(
1718 all_selections.len(),
1719 expected_query_matches_count,
1720 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1721 );
1722 assert_eq!(
1723 search_bar.active_match_index,
1724 Some(1),
1725 "Match index should not change after selecting all matches"
1726 );
1727 });
1728 search_bar.update(cx, |search_bar, cx| {
1729 search_bar.select_prev_match(&SelectPrevMatch, cx);
1730 });
1731 })
1732 .unwrap();
1733 let last_match_selections = window
1734 .update(cx, |_, cx| {
1735 assert!(
1736 editor.read(cx).is_focused(cx),
1737 "Should still have editor focused after SelectPrevMatch"
1738 );
1739
1740 search_bar.update(cx, |search_bar, cx| {
1741 let all_selections =
1742 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1743 assert_eq!(
1744 all_selections.len(),
1745 1,
1746 "On previous match, should deselect items and select the previous item"
1747 );
1748 assert_eq!(
1749 all_selections, initial_selections,
1750 "Previous match should be the same as the first selection"
1751 );
1752 assert_eq!(
1753 search_bar.active_match_index,
1754 Some(0),
1755 "Match index should be updated to the previous one"
1756 );
1757 all_selections
1758 })
1759 })
1760 .unwrap();
1761
1762 window
1763 .update(cx, |_, cx| {
1764 search_bar.update(cx, |search_bar, cx| {
1765 let handle = search_bar.query_editor.focus_handle(cx);
1766 cx.focus(&handle);
1767 search_bar.search("abas_nonexistent_match", None, cx)
1768 })
1769 })
1770 .unwrap()
1771 .await
1772 .unwrap();
1773 window
1774 .update(cx, |_, cx| {
1775 search_bar.update(cx, |search_bar, cx| {
1776 search_bar.select_all_matches(&SelectAllMatches, cx);
1777 });
1778 assert!(
1779 editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
1780 "Should not switch focus to editor if SelectAllMatches does not find any matches"
1781 );
1782 search_bar.update(cx, |search_bar, cx| {
1783 let all_selections =
1784 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1785 assert_eq!(
1786 all_selections, last_match_selections,
1787 "Should not select anything new if there are no matches"
1788 );
1789 assert!(
1790 search_bar.active_match_index.is_none(),
1791 "For no matches, there should be no active match index"
1792 );
1793 });
1794 })
1795 .unwrap();
1796 }
1797
1798 #[gpui::test]
1799 async fn test_search_query_history(cx: &mut TestAppContext) {
1800 init_globals(cx);
1801 let buffer_text = r#"
1802 A regular expression (shortened as regex or regexp;[1] also referred to as
1803 rational expression[2][3]) is a sequence of characters that specifies a search
1804 pattern in text. Usually such patterns are used by string-searching algorithms
1805 for "find" or "find and replace" operations on strings, or for input validation.
1806 "#
1807 .unindent();
1808 let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1809 let cx = cx.add_empty_window();
1810
1811 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1812
1813 let search_bar = cx.new_view(|cx| {
1814 let mut search_bar = BufferSearchBar::new(cx);
1815 search_bar.set_active_pane_item(Some(&editor), cx);
1816 search_bar.show(cx);
1817 search_bar
1818 });
1819
1820 // Add 3 search items into the history.
1821 search_bar
1822 .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1823 .await
1824 .unwrap();
1825 search_bar
1826 .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1827 .await
1828 .unwrap();
1829 search_bar
1830 .update(cx, |search_bar, cx| {
1831 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1832 })
1833 .await
1834 .unwrap();
1835 // Ensure that the latest search is active.
1836 search_bar.update(cx, |search_bar, cx| {
1837 assert_eq!(search_bar.query(cx), "c");
1838 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1839 });
1840
1841 // Next history query after the latest should set the query to the empty string.
1842 search_bar.update(cx, |search_bar, cx| {
1843 search_bar.next_history_query(&NextHistoryQuery, cx);
1844 });
1845 search_bar.update(cx, |search_bar, cx| {
1846 assert_eq!(search_bar.query(cx), "");
1847 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1848 });
1849 search_bar.update(cx, |search_bar, cx| {
1850 search_bar.next_history_query(&NextHistoryQuery, cx);
1851 });
1852 search_bar.update(cx, |search_bar, cx| {
1853 assert_eq!(search_bar.query(cx), "");
1854 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1855 });
1856
1857 // First previous query for empty current query should set the query to the latest.
1858 search_bar.update(cx, |search_bar, cx| {
1859 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1860 });
1861 search_bar.update(cx, |search_bar, cx| {
1862 assert_eq!(search_bar.query(cx), "c");
1863 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1864 });
1865
1866 // Further previous items should go over the history in reverse order.
1867 search_bar.update(cx, |search_bar, cx| {
1868 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1869 });
1870 search_bar.update(cx, |search_bar, cx| {
1871 assert_eq!(search_bar.query(cx), "b");
1872 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1873 });
1874
1875 // Previous items should never go behind the first history item.
1876 search_bar.update(cx, |search_bar, cx| {
1877 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1878 });
1879 search_bar.update(cx, |search_bar, cx| {
1880 assert_eq!(search_bar.query(cx), "a");
1881 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1882 });
1883 search_bar.update(cx, |search_bar, cx| {
1884 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1885 });
1886 search_bar.update(cx, |search_bar, cx| {
1887 assert_eq!(search_bar.query(cx), "a");
1888 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1889 });
1890
1891 // Next items should go over the history in the original order.
1892 search_bar.update(cx, |search_bar, cx| {
1893 search_bar.next_history_query(&NextHistoryQuery, cx);
1894 });
1895 search_bar.update(cx, |search_bar, cx| {
1896 assert_eq!(search_bar.query(cx), "b");
1897 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1898 });
1899
1900 search_bar
1901 .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1902 .await
1903 .unwrap();
1904 search_bar.update(cx, |search_bar, cx| {
1905 assert_eq!(search_bar.query(cx), "ba");
1906 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1907 });
1908
1909 // New search input should add another entry to history and move the selection to the end of the history.
1910 search_bar.update(cx, |search_bar, cx| {
1911 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1912 });
1913 search_bar.update(cx, |search_bar, cx| {
1914 assert_eq!(search_bar.query(cx), "c");
1915 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1916 });
1917 search_bar.update(cx, |search_bar, cx| {
1918 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1919 });
1920 search_bar.update(cx, |search_bar, cx| {
1921 assert_eq!(search_bar.query(cx), "b");
1922 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1923 });
1924 search_bar.update(cx, |search_bar, cx| {
1925 search_bar.next_history_query(&NextHistoryQuery, cx);
1926 });
1927 search_bar.update(cx, |search_bar, cx| {
1928 assert_eq!(search_bar.query(cx), "c");
1929 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1930 });
1931 search_bar.update(cx, |search_bar, cx| {
1932 search_bar.next_history_query(&NextHistoryQuery, cx);
1933 });
1934 search_bar.update(cx, |search_bar, cx| {
1935 assert_eq!(search_bar.query(cx), "ba");
1936 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1937 });
1938 search_bar.update(cx, |search_bar, cx| {
1939 search_bar.next_history_query(&NextHistoryQuery, cx);
1940 });
1941 search_bar.update(cx, |search_bar, cx| {
1942 assert_eq!(search_bar.query(cx), "");
1943 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1944 });
1945 }
1946
1947 #[gpui::test]
1948 async fn test_replace_simple(cx: &mut TestAppContext) {
1949 let (editor, search_bar, cx) = init_test(cx);
1950
1951 search_bar
1952 .update(cx, |search_bar, cx| {
1953 search_bar.search("expression", None, cx)
1954 })
1955 .await
1956 .unwrap();
1957
1958 search_bar.update(cx, |search_bar, cx| {
1959 search_bar.replacement_editor.update(cx, |editor, cx| {
1960 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
1961 editor.set_text("expr$1", cx);
1962 });
1963 search_bar.replace_all(&ReplaceAll, cx)
1964 });
1965 assert_eq!(
1966 editor.update(cx, |this, cx| { this.text(cx) }),
1967 r#"
1968 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
1969 rational expr$1[2][3]) is a sequence of characters that specifies a search
1970 pattern in text. Usually such patterns are used by string-searching algorithms
1971 for "find" or "find and replace" operations on strings, or for input validation.
1972 "#
1973 .unindent()
1974 );
1975
1976 // Search for word boundaries and replace just a single one.
1977 search_bar
1978 .update(cx, |search_bar, cx| {
1979 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
1980 })
1981 .await
1982 .unwrap();
1983
1984 search_bar.update(cx, |search_bar, cx| {
1985 search_bar.replacement_editor.update(cx, |editor, cx| {
1986 editor.set_text("banana", cx);
1987 });
1988 search_bar.replace_next(&ReplaceNext, cx)
1989 });
1990 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
1991 assert_eq!(
1992 editor.update(cx, |this, cx| { this.text(cx) }),
1993 r#"
1994 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
1995 rational expr$1[2][3]) is a sequence of characters that specifies a search
1996 pattern in text. Usually such patterns are used by string-searching algorithms
1997 for "find" or "find and replace" operations on strings, or for input validation.
1998 "#
1999 .unindent()
2000 );
2001 // Let's turn on regex mode.
2002 search_bar
2003 .update(cx, |search_bar, cx| {
2004 search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), cx)
2005 })
2006 .await
2007 .unwrap();
2008 search_bar.update(cx, |search_bar, cx| {
2009 search_bar.replacement_editor.update(cx, |editor, cx| {
2010 editor.set_text("${1}number", cx);
2011 });
2012 search_bar.replace_all(&ReplaceAll, cx)
2013 });
2014 assert_eq!(
2015 editor.update(cx, |this, cx| { this.text(cx) }),
2016 r#"
2017 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2018 rational expr$12number3number) is a sequence of characters that specifies a search
2019 pattern in text. Usually such patterns are used by string-searching algorithms
2020 for "find" or "find and replace" operations on strings, or for input validation.
2021 "#
2022 .unindent()
2023 );
2024 // Now with a whole-word twist.
2025 search_bar
2026 .update(cx, |search_bar, cx| {
2027 search_bar.search(
2028 "a\\w+s",
2029 Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2030 cx,
2031 )
2032 })
2033 .await
2034 .unwrap();
2035 search_bar.update(cx, |search_bar, cx| {
2036 search_bar.replacement_editor.update(cx, |editor, cx| {
2037 editor.set_text("things", cx);
2038 });
2039 search_bar.replace_all(&ReplaceAll, cx)
2040 });
2041 // The only word affected by this edit should be `algorithms`, even though there's a bunch
2042 // of words in this text that would match this regex if not for WHOLE_WORD.
2043 assert_eq!(
2044 editor.update(cx, |this, cx| { this.text(cx) }),
2045 r#"
2046 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2047 rational expr$12number3number) is a sequence of characters that specifies a search
2048 pattern in text. Usually such patterns are used by string-searching things
2049 for "find" or "find and replace" operations on strings, or for input validation.
2050 "#
2051 .unindent()
2052 );
2053 }
2054
2055 struct ReplacementTestParams<'a> {
2056 editor: &'a View<Editor>,
2057 search_bar: &'a View<BufferSearchBar>,
2058 cx: &'a mut VisualTestContext,
2059 search_text: &'static str,
2060 search_options: Option<SearchOptions>,
2061 replacement_text: &'static str,
2062 replace_all: bool,
2063 expected_text: String,
2064 }
2065
2066 async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2067 options
2068 .search_bar
2069 .update(options.cx, |search_bar, cx| {
2070 if let Some(options) = options.search_options {
2071 search_bar.set_search_options(options, cx);
2072 }
2073 search_bar.search(options.search_text, options.search_options, cx)
2074 })
2075 .await
2076 .unwrap();
2077
2078 options.search_bar.update(options.cx, |search_bar, cx| {
2079 search_bar.replacement_editor.update(cx, |editor, cx| {
2080 editor.set_text(options.replacement_text, cx);
2081 });
2082
2083 if options.replace_all {
2084 search_bar.replace_all(&ReplaceAll, cx)
2085 } else {
2086 search_bar.replace_next(&ReplaceNext, cx)
2087 }
2088 });
2089
2090 assert_eq!(
2091 options
2092 .editor
2093 .update(options.cx, |this, cx| { this.text(cx) }),
2094 options.expected_text
2095 );
2096 }
2097
2098 #[gpui::test]
2099 async fn test_replace_special_characters(cx: &mut TestAppContext) {
2100 let (editor, search_bar, cx) = init_test(cx);
2101
2102 run_replacement_test(ReplacementTestParams {
2103 editor: &editor,
2104 search_bar: &search_bar,
2105 cx,
2106 search_text: "expression",
2107 search_options: None,
2108 replacement_text: r"\n",
2109 replace_all: true,
2110 expected_text: r#"
2111 A regular \n (shortened as regex or regexp;[1] also referred to as
2112 rational \n[2][3]) is a sequence of characters that specifies a search
2113 pattern in text. Usually such patterns are used by string-searching algorithms
2114 for "find" or "find and replace" operations on strings, or for input validation.
2115 "#
2116 .unindent(),
2117 })
2118 .await;
2119
2120 run_replacement_test(ReplacementTestParams {
2121 editor: &editor,
2122 search_bar: &search_bar,
2123 cx,
2124 search_text: "or",
2125 search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2126 replacement_text: r"\\\n\\\\",
2127 replace_all: false,
2128 expected_text: r#"
2129 A regular \n (shortened as regex \
2130 \\ regexp;[1] also referred to as
2131 rational \n[2][3]) is a sequence of characters that specifies a search
2132 pattern in text. Usually such patterns are used by string-searching algorithms
2133 for "find" or "find and replace" operations on strings, or for input validation.
2134 "#
2135 .unindent(),
2136 })
2137 .await;
2138
2139 run_replacement_test(ReplacementTestParams {
2140 editor: &editor,
2141 search_bar: &search_bar,
2142 cx,
2143 search_text: r"(that|used) ",
2144 search_options: Some(SearchOptions::REGEX),
2145 replacement_text: r"$1\n",
2146 replace_all: true,
2147 expected_text: r#"
2148 A regular \n (shortened as regex \
2149 \\ regexp;[1] also referred to as
2150 rational \n[2][3]) is a sequence of characters that
2151 specifies a search
2152 pattern in text. Usually such patterns are used
2153 by string-searching algorithms
2154 for "find" or "find and replace" operations on strings, or for input validation.
2155 "#
2156 .unindent(),
2157 })
2158 .await;
2159 }
2160
2161 #[gpui::test]
2162 async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2163 cx: &mut TestAppContext,
2164 ) {
2165 init_globals(cx);
2166 let buffer = cx.new_model(|cx| {
2167 Buffer::local(
2168 r#"
2169 aaa bbb aaa ccc
2170 aaa bbb aaa ccc
2171 aaa bbb aaa ccc
2172 aaa bbb aaa ccc
2173 aaa bbb aaa ccc
2174 aaa bbb aaa ccc
2175 "#
2176 .unindent(),
2177 cx,
2178 )
2179 });
2180 let cx = cx.add_empty_window();
2181 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
2182
2183 let search_bar = cx.new_view(|cx| {
2184 let mut search_bar = BufferSearchBar::new(cx);
2185 search_bar.set_active_pane_item(Some(&editor), cx);
2186 search_bar.show(cx);
2187 search_bar
2188 });
2189
2190 editor.update(cx, |editor, cx| {
2191 editor.change_selections(None, cx, |s| {
2192 s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2193 })
2194 });
2195
2196 search_bar.update(cx, |search_bar, cx| {
2197 let deploy = Deploy {
2198 focus: true,
2199 replace_enabled: false,
2200 selection_search_enabled: true,
2201 };
2202 search_bar.deploy(&deploy, cx);
2203 });
2204
2205 cx.run_until_parked();
2206
2207 search_bar
2208 .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2209 .await
2210 .unwrap();
2211
2212 editor.update(cx, |editor, cx| {
2213 assert_eq!(
2214 editor.search_background_highlights(cx),
2215 &[
2216 Point::new(1, 0)..Point::new(1, 3),
2217 Point::new(1, 8)..Point::new(1, 11),
2218 Point::new(2, 0)..Point::new(2, 3),
2219 ]
2220 );
2221 });
2222 }
2223
2224 #[gpui::test]
2225 async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2226 cx: &mut TestAppContext,
2227 ) {
2228 init_globals(cx);
2229 let text = r#"
2230 aaa bbb aaa ccc
2231 aaa bbb aaa ccc
2232 aaa bbb aaa ccc
2233 aaa bbb aaa ccc
2234 aaa bbb aaa ccc
2235 aaa bbb aaa ccc
2236
2237 aaa bbb aaa ccc
2238 aaa bbb aaa ccc
2239 aaa bbb aaa ccc
2240 aaa bbb aaa ccc
2241 aaa bbb aaa ccc
2242 aaa bbb aaa ccc
2243 "#
2244 .unindent();
2245
2246 let cx = cx.add_empty_window();
2247 let editor = cx.new_view(|cx| {
2248 let multibuffer = MultiBuffer::build_multi(
2249 [
2250 (
2251 &text,
2252 vec![
2253 Point::new(0, 0)..Point::new(2, 0),
2254 Point::new(4, 0)..Point::new(5, 0),
2255 ],
2256 ),
2257 (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2258 ],
2259 cx,
2260 );
2261 Editor::for_multibuffer(multibuffer, None, false, cx)
2262 });
2263
2264 let search_bar = cx.new_view(|cx| {
2265 let mut search_bar = BufferSearchBar::new(cx);
2266 search_bar.set_active_pane_item(Some(&editor), cx);
2267 search_bar.show(cx);
2268 search_bar
2269 });
2270
2271 editor.update(cx, |editor, cx| {
2272 editor.change_selections(None, cx, |s| {
2273 s.select_ranges(vec![
2274 Point::new(1, 0)..Point::new(1, 4),
2275 Point::new(5, 3)..Point::new(6, 4),
2276 ])
2277 })
2278 });
2279
2280 search_bar.update(cx, |search_bar, cx| {
2281 let deploy = Deploy {
2282 focus: true,
2283 replace_enabled: false,
2284 selection_search_enabled: true,
2285 };
2286 search_bar.deploy(&deploy, cx);
2287 });
2288
2289 cx.run_until_parked();
2290
2291 search_bar
2292 .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2293 .await
2294 .unwrap();
2295
2296 editor.update(cx, |editor, cx| {
2297 assert_eq!(
2298 editor.search_background_highlights(cx),
2299 &[
2300 Point::new(1, 0)..Point::new(1, 3),
2301 Point::new(5, 8)..Point::new(5, 11),
2302 Point::new(6, 0)..Point::new(6, 3),
2303 ]
2304 );
2305 });
2306 }
2307
2308 #[gpui::test]
2309 async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2310 let (editor, search_bar, cx) = init_test(cx);
2311 // Search using valid regexp
2312 search_bar
2313 .update(cx, |search_bar, cx| {
2314 search_bar.enable_search_option(SearchOptions::REGEX, cx);
2315 search_bar.search("expression", None, cx)
2316 })
2317 .await
2318 .unwrap();
2319 editor.update(cx, |editor, cx| {
2320 assert_eq!(
2321 display_points_of(editor.all_text_background_highlights(cx)),
2322 &[
2323 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2324 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2325 ],
2326 );
2327 });
2328
2329 // Now, the expression is invalid
2330 search_bar
2331 .update(cx, |search_bar, cx| {
2332 search_bar.search("expression (", None, cx)
2333 })
2334 .await
2335 .unwrap_err();
2336 editor.update(cx, |editor, cx| {
2337 assert!(display_points_of(editor.all_text_background_highlights(cx)).is_empty(),);
2338 });
2339 }
2340
2341 #[gpui::test]
2342 async fn test_search_options_changes(cx: &mut TestAppContext) {
2343 let (_editor, search_bar, cx) = init_test(cx);
2344 update_search_settings(
2345 SearchSettings {
2346 whole_word: false,
2347 case_sensitive: false,
2348 include_ignored: false,
2349 regex: false,
2350 },
2351 cx,
2352 );
2353
2354 let deploy = Deploy {
2355 focus: true,
2356 replace_enabled: false,
2357 selection_search_enabled: true,
2358 };
2359
2360 search_bar.update(cx, |search_bar, cx| {
2361 assert_eq!(
2362 search_bar.search_options,
2363 SearchOptions::NONE,
2364 "Should have no search options enabled by default"
2365 );
2366 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
2367 assert_eq!(
2368 search_bar.search_options,
2369 SearchOptions::WHOLE_WORD,
2370 "Should enable the option toggled"
2371 );
2372 assert!(
2373 !search_bar.dismissed,
2374 "Search bar should be present and visible"
2375 );
2376 search_bar.deploy(&deploy, cx);
2377 assert_eq!(
2378 search_bar.configured_options,
2379 SearchOptions::NONE,
2380 "Should have configured search options matching the settings"
2381 );
2382 assert_eq!(
2383 search_bar.search_options,
2384 SearchOptions::WHOLE_WORD,
2385 "After (re)deploying, the option should still be enabled"
2386 );
2387
2388 search_bar.dismiss(&Dismiss, cx);
2389 search_bar.deploy(&deploy, cx);
2390 assert_eq!(
2391 search_bar.search_options,
2392 SearchOptions::NONE,
2393 "After hiding and showing the search bar, default options should be used"
2394 );
2395
2396 search_bar.toggle_search_option(SearchOptions::REGEX, cx);
2397 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
2398 assert_eq!(
2399 search_bar.search_options,
2400 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2401 "Should enable the options toggled"
2402 );
2403 assert!(
2404 !search_bar.dismissed,
2405 "Search bar should be present and visible"
2406 );
2407 });
2408
2409 update_search_settings(
2410 SearchSettings {
2411 whole_word: false,
2412 case_sensitive: true,
2413 include_ignored: false,
2414 regex: false,
2415 },
2416 cx,
2417 );
2418 search_bar.update(cx, |search_bar, cx| {
2419 assert_eq!(
2420 search_bar.search_options,
2421 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2422 "Should have no search options enabled by default"
2423 );
2424
2425 search_bar.deploy(&deploy, cx);
2426 assert_eq!(
2427 search_bar.configured_options,
2428 SearchOptions::CASE_SENSITIVE,
2429 "Should have configured search options matching the settings"
2430 );
2431 assert_eq!(
2432 search_bar.search_options,
2433 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2434 "Toggling a non-dismissed search bar with custom options should not change the default options"
2435 );
2436 search_bar.dismiss(&Dismiss, cx);
2437 search_bar.deploy(&deploy, cx);
2438 assert_eq!(
2439 search_bar.search_options,
2440 SearchOptions::CASE_SENSITIVE,
2441 "After hiding and showing the search bar, default options should be used"
2442 );
2443 });
2444 }
2445
2446 fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2447 cx.update(|cx| {
2448 SettingsStore::update_global(cx, |store, cx| {
2449 store.update_user_settings::<EditorSettings>(cx, |settings| {
2450 settings.search = Some(search_settings);
2451 });
2452 });
2453 });
2454 }
2455}