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