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