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