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