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(Editor::single_line);
502 cx.subscribe(&query_editor, Self::on_query_editor_event)
503 .detach();
504 let replacement_editor = cx.new_view(Editor::single_line);
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 self.focus(&handle, cx);
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.smartcase(cx);
571 self.replace_enabled = deploy.replace_enabled;
572 self.selection_search_enabled = deploy.selection_search_enabled;
573 if deploy.focus {
574 let mut handle = self.query_editor.focus_handle(cx).clone();
575 let mut select_query = true;
576 if deploy.replace_enabled && handle.is_focused(cx) {
577 handle = self.replacement_editor.focus_handle(cx).clone();
578 select_query = false;
579 };
580
581 if select_query {
582 self.select_query(cx);
583 }
584
585 cx.focus(&handle);
586 }
587 return true;
588 }
589
590 false
591 }
592
593 pub fn toggle(&mut self, action: &Deploy, cx: &mut ViewContext<Self>) {
594 if self.is_dismissed() {
595 self.deploy(action, cx);
596 } else {
597 self.dismiss(&Dismiss, cx);
598 }
599 }
600
601 pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
602 let Some(handle) = self.active_searchable_item.as_ref() else {
603 return false;
604 };
605
606 self.dismissed = false;
607 handle.search_bar_visibility_changed(true, cx);
608 cx.notify();
609 cx.emit(Event::UpdateLocation);
610 cx.emit(ToolbarItemEvent::ChangeLocation(
611 ToolbarItemLocation::Secondary,
612 ));
613 true
614 }
615
616 fn supported_options(&self) -> workspace::searchable::SearchOptions {
617 self.active_searchable_item
618 .as_deref()
619 .map(SearchableItemHandle::supported_options)
620 .unwrap_or_default()
621 }
622 pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
623 let search = self
624 .query_suggestion(cx)
625 .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
626
627 if let Some(search) = search {
628 cx.spawn(|this, mut cx| async move {
629 search.await?;
630 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
631 })
632 .detach_and_log_err(cx);
633 }
634 }
635
636 pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
637 if let Some(match_ix) = self.active_match_index {
638 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
639 if let Some(matches) = self
640 .searchable_items_with_matches
641 .get(&active_searchable_item.downgrade())
642 {
643 active_searchable_item.activate_match(match_ix, matches, cx)
644 }
645 }
646 }
647 }
648
649 pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
650 self.query_editor.update(cx, |query_editor, cx| {
651 query_editor.select_all(&Default::default(), cx);
652 });
653 }
654
655 pub fn query(&self, cx: &WindowContext) -> String {
656 self.query_editor.read(cx).text(cx)
657 }
658 pub fn replacement(&self, cx: &WindowContext) -> String {
659 self.replacement_editor.read(cx).text(cx)
660 }
661 pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
662 self.active_searchable_item
663 .as_ref()
664 .map(|searchable_item| searchable_item.query_suggestion(cx))
665 .filter(|suggestion| !suggestion.is_empty())
666 }
667
668 pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
669 if replacement.is_none() {
670 self.replace_enabled = false;
671 return;
672 }
673 self.replace_enabled = true;
674 self.replacement_editor
675 .update(cx, |replacement_editor, cx| {
676 replacement_editor
677 .buffer()
678 .update(cx, |replacement_buffer, cx| {
679 let len = replacement_buffer.len(cx);
680 replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
681 });
682 });
683 }
684
685 pub fn search(
686 &mut self,
687 query: &str,
688 options: Option<SearchOptions>,
689 cx: &mut ViewContext<Self>,
690 ) -> oneshot::Receiver<()> {
691 let options = options.unwrap_or(self.default_options);
692 if query != self.query(cx) || self.search_options != options {
693 self.query_editor.update(cx, |query_editor, cx| {
694 query_editor.buffer().update(cx, |query_buffer, cx| {
695 let len = query_buffer.len(cx);
696 query_buffer.edit([(0..len, query)], None, cx);
697 });
698 });
699 self.search_options = options;
700 self.clear_matches(cx);
701 cx.notify();
702 }
703 self.update_matches(cx)
704 }
705
706 fn render_search_option_button(
707 &self,
708 option: SearchOptions,
709 action: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
710 ) -> impl IntoElement {
711 let is_active = self.search_options.contains(option);
712 option.as_button(is_active, action)
713 }
714
715 pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
716 if let Some(active_editor) = self.active_searchable_item.as_ref() {
717 let handle = active_editor.focus_handle(cx);
718 cx.focus(&handle);
719 }
720 }
721
722 pub fn toggle_search_option(
723 &mut self,
724 search_option: SearchOptions,
725 cx: &mut ViewContext<Self>,
726 ) {
727 self.search_options.toggle(search_option);
728 self.default_options = self.search_options;
729 drop(self.update_matches(cx));
730 cx.notify();
731 }
732
733 pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
734 self.search_options.contains(search_option)
735 }
736
737 pub fn enable_search_option(
738 &mut self,
739 search_option: SearchOptions,
740 cx: &mut ViewContext<Self>,
741 ) {
742 if !self.search_options.contains(search_option) {
743 self.toggle_search_option(search_option, cx)
744 }
745 }
746
747 pub fn set_search_options(
748 &mut self,
749 search_options: SearchOptions,
750 cx: &mut ViewContext<Self>,
751 ) {
752 self.search_options = search_options;
753 cx.notify();
754 }
755
756 fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
757 self.select_match(Direction::Next, 1, cx);
758 }
759
760 fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
761 self.select_match(Direction::Prev, 1, cx);
762 }
763
764 fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
765 if !self.dismissed && self.active_match_index.is_some() {
766 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
767 if let Some(matches) = self
768 .searchable_items_with_matches
769 .get(&searchable_item.downgrade())
770 {
771 searchable_item.select_matches(matches, cx);
772 self.focus_editor(&FocusEditor, cx);
773 }
774 }
775 }
776 }
777
778 pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
779 if let Some(index) = self.active_match_index {
780 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
781 if let Some(matches) = self
782 .searchable_items_with_matches
783 .get(&searchable_item.downgrade())
784 .filter(|matches| !matches.is_empty())
785 {
786 // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
787 if !EditorSettings::get_global(cx).search_wrap
788 && ((direction == Direction::Next && index + count >= matches.len())
789 || (direction == Direction::Prev && index < count))
790 {
791 crate::show_no_more_matches(cx);
792 return;
793 }
794 let new_match_index = searchable_item
795 .match_index_for_direction(matches, index, direction, count, cx);
796
797 searchable_item.update_matches(matches, cx);
798 searchable_item.activate_match(new_match_index, matches, cx);
799 }
800 }
801 }
802 }
803
804 pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
805 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
806 if let Some(matches) = self
807 .searchable_items_with_matches
808 .get(&searchable_item.downgrade())
809 {
810 if matches.is_empty() {
811 return;
812 }
813 let new_match_index = matches.len() - 1;
814 searchable_item.update_matches(matches, cx);
815 searchable_item.activate_match(new_match_index, matches, cx);
816 }
817 }
818 }
819
820 fn on_query_editor_event(
821 &mut self,
822 editor: View<Editor>,
823 event: &editor::EditorEvent,
824 cx: &mut ViewContext<Self>,
825 ) {
826 match event {
827 editor::EditorEvent::Focused => self.query_editor_focused = true,
828 editor::EditorEvent::Blurred => self.query_editor_focused = false,
829 editor::EditorEvent::Edited { .. } => {
830 self.smartcase(cx);
831 self.clear_matches(cx);
832 let search = self.update_matches(cx);
833
834 let width = editor.update(cx, |editor, cx| {
835 let text_layout_details = editor.text_layout_details(cx);
836 let snapshot = editor.snapshot(cx).display_snapshot;
837
838 snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
839 - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
840 });
841 self.editor_needed_width = width;
842 cx.notify();
843
844 cx.spawn(|this, mut cx| async move {
845 search.await?;
846 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
847 })
848 .detach_and_log_err(cx);
849 }
850 _ => {}
851 }
852 }
853
854 fn on_replacement_editor_event(
855 &mut self,
856 _: View<Editor>,
857 event: &editor::EditorEvent,
858 _: &mut ViewContext<Self>,
859 ) {
860 match event {
861 editor::EditorEvent::Focused => self.replacement_editor_focused = true,
862 editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
863 _ => {}
864 }
865 }
866
867 fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext<Self>) {
868 match event {
869 SearchEvent::MatchesInvalidated => {
870 drop(self.update_matches(cx));
871 }
872 SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
873 }
874 }
875
876 fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext<Self>) {
877 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
878 }
879
880 fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext<Self>) {
881 self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
882 }
883
884 fn toggle_selection(&mut self, _: &ToggleSelection, cx: &mut ViewContext<Self>) {
885 if let Some(active_item) = self.active_searchable_item.as_mut() {
886 self.selection_search_enabled = !self.selection_search_enabled;
887 active_item.toggle_filtered_search_ranges(self.selection_search_enabled, cx);
888 drop(self.update_matches(cx));
889 cx.notify();
890 }
891 }
892
893 fn toggle_regex(&mut self, _: &ToggleRegex, cx: &mut ViewContext<Self>) {
894 self.toggle_search_option(SearchOptions::REGEX, cx)
895 }
896
897 fn clear_active_searchable_item_matches(&mut self, cx: &mut WindowContext) {
898 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
899 self.active_match_index = None;
900 self.searchable_items_with_matches
901 .remove(&active_searchable_item.downgrade());
902 active_searchable_item.clear_matches(cx);
903 }
904 }
905
906 pub fn has_active_match(&self) -> bool {
907 self.active_match_index.is_some()
908 }
909
910 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
911 let mut active_item_matches = None;
912 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
913 if let Some(searchable_item) =
914 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
915 {
916 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
917 active_item_matches = Some((searchable_item.downgrade(), matches));
918 } else {
919 searchable_item.clear_matches(cx);
920 }
921 }
922 }
923
924 self.searchable_items_with_matches
925 .extend(active_item_matches);
926 }
927
928 fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
929 let (done_tx, done_rx) = oneshot::channel();
930 let query = self.query(cx);
931 self.pending_search.take();
932
933 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
934 self.query_contains_error = false;
935 if query.is_empty() {
936 self.clear_active_searchable_item_matches(cx);
937 let _ = done_tx.send(());
938 cx.notify();
939 } else {
940 let query: Arc<_> = if self.search_options.contains(SearchOptions::REGEX) {
941 match SearchQuery::regex(
942 query,
943 self.search_options.contains(SearchOptions::WHOLE_WORD),
944 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
945 false,
946 Default::default(),
947 Default::default(),
948 None,
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 } else {
959 match SearchQuery::text(
960 query,
961 self.search_options.contains(SearchOptions::WHOLE_WORD),
962 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
963 false,
964 Default::default(),
965 Default::default(),
966 None,
967 ) {
968 Ok(query) => query.with_replacement(self.replacement(cx)),
969 Err(_) => {
970 self.query_contains_error = true;
971 self.clear_active_searchable_item_matches(cx);
972 cx.notify();
973 return done_rx;
974 }
975 }
976 }
977 .into();
978 self.active_search = Some(query.clone());
979 let query_text = query.as_str().to_string();
980
981 let matches = active_searchable_item.find_matches(query, cx);
982
983 let active_searchable_item = active_searchable_item.downgrade();
984 self.pending_search = Some(cx.spawn(|this, mut cx| async move {
985 let matches = matches.await;
986
987 this.update(&mut cx, |this, cx| {
988 if let Some(active_searchable_item) =
989 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
990 {
991 this.searchable_items_with_matches
992 .insert(active_searchable_item.downgrade(), matches);
993
994 this.update_match_index(cx);
995 this.search_history
996 .add(&mut this.search_history_cursor, query_text);
997 if !this.dismissed {
998 let matches = this
999 .searchable_items_with_matches
1000 .get(&active_searchable_item.downgrade())
1001 .unwrap();
1002 if matches.is_empty() {
1003 active_searchable_item.clear_matches(cx);
1004 } else {
1005 active_searchable_item.update_matches(matches, cx);
1006 }
1007 let _ = done_tx.send(());
1008 }
1009 cx.notify();
1010 }
1011 })
1012 .log_err();
1013 }));
1014 }
1015 }
1016 done_rx
1017 }
1018
1019 pub fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
1020 let new_index = self
1021 .active_searchable_item
1022 .as_ref()
1023 .and_then(|searchable_item| {
1024 let matches = self
1025 .searchable_items_with_matches
1026 .get(&searchable_item.downgrade())?;
1027 searchable_item.active_match_index(matches, cx)
1028 });
1029 if new_index != self.active_match_index {
1030 self.active_match_index = new_index;
1031 cx.notify();
1032 }
1033 }
1034
1035 fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
1036 // Search -> Replace -> Editor
1037 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1038 self.replacement_editor.focus_handle(cx)
1039 } else if let Some(item) = self.active_searchable_item.as_ref() {
1040 item.focus_handle(cx)
1041 } else {
1042 return;
1043 };
1044 self.focus(&focus_handle, cx);
1045 cx.stop_propagation();
1046 }
1047
1048 fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext<Self>) {
1049 // Search -> Replace -> Search
1050 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1051 self.replacement_editor.focus_handle(cx)
1052 } else if self.replacement_editor_focused {
1053 self.query_editor.focus_handle(cx)
1054 } else {
1055 return;
1056 };
1057 self.focus(&focus_handle, cx);
1058 cx.stop_propagation();
1059 }
1060
1061 fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
1062 if let Some(new_query) = self
1063 .search_history
1064 .next(&mut self.search_history_cursor)
1065 .map(str::to_string)
1066 {
1067 drop(self.search(&new_query, Some(self.search_options), cx));
1068 } else {
1069 self.search_history_cursor.reset();
1070 drop(self.search("", Some(self.search_options), cx));
1071 }
1072 }
1073
1074 fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
1075 if self.query(cx).is_empty() {
1076 if let Some(new_query) = self
1077 .search_history
1078 .current(&mut self.search_history_cursor)
1079 .map(str::to_string)
1080 {
1081 drop(self.search(&new_query, Some(self.search_options), cx));
1082 return;
1083 }
1084 }
1085
1086 if let Some(new_query) = self
1087 .search_history
1088 .previous(&mut self.search_history_cursor)
1089 .map(str::to_string)
1090 {
1091 drop(self.search(&new_query, Some(self.search_options), cx));
1092 }
1093 }
1094
1095 fn focus(&self, handle: &gpui::FocusHandle, cx: &mut ViewContext<Self>) {
1096 cx.on_next_frame(|_, cx| {
1097 cx.invalidate_character_coordinates();
1098 });
1099 cx.focus(handle);
1100 }
1101 fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
1102 if self.active_searchable_item.is_some() {
1103 self.replace_enabled = !self.replace_enabled;
1104 let handle = if self.replace_enabled {
1105 self.replacement_editor.focus_handle(cx)
1106 } else {
1107 self.query_editor.focus_handle(cx)
1108 };
1109 self.focus(&handle, cx);
1110 cx.notify();
1111 }
1112 }
1113 fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
1114 let mut should_propagate = true;
1115 if !self.dismissed && self.active_search.is_some() {
1116 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1117 if let Some(query) = self.active_search.as_ref() {
1118 if let Some(matches) = self
1119 .searchable_items_with_matches
1120 .get(&searchable_item.downgrade())
1121 {
1122 if let Some(active_index) = self.active_match_index {
1123 let query = query
1124 .as_ref()
1125 .clone()
1126 .with_replacement(self.replacement(cx));
1127 searchable_item.replace(matches.at(active_index), &query, cx);
1128 self.select_next_match(&SelectNextMatch, cx);
1129 }
1130 should_propagate = false;
1131 self.focus_editor(&FocusEditor, cx);
1132 }
1133 }
1134 }
1135 }
1136 if !should_propagate {
1137 cx.stop_propagation();
1138 }
1139 }
1140 pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
1141 if !self.dismissed && self.active_search.is_some() {
1142 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1143 if let Some(query) = self.active_search.as_ref() {
1144 if let Some(matches) = self
1145 .searchable_items_with_matches
1146 .get(&searchable_item.downgrade())
1147 {
1148 let query = query
1149 .as_ref()
1150 .clone()
1151 .with_replacement(self.replacement(cx));
1152 searchable_item.replace_all(&mut matches.iter(), &query, cx);
1153 }
1154 }
1155 }
1156 }
1157 }
1158
1159 pub fn match_exists(&mut self, cx: &mut ViewContext<Self>) -> bool {
1160 self.update_match_index(cx);
1161 self.active_match_index.is_some()
1162 }
1163
1164 pub fn should_use_smartcase_search(&mut self, cx: &mut ViewContext<Self>) -> bool {
1165 EditorSettings::get_global(cx).use_smartcase_search
1166 }
1167
1168 pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1169 str.chars().any(|c| c.is_uppercase())
1170 }
1171
1172 fn smartcase(&mut self, cx: &mut ViewContext<Self>) {
1173 if self.should_use_smartcase_search(cx) {
1174 let query = self.query(cx);
1175 if !query.is_empty() {
1176 let is_case = self.is_contains_uppercase(&query);
1177 if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1178 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1179 }
1180 }
1181 }
1182 }
1183}
1184
1185#[cfg(test)]
1186mod tests {
1187 use std::ops::Range;
1188
1189 use super::*;
1190 use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer};
1191 use gpui::{Context, Hsla, TestAppContext, VisualTestContext};
1192 use language::{Buffer, Point};
1193 use project::Project;
1194 use smol::stream::StreamExt as _;
1195 use unindent::Unindent as _;
1196
1197 fn init_globals(cx: &mut TestAppContext) {
1198 cx.update(|cx| {
1199 let store = settings::SettingsStore::test(cx);
1200 cx.set_global(store);
1201 editor::init(cx);
1202
1203 language::init(cx);
1204 Project::init_settings(cx);
1205 theme::init(theme::LoadThemes::JustBase, cx);
1206 });
1207 }
1208
1209 fn init_test(
1210 cx: &mut TestAppContext,
1211 ) -> (View<Editor>, View<BufferSearchBar>, &mut VisualTestContext) {
1212 init_globals(cx);
1213 let buffer = cx.new_model(|cx| {
1214 Buffer::local(
1215 r#"
1216 A regular expression (shortened as regex or regexp;[1] also referred to as
1217 rational expression[2][3]) is a sequence of characters that specifies a search
1218 pattern in text. Usually such patterns are used by string-searching algorithms
1219 for "find" or "find and replace" operations on strings, or for input validation.
1220 "#
1221 .unindent(),
1222 cx,
1223 )
1224 });
1225 let cx = cx.add_empty_window();
1226 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1227
1228 let search_bar = cx.new_view(|cx| {
1229 let mut search_bar = BufferSearchBar::new(cx);
1230 search_bar.set_active_pane_item(Some(&editor), cx);
1231 search_bar.show(cx);
1232 search_bar
1233 });
1234
1235 (editor, search_bar, cx)
1236 }
1237
1238 #[gpui::test]
1239 async fn test_search_simple(cx: &mut TestAppContext) {
1240 let (editor, search_bar, cx) = init_test(cx);
1241 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1242 background_highlights
1243 .into_iter()
1244 .map(|(range, _)| range)
1245 .collect::<Vec<_>>()
1246 };
1247 // Search for a string that appears with different casing.
1248 // By default, search is case-insensitive.
1249 search_bar
1250 .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
1251 .await
1252 .unwrap();
1253 editor.update(cx, |editor, cx| {
1254 assert_eq!(
1255 display_points_of(editor.all_text_background_highlights(cx)),
1256 &[
1257 DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1258 DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1259 ]
1260 );
1261 });
1262
1263 // Switch to a case sensitive search.
1264 search_bar.update(cx, |search_bar, cx| {
1265 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1266 });
1267 let mut editor_notifications = cx.notifications(&editor);
1268 editor_notifications.next().await;
1269 editor.update(cx, |editor, cx| {
1270 assert_eq!(
1271 display_points_of(editor.all_text_background_highlights(cx)),
1272 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1273 );
1274 });
1275
1276 // Search for a string that appears both as a whole word and
1277 // within other words. By default, all results are found.
1278 search_bar
1279 .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1280 .await
1281 .unwrap();
1282 editor.update(cx, |editor, cx| {
1283 assert_eq!(
1284 display_points_of(editor.all_text_background_highlights(cx)),
1285 &[
1286 DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1287 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1288 DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1289 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1290 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1291 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1292 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1293 ]
1294 );
1295 });
1296
1297 // Switch to a whole word search.
1298 search_bar.update(cx, |search_bar, cx| {
1299 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1300 });
1301 let mut editor_notifications = cx.notifications(&editor);
1302 editor_notifications.next().await;
1303 editor.update(cx, |editor, cx| {
1304 assert_eq!(
1305 display_points_of(editor.all_text_background_highlights(cx)),
1306 &[
1307 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1308 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1309 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1310 ]
1311 );
1312 });
1313
1314 editor.update(cx, |editor, cx| {
1315 editor.change_selections(None, cx, |s| {
1316 s.select_display_ranges([
1317 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1318 ])
1319 });
1320 });
1321 search_bar.update(cx, |search_bar, cx| {
1322 assert_eq!(search_bar.active_match_index, Some(0));
1323 search_bar.select_next_match(&SelectNextMatch, cx);
1324 assert_eq!(
1325 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1326 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1327 );
1328 });
1329 search_bar.update(cx, |search_bar, _| {
1330 assert_eq!(search_bar.active_match_index, Some(0));
1331 });
1332
1333 search_bar.update(cx, |search_bar, cx| {
1334 search_bar.select_next_match(&SelectNextMatch, cx);
1335 assert_eq!(
1336 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1337 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1338 );
1339 });
1340 search_bar.update(cx, |search_bar, _| {
1341 assert_eq!(search_bar.active_match_index, Some(1));
1342 });
1343
1344 search_bar.update(cx, |search_bar, cx| {
1345 search_bar.select_next_match(&SelectNextMatch, cx);
1346 assert_eq!(
1347 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1348 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1349 );
1350 });
1351 search_bar.update(cx, |search_bar, _| {
1352 assert_eq!(search_bar.active_match_index, Some(2));
1353 });
1354
1355 search_bar.update(cx, |search_bar, cx| {
1356 search_bar.select_next_match(&SelectNextMatch, cx);
1357 assert_eq!(
1358 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1359 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1360 );
1361 });
1362 search_bar.update(cx, |search_bar, _| {
1363 assert_eq!(search_bar.active_match_index, Some(0));
1364 });
1365
1366 search_bar.update(cx, |search_bar, cx| {
1367 search_bar.select_prev_match(&SelectPrevMatch, cx);
1368 assert_eq!(
1369 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1370 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1371 );
1372 });
1373 search_bar.update(cx, |search_bar, _| {
1374 assert_eq!(search_bar.active_match_index, Some(2));
1375 });
1376
1377 search_bar.update(cx, |search_bar, cx| {
1378 search_bar.select_prev_match(&SelectPrevMatch, cx);
1379 assert_eq!(
1380 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1381 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1382 );
1383 });
1384 search_bar.update(cx, |search_bar, _| {
1385 assert_eq!(search_bar.active_match_index, Some(1));
1386 });
1387
1388 search_bar.update(cx, |search_bar, cx| {
1389 search_bar.select_prev_match(&SelectPrevMatch, cx);
1390 assert_eq!(
1391 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1392 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1393 );
1394 });
1395 search_bar.update(cx, |search_bar, _| {
1396 assert_eq!(search_bar.active_match_index, Some(0));
1397 });
1398
1399 // Park the cursor in between matches and ensure that going to the previous match selects
1400 // the closest match to the left.
1401 editor.update(cx, |editor, cx| {
1402 editor.change_selections(None, cx, |s| {
1403 s.select_display_ranges([
1404 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1405 ])
1406 });
1407 });
1408 search_bar.update(cx, |search_bar, cx| {
1409 assert_eq!(search_bar.active_match_index, Some(1));
1410 search_bar.select_prev_match(&SelectPrevMatch, cx);
1411 assert_eq!(
1412 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1413 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1414 );
1415 });
1416 search_bar.update(cx, |search_bar, _| {
1417 assert_eq!(search_bar.active_match_index, Some(0));
1418 });
1419
1420 // Park the cursor in between matches and ensure that going to the next match selects the
1421 // closest match to the right.
1422 editor.update(cx, |editor, cx| {
1423 editor.change_selections(None, cx, |s| {
1424 s.select_display_ranges([
1425 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1426 ])
1427 });
1428 });
1429 search_bar.update(cx, |search_bar, cx| {
1430 assert_eq!(search_bar.active_match_index, Some(1));
1431 search_bar.select_next_match(&SelectNextMatch, cx);
1432 assert_eq!(
1433 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1434 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1435 );
1436 });
1437 search_bar.update(cx, |search_bar, _| {
1438 assert_eq!(search_bar.active_match_index, Some(1));
1439 });
1440
1441 // Park the cursor after the last match and ensure that going to the previous match selects
1442 // the last match.
1443 editor.update(cx, |editor, cx| {
1444 editor.change_selections(None, cx, |s| {
1445 s.select_display_ranges([
1446 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1447 ])
1448 });
1449 });
1450 search_bar.update(cx, |search_bar, cx| {
1451 assert_eq!(search_bar.active_match_index, Some(2));
1452 search_bar.select_prev_match(&SelectPrevMatch, cx);
1453 assert_eq!(
1454 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1455 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1456 );
1457 });
1458 search_bar.update(cx, |search_bar, _| {
1459 assert_eq!(search_bar.active_match_index, Some(2));
1460 });
1461
1462 // Park the cursor after the last match and ensure that going to the next match selects the
1463 // first match.
1464 editor.update(cx, |editor, cx| {
1465 editor.change_selections(None, cx, |s| {
1466 s.select_display_ranges([
1467 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1468 ])
1469 });
1470 });
1471 search_bar.update(cx, |search_bar, cx| {
1472 assert_eq!(search_bar.active_match_index, Some(2));
1473 search_bar.select_next_match(&SelectNextMatch, cx);
1474 assert_eq!(
1475 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1476 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1477 );
1478 });
1479 search_bar.update(cx, |search_bar, _| {
1480 assert_eq!(search_bar.active_match_index, Some(0));
1481 });
1482
1483 // Park the cursor before the first match and ensure that going to the previous match
1484 // selects the last match.
1485 editor.update(cx, |editor, cx| {
1486 editor.change_selections(None, cx, |s| {
1487 s.select_display_ranges([
1488 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1489 ])
1490 });
1491 });
1492 search_bar.update(cx, |search_bar, cx| {
1493 assert_eq!(search_bar.active_match_index, Some(0));
1494 search_bar.select_prev_match(&SelectPrevMatch, cx);
1495 assert_eq!(
1496 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1497 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1498 );
1499 });
1500 search_bar.update(cx, |search_bar, _| {
1501 assert_eq!(search_bar.active_match_index, Some(2));
1502 });
1503 }
1504
1505 fn display_points_of(
1506 background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1507 ) -> Vec<Range<DisplayPoint>> {
1508 background_highlights
1509 .into_iter()
1510 .map(|(range, _)| range)
1511 .collect::<Vec<_>>()
1512 }
1513
1514 #[gpui::test]
1515 async fn test_search_option_handling(cx: &mut TestAppContext) {
1516 let (editor, search_bar, cx) = init_test(cx);
1517
1518 // show with options should make current search case sensitive
1519 search_bar
1520 .update(cx, |search_bar, cx| {
1521 search_bar.show(cx);
1522 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1523 })
1524 .await
1525 .unwrap();
1526 editor.update(cx, |editor, cx| {
1527 assert_eq!(
1528 display_points_of(editor.all_text_background_highlights(cx)),
1529 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1530 );
1531 });
1532
1533 // search_suggested should restore default options
1534 search_bar.update(cx, |search_bar, cx| {
1535 search_bar.search_suggested(cx);
1536 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1537 });
1538
1539 // toggling a search option should update the defaults
1540 search_bar
1541 .update(cx, |search_bar, cx| {
1542 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1543 })
1544 .await
1545 .unwrap();
1546 search_bar.update(cx, |search_bar, cx| {
1547 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1548 });
1549 let mut editor_notifications = cx.notifications(&editor);
1550 editor_notifications.next().await;
1551 editor.update(cx, |editor, cx| {
1552 assert_eq!(
1553 display_points_of(editor.all_text_background_highlights(cx)),
1554 &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1555 );
1556 });
1557
1558 // defaults should still include whole word
1559 search_bar.update(cx, |search_bar, cx| {
1560 search_bar.search_suggested(cx);
1561 assert_eq!(
1562 search_bar.search_options,
1563 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1564 )
1565 });
1566 }
1567
1568 #[gpui::test]
1569 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1570 init_globals(cx);
1571 let buffer_text = r#"
1572 A regular expression (shortened as regex or regexp;[1] also referred to as
1573 rational expression[2][3]) is a sequence of characters that specifies a search
1574 pattern in text. Usually such patterns are used by string-searching algorithms
1575 for "find" or "find and replace" operations on strings, or for input validation.
1576 "#
1577 .unindent();
1578 let expected_query_matches_count = buffer_text
1579 .chars()
1580 .filter(|c| c.to_ascii_lowercase() == 'a')
1581 .count();
1582 assert!(
1583 expected_query_matches_count > 1,
1584 "Should pick a query with multiple results"
1585 );
1586 let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1587 let window = cx.add_window(|_| gpui::Empty);
1588
1589 let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1590
1591 let search_bar = window.build_view(cx, |cx| {
1592 let mut search_bar = BufferSearchBar::new(cx);
1593 search_bar.set_active_pane_item(Some(&editor), cx);
1594 search_bar.show(cx);
1595 search_bar
1596 });
1597
1598 window
1599 .update(cx, |_, cx| {
1600 search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1601 })
1602 .unwrap()
1603 .await
1604 .unwrap();
1605 let initial_selections = window
1606 .update(cx, |_, cx| {
1607 search_bar.update(cx, |search_bar, cx| {
1608 let handle = search_bar.query_editor.focus_handle(cx);
1609 cx.focus(&handle);
1610 search_bar.activate_current_match(cx);
1611 });
1612 assert!(
1613 !editor.read(cx).is_focused(cx),
1614 "Initially, the editor should not be focused"
1615 );
1616 let initial_selections = editor.update(cx, |editor, cx| {
1617 let initial_selections = editor.selections.display_ranges(cx);
1618 assert_eq!(
1619 initial_selections.len(), 1,
1620 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1621 );
1622 initial_selections
1623 });
1624 search_bar.update(cx, |search_bar, cx| {
1625 assert_eq!(search_bar.active_match_index, Some(0));
1626 let handle = search_bar.query_editor.focus_handle(cx);
1627 cx.focus(&handle);
1628 search_bar.select_all_matches(&SelectAllMatches, cx);
1629 });
1630 assert!(
1631 editor.read(cx).is_focused(cx),
1632 "Should focus editor after successful SelectAllMatches"
1633 );
1634 search_bar.update(cx, |search_bar, cx| {
1635 let all_selections =
1636 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1637 assert_eq!(
1638 all_selections.len(),
1639 expected_query_matches_count,
1640 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1641 );
1642 assert_eq!(
1643 search_bar.active_match_index,
1644 Some(0),
1645 "Match index should not change after selecting all matches"
1646 );
1647 });
1648
1649 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
1650 initial_selections
1651 }).unwrap();
1652
1653 window
1654 .update(cx, |_, cx| {
1655 assert!(
1656 editor.read(cx).is_focused(cx),
1657 "Should still have editor focused after SelectNextMatch"
1658 );
1659 search_bar.update(cx, |search_bar, cx| {
1660 let all_selections =
1661 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1662 assert_eq!(
1663 all_selections.len(),
1664 1,
1665 "On next match, should deselect items and select the next match"
1666 );
1667 assert_ne!(
1668 all_selections, initial_selections,
1669 "Next match should be different from the first selection"
1670 );
1671 assert_eq!(
1672 search_bar.active_match_index,
1673 Some(1),
1674 "Match index should be updated to the next one"
1675 );
1676 let handle = search_bar.query_editor.focus_handle(cx);
1677 cx.focus(&handle);
1678 search_bar.select_all_matches(&SelectAllMatches, cx);
1679 });
1680 })
1681 .unwrap();
1682 window
1683 .update(cx, |_, cx| {
1684 assert!(
1685 editor.read(cx).is_focused(cx),
1686 "Should focus editor after successful SelectAllMatches"
1687 );
1688 search_bar.update(cx, |search_bar, cx| {
1689 let all_selections =
1690 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1691 assert_eq!(
1692 all_selections.len(),
1693 expected_query_matches_count,
1694 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1695 );
1696 assert_eq!(
1697 search_bar.active_match_index,
1698 Some(1),
1699 "Match index should not change after selecting all matches"
1700 );
1701 });
1702 search_bar.update(cx, |search_bar, cx| {
1703 search_bar.select_prev_match(&SelectPrevMatch, cx);
1704 });
1705 })
1706 .unwrap();
1707 let last_match_selections = window
1708 .update(cx, |_, cx| {
1709 assert!(
1710 editor.read(cx).is_focused(cx),
1711 "Should still have editor focused after SelectPrevMatch"
1712 );
1713
1714 search_bar.update(cx, |search_bar, cx| {
1715 let all_selections =
1716 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1717 assert_eq!(
1718 all_selections.len(),
1719 1,
1720 "On previous match, should deselect items and select the previous item"
1721 );
1722 assert_eq!(
1723 all_selections, initial_selections,
1724 "Previous match should be the same as the first selection"
1725 );
1726 assert_eq!(
1727 search_bar.active_match_index,
1728 Some(0),
1729 "Match index should be updated to the previous one"
1730 );
1731 all_selections
1732 })
1733 })
1734 .unwrap();
1735
1736 window
1737 .update(cx, |_, cx| {
1738 search_bar.update(cx, |search_bar, cx| {
1739 let handle = search_bar.query_editor.focus_handle(cx);
1740 cx.focus(&handle);
1741 search_bar.search("abas_nonexistent_match", None, cx)
1742 })
1743 })
1744 .unwrap()
1745 .await
1746 .unwrap();
1747 window
1748 .update(cx, |_, cx| {
1749 search_bar.update(cx, |search_bar, cx| {
1750 search_bar.select_all_matches(&SelectAllMatches, cx);
1751 });
1752 assert!(
1753 editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
1754 "Should not switch focus to editor if SelectAllMatches does not find any matches"
1755 );
1756 search_bar.update(cx, |search_bar, cx| {
1757 let all_selections =
1758 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1759 assert_eq!(
1760 all_selections, last_match_selections,
1761 "Should not select anything new if there are no matches"
1762 );
1763 assert!(
1764 search_bar.active_match_index.is_none(),
1765 "For no matches, there should be no active match index"
1766 );
1767 });
1768 })
1769 .unwrap();
1770 }
1771
1772 #[gpui::test]
1773 async fn test_search_query_history(cx: &mut TestAppContext) {
1774 init_globals(cx);
1775 let buffer_text = r#"
1776 A regular expression (shortened as regex or regexp;[1] also referred to as
1777 rational expression[2][3]) is a sequence of characters that specifies a search
1778 pattern in text. Usually such patterns are used by string-searching algorithms
1779 for "find" or "find and replace" operations on strings, or for input validation.
1780 "#
1781 .unindent();
1782 let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1783 let cx = cx.add_empty_window();
1784
1785 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1786
1787 let search_bar = cx.new_view(|cx| {
1788 let mut search_bar = BufferSearchBar::new(cx);
1789 search_bar.set_active_pane_item(Some(&editor), cx);
1790 search_bar.show(cx);
1791 search_bar
1792 });
1793
1794 // Add 3 search items into the history.
1795 search_bar
1796 .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1797 .await
1798 .unwrap();
1799 search_bar
1800 .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1801 .await
1802 .unwrap();
1803 search_bar
1804 .update(cx, |search_bar, cx| {
1805 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1806 })
1807 .await
1808 .unwrap();
1809 // Ensure that the latest search is active.
1810 search_bar.update(cx, |search_bar, cx| {
1811 assert_eq!(search_bar.query(cx), "c");
1812 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1813 });
1814
1815 // Next history query after the latest should set the query to the empty string.
1816 search_bar.update(cx, |search_bar, cx| {
1817 search_bar.next_history_query(&NextHistoryQuery, cx);
1818 });
1819 search_bar.update(cx, |search_bar, cx| {
1820 assert_eq!(search_bar.query(cx), "");
1821 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1822 });
1823 search_bar.update(cx, |search_bar, cx| {
1824 search_bar.next_history_query(&NextHistoryQuery, cx);
1825 });
1826 search_bar.update(cx, |search_bar, cx| {
1827 assert_eq!(search_bar.query(cx), "");
1828 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1829 });
1830
1831 // First previous query for empty current query should set the query to the latest.
1832 search_bar.update(cx, |search_bar, cx| {
1833 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1834 });
1835 search_bar.update(cx, |search_bar, cx| {
1836 assert_eq!(search_bar.query(cx), "c");
1837 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1838 });
1839
1840 // Further previous items should go over the history in reverse order.
1841 search_bar.update(cx, |search_bar, cx| {
1842 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1843 });
1844 search_bar.update(cx, |search_bar, cx| {
1845 assert_eq!(search_bar.query(cx), "b");
1846 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1847 });
1848
1849 // Previous items should never go behind the first history item.
1850 search_bar.update(cx, |search_bar, cx| {
1851 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1852 });
1853 search_bar.update(cx, |search_bar, cx| {
1854 assert_eq!(search_bar.query(cx), "a");
1855 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1856 });
1857 search_bar.update(cx, |search_bar, cx| {
1858 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1859 });
1860 search_bar.update(cx, |search_bar, cx| {
1861 assert_eq!(search_bar.query(cx), "a");
1862 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1863 });
1864
1865 // Next items should go over the history in the original order.
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), "b");
1871 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1872 });
1873
1874 search_bar
1875 .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1876 .await
1877 .unwrap();
1878 search_bar.update(cx, |search_bar, cx| {
1879 assert_eq!(search_bar.query(cx), "ba");
1880 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1881 });
1882
1883 // New search input should add another entry to history and move the selection to the end of the history.
1884 search_bar.update(cx, |search_bar, cx| {
1885 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1886 });
1887 search_bar.update(cx, |search_bar, cx| {
1888 assert_eq!(search_bar.query(cx), "c");
1889 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1890 });
1891 search_bar.update(cx, |search_bar, cx| {
1892 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1893 });
1894 search_bar.update(cx, |search_bar, cx| {
1895 assert_eq!(search_bar.query(cx), "b");
1896 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1897 });
1898 search_bar.update(cx, |search_bar, cx| {
1899 search_bar.next_history_query(&NextHistoryQuery, cx);
1900 });
1901 search_bar.update(cx, |search_bar, cx| {
1902 assert_eq!(search_bar.query(cx), "c");
1903 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1904 });
1905 search_bar.update(cx, |search_bar, cx| {
1906 search_bar.next_history_query(&NextHistoryQuery, cx);
1907 });
1908 search_bar.update(cx, |search_bar, cx| {
1909 assert_eq!(search_bar.query(cx), "ba");
1910 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1911 });
1912 search_bar.update(cx, |search_bar, cx| {
1913 search_bar.next_history_query(&NextHistoryQuery, cx);
1914 });
1915 search_bar.update(cx, |search_bar, cx| {
1916 assert_eq!(search_bar.query(cx), "");
1917 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1918 });
1919 }
1920
1921 #[gpui::test]
1922 async fn test_replace_simple(cx: &mut TestAppContext) {
1923 let (editor, search_bar, cx) = init_test(cx);
1924
1925 search_bar
1926 .update(cx, |search_bar, cx| {
1927 search_bar.search("expression", None, cx)
1928 })
1929 .await
1930 .unwrap();
1931
1932 search_bar.update(cx, |search_bar, cx| {
1933 search_bar.replacement_editor.update(cx, |editor, cx| {
1934 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
1935 editor.set_text("expr$1", cx);
1936 });
1937 search_bar.replace_all(&ReplaceAll, cx)
1938 });
1939 assert_eq!(
1940 editor.update(cx, |this, cx| { this.text(cx) }),
1941 r#"
1942 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
1943 rational expr$1[2][3]) is a sequence of characters that specifies a search
1944 pattern in text. Usually such patterns are used by string-searching algorithms
1945 for "find" or "find and replace" operations on strings, or for input validation.
1946 "#
1947 .unindent()
1948 );
1949
1950 // Search for word boundaries and replace just a single one.
1951 search_bar
1952 .update(cx, |search_bar, cx| {
1953 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
1954 })
1955 .await
1956 .unwrap();
1957
1958 search_bar.update(cx, |search_bar, cx| {
1959 search_bar.replacement_editor.update(cx, |editor, cx| {
1960 editor.set_text("banana", cx);
1961 });
1962 search_bar.replace_next(&ReplaceNext, cx)
1963 });
1964 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
1965 assert_eq!(
1966 editor.update(cx, |this, cx| { this.text(cx) }),
1967 r#"
1968 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
1969 rational expr$1[2][3]) is a sequence of characters that specifies a search
1970 pattern in text. Usually such patterns are used by string-searching algorithms
1971 for "find" or "find and replace" operations on strings, or for input validation.
1972 "#
1973 .unindent()
1974 );
1975 // Let's turn on regex mode.
1976 search_bar
1977 .update(cx, |search_bar, cx| {
1978 search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), cx)
1979 })
1980 .await
1981 .unwrap();
1982 search_bar.update(cx, |search_bar, cx| {
1983 search_bar.replacement_editor.update(cx, |editor, cx| {
1984 editor.set_text("${1}number", cx);
1985 });
1986 search_bar.replace_all(&ReplaceAll, cx)
1987 });
1988 assert_eq!(
1989 editor.update(cx, |this, cx| { this.text(cx) }),
1990 r#"
1991 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1992 rational expr$12number3number) is a sequence of characters that specifies a search
1993 pattern in text. Usually such patterns are used by string-searching algorithms
1994 for "find" or "find and replace" operations on strings, or for input validation.
1995 "#
1996 .unindent()
1997 );
1998 // Now with a whole-word twist.
1999 search_bar
2000 .update(cx, |search_bar, cx| {
2001 search_bar.search(
2002 "a\\w+s",
2003 Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2004 cx,
2005 )
2006 })
2007 .await
2008 .unwrap();
2009 search_bar.update(cx, |search_bar, cx| {
2010 search_bar.replacement_editor.update(cx, |editor, cx| {
2011 editor.set_text("things", cx);
2012 });
2013 search_bar.replace_all(&ReplaceAll, cx)
2014 });
2015 // The only word affected by this edit should be `algorithms`, even though there's a bunch
2016 // of words in this text that would match this regex if not for WHOLE_WORD.
2017 assert_eq!(
2018 editor.update(cx, |this, cx| { this.text(cx) }),
2019 r#"
2020 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2021 rational expr$12number3number) is a sequence of characters that specifies a search
2022 pattern in text. Usually such patterns are used by string-searching things
2023 for "find" or "find and replace" operations on strings, or for input validation.
2024 "#
2025 .unindent()
2026 );
2027 }
2028
2029 struct ReplacementTestParams<'a> {
2030 editor: &'a View<Editor>,
2031 search_bar: &'a View<BufferSearchBar>,
2032 cx: &'a mut VisualTestContext,
2033 search_text: &'static str,
2034 search_options: Option<SearchOptions>,
2035 replacement_text: &'static str,
2036 replace_all: bool,
2037 expected_text: String,
2038 }
2039
2040 async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2041 options
2042 .search_bar
2043 .update(options.cx, |search_bar, cx| {
2044 if let Some(options) = options.search_options {
2045 search_bar.set_search_options(options, cx);
2046 }
2047 search_bar.search(options.search_text, options.search_options, cx)
2048 })
2049 .await
2050 .unwrap();
2051
2052 options.search_bar.update(options.cx, |search_bar, cx| {
2053 search_bar.replacement_editor.update(cx, |editor, cx| {
2054 editor.set_text(options.replacement_text, cx);
2055 });
2056
2057 if options.replace_all {
2058 search_bar.replace_all(&ReplaceAll, cx)
2059 } else {
2060 search_bar.replace_next(&ReplaceNext, cx)
2061 }
2062 });
2063
2064 assert_eq!(
2065 options
2066 .editor
2067 .update(options.cx, |this, cx| { this.text(cx) }),
2068 options.expected_text
2069 );
2070 }
2071
2072 #[gpui::test]
2073 async fn test_replace_special_characters(cx: &mut TestAppContext) {
2074 let (editor, search_bar, cx) = init_test(cx);
2075
2076 run_replacement_test(ReplacementTestParams {
2077 editor: &editor,
2078 search_bar: &search_bar,
2079 cx,
2080 search_text: "expression",
2081 search_options: None,
2082 replacement_text: r"\n",
2083 replace_all: true,
2084 expected_text: r#"
2085 A regular \n (shortened as regex or regexp;[1] also referred to as
2086 rational \n[2][3]) is a sequence of characters that specifies a search
2087 pattern in text. Usually such patterns are used by string-searching algorithms
2088 for "find" or "find and replace" operations on strings, or for input validation.
2089 "#
2090 .unindent(),
2091 })
2092 .await;
2093
2094 run_replacement_test(ReplacementTestParams {
2095 editor: &editor,
2096 search_bar: &search_bar,
2097 cx,
2098 search_text: "or",
2099 search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2100 replacement_text: r"\\\n\\\\",
2101 replace_all: false,
2102 expected_text: r#"
2103 A regular \n (shortened as regex \
2104 \\ regexp;[1] also referred to as
2105 rational \n[2][3]) is a sequence of characters that specifies a search
2106 pattern in text. Usually such patterns are used by string-searching algorithms
2107 for "find" or "find and replace" operations on strings, or for input validation.
2108 "#
2109 .unindent(),
2110 })
2111 .await;
2112
2113 run_replacement_test(ReplacementTestParams {
2114 editor: &editor,
2115 search_bar: &search_bar,
2116 cx,
2117 search_text: r"(that|used) ",
2118 search_options: Some(SearchOptions::REGEX),
2119 replacement_text: r"$1\n",
2120 replace_all: true,
2121 expected_text: r#"
2122 A regular \n (shortened as regex \
2123 \\ regexp;[1] also referred to as
2124 rational \n[2][3]) is a sequence of characters that
2125 specifies a search
2126 pattern in text. Usually such patterns are used
2127 by string-searching algorithms
2128 for "find" or "find and replace" operations on strings, or for input validation.
2129 "#
2130 .unindent(),
2131 })
2132 .await;
2133 }
2134
2135 #[gpui::test]
2136 async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2137 cx: &mut TestAppContext,
2138 ) {
2139 init_globals(cx);
2140 let buffer = cx.new_model(|cx| {
2141 Buffer::local(
2142 r#"
2143 aaa bbb aaa ccc
2144 aaa bbb aaa ccc
2145 aaa bbb aaa ccc
2146 aaa bbb aaa ccc
2147 aaa bbb aaa ccc
2148 aaa bbb aaa ccc
2149 "#
2150 .unindent(),
2151 cx,
2152 )
2153 });
2154 let cx = cx.add_empty_window();
2155 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
2156
2157 let search_bar = cx.new_view(|cx| {
2158 let mut search_bar = BufferSearchBar::new(cx);
2159 search_bar.set_active_pane_item(Some(&editor), cx);
2160 search_bar.show(cx);
2161 search_bar
2162 });
2163
2164 editor.update(cx, |editor, cx| {
2165 editor.change_selections(None, cx, |s| {
2166 s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2167 })
2168 });
2169
2170 search_bar.update(cx, |search_bar, cx| {
2171 let deploy = Deploy {
2172 focus: true,
2173 replace_enabled: false,
2174 selection_search_enabled: true,
2175 };
2176 search_bar.deploy(&deploy, cx);
2177 });
2178
2179 cx.run_until_parked();
2180
2181 search_bar
2182 .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2183 .await
2184 .unwrap();
2185
2186 editor.update(cx, |editor, cx| {
2187 assert_eq!(
2188 editor.search_background_highlights(cx),
2189 &[
2190 Point::new(1, 0)..Point::new(1, 3),
2191 Point::new(1, 8)..Point::new(1, 11),
2192 Point::new(2, 0)..Point::new(2, 3),
2193 ]
2194 );
2195 });
2196 }
2197
2198 #[gpui::test]
2199 async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2200 cx: &mut TestAppContext,
2201 ) {
2202 init_globals(cx);
2203 let text = r#"
2204 aaa bbb aaa ccc
2205 aaa bbb aaa ccc
2206 aaa bbb aaa ccc
2207 aaa bbb aaa ccc
2208 aaa bbb aaa ccc
2209 aaa bbb aaa ccc
2210
2211 aaa bbb aaa ccc
2212 aaa bbb aaa ccc
2213 aaa bbb aaa ccc
2214 aaa bbb aaa ccc
2215 aaa bbb aaa ccc
2216 aaa bbb aaa ccc
2217 "#
2218 .unindent();
2219
2220 let cx = cx.add_empty_window();
2221 let editor = cx.new_view(|cx| {
2222 let multibuffer = MultiBuffer::build_multi(
2223 [
2224 (
2225 &text,
2226 vec![
2227 Point::new(0, 0)..Point::new(2, 0),
2228 Point::new(4, 0)..Point::new(5, 0),
2229 ],
2230 ),
2231 (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2232 ],
2233 cx,
2234 );
2235 Editor::for_multibuffer(multibuffer, None, false, cx)
2236 });
2237
2238 let search_bar = cx.new_view(|cx| {
2239 let mut search_bar = BufferSearchBar::new(cx);
2240 search_bar.set_active_pane_item(Some(&editor), cx);
2241 search_bar.show(cx);
2242 search_bar
2243 });
2244
2245 editor.update(cx, |editor, cx| {
2246 editor.change_selections(None, cx, |s| {
2247 s.select_ranges(vec![
2248 Point::new(1, 0)..Point::new(1, 4),
2249 Point::new(5, 3)..Point::new(6, 4),
2250 ])
2251 })
2252 });
2253
2254 search_bar.update(cx, |search_bar, cx| {
2255 let deploy = Deploy {
2256 focus: true,
2257 replace_enabled: false,
2258 selection_search_enabled: true,
2259 };
2260 search_bar.deploy(&deploy, cx);
2261 });
2262
2263 cx.run_until_parked();
2264
2265 search_bar
2266 .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2267 .await
2268 .unwrap();
2269
2270 editor.update(cx, |editor, cx| {
2271 assert_eq!(
2272 editor.search_background_highlights(cx),
2273 &[
2274 Point::new(1, 0)..Point::new(1, 3),
2275 Point::new(5, 8)..Point::new(5, 11),
2276 Point::new(6, 0)..Point::new(6, 3),
2277 ]
2278 );
2279 });
2280 }
2281
2282 #[gpui::test]
2283 async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2284 let (editor, search_bar, cx) = init_test(cx);
2285 // Search using valid regexp
2286 search_bar
2287 .update(cx, |search_bar, cx| {
2288 search_bar.enable_search_option(SearchOptions::REGEX, cx);
2289 search_bar.search("expression", None, cx)
2290 })
2291 .await
2292 .unwrap();
2293 editor.update(cx, |editor, cx| {
2294 assert_eq!(
2295 display_points_of(editor.all_text_background_highlights(cx)),
2296 &[
2297 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2298 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2299 ],
2300 );
2301 });
2302
2303 // Now, the expression is invalid
2304 search_bar
2305 .update(cx, |search_bar, cx| {
2306 search_bar.search("expression (", None, cx)
2307 })
2308 .await
2309 .unwrap_err();
2310 editor.update(cx, |editor, cx| {
2311 assert!(display_points_of(editor.all_text_background_highlights(cx)).is_empty(),);
2312 });
2313 }
2314}