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