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