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, Div, EventEmitter, InteractiveElement as _,
14 ParentElement as _, Render, RenderOnce, 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<Self> 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 RenderOnce<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 (_, 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
1328 .update(cx, |_, cx| {
1329 assert!(
1330 editor.read(cx).is_focused(cx),
1331 "Should still have editor focused after SelectNextMatch"
1332 );
1333 search_bar.update(cx, |search_bar, cx| {
1334 let all_selections =
1335 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1336 assert_eq!(
1337 all_selections.len(),
1338 1,
1339 "On next match, should deselect items and select the next match"
1340 );
1341 assert_ne!(
1342 all_selections, initial_selections,
1343 "Next match should be different from the first selection"
1344 );
1345 assert_eq!(
1346 search_bar.active_match_index,
1347 Some(1),
1348 "Match index should be updated to the next one"
1349 );
1350 let handle = search_bar.query_editor.focus_handle(cx);
1351 cx.focus(&handle);
1352 search_bar.select_all_matches(&SelectAllMatches, cx);
1353 });
1354 })
1355 .unwrap();
1356 window
1357 .update(cx, |_, cx| {
1358 assert!(
1359 editor.read(cx).is_focused(cx),
1360 "Should focus editor after successful SelectAllMatches"
1361 );
1362 search_bar.update(cx, |search_bar, cx| {
1363 let all_selections =
1364 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1365 assert_eq!(
1366 all_selections.len(),
1367 expected_query_matches_count,
1368 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1369 );
1370 assert_eq!(
1371 search_bar.active_match_index,
1372 Some(1),
1373 "Match index should not change after selecting all matches"
1374 );
1375 });
1376 search_bar.update(cx, |search_bar, cx| {
1377 search_bar.select_prev_match(&SelectPrevMatch, cx);
1378 });
1379 })
1380 .unwrap();
1381 let last_match_selections = window
1382 .update(cx, |_, cx| {
1383 assert!(
1384 editor.read(cx).is_focused(&cx),
1385 "Should still have editor focused after SelectPrevMatch"
1386 );
1387
1388 search_bar.update(cx, |search_bar, cx| {
1389 let all_selections =
1390 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1391 assert_eq!(
1392 all_selections.len(),
1393 1,
1394 "On previous match, should deselect items and select the previous item"
1395 );
1396 assert_eq!(
1397 all_selections, initial_selections,
1398 "Previous match should be the same as the first selection"
1399 );
1400 assert_eq!(
1401 search_bar.active_match_index,
1402 Some(0),
1403 "Match index should be updated to the previous one"
1404 );
1405 all_selections
1406 })
1407 })
1408 .unwrap();
1409
1410 window
1411 .update(cx, |_, cx| {
1412 search_bar.update(cx, |search_bar, cx| {
1413 let handle = search_bar.query_editor.focus_handle(cx);
1414 cx.focus(&handle);
1415 search_bar.search("abas_nonexistent_match", None, cx)
1416 })
1417 })
1418 .unwrap()
1419 .await
1420 .unwrap();
1421 window
1422 .update(cx, |_, cx| {
1423 search_bar.update(cx, |search_bar, cx| {
1424 search_bar.select_all_matches(&SelectAllMatches, cx);
1425 });
1426 assert!(
1427 editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
1428 "Should not switch focus to editor if SelectAllMatches does not find any matches"
1429 );
1430 search_bar.update(cx, |search_bar, cx| {
1431 let all_selections =
1432 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1433 assert_eq!(
1434 all_selections, last_match_selections,
1435 "Should not select anything new if there are no matches"
1436 );
1437 assert!(
1438 search_bar.active_match_index.is_none(),
1439 "For no matches, there should be no active match index"
1440 );
1441 });
1442 })
1443 .unwrap();
1444 }
1445
1446 #[gpui::test]
1447 async fn test_search_query_history(cx: &mut TestAppContext) {
1448 //crate::project_search::tests::init_test(cx);
1449 init_globals(cx);
1450 let buffer_text = r#"
1451 A regular expression (shortened as regex or regexp;[1] also referred to as
1452 rational expression[2][3]) is a sequence of characters that specifies a search
1453 pattern in text. Usually such patterns are used by string-searching algorithms
1454 for "find" or "find and replace" operations on strings, or for input validation.
1455 "#
1456 .unindent();
1457 let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
1458 let (_, cx) = cx.add_window_view(|_| EmptyView {});
1459
1460 let editor = cx.build_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1461
1462 let search_bar = cx.build_view(|cx| {
1463 let mut search_bar = BufferSearchBar::new(cx);
1464 search_bar.set_active_pane_item(Some(&editor), cx);
1465 search_bar.show(cx);
1466 search_bar
1467 });
1468
1469 // Add 3 search items into the history.
1470 search_bar
1471 .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1472 .await
1473 .unwrap();
1474 search_bar
1475 .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1476 .await
1477 .unwrap();
1478 search_bar
1479 .update(cx, |search_bar, cx| {
1480 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1481 })
1482 .await
1483 .unwrap();
1484 // Ensure that the latest search is active.
1485 search_bar.update(cx, |search_bar, cx| {
1486 assert_eq!(search_bar.query(cx), "c");
1487 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1488 });
1489
1490 // Next history query after the latest should set the query to the empty string.
1491 search_bar.update(cx, |search_bar, cx| {
1492 search_bar.next_history_query(&NextHistoryQuery, cx);
1493 });
1494 search_bar.update(cx, |search_bar, cx| {
1495 assert_eq!(search_bar.query(cx), "");
1496 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1497 });
1498 search_bar.update(cx, |search_bar, cx| {
1499 search_bar.next_history_query(&NextHistoryQuery, cx);
1500 });
1501 search_bar.update(cx, |search_bar, cx| {
1502 assert_eq!(search_bar.query(cx), "");
1503 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1504 });
1505
1506 // First previous query for empty current query should set the query to the latest.
1507 search_bar.update(cx, |search_bar, cx| {
1508 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1509 });
1510 search_bar.update(cx, |search_bar, cx| {
1511 assert_eq!(search_bar.query(cx), "c");
1512 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1513 });
1514
1515 // Further previous items should go over the history in reverse order.
1516 search_bar.update(cx, |search_bar, cx| {
1517 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1518 });
1519 search_bar.update(cx, |search_bar, cx| {
1520 assert_eq!(search_bar.query(cx), "b");
1521 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1522 });
1523
1524 // Previous items should never go behind the first history item.
1525 search_bar.update(cx, |search_bar, cx| {
1526 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1527 });
1528 search_bar.update(cx, |search_bar, cx| {
1529 assert_eq!(search_bar.query(cx), "a");
1530 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1531 });
1532 search_bar.update(cx, |search_bar, cx| {
1533 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1534 });
1535 search_bar.update(cx, |search_bar, cx| {
1536 assert_eq!(search_bar.query(cx), "a");
1537 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1538 });
1539
1540 // Next items should go over the history in the original order.
1541 search_bar.update(cx, |search_bar, cx| {
1542 search_bar.next_history_query(&NextHistoryQuery, cx);
1543 });
1544 search_bar.update(cx, |search_bar, cx| {
1545 assert_eq!(search_bar.query(cx), "b");
1546 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1547 });
1548
1549 search_bar
1550 .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1551 .await
1552 .unwrap();
1553 search_bar.update(cx, |search_bar, cx| {
1554 assert_eq!(search_bar.query(cx), "ba");
1555 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1556 });
1557
1558 // New search input should add another entry to history and move the selection to the end of the history.
1559 search_bar.update(cx, |search_bar, cx| {
1560 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1561 });
1562 search_bar.update(cx, |search_bar, cx| {
1563 assert_eq!(search_bar.query(cx), "c");
1564 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1565 });
1566 search_bar.update(cx, |search_bar, cx| {
1567 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1568 });
1569 search_bar.update(cx, |search_bar, cx| {
1570 assert_eq!(search_bar.query(cx), "b");
1571 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1572 });
1573 search_bar.update(cx, |search_bar, cx| {
1574 search_bar.next_history_query(&NextHistoryQuery, cx);
1575 });
1576 search_bar.update(cx, |search_bar, cx| {
1577 assert_eq!(search_bar.query(cx), "c");
1578 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1579 });
1580 search_bar.update(cx, |search_bar, cx| {
1581 search_bar.next_history_query(&NextHistoryQuery, cx);
1582 });
1583 search_bar.update(cx, |search_bar, cx| {
1584 assert_eq!(search_bar.query(cx), "ba");
1585 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1586 });
1587 search_bar.update(cx, |search_bar, cx| {
1588 search_bar.next_history_query(&NextHistoryQuery, cx);
1589 });
1590 search_bar.update(cx, |search_bar, cx| {
1591 assert_eq!(search_bar.query(cx), "");
1592 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1593 });
1594 }
1595 #[gpui::test]
1596 async fn test_replace_simple(cx: &mut TestAppContext) {
1597 let (editor, search_bar, cx) = init_test(cx);
1598
1599 search_bar
1600 .update(cx, |search_bar, cx| {
1601 search_bar.search("expression", None, cx)
1602 })
1603 .await
1604 .unwrap();
1605
1606 search_bar.update(cx, |search_bar, cx| {
1607 search_bar.replacement_editor.update(cx, |editor, cx| {
1608 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
1609 editor.set_text("expr$1", cx);
1610 });
1611 search_bar.replace_all(&ReplaceAll, cx)
1612 });
1613 assert_eq!(
1614 editor.update(cx, |this, cx| { this.text(cx) }),
1615 r#"
1616 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
1617 rational expr$1[2][3]) is a sequence of characters that specifies a search
1618 pattern in text. Usually such patterns are used by string-searching algorithms
1619 for "find" or "find and replace" operations on strings, or for input validation.
1620 "#
1621 .unindent()
1622 );
1623
1624 // Search for word boundaries and replace just a single one.
1625 search_bar
1626 .update(cx, |search_bar, cx| {
1627 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
1628 })
1629 .await
1630 .unwrap();
1631
1632 search_bar.update(cx, |search_bar, cx| {
1633 search_bar.replacement_editor.update(cx, |editor, cx| {
1634 editor.set_text("banana", cx);
1635 });
1636 search_bar.replace_next(&ReplaceNext, cx)
1637 });
1638 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
1639 assert_eq!(
1640 editor.update(cx, |this, cx| { this.text(cx) }),
1641 r#"
1642 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
1643 rational expr$1[2][3]) is a sequence of characters that specifies a search
1644 pattern in text. Usually such patterns are used by string-searching algorithms
1645 for "find" or "find and replace" operations on strings, or for input validation.
1646 "#
1647 .unindent()
1648 );
1649 // Let's turn on regex mode.
1650 search_bar
1651 .update(cx, |search_bar, cx| {
1652 search_bar.activate_search_mode(SearchMode::Regex, cx);
1653 search_bar.search("\\[([^\\]]+)\\]", None, cx)
1654 })
1655 .await
1656 .unwrap();
1657 search_bar.update(cx, |search_bar, cx| {
1658 search_bar.replacement_editor.update(cx, |editor, cx| {
1659 editor.set_text("${1}number", cx);
1660 });
1661 search_bar.replace_all(&ReplaceAll, cx)
1662 });
1663 assert_eq!(
1664 editor.update(cx, |this, cx| { this.text(cx) }),
1665 r#"
1666 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1667 rational expr$12number3number) is a sequence of characters that specifies a search
1668 pattern in text. Usually such patterns are used by string-searching algorithms
1669 for "find" or "find and replace" operations on strings, or for input validation.
1670 "#
1671 .unindent()
1672 );
1673 // Now with a whole-word twist.
1674 search_bar
1675 .update(cx, |search_bar, cx| {
1676 search_bar.activate_search_mode(SearchMode::Regex, cx);
1677 search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
1678 })
1679 .await
1680 .unwrap();
1681 search_bar.update(cx, |search_bar, cx| {
1682 search_bar.replacement_editor.update(cx, |editor, cx| {
1683 editor.set_text("things", cx);
1684 });
1685 search_bar.replace_all(&ReplaceAll, cx)
1686 });
1687 // The only word affected by this edit should be `algorithms`, even though there's a bunch
1688 // of words in this text that would match this regex if not for WHOLE_WORD.
1689 assert_eq!(
1690 editor.update(cx, |this, cx| { this.text(cx) }),
1691 r#"
1692 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1693 rational expr$12number3number) is a sequence of characters that specifies a search
1694 pattern in text. Usually such patterns are used by string-searching things
1695 for "find" or "find and replace" operations on strings, or for input validation.
1696 "#
1697 .unindent()
1698 );
1699 }
1700}