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