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