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