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