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