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