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