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