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