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