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 None,
940 ) {
941 Ok(query) => query.with_replacement(self.replacement(cx)),
942 Err(_) => {
943 self.query_contains_error = true;
944 self.clear_active_searchable_item_matches(cx);
945 cx.notify();
946 return done_rx;
947 }
948 }
949 } else {
950 match SearchQuery::text(
951 query,
952 self.search_options.contains(SearchOptions::WHOLE_WORD),
953 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
954 false,
955 Default::default(),
956 Default::default(),
957 None,
958 ) {
959 Ok(query) => query.with_replacement(self.replacement(cx)),
960 Err(_) => {
961 self.query_contains_error = true;
962 self.clear_active_searchable_item_matches(cx);
963 cx.notify();
964 return done_rx;
965 }
966 }
967 }
968 .into();
969 self.active_search = Some(query.clone());
970 let query_text = query.as_str().to_string();
971
972 let matches = active_searchable_item.find_matches(query, cx);
973
974 let active_searchable_item = active_searchable_item.downgrade();
975 self.pending_search = Some(cx.spawn(|this, mut cx| async move {
976 let matches = matches.await;
977
978 this.update(&mut cx, |this, cx| {
979 if let Some(active_searchable_item) =
980 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
981 {
982 this.searchable_items_with_matches
983 .insert(active_searchable_item.downgrade(), matches);
984
985 this.update_match_index(cx);
986 this.search_history
987 .add(&mut this.search_history_cursor, query_text);
988 if !this.dismissed {
989 let matches = this
990 .searchable_items_with_matches
991 .get(&active_searchable_item.downgrade())
992 .unwrap();
993 if matches.is_empty() {
994 active_searchable_item.clear_matches(cx);
995 } else {
996 active_searchable_item.update_matches(matches, cx);
997 }
998 let _ = done_tx.send(());
999 }
1000 cx.notify();
1001 }
1002 })
1003 .log_err();
1004 }));
1005 }
1006 }
1007 done_rx
1008 }
1009
1010 pub fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
1011 let new_index = self
1012 .active_searchable_item
1013 .as_ref()
1014 .and_then(|searchable_item| {
1015 let matches = self
1016 .searchable_items_with_matches
1017 .get(&searchable_item.downgrade())?;
1018 searchable_item.active_match_index(matches, cx)
1019 });
1020 if new_index != self.active_match_index {
1021 self.active_match_index = new_index;
1022 cx.notify();
1023 }
1024 }
1025
1026 fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
1027 // Search -> Replace -> Editor
1028 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1029 self.replacement_editor.focus_handle(cx)
1030 } else if let Some(item) = self.active_searchable_item.as_ref() {
1031 item.focus_handle(cx)
1032 } else {
1033 return;
1034 };
1035 self.focus(&focus_handle, cx);
1036 cx.stop_propagation();
1037 }
1038
1039 fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext<Self>) {
1040 // Search -> Replace -> Search
1041 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1042 self.replacement_editor.focus_handle(cx)
1043 } else if self.replacement_editor_focused {
1044 self.query_editor.focus_handle(cx)
1045 } else {
1046 return;
1047 };
1048 self.focus(&focus_handle, cx);
1049 cx.stop_propagation();
1050 }
1051
1052 fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
1053 if let Some(new_query) = self
1054 .search_history
1055 .next(&mut self.search_history_cursor)
1056 .map(str::to_string)
1057 {
1058 drop(self.search(&new_query, Some(self.search_options), cx));
1059 } else {
1060 self.search_history_cursor.reset();
1061 drop(self.search("", Some(self.search_options), cx));
1062 }
1063 }
1064
1065 fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
1066 if self.query(cx).is_empty() {
1067 if let Some(new_query) = self
1068 .search_history
1069 .current(&mut self.search_history_cursor)
1070 .map(str::to_string)
1071 {
1072 drop(self.search(&new_query, Some(self.search_options), cx));
1073 return;
1074 }
1075 }
1076
1077 if let Some(new_query) = self
1078 .search_history
1079 .previous(&mut self.search_history_cursor)
1080 .map(str::to_string)
1081 {
1082 drop(self.search(&new_query, Some(self.search_options), cx));
1083 }
1084 }
1085
1086 fn focus(&self, handle: &gpui::FocusHandle, cx: &mut ViewContext<Self>) {
1087 cx.on_next_frame(|_, cx| {
1088 cx.invalidate_character_coordinates();
1089 });
1090 cx.focus(handle);
1091 }
1092 fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
1093 if let Some(_) = &self.active_searchable_item {
1094 self.replace_enabled = !self.replace_enabled;
1095 let handle = if self.replace_enabled {
1096 self.replacement_editor.focus_handle(cx)
1097 } else {
1098 self.query_editor.focus_handle(cx)
1099 };
1100 self.focus(&handle, cx);
1101 cx.notify();
1102 }
1103 }
1104 fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
1105 let mut should_propagate = true;
1106 if !self.dismissed && self.active_search.is_some() {
1107 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1108 if let Some(query) = self.active_search.as_ref() {
1109 if let Some(matches) = self
1110 .searchable_items_with_matches
1111 .get(&searchable_item.downgrade())
1112 {
1113 if let Some(active_index) = self.active_match_index {
1114 let query = query
1115 .as_ref()
1116 .clone()
1117 .with_replacement(self.replacement(cx));
1118 searchable_item.replace(matches.at(active_index), &query, cx);
1119 self.select_next_match(&SelectNextMatch, cx);
1120 }
1121 should_propagate = false;
1122 self.focus_editor(&FocusEditor, cx);
1123 }
1124 }
1125 }
1126 }
1127 if !should_propagate {
1128 cx.stop_propagation();
1129 }
1130 }
1131 pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
1132 if !self.dismissed && self.active_search.is_some() {
1133 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1134 if let Some(query) = self.active_search.as_ref() {
1135 if let Some(matches) = self
1136 .searchable_items_with_matches
1137 .get(&searchable_item.downgrade())
1138 {
1139 let query = query
1140 .as_ref()
1141 .clone()
1142 .with_replacement(self.replacement(cx));
1143 searchable_item.replace_all(&mut matches.iter(), &query, cx);
1144 }
1145 }
1146 }
1147 }
1148 }
1149
1150 pub fn match_exists(&mut self, cx: &mut ViewContext<Self>) -> bool {
1151 self.update_match_index(cx);
1152 self.active_match_index.is_some()
1153 }
1154}
1155
1156#[cfg(test)]
1157mod tests {
1158 use std::ops::Range;
1159
1160 use super::*;
1161 use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer};
1162 use gpui::{Context, Hsla, TestAppContext, VisualTestContext};
1163 use language::{Buffer, Point};
1164 use project::Project;
1165 use smol::stream::StreamExt as _;
1166 use unindent::Unindent as _;
1167
1168 fn init_globals(cx: &mut TestAppContext) {
1169 cx.update(|cx| {
1170 let store = settings::SettingsStore::test(cx);
1171 cx.set_global(store);
1172 editor::init(cx);
1173
1174 language::init(cx);
1175 Project::init_settings(cx);
1176 theme::init(theme::LoadThemes::JustBase, cx);
1177 });
1178 }
1179
1180 fn init_test(
1181 cx: &mut TestAppContext,
1182 ) -> (View<Editor>, View<BufferSearchBar>, &mut VisualTestContext) {
1183 init_globals(cx);
1184 let buffer = cx.new_model(|cx| {
1185 Buffer::local(
1186 r#"
1187 A regular expression (shortened as regex or regexp;[1] also referred to as
1188 rational expression[2][3]) is a sequence of characters that specifies a search
1189 pattern in text. Usually such patterns are used by string-searching algorithms
1190 for "find" or "find and replace" operations on strings, or for input validation.
1191 "#
1192 .unindent(),
1193 cx,
1194 )
1195 });
1196 let cx = cx.add_empty_window();
1197 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1198
1199 let search_bar = cx.new_view(|cx| {
1200 let mut search_bar = BufferSearchBar::new(cx);
1201 search_bar.set_active_pane_item(Some(&editor), cx);
1202 search_bar.show(cx);
1203 search_bar
1204 });
1205
1206 (editor, search_bar, cx)
1207 }
1208
1209 #[gpui::test]
1210 async fn test_search_simple(cx: &mut TestAppContext) {
1211 let (editor, search_bar, cx) = init_test(cx);
1212 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1213 background_highlights
1214 .into_iter()
1215 .map(|(range, _)| range)
1216 .collect::<Vec<_>>()
1217 };
1218 // Search for a string that appears with different casing.
1219 // By default, search is case-insensitive.
1220 search_bar
1221 .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
1222 .await
1223 .unwrap();
1224 editor.update(cx, |editor, cx| {
1225 assert_eq!(
1226 display_points_of(editor.all_text_background_highlights(cx)),
1227 &[
1228 DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1229 DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1230 ]
1231 );
1232 });
1233
1234 // Switch to a case sensitive search.
1235 search_bar.update(cx, |search_bar, cx| {
1236 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1237 });
1238 let mut editor_notifications = cx.notifications(&editor);
1239 editor_notifications.next().await;
1240 editor.update(cx, |editor, cx| {
1241 assert_eq!(
1242 display_points_of(editor.all_text_background_highlights(cx)),
1243 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1244 );
1245 });
1246
1247 // Search for a string that appears both as a whole word and
1248 // within other words. By default, all results are found.
1249 search_bar
1250 .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1251 .await
1252 .unwrap();
1253 editor.update(cx, |editor, cx| {
1254 assert_eq!(
1255 display_points_of(editor.all_text_background_highlights(cx)),
1256 &[
1257 DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1258 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1259 DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1260 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1261 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1262 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1263 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1264 ]
1265 );
1266 });
1267
1268 // Switch to a whole word search.
1269 search_bar.update(cx, |search_bar, cx| {
1270 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1271 });
1272 let mut editor_notifications = cx.notifications(&editor);
1273 editor_notifications.next().await;
1274 editor.update(cx, |editor, cx| {
1275 assert_eq!(
1276 display_points_of(editor.all_text_background_highlights(cx)),
1277 &[
1278 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1279 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1280 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1281 ]
1282 );
1283 });
1284
1285 editor.update(cx, |editor, cx| {
1286 editor.change_selections(None, cx, |s| {
1287 s.select_display_ranges([
1288 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1289 ])
1290 });
1291 });
1292 search_bar.update(cx, |search_bar, cx| {
1293 assert_eq!(search_bar.active_match_index, Some(0));
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(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1298 );
1299 });
1300 search_bar.update(cx, |search_bar, _| {
1301 assert_eq!(search_bar.active_match_index, Some(0));
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(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1309 );
1310 });
1311 search_bar.update(cx, |search_bar, _| {
1312 assert_eq!(search_bar.active_match_index, Some(1));
1313 });
1314
1315 search_bar.update(cx, |search_bar, cx| {
1316 search_bar.select_next_match(&SelectNextMatch, cx);
1317 assert_eq!(
1318 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1319 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(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_next_match(&SelectNextMatch, cx);
1328 assert_eq!(
1329 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1330 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1331 );
1332 });
1333 search_bar.update(cx, |search_bar, _| {
1334 assert_eq!(search_bar.active_match_index, Some(0));
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(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1342 );
1343 });
1344 search_bar.update(cx, |search_bar, _| {
1345 assert_eq!(search_bar.active_match_index, Some(2));
1346 });
1347
1348 search_bar.update(cx, |search_bar, cx| {
1349 search_bar.select_prev_match(&SelectPrevMatch, cx);
1350 assert_eq!(
1351 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1352 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1353 );
1354 });
1355 search_bar.update(cx, |search_bar, _| {
1356 assert_eq!(search_bar.active_match_index, Some(1));
1357 });
1358
1359 search_bar.update(cx, |search_bar, cx| {
1360 search_bar.select_prev_match(&SelectPrevMatch, cx);
1361 assert_eq!(
1362 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1363 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1364 );
1365 });
1366 search_bar.update(cx, |search_bar, _| {
1367 assert_eq!(search_bar.active_match_index, Some(0));
1368 });
1369
1370 // Park the cursor in between matches and ensure that going to the previous match selects
1371 // the closest match to the left.
1372 editor.update(cx, |editor, cx| {
1373 editor.change_selections(None, cx, |s| {
1374 s.select_display_ranges([
1375 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1376 ])
1377 });
1378 });
1379 search_bar.update(cx, |search_bar, cx| {
1380 assert_eq!(search_bar.active_match_index, Some(1));
1381 search_bar.select_prev_match(&SelectPrevMatch, cx);
1382 assert_eq!(
1383 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1384 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1385 );
1386 });
1387 search_bar.update(cx, |search_bar, _| {
1388 assert_eq!(search_bar.active_match_index, Some(0));
1389 });
1390
1391 // Park the cursor in between matches and ensure that going to the next match selects the
1392 // closest match to the right.
1393 editor.update(cx, |editor, cx| {
1394 editor.change_selections(None, cx, |s| {
1395 s.select_display_ranges([
1396 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1397 ])
1398 });
1399 });
1400 search_bar.update(cx, |search_bar, cx| {
1401 assert_eq!(search_bar.active_match_index, Some(1));
1402 search_bar.select_next_match(&SelectNextMatch, cx);
1403 assert_eq!(
1404 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1405 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1406 );
1407 });
1408 search_bar.update(cx, |search_bar, _| {
1409 assert_eq!(search_bar.active_match_index, Some(1));
1410 });
1411
1412 // Park the cursor after the last match and ensure that going to the previous match selects
1413 // the last match.
1414 editor.update(cx, |editor, cx| {
1415 editor.change_selections(None, cx, |s| {
1416 s.select_display_ranges([
1417 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1418 ])
1419 });
1420 });
1421 search_bar.update(cx, |search_bar, cx| {
1422 assert_eq!(search_bar.active_match_index, Some(2));
1423 search_bar.select_prev_match(&SelectPrevMatch, cx);
1424 assert_eq!(
1425 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1426 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1427 );
1428 });
1429 search_bar.update(cx, |search_bar, _| {
1430 assert_eq!(search_bar.active_match_index, Some(2));
1431 });
1432
1433 // Park the cursor after the last match and ensure that going to the next match selects the
1434 // first match.
1435 editor.update(cx, |editor, cx| {
1436 editor.change_selections(None, cx, |s| {
1437 s.select_display_ranges([
1438 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1439 ])
1440 });
1441 });
1442 search_bar.update(cx, |search_bar, cx| {
1443 assert_eq!(search_bar.active_match_index, Some(2));
1444 search_bar.select_next_match(&SelectNextMatch, cx);
1445 assert_eq!(
1446 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1447 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1448 );
1449 });
1450 search_bar.update(cx, |search_bar, _| {
1451 assert_eq!(search_bar.active_match_index, Some(0));
1452 });
1453
1454 // Park the cursor before the first match and ensure that going to the previous match
1455 // selects the last match.
1456 editor.update(cx, |editor, cx| {
1457 editor.change_selections(None, cx, |s| {
1458 s.select_display_ranges([
1459 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1460 ])
1461 });
1462 });
1463 search_bar.update(cx, |search_bar, cx| {
1464 assert_eq!(search_bar.active_match_index, Some(0));
1465 search_bar.select_prev_match(&SelectPrevMatch, cx);
1466 assert_eq!(
1467 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1468 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1469 );
1470 });
1471 search_bar.update(cx, |search_bar, _| {
1472 assert_eq!(search_bar.active_match_index, Some(2));
1473 });
1474 }
1475
1476 fn display_points_of(
1477 background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1478 ) -> Vec<Range<DisplayPoint>> {
1479 background_highlights
1480 .into_iter()
1481 .map(|(range, _)| range)
1482 .collect::<Vec<_>>()
1483 }
1484
1485 #[gpui::test]
1486 async fn test_search_option_handling(cx: &mut TestAppContext) {
1487 let (editor, search_bar, cx) = init_test(cx);
1488
1489 // show with options should make current search case sensitive
1490 search_bar
1491 .update(cx, |search_bar, cx| {
1492 search_bar.show(cx);
1493 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1494 })
1495 .await
1496 .unwrap();
1497 editor.update(cx, |editor, cx| {
1498 assert_eq!(
1499 display_points_of(editor.all_text_background_highlights(cx)),
1500 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1501 );
1502 });
1503
1504 // search_suggested should restore default options
1505 search_bar.update(cx, |search_bar, cx| {
1506 search_bar.search_suggested(cx);
1507 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1508 });
1509
1510 // toggling a search option should update the defaults
1511 search_bar
1512 .update(cx, |search_bar, cx| {
1513 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1514 })
1515 .await
1516 .unwrap();
1517 search_bar.update(cx, |search_bar, cx| {
1518 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1519 });
1520 let mut editor_notifications = cx.notifications(&editor);
1521 editor_notifications.next().await;
1522 editor.update(cx, |editor, cx| {
1523 assert_eq!(
1524 display_points_of(editor.all_text_background_highlights(cx)),
1525 &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1526 );
1527 });
1528
1529 // defaults should still include whole word
1530 search_bar.update(cx, |search_bar, cx| {
1531 search_bar.search_suggested(cx);
1532 assert_eq!(
1533 search_bar.search_options,
1534 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1535 )
1536 });
1537 }
1538
1539 #[gpui::test]
1540 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1541 init_globals(cx);
1542 let buffer_text = r#"
1543 A regular expression (shortened as regex or regexp;[1] also referred to as
1544 rational expression[2][3]) is a sequence of characters that specifies a search
1545 pattern in text. Usually such patterns are used by string-searching algorithms
1546 for "find" or "find and replace" operations on strings, or for input validation.
1547 "#
1548 .unindent();
1549 let expected_query_matches_count = buffer_text
1550 .chars()
1551 .filter(|c| c.to_ascii_lowercase() == 'a')
1552 .count();
1553 assert!(
1554 expected_query_matches_count > 1,
1555 "Should pick a query with multiple results"
1556 );
1557 let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1558 let window = cx.add_window(|_| gpui::Empty);
1559
1560 let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1561
1562 let search_bar = window.build_view(cx, |cx| {
1563 let mut search_bar = BufferSearchBar::new(cx);
1564 search_bar.set_active_pane_item(Some(&editor), cx);
1565 search_bar.show(cx);
1566 search_bar
1567 });
1568
1569 window
1570 .update(cx, |_, cx| {
1571 search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1572 })
1573 .unwrap()
1574 .await
1575 .unwrap();
1576 let initial_selections = window
1577 .update(cx, |_, cx| {
1578 search_bar.update(cx, |search_bar, cx| {
1579 let handle = search_bar.query_editor.focus_handle(cx);
1580 cx.focus(&handle);
1581 search_bar.activate_current_match(cx);
1582 });
1583 assert!(
1584 !editor.read(cx).is_focused(cx),
1585 "Initially, the editor should not be focused"
1586 );
1587 let initial_selections = editor.update(cx, |editor, cx| {
1588 let initial_selections = editor.selections.display_ranges(cx);
1589 assert_eq!(
1590 initial_selections.len(), 1,
1591 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1592 );
1593 initial_selections
1594 });
1595 search_bar.update(cx, |search_bar, cx| {
1596 assert_eq!(search_bar.active_match_index, Some(0));
1597 let handle = search_bar.query_editor.focus_handle(cx);
1598 cx.focus(&handle);
1599 search_bar.select_all_matches(&SelectAllMatches, cx);
1600 });
1601 assert!(
1602 editor.read(cx).is_focused(cx),
1603 "Should focus editor after successful SelectAllMatches"
1604 );
1605 search_bar.update(cx, |search_bar, cx| {
1606 let all_selections =
1607 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1608 assert_eq!(
1609 all_selections.len(),
1610 expected_query_matches_count,
1611 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1612 );
1613 assert_eq!(
1614 search_bar.active_match_index,
1615 Some(0),
1616 "Match index should not change after selecting all matches"
1617 );
1618 });
1619
1620 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
1621 initial_selections
1622 }).unwrap();
1623
1624 window
1625 .update(cx, |_, cx| {
1626 assert!(
1627 editor.read(cx).is_focused(cx),
1628 "Should still have editor focused after SelectNextMatch"
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 1,
1636 "On next match, should deselect items and select the next match"
1637 );
1638 assert_ne!(
1639 all_selections, initial_selections,
1640 "Next match should be different from the first selection"
1641 );
1642 assert_eq!(
1643 search_bar.active_match_index,
1644 Some(1),
1645 "Match index should be updated to the next one"
1646 );
1647 let handle = search_bar.query_editor.focus_handle(cx);
1648 cx.focus(&handle);
1649 search_bar.select_all_matches(&SelectAllMatches, cx);
1650 });
1651 })
1652 .unwrap();
1653 window
1654 .update(cx, |_, cx| {
1655 assert!(
1656 editor.read(cx).is_focused(cx),
1657 "Should focus editor after successful SelectAllMatches"
1658 );
1659 search_bar.update(cx, |search_bar, cx| {
1660 let all_selections =
1661 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1662 assert_eq!(
1663 all_selections.len(),
1664 expected_query_matches_count,
1665 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1666 );
1667 assert_eq!(
1668 search_bar.active_match_index,
1669 Some(1),
1670 "Match index should not change after selecting all matches"
1671 );
1672 });
1673 search_bar.update(cx, |search_bar, cx| {
1674 search_bar.select_prev_match(&SelectPrevMatch, cx);
1675 });
1676 })
1677 .unwrap();
1678 let last_match_selections = window
1679 .update(cx, |_, cx| {
1680 assert!(
1681 editor.read(cx).is_focused(&cx),
1682 "Should still have editor focused after SelectPrevMatch"
1683 );
1684
1685 search_bar.update(cx, |search_bar, cx| {
1686 let all_selections =
1687 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1688 assert_eq!(
1689 all_selections.len(),
1690 1,
1691 "On previous match, should deselect items and select the previous item"
1692 );
1693 assert_eq!(
1694 all_selections, initial_selections,
1695 "Previous match should be the same as the first selection"
1696 );
1697 assert_eq!(
1698 search_bar.active_match_index,
1699 Some(0),
1700 "Match index should be updated to the previous one"
1701 );
1702 all_selections
1703 })
1704 })
1705 .unwrap();
1706
1707 window
1708 .update(cx, |_, cx| {
1709 search_bar.update(cx, |search_bar, cx| {
1710 let handle = search_bar.query_editor.focus_handle(cx);
1711 cx.focus(&handle);
1712 search_bar.search("abas_nonexistent_match", None, cx)
1713 })
1714 })
1715 .unwrap()
1716 .await
1717 .unwrap();
1718 window
1719 .update(cx, |_, cx| {
1720 search_bar.update(cx, |search_bar, cx| {
1721 search_bar.select_all_matches(&SelectAllMatches, cx);
1722 });
1723 assert!(
1724 editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
1725 "Should not switch focus to editor if SelectAllMatches does not find any matches"
1726 );
1727 search_bar.update(cx, |search_bar, cx| {
1728 let all_selections =
1729 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1730 assert_eq!(
1731 all_selections, last_match_selections,
1732 "Should not select anything new if there are no matches"
1733 );
1734 assert!(
1735 search_bar.active_match_index.is_none(),
1736 "For no matches, there should be no active match index"
1737 );
1738 });
1739 })
1740 .unwrap();
1741 }
1742
1743 #[gpui::test]
1744 async fn test_search_query_history(cx: &mut TestAppContext) {
1745 init_globals(cx);
1746 let buffer_text = r#"
1747 A regular expression (shortened as regex or regexp;[1] also referred to as
1748 rational expression[2][3]) is a sequence of characters that specifies a search
1749 pattern in text. Usually such patterns are used by string-searching algorithms
1750 for "find" or "find and replace" operations on strings, or for input validation.
1751 "#
1752 .unindent();
1753 let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1754 let cx = cx.add_empty_window();
1755
1756 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1757
1758 let search_bar = cx.new_view(|cx| {
1759 let mut search_bar = BufferSearchBar::new(cx);
1760 search_bar.set_active_pane_item(Some(&editor), cx);
1761 search_bar.show(cx);
1762 search_bar
1763 });
1764
1765 // Add 3 search items into the history.
1766 search_bar
1767 .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1768 .await
1769 .unwrap();
1770 search_bar
1771 .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1772 .await
1773 .unwrap();
1774 search_bar
1775 .update(cx, |search_bar, cx| {
1776 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1777 })
1778 .await
1779 .unwrap();
1780 // Ensure that the latest search is active.
1781 search_bar.update(cx, |search_bar, cx| {
1782 assert_eq!(search_bar.query(cx), "c");
1783 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1784 });
1785
1786 // Next history query after the latest should set the query to the empty string.
1787 search_bar.update(cx, |search_bar, cx| {
1788 search_bar.next_history_query(&NextHistoryQuery, cx);
1789 });
1790 search_bar.update(cx, |search_bar, cx| {
1791 assert_eq!(search_bar.query(cx), "");
1792 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1793 });
1794 search_bar.update(cx, |search_bar, cx| {
1795 search_bar.next_history_query(&NextHistoryQuery, cx);
1796 });
1797 search_bar.update(cx, |search_bar, cx| {
1798 assert_eq!(search_bar.query(cx), "");
1799 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1800 });
1801
1802 // First previous query for empty current query should set the query to the latest.
1803 search_bar.update(cx, |search_bar, cx| {
1804 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1805 });
1806 search_bar.update(cx, |search_bar, cx| {
1807 assert_eq!(search_bar.query(cx), "c");
1808 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1809 });
1810
1811 // Further previous items should go over the history in reverse order.
1812 search_bar.update(cx, |search_bar, cx| {
1813 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1814 });
1815 search_bar.update(cx, |search_bar, cx| {
1816 assert_eq!(search_bar.query(cx), "b");
1817 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1818 });
1819
1820 // Previous items should never go behind the first history item.
1821 search_bar.update(cx, |search_bar, cx| {
1822 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1823 });
1824 search_bar.update(cx, |search_bar, cx| {
1825 assert_eq!(search_bar.query(cx), "a");
1826 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1827 });
1828 search_bar.update(cx, |search_bar, cx| {
1829 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1830 });
1831 search_bar.update(cx, |search_bar, cx| {
1832 assert_eq!(search_bar.query(cx), "a");
1833 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1834 });
1835
1836 // Next items should go over the history in the original order.
1837 search_bar.update(cx, |search_bar, cx| {
1838 search_bar.next_history_query(&NextHistoryQuery, cx);
1839 });
1840 search_bar.update(cx, |search_bar, cx| {
1841 assert_eq!(search_bar.query(cx), "b");
1842 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1843 });
1844
1845 search_bar
1846 .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1847 .await
1848 .unwrap();
1849 search_bar.update(cx, |search_bar, cx| {
1850 assert_eq!(search_bar.query(cx), "ba");
1851 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1852 });
1853
1854 // New search input should add another entry to history and move the selection to the end of the history.
1855 search_bar.update(cx, |search_bar, cx| {
1856 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1857 });
1858 search_bar.update(cx, |search_bar, cx| {
1859 assert_eq!(search_bar.query(cx), "c");
1860 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1861 });
1862 search_bar.update(cx, |search_bar, cx| {
1863 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1864 });
1865 search_bar.update(cx, |search_bar, cx| {
1866 assert_eq!(search_bar.query(cx), "b");
1867 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1868 });
1869 search_bar.update(cx, |search_bar, cx| {
1870 search_bar.next_history_query(&NextHistoryQuery, cx);
1871 });
1872 search_bar.update(cx, |search_bar, cx| {
1873 assert_eq!(search_bar.query(cx), "c");
1874 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1875 });
1876 search_bar.update(cx, |search_bar, cx| {
1877 search_bar.next_history_query(&NextHistoryQuery, cx);
1878 });
1879 search_bar.update(cx, |search_bar, cx| {
1880 assert_eq!(search_bar.query(cx), "ba");
1881 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1882 });
1883 search_bar.update(cx, |search_bar, cx| {
1884 search_bar.next_history_query(&NextHistoryQuery, cx);
1885 });
1886 search_bar.update(cx, |search_bar, cx| {
1887 assert_eq!(search_bar.query(cx), "");
1888 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1889 });
1890 }
1891
1892 #[gpui::test]
1893 async fn test_replace_simple(cx: &mut TestAppContext) {
1894 let (editor, search_bar, cx) = init_test(cx);
1895
1896 search_bar
1897 .update(cx, |search_bar, cx| {
1898 search_bar.search("expression", None, cx)
1899 })
1900 .await
1901 .unwrap();
1902
1903 search_bar.update(cx, |search_bar, cx| {
1904 search_bar.replacement_editor.update(cx, |editor, cx| {
1905 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
1906 editor.set_text("expr$1", cx);
1907 });
1908 search_bar.replace_all(&ReplaceAll, cx)
1909 });
1910 assert_eq!(
1911 editor.update(cx, |this, cx| { this.text(cx) }),
1912 r#"
1913 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
1914 rational expr$1[2][3]) is a sequence of characters that specifies a search
1915 pattern in text. Usually such patterns are used by string-searching algorithms
1916 for "find" or "find and replace" operations on strings, or for input validation.
1917 "#
1918 .unindent()
1919 );
1920
1921 // Search for word boundaries and replace just a single one.
1922 search_bar
1923 .update(cx, |search_bar, cx| {
1924 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
1925 })
1926 .await
1927 .unwrap();
1928
1929 search_bar.update(cx, |search_bar, cx| {
1930 search_bar.replacement_editor.update(cx, |editor, cx| {
1931 editor.set_text("banana", cx);
1932 });
1933 search_bar.replace_next(&ReplaceNext, cx)
1934 });
1935 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
1936 assert_eq!(
1937 editor.update(cx, |this, cx| { this.text(cx) }),
1938 r#"
1939 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
1940 rational expr$1[2][3]) is a sequence of characters that specifies a search
1941 pattern in text. Usually such patterns are used by string-searching algorithms
1942 for "find" or "find and replace" operations on strings, or for input validation.
1943 "#
1944 .unindent()
1945 );
1946 // Let's turn on regex mode.
1947 search_bar
1948 .update(cx, |search_bar, cx| {
1949 search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), cx)
1950 })
1951 .await
1952 .unwrap();
1953 search_bar.update(cx, |search_bar, cx| {
1954 search_bar.replacement_editor.update(cx, |editor, cx| {
1955 editor.set_text("${1}number", cx);
1956 });
1957 search_bar.replace_all(&ReplaceAll, cx)
1958 });
1959 assert_eq!(
1960 editor.update(cx, |this, cx| { this.text(cx) }),
1961 r#"
1962 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1963 rational expr$12number3number) is a sequence of characters that specifies a search
1964 pattern in text. Usually such patterns are used by string-searching algorithms
1965 for "find" or "find and replace" operations on strings, or for input validation.
1966 "#
1967 .unindent()
1968 );
1969 // Now with a whole-word twist.
1970 search_bar
1971 .update(cx, |search_bar, cx| {
1972 search_bar.search(
1973 "a\\w+s",
1974 Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
1975 cx,
1976 )
1977 })
1978 .await
1979 .unwrap();
1980 search_bar.update(cx, |search_bar, cx| {
1981 search_bar.replacement_editor.update(cx, |editor, cx| {
1982 editor.set_text("things", cx);
1983 });
1984 search_bar.replace_all(&ReplaceAll, cx)
1985 });
1986 // The only word affected by this edit should be `algorithms`, even though there's a bunch
1987 // of words in this text that would match this regex if not for WHOLE_WORD.
1988 assert_eq!(
1989 editor.update(cx, |this, cx| { this.text(cx) }),
1990 r#"
1991 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1992 rational expr$12number3number) is a sequence of characters that specifies a search
1993 pattern in text. Usually such patterns are used by string-searching things
1994 for "find" or "find and replace" operations on strings, or for input validation.
1995 "#
1996 .unindent()
1997 );
1998 }
1999
2000 struct ReplacementTestParams<'a> {
2001 editor: &'a View<Editor>,
2002 search_bar: &'a View<BufferSearchBar>,
2003 cx: &'a mut VisualTestContext,
2004 search_text: &'static str,
2005 search_options: Option<SearchOptions>,
2006 replacement_text: &'static str,
2007 replace_all: bool,
2008 expected_text: String,
2009 }
2010
2011 async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2012 options
2013 .search_bar
2014 .update(options.cx, |search_bar, cx| {
2015 if let Some(options) = options.search_options {
2016 search_bar.set_search_options(options, cx);
2017 }
2018 search_bar.search(options.search_text, options.search_options, cx)
2019 })
2020 .await
2021 .unwrap();
2022
2023 options.search_bar.update(options.cx, |search_bar, cx| {
2024 search_bar.replacement_editor.update(cx, |editor, cx| {
2025 editor.set_text(options.replacement_text, cx);
2026 });
2027
2028 if options.replace_all {
2029 search_bar.replace_all(&ReplaceAll, cx)
2030 } else {
2031 search_bar.replace_next(&ReplaceNext, cx)
2032 }
2033 });
2034
2035 assert_eq!(
2036 options
2037 .editor
2038 .update(options.cx, |this, cx| { this.text(cx) }),
2039 options.expected_text
2040 );
2041 }
2042
2043 #[gpui::test]
2044 async fn test_replace_special_characters(cx: &mut TestAppContext) {
2045 let (editor, search_bar, cx) = init_test(cx);
2046
2047 run_replacement_test(ReplacementTestParams {
2048 editor: &editor,
2049 search_bar: &search_bar,
2050 cx,
2051 search_text: "expression",
2052 search_options: None,
2053 replacement_text: r"\n",
2054 replace_all: true,
2055 expected_text: r#"
2056 A regular \n (shortened as regex or regexp;[1] also referred to as
2057 rational \n[2][3]) is a sequence of characters that specifies a search
2058 pattern in text. Usually such patterns are used by string-searching algorithms
2059 for "find" or "find and replace" operations on strings, or for input validation.
2060 "#
2061 .unindent(),
2062 })
2063 .await;
2064
2065 run_replacement_test(ReplacementTestParams {
2066 editor: &editor,
2067 search_bar: &search_bar,
2068 cx,
2069 search_text: "or",
2070 search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2071 replacement_text: r"\\\n\\\\",
2072 replace_all: false,
2073 expected_text: r#"
2074 A regular \n (shortened as regex \
2075 \\ regexp;[1] also referred to as
2076 rational \n[2][3]) is a sequence of characters that specifies a search
2077 pattern in text. Usually such patterns are used by string-searching algorithms
2078 for "find" or "find and replace" operations on strings, or for input validation.
2079 "#
2080 .unindent(),
2081 })
2082 .await;
2083
2084 run_replacement_test(ReplacementTestParams {
2085 editor: &editor,
2086 search_bar: &search_bar,
2087 cx,
2088 search_text: r"(that|used) ",
2089 search_options: Some(SearchOptions::REGEX),
2090 replacement_text: r"$1\n",
2091 replace_all: true,
2092 expected_text: r#"
2093 A regular \n (shortened as regex \
2094 \\ regexp;[1] also referred to as
2095 rational \n[2][3]) is a sequence of characters that
2096 specifies a search
2097 pattern in text. Usually such patterns are used
2098 by string-searching algorithms
2099 for "find" or "find and replace" operations on strings, or for input validation.
2100 "#
2101 .unindent(),
2102 })
2103 .await;
2104 }
2105
2106 #[gpui::test]
2107 async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2108 cx: &mut TestAppContext,
2109 ) {
2110 init_globals(cx);
2111 let buffer = cx.new_model(|cx| {
2112 Buffer::local(
2113 r#"
2114 aaa bbb aaa ccc
2115 aaa bbb aaa ccc
2116 aaa bbb aaa ccc
2117 aaa bbb aaa ccc
2118 aaa bbb aaa ccc
2119 aaa bbb aaa ccc
2120 "#
2121 .unindent(),
2122 cx,
2123 )
2124 });
2125 let cx = cx.add_empty_window();
2126 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
2127
2128 let search_bar = cx.new_view(|cx| {
2129 let mut search_bar = BufferSearchBar::new(cx);
2130 search_bar.set_active_pane_item(Some(&editor), cx);
2131 search_bar.show(cx);
2132 search_bar
2133 });
2134
2135 editor.update(cx, |editor, cx| {
2136 editor.change_selections(None, cx, |s| {
2137 s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2138 })
2139 });
2140
2141 search_bar.update(cx, |search_bar, cx| {
2142 let deploy = Deploy {
2143 focus: true,
2144 replace_enabled: false,
2145 selection_search_enabled: true,
2146 };
2147 search_bar.deploy(&deploy, cx);
2148 });
2149
2150 cx.run_until_parked();
2151
2152 search_bar
2153 .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2154 .await
2155 .unwrap();
2156
2157 editor.update(cx, |editor, cx| {
2158 assert_eq!(
2159 editor.search_background_highlights(cx),
2160 &[
2161 Point::new(1, 0)..Point::new(1, 3),
2162 Point::new(1, 8)..Point::new(1, 11),
2163 Point::new(2, 0)..Point::new(2, 3),
2164 ]
2165 );
2166 });
2167 }
2168
2169 #[gpui::test]
2170 async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2171 cx: &mut TestAppContext,
2172 ) {
2173 init_globals(cx);
2174 let text = r#"
2175 aaa bbb aaa ccc
2176 aaa bbb aaa ccc
2177 aaa bbb aaa ccc
2178 aaa bbb aaa ccc
2179 aaa bbb aaa ccc
2180 aaa bbb aaa ccc
2181
2182 aaa bbb aaa ccc
2183 aaa bbb aaa ccc
2184 aaa bbb aaa ccc
2185 aaa bbb aaa ccc
2186 aaa bbb aaa ccc
2187 aaa bbb aaa ccc
2188 "#
2189 .unindent();
2190
2191 let cx = cx.add_empty_window();
2192 let editor = cx.new_view(|cx| {
2193 let multibuffer = MultiBuffer::build_multi(
2194 [
2195 (
2196 &text,
2197 vec![
2198 Point::new(0, 0)..Point::new(2, 0),
2199 Point::new(4, 0)..Point::new(5, 0),
2200 ],
2201 ),
2202 (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2203 ],
2204 cx,
2205 );
2206 Editor::for_multibuffer(multibuffer, None, false, cx)
2207 });
2208
2209 let search_bar = cx.new_view(|cx| {
2210 let mut search_bar = BufferSearchBar::new(cx);
2211 search_bar.set_active_pane_item(Some(&editor), cx);
2212 search_bar.show(cx);
2213 search_bar
2214 });
2215
2216 editor.update(cx, |editor, cx| {
2217 editor.change_selections(None, cx, |s| {
2218 s.select_ranges(vec![
2219 Point::new(1, 0)..Point::new(1, 4),
2220 Point::new(5, 3)..Point::new(6, 4),
2221 ])
2222 })
2223 });
2224
2225 search_bar.update(cx, |search_bar, cx| {
2226 let deploy = Deploy {
2227 focus: true,
2228 replace_enabled: false,
2229 selection_search_enabled: true,
2230 };
2231 search_bar.deploy(&deploy, cx);
2232 });
2233
2234 cx.run_until_parked();
2235
2236 search_bar
2237 .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2238 .await
2239 .unwrap();
2240
2241 editor.update(cx, |editor, cx| {
2242 assert_eq!(
2243 editor.search_background_highlights(cx),
2244 &[
2245 Point::new(1, 0)..Point::new(1, 3),
2246 Point::new(5, 8)..Point::new(5, 11),
2247 Point::new(6, 0)..Point::new(6, 3),
2248 ]
2249 );
2250 });
2251 }
2252
2253 #[gpui::test]
2254 async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2255 let (editor, search_bar, cx) = init_test(cx);
2256 // Search using valid regexp
2257 search_bar
2258 .update(cx, |search_bar, cx| {
2259 search_bar.enable_search_option(SearchOptions::REGEX, cx);
2260 search_bar.search("expression", None, cx)
2261 })
2262 .await
2263 .unwrap();
2264 editor.update(cx, |editor, cx| {
2265 assert_eq!(
2266 display_points_of(editor.all_text_background_highlights(cx)),
2267 &[
2268 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2269 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2270 ],
2271 );
2272 });
2273
2274 // Now, the expression is invalid
2275 search_bar
2276 .update(cx, |search_bar, cx| {
2277 search_bar.search("expression (", None, cx)
2278 })
2279 .await
2280 .unwrap_err();
2281 editor.update(cx, |editor, cx| {
2282 assert!(display_points_of(editor.all_text_background_highlights(cx)).is_empty(),);
2283 });
2284 }
2285}