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