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