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