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::EditorEvent,
633 cx: &mut ViewContext<Self>,
634 ) {
635 if let editor::EditorEvent::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 false,
698 Vec::new(),
699 Vec::new(),
700 ) {
701 Ok(query) => query.with_replacement(self.replacement(cx)),
702 Err(_) => {
703 self.query_contains_error = true;
704 cx.notify();
705 return done_rx;
706 }
707 }
708 } else {
709 match SearchQuery::text(
710 query,
711 self.search_options.contains(SearchOptions::WHOLE_WORD),
712 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
713 false,
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 replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
813 let mut should_propagate = true;
814 if !self.dismissed && self.active_search.is_some() {
815 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
816 if let Some(query) = self.active_search.as_ref() {
817 if let Some(matches) = self
818 .searchable_items_with_matches
819 .get(&searchable_item.downgrade())
820 {
821 if let Some(active_index) = self.active_match_index {
822 let query = query
823 .as_ref()
824 .clone()
825 .with_replacement(self.replacement(cx));
826 searchable_item.replace(&matches[active_index], &query, cx);
827 self.select_next_match(&SelectNextMatch, cx);
828 }
829 should_propagate = false;
830 self.focus_editor(&FocusEditor, cx);
831 }
832 }
833 }
834 }
835 if !should_propagate {
836 cx.stop_propagation();
837 }
838 }
839 pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
840 if !self.dismissed && self.active_search.is_some() {
841 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
842 if let Some(query) = self.active_search.as_ref() {
843 if let Some(matches) = self
844 .searchable_items_with_matches
845 .get(&searchable_item.downgrade())
846 {
847 let query = query
848 .as_ref()
849 .clone()
850 .with_replacement(self.replacement(cx));
851 for m in matches {
852 searchable_item.replace(m, &query, cx);
853 }
854 }
855 }
856 }
857 }
858 }
859}
860
861#[cfg(test)]
862mod tests {
863 use std::ops::Range;
864
865 use super::*;
866 use editor::{DisplayPoint, Editor};
867 use gpui::{Context, EmptyView, Hsla, TestAppContext, VisualTestContext};
868 use language::Buffer;
869 use smol::stream::StreamExt as _;
870 use unindent::Unindent as _;
871
872 fn init_globals(cx: &mut TestAppContext) {
873 cx.update(|cx| {
874 let store = settings::SettingsStore::test(cx);
875 cx.set_global(store);
876 editor::init(cx);
877 ui::init(cx);
878 language::init(cx);
879 theme::init(theme::LoadThemes::JustBase, cx);
880 });
881 }
882 fn init_test(
883 cx: &mut TestAppContext,
884 ) -> (
885 View<Editor>,
886 View<BufferSearchBar>,
887 &mut VisualTestContext<'_>,
888 ) {
889 init_globals(cx);
890 let buffer = cx.build_model(|cx| {
891 Buffer::new(
892 0,
893 cx.entity_id().as_u64(),
894 r#"
895 A regular expression (shortened as regex or regexp;[1] also referred to as
896 rational expression[2][3]) is a sequence of characters that specifies a search
897 pattern in text. Usually such patterns are used by string-searching algorithms
898 for "find" or "find and replace" operations on strings, or for input validation.
899 "#
900 .unindent(),
901 )
902 });
903 let (window, cx) = cx.add_window_view(|_| EmptyView {});
904 let editor = cx.build_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
905
906 let search_bar = cx.build_view(|cx| {
907 let mut search_bar = BufferSearchBar::new(cx);
908 search_bar.set_active_pane_item(Some(&editor), cx);
909 search_bar.show(cx);
910 search_bar
911 });
912
913 (editor, search_bar, cx)
914 }
915
916 #[gpui::test]
917 async fn test_search_simple(cx: &mut TestAppContext) {
918 let (editor, search_bar, cx) = init_test(cx);
919 // todo! osiewicz: these tests asserted on background color as well, that should be brought back.
920 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
921 background_highlights
922 .into_iter()
923 .map(|(range, _)| range)
924 .collect::<Vec<_>>()
925 };
926 // Search for a string that appears with different casing.
927 // By default, search is case-insensitive.
928 search_bar
929 .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
930 .await
931 .unwrap();
932 editor.update(cx, |editor, cx| {
933 assert_eq!(
934 display_points_of(editor.all_text_background_highlights(cx)),
935 &[
936 DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
937 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
938 ]
939 );
940 });
941
942 // Switch to a case sensitive search.
943 search_bar.update(cx, |search_bar, cx| {
944 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
945 });
946 let mut editor_notifications = cx.notifications(&editor);
947 editor_notifications.next().await;
948 editor.update(cx, |editor, cx| {
949 assert_eq!(
950 display_points_of(editor.all_text_background_highlights(cx)),
951 &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),]
952 );
953 });
954
955 // Search for a string that appears both as a whole word and
956 // within other words. By default, all results are found.
957 search_bar
958 .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
959 .await
960 .unwrap();
961 editor.update(cx, |editor, cx| {
962 assert_eq!(
963 display_points_of(editor.all_text_background_highlights(cx)),
964 &[
965 DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
966 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
967 DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
968 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
969 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
970 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
971 DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
972 ]
973 );
974 });
975
976 // Switch to a whole word search.
977 search_bar.update(cx, |search_bar, cx| {
978 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
979 });
980 let mut editor_notifications = cx.notifications(&editor);
981 editor_notifications.next().await;
982 editor.update(cx, |editor, cx| {
983 assert_eq!(
984 display_points_of(editor.all_text_background_highlights(cx)),
985 &[
986 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
987 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
988 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
989 ]
990 );
991 });
992
993 editor.update(cx, |editor, cx| {
994 editor.change_selections(None, cx, |s| {
995 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
996 });
997 });
998 search_bar.update(cx, |search_bar, cx| {
999 assert_eq!(search_bar.active_match_index, Some(0));
1000 search_bar.select_next_match(&SelectNextMatch, cx);
1001 assert_eq!(
1002 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1003 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1004 );
1005 });
1006 search_bar.update(cx, |search_bar, _| {
1007 assert_eq!(search_bar.active_match_index, Some(0));
1008 });
1009
1010 search_bar.update(cx, |search_bar, cx| {
1011 search_bar.select_next_match(&SelectNextMatch, cx);
1012 assert_eq!(
1013 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1014 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1015 );
1016 });
1017 search_bar.update(cx, |search_bar, _| {
1018 assert_eq!(search_bar.active_match_index, Some(1));
1019 });
1020
1021 search_bar.update(cx, |search_bar, cx| {
1022 search_bar.select_next_match(&SelectNextMatch, cx);
1023 assert_eq!(
1024 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1025 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1026 );
1027 });
1028 search_bar.update(cx, |search_bar, _| {
1029 assert_eq!(search_bar.active_match_index, Some(2));
1030 });
1031
1032 search_bar.update(cx, |search_bar, cx| {
1033 search_bar.select_next_match(&SelectNextMatch, cx);
1034 assert_eq!(
1035 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1036 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1037 );
1038 });
1039 search_bar.update(cx, |search_bar, _| {
1040 assert_eq!(search_bar.active_match_index, Some(0));
1041 });
1042
1043 search_bar.update(cx, |search_bar, cx| {
1044 search_bar.select_prev_match(&SelectPrevMatch, cx);
1045 assert_eq!(
1046 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1047 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1048 );
1049 });
1050 search_bar.update(cx, |search_bar, _| {
1051 assert_eq!(search_bar.active_match_index, Some(2));
1052 });
1053
1054 search_bar.update(cx, |search_bar, cx| {
1055 search_bar.select_prev_match(&SelectPrevMatch, cx);
1056 assert_eq!(
1057 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1058 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1059 );
1060 });
1061 search_bar.update(cx, |search_bar, _| {
1062 assert_eq!(search_bar.active_match_index, Some(1));
1063 });
1064
1065 search_bar.update(cx, |search_bar, cx| {
1066 search_bar.select_prev_match(&SelectPrevMatch, cx);
1067 assert_eq!(
1068 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1069 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1070 );
1071 });
1072 search_bar.update(cx, |search_bar, _| {
1073 assert_eq!(search_bar.active_match_index, Some(0));
1074 });
1075
1076 // Park the cursor in between matches and ensure that going to the previous match selects
1077 // the closest match to the left.
1078 editor.update(cx, |editor, cx| {
1079 editor.change_selections(None, cx, |s| {
1080 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1081 });
1082 });
1083 search_bar.update(cx, |search_bar, cx| {
1084 assert_eq!(search_bar.active_match_index, Some(1));
1085 search_bar.select_prev_match(&SelectPrevMatch, cx);
1086 assert_eq!(
1087 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1088 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1089 );
1090 });
1091 search_bar.update(cx, |search_bar, _| {
1092 assert_eq!(search_bar.active_match_index, Some(0));
1093 });
1094
1095 // Park the cursor in between matches and ensure that going to the next match selects the
1096 // closest match to the right.
1097 editor.update(cx, |editor, cx| {
1098 editor.change_selections(None, cx, |s| {
1099 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1100 });
1101 });
1102 search_bar.update(cx, |search_bar, cx| {
1103 assert_eq!(search_bar.active_match_index, Some(1));
1104 search_bar.select_next_match(&SelectNextMatch, cx);
1105 assert_eq!(
1106 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1107 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1108 );
1109 });
1110 search_bar.update(cx, |search_bar, _| {
1111 assert_eq!(search_bar.active_match_index, Some(1));
1112 });
1113
1114 // Park the cursor after the last match and ensure that going to the previous match selects
1115 // the last match.
1116 editor.update(cx, |editor, cx| {
1117 editor.change_selections(None, cx, |s| {
1118 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1119 });
1120 });
1121 search_bar.update(cx, |search_bar, cx| {
1122 assert_eq!(search_bar.active_match_index, Some(2));
1123 search_bar.select_prev_match(&SelectPrevMatch, cx);
1124 assert_eq!(
1125 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1126 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1127 );
1128 });
1129 search_bar.update(cx, |search_bar, _| {
1130 assert_eq!(search_bar.active_match_index, Some(2));
1131 });
1132
1133 // Park the cursor after the last match and ensure that going to the next match selects the
1134 // first match.
1135 editor.update(cx, |editor, cx| {
1136 editor.change_selections(None, cx, |s| {
1137 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1138 });
1139 });
1140 search_bar.update(cx, |search_bar, cx| {
1141 assert_eq!(search_bar.active_match_index, Some(2));
1142 search_bar.select_next_match(&SelectNextMatch, cx);
1143 assert_eq!(
1144 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1145 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1146 );
1147 });
1148 search_bar.update(cx, |search_bar, _| {
1149 assert_eq!(search_bar.active_match_index, Some(0));
1150 });
1151
1152 // Park the cursor before the first match and ensure that going to the previous match
1153 // selects the last match.
1154 editor.update(cx, |editor, cx| {
1155 editor.change_selections(None, cx, |s| {
1156 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1157 });
1158 });
1159 search_bar.update(cx, |search_bar, cx| {
1160 assert_eq!(search_bar.active_match_index, Some(0));
1161 search_bar.select_prev_match(&SelectPrevMatch, cx);
1162 assert_eq!(
1163 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1164 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1165 );
1166 });
1167 search_bar.update(cx, |search_bar, _| {
1168 assert_eq!(search_bar.active_match_index, Some(2));
1169 });
1170 }
1171
1172 #[gpui::test]
1173 async fn test_search_option_handling(cx: &mut TestAppContext) {
1174 let (editor, search_bar, cx) = init_test(cx);
1175
1176 // show with options should make current search case sensitive
1177 search_bar
1178 .update(cx, |search_bar, cx| {
1179 search_bar.show(cx);
1180 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1181 })
1182 .await
1183 .unwrap();
1184 // todo! osiewicz: these tests previously asserted on background color highlights; that should be introduced back.
1185 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1186 background_highlights
1187 .into_iter()
1188 .map(|(range, _)| range)
1189 .collect::<Vec<_>>()
1190 };
1191 editor.update(cx, |editor, cx| {
1192 assert_eq!(
1193 display_points_of(editor.all_text_background_highlights(cx)),
1194 &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),]
1195 );
1196 });
1197
1198 // search_suggested should restore default options
1199 search_bar.update(cx, |search_bar, cx| {
1200 search_bar.search_suggested(cx);
1201 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1202 });
1203
1204 // toggling a search option should update the defaults
1205 search_bar
1206 .update(cx, |search_bar, cx| {
1207 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1208 })
1209 .await
1210 .unwrap();
1211 search_bar.update(cx, |search_bar, cx| {
1212 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1213 });
1214 let mut editor_notifications = cx.notifications(&editor);
1215 editor_notifications.next().await;
1216 editor.update(cx, |editor, cx| {
1217 assert_eq!(
1218 display_points_of(editor.all_text_background_highlights(cx)),
1219 &[DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),]
1220 );
1221 });
1222
1223 // defaults should still include whole word
1224 search_bar.update(cx, |search_bar, cx| {
1225 search_bar.search_suggested(cx);
1226 assert_eq!(
1227 search_bar.search_options,
1228 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1229 )
1230 });
1231 }
1232
1233 #[gpui::test]
1234 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1235 init_globals(cx);
1236 let buffer_text = r#"
1237 A regular expression (shortened as regex or regexp;[1] also referred to as
1238 rational expression[2][3]) is a sequence of characters that specifies a search
1239 pattern in text. Usually such patterns are used by string-searching algorithms
1240 for "find" or "find and replace" operations on strings, or for input validation.
1241 "#
1242 .unindent();
1243 let expected_query_matches_count = buffer_text
1244 .chars()
1245 .filter(|c| c.to_ascii_lowercase() == 'a')
1246 .count();
1247 assert!(
1248 expected_query_matches_count > 1,
1249 "Should pick a query with multiple results"
1250 );
1251 let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
1252 let window = cx.add_window(|_| EmptyView {});
1253
1254 let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1255
1256 let search_bar = window.build_view(cx, |cx| {
1257 let mut search_bar = BufferSearchBar::new(cx);
1258 search_bar.set_active_pane_item(Some(&editor), cx);
1259 search_bar.show(cx);
1260 search_bar
1261 });
1262
1263 window
1264 .update(cx, |_, cx| {
1265 search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1266 })
1267 .unwrap()
1268 .await
1269 .unwrap();
1270 let initial_selections = window
1271 .update(cx, |_, cx| {
1272 search_bar.update(cx, |search_bar, cx| {
1273 let handle = search_bar.query_editor.focus_handle(cx);
1274 cx.focus(&handle);
1275 search_bar.activate_current_match(cx);
1276 });
1277 assert!(
1278 !editor.read(cx).is_focused(cx),
1279 "Initially, the editor should not be focused"
1280 );
1281 let initial_selections = editor.update(cx, |editor, cx| {
1282 let initial_selections = editor.selections.display_ranges(cx);
1283 assert_eq!(
1284 initial_selections.len(), 1,
1285 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1286 );
1287 initial_selections
1288 });
1289 search_bar.update(cx, |search_bar, cx| {
1290 assert_eq!(search_bar.active_match_index, Some(0));
1291 let handle = search_bar.query_editor.focus_handle(cx);
1292 cx.focus(&handle);
1293 search_bar.select_all_matches(&SelectAllMatches, cx);
1294 });
1295 assert!(
1296 editor.read(cx).is_focused(cx),
1297 "Should focus editor after successful SelectAllMatches"
1298 );
1299 search_bar.update(cx, |search_bar, cx| {
1300 let all_selections =
1301 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1302 assert_eq!(
1303 all_selections.len(),
1304 expected_query_matches_count,
1305 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1306 );
1307 assert_eq!(
1308 search_bar.active_match_index,
1309 Some(0),
1310 "Match index should not change after selecting all matches"
1311 );
1312 });
1313
1314 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
1315 initial_selections
1316 }).unwrap();
1317
1318 window.update(cx, |_, cx| {
1319 assert!(
1320 editor.read(cx).is_focused(cx),
1321 "Should still have editor focused after SelectNextMatch"
1322 );
1323 search_bar.update(cx, |search_bar, cx| {
1324 let all_selections =
1325 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1326 assert_eq!(
1327 all_selections.len(),
1328 1,
1329 "On next match, should deselect items and select the next match"
1330 );
1331 assert_ne!(
1332 all_selections, initial_selections,
1333 "Next match should be different from the first selection"
1334 );
1335 assert_eq!(
1336 search_bar.active_match_index,
1337 Some(1),
1338 "Match index should be updated to the next one"
1339 );
1340 let handle = search_bar.query_editor.focus_handle(cx);
1341 cx.focus(&handle);
1342 search_bar.select_all_matches(&SelectAllMatches, cx);
1343 });
1344 });
1345 window.update(cx, |_, cx| {
1346 assert!(
1347 editor.read(cx).is_focused(cx),
1348 "Should focus editor after successful SelectAllMatches"
1349 );
1350 search_bar.update(cx, |search_bar, cx| {
1351 let all_selections =
1352 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1353 assert_eq!(
1354 all_selections.len(),
1355 expected_query_matches_count,
1356 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1357 );
1358 assert_eq!(
1359 search_bar.active_match_index,
1360 Some(1),
1361 "Match index should not change after selecting all matches"
1362 );
1363 });
1364 search_bar.update(cx, |search_bar, cx| {
1365 search_bar.select_prev_match(&SelectPrevMatch, cx);
1366 });
1367 });
1368 let last_match_selections = window
1369 .update(cx, |_, cx| {
1370 assert!(
1371 editor.read(cx).is_focused(&cx),
1372 "Should still have editor focused after SelectPrevMatch"
1373 );
1374
1375 search_bar.update(cx, |search_bar, cx| {
1376 let all_selections =
1377 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1378 assert_eq!(
1379 all_selections.len(),
1380 1,
1381 "On previous match, should deselect items and select the previous item"
1382 );
1383 assert_eq!(
1384 all_selections, initial_selections,
1385 "Previous match should be the same as the first selection"
1386 );
1387 assert_eq!(
1388 search_bar.active_match_index,
1389 Some(0),
1390 "Match index should be updated to the previous one"
1391 );
1392 all_selections
1393 })
1394 })
1395 .unwrap();
1396
1397 window
1398 .update(cx, |_, cx| {
1399 search_bar.update(cx, |search_bar, cx| {
1400 let handle = search_bar.query_editor.focus_handle(cx);
1401 cx.focus(&handle);
1402 search_bar.search("abas_nonexistent_match", None, cx)
1403 })
1404 })
1405 .unwrap()
1406 .await
1407 .unwrap();
1408 window.update(cx, |_, cx| {
1409 search_bar.update(cx, |search_bar, cx| {
1410 search_bar.select_all_matches(&SelectAllMatches, cx);
1411 });
1412 assert!(
1413 editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
1414 "Should not switch focus to editor if SelectAllMatches does not find any matches"
1415 );
1416 search_bar.update(cx, |search_bar, cx| {
1417 let all_selections =
1418 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1419 assert_eq!(
1420 all_selections, last_match_selections,
1421 "Should not select anything new if there are no matches"
1422 );
1423 assert!(
1424 search_bar.active_match_index.is_none(),
1425 "For no matches, there should be no active match index"
1426 );
1427 });
1428 });
1429 }
1430
1431 #[gpui::test]
1432 async fn test_search_query_history(cx: &mut TestAppContext) {
1433 //crate::project_search::tests::init_test(cx);
1434 init_globals(cx);
1435 let buffer_text = r#"
1436 A regular expression (shortened as regex or regexp;[1] also referred to as
1437 rational expression[2][3]) is a sequence of characters that specifies a search
1438 pattern in text. Usually such patterns are used by string-searching algorithms
1439 for "find" or "find and replace" operations on strings, or for input validation.
1440 "#
1441 .unindent();
1442 let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
1443 let (window, cx) = cx.add_window_view(|_| EmptyView {});
1444
1445 let editor = cx.build_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1446
1447 let search_bar = cx.build_view(|cx| {
1448 let mut search_bar = BufferSearchBar::new(cx);
1449 search_bar.set_active_pane_item(Some(&editor), cx);
1450 search_bar.show(cx);
1451 search_bar
1452 });
1453
1454 // Add 3 search items into the history.
1455 search_bar
1456 .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1457 .await
1458 .unwrap();
1459 search_bar
1460 .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1461 .await
1462 .unwrap();
1463 search_bar
1464 .update(cx, |search_bar, cx| {
1465 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1466 })
1467 .await
1468 .unwrap();
1469 // Ensure that the latest search is active.
1470 search_bar.update(cx, |search_bar, cx| {
1471 assert_eq!(search_bar.query(cx), "c");
1472 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1473 });
1474
1475 // Next history query after the latest should set the query to the empty string.
1476 search_bar.update(cx, |search_bar, cx| {
1477 search_bar.next_history_query(&NextHistoryQuery, cx);
1478 });
1479 search_bar.update(cx, |search_bar, cx| {
1480 assert_eq!(search_bar.query(cx), "");
1481 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1482 });
1483 search_bar.update(cx, |search_bar, cx| {
1484 search_bar.next_history_query(&NextHistoryQuery, cx);
1485 });
1486 search_bar.update(cx, |search_bar, cx| {
1487 assert_eq!(search_bar.query(cx), "");
1488 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1489 });
1490
1491 // First previous query for empty current query should set the query to the latest.
1492 search_bar.update(cx, |search_bar, cx| {
1493 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1494 });
1495 search_bar.update(cx, |search_bar, cx| {
1496 assert_eq!(search_bar.query(cx), "c");
1497 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1498 });
1499
1500 // Further previous items should go over the history in reverse order.
1501 search_bar.update(cx, |search_bar, cx| {
1502 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1503 });
1504 search_bar.update(cx, |search_bar, cx| {
1505 assert_eq!(search_bar.query(cx), "b");
1506 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1507 });
1508
1509 // Previous items should never go behind the first history item.
1510 search_bar.update(cx, |search_bar, cx| {
1511 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1512 });
1513 search_bar.update(cx, |search_bar, cx| {
1514 assert_eq!(search_bar.query(cx), "a");
1515 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1516 });
1517 search_bar.update(cx, |search_bar, cx| {
1518 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1519 });
1520 search_bar.update(cx, |search_bar, cx| {
1521 assert_eq!(search_bar.query(cx), "a");
1522 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1523 });
1524
1525 // Next items should go over the history in the original order.
1526 search_bar.update(cx, |search_bar, cx| {
1527 search_bar.next_history_query(&NextHistoryQuery, cx);
1528 });
1529 search_bar.update(cx, |search_bar, cx| {
1530 assert_eq!(search_bar.query(cx), "b");
1531 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1532 });
1533
1534 search_bar
1535 .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1536 .await
1537 .unwrap();
1538 search_bar.update(cx, |search_bar, cx| {
1539 assert_eq!(search_bar.query(cx), "ba");
1540 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1541 });
1542
1543 // New search input should add another entry to history and move the selection to the end of the history.
1544 search_bar.update(cx, |search_bar, cx| {
1545 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1546 });
1547 search_bar.update(cx, |search_bar, cx| {
1548 assert_eq!(search_bar.query(cx), "c");
1549 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1550 });
1551 search_bar.update(cx, |search_bar, cx| {
1552 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1553 });
1554 search_bar.update(cx, |search_bar, cx| {
1555 assert_eq!(search_bar.query(cx), "b");
1556 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1557 });
1558 search_bar.update(cx, |search_bar, cx| {
1559 search_bar.next_history_query(&NextHistoryQuery, cx);
1560 });
1561 search_bar.update(cx, |search_bar, cx| {
1562 assert_eq!(search_bar.query(cx), "c");
1563 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1564 });
1565 search_bar.update(cx, |search_bar, cx| {
1566 search_bar.next_history_query(&NextHistoryQuery, cx);
1567 });
1568 search_bar.update(cx, |search_bar, cx| {
1569 assert_eq!(search_bar.query(cx), "ba");
1570 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1571 });
1572 search_bar.update(cx, |search_bar, cx| {
1573 search_bar.next_history_query(&NextHistoryQuery, cx);
1574 });
1575 search_bar.update(cx, |search_bar, cx| {
1576 assert_eq!(search_bar.query(cx), "");
1577 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1578 });
1579 }
1580 #[gpui::test]
1581 async fn test_replace_simple(cx: &mut TestAppContext) {
1582 let (editor, search_bar, cx) = init_test(cx);
1583
1584 search_bar
1585 .update(cx, |search_bar, cx| {
1586 search_bar.search("expression", None, cx)
1587 })
1588 .await
1589 .unwrap();
1590
1591 search_bar.update(cx, |search_bar, cx| {
1592 search_bar.replacement_editor.update(cx, |editor, cx| {
1593 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
1594 editor.set_text("expr$1", cx);
1595 });
1596 search_bar.replace_all(&ReplaceAll, cx)
1597 });
1598 assert_eq!(
1599 editor.update(cx, |this, cx| { this.text(cx) }),
1600 r#"
1601 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
1602 rational expr$1[2][3]) is a sequence of characters that specifies a search
1603 pattern in text. Usually such patterns are used by string-searching algorithms
1604 for "find" or "find and replace" operations on strings, or for input validation.
1605 "#
1606 .unindent()
1607 );
1608
1609 // Search for word boundaries and replace just a single one.
1610 search_bar
1611 .update(cx, |search_bar, cx| {
1612 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
1613 })
1614 .await
1615 .unwrap();
1616
1617 search_bar.update(cx, |search_bar, cx| {
1618 search_bar.replacement_editor.update(cx, |editor, cx| {
1619 editor.set_text("banana", cx);
1620 });
1621 search_bar.replace_next(&ReplaceNext, cx)
1622 });
1623 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
1624 assert_eq!(
1625 editor.update(cx, |this, cx| { this.text(cx) }),
1626 r#"
1627 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
1628 rational expr$1[2][3]) is a sequence of characters that specifies a search
1629 pattern in text. Usually such patterns are used by string-searching algorithms
1630 for "find" or "find and replace" operations on strings, or for input validation.
1631 "#
1632 .unindent()
1633 );
1634 // Let's turn on regex mode.
1635 search_bar
1636 .update(cx, |search_bar, cx| {
1637 search_bar.activate_search_mode(SearchMode::Regex, cx);
1638 search_bar.search("\\[([^\\]]+)\\]", None, cx)
1639 })
1640 .await
1641 .unwrap();
1642 search_bar.update(cx, |search_bar, cx| {
1643 search_bar.replacement_editor.update(cx, |editor, cx| {
1644 editor.set_text("${1}number", cx);
1645 });
1646 search_bar.replace_all(&ReplaceAll, cx)
1647 });
1648 assert_eq!(
1649 editor.update(cx, |this, cx| { this.text(cx) }),
1650 r#"
1651 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1652 rational expr$12number3number) is a sequence of characters that specifies a search
1653 pattern in text. Usually such patterns are used by string-searching algorithms
1654 for "find" or "find and replace" operations on strings, or for input validation.
1655 "#
1656 .unindent()
1657 );
1658 // Now with a whole-word twist.
1659 search_bar
1660 .update(cx, |search_bar, cx| {
1661 search_bar.activate_search_mode(SearchMode::Regex, cx);
1662 search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
1663 })
1664 .await
1665 .unwrap();
1666 search_bar.update(cx, |search_bar, cx| {
1667 search_bar.replacement_editor.update(cx, |editor, cx| {
1668 editor.set_text("things", cx);
1669 });
1670 search_bar.replace_all(&ReplaceAll, cx)
1671 });
1672 // The only word affected by this edit should be `algorithms`, even though there's a bunch
1673 // of words in this text that would match this regex if not for WHOLE_WORD.
1674 assert_eq!(
1675 editor.update(cx, |this, cx| { this.text(cx) }),
1676 r#"
1677 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1678 rational expr$12number3number) is a sequence of characters that specifies a search
1679 pattern in text. Usually such patterns are used by string-searching things
1680 for "find" or "find and replace" operations on strings, or for input validation.
1681 "#
1682 .unindent()
1683 );
1684 }
1685}