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