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