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