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