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