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