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