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