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