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