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