1use crate::{
2 history::SearchHistory, mode::SearchMode, ActivateRegexMode, ActivateTextMode, CycleMode,
3 NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
4 SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
5};
6use anyhow::{Context as _, Result};
7use collections::HashMap;
8use editor::{
9 items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, EditorEvent,
10 MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN,
11};
12use gpui::{
13 actions, div, white, AnyElement, AnyView, AppContext, Context as _, Div, Element, EntityId,
14 EventEmitter, FocusableView, InteractiveElement, IntoElement, KeyContext, Model, ModelContext,
15 ParentElement, PromptLevel, Render, SharedString, Styled, Subscription, Task, View,
16 ViewContext, VisualContext, WeakModel, WeakView, WindowContext,
17};
18use menu::Confirm;
19use project::{
20 search::{SearchInputs, SearchQuery},
21 Entry, Project,
22};
23use semantic_index::{SemanticIndex, SemanticIndexStatus};
24
25use smol::stream::StreamExt;
26use std::{
27 any::{Any, TypeId},
28 collections::HashSet,
29 mem,
30 ops::{Not, Range},
31 path::PathBuf,
32 time::Duration,
33};
34
35use ui::{
36 h_stack, v_stack, Button, ButtonCommon, Clickable, Disableable, Icon, IconButton, IconElement,
37 Label, LabelCommon, LabelSize, Selectable, Tooltip,
38};
39use util::{paths::PathMatcher, ResultExt as _};
40use workspace::{
41 item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
42 searchable::{Direction, SearchableItem, SearchableItemHandle},
43 ItemNavHistory, Pane, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
44 WorkspaceId,
45};
46
47actions!(
48 project_search,
49 [SearchInNew, ToggleFocus, NextField, ToggleFilters]
50);
51
52#[derive(Default)]
53struct ActiveSearches(HashMap<WeakModel<Project>, WeakView<ProjectSearchView>>);
54
55#[derive(Default)]
56struct ActiveSettings(HashMap<WeakModel<Project>, ProjectSearchSettings>);
57
58pub fn init(cx: &mut AppContext) {
59 // todo!() po
60 cx.set_global(ActiveSearches::default());
61 cx.set_global(ActiveSettings::default());
62 cx.observe_new_views(|workspace: &mut Workspace, _cx| {
63 workspace
64 .register_action(ProjectSearchView::deploy)
65 .register_action(ProjectSearchBar::search_in_new);
66 })
67 .detach();
68}
69
70struct ProjectSearch {
71 project: Model<Project>,
72 excerpts: Model<MultiBuffer>,
73 pending_search: Option<Task<Option<()>>>,
74 match_ranges: Vec<Range<Anchor>>,
75 active_query: Option<SearchQuery>,
76 search_id: usize,
77 search_history: SearchHistory,
78 no_results: Option<bool>,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82enum InputPanel {
83 Query,
84 Exclude,
85 Include,
86}
87
88pub struct ProjectSearchView {
89 model: Model<ProjectSearch>,
90 query_editor: View<Editor>,
91 replacement_editor: View<Editor>,
92 results_editor: View<Editor>,
93 semantic_state: Option<SemanticState>,
94 semantic_permissioned: Option<bool>,
95 search_options: SearchOptions,
96 panels_with_errors: HashSet<InputPanel>,
97 active_match_index: Option<usize>,
98 search_id: usize,
99 query_editor_was_focused: bool,
100 included_files_editor: View<Editor>,
101 excluded_files_editor: View<Editor>,
102 filters_enabled: bool,
103 replace_enabled: bool,
104 current_mode: SearchMode,
105}
106
107struct SemanticState {
108 index_status: SemanticIndexStatus,
109 maintain_rate_limit: Option<Task<()>>,
110 _subscription: Subscription,
111}
112
113#[derive(Debug, Clone)]
114struct ProjectSearchSettings {
115 search_options: SearchOptions,
116 filters_enabled: bool,
117 current_mode: SearchMode,
118}
119
120pub struct ProjectSearchBar {
121 active_project_search: Option<View<ProjectSearchView>>,
122 subscription: Option<Subscription>,
123}
124
125impl ProjectSearch {
126 fn new(project: Model<Project>, cx: &mut ModelContext<Self>) -> Self {
127 let replica_id = project.read(cx).replica_id();
128 Self {
129 project,
130 excerpts: cx.build_model(|_| MultiBuffer::new(replica_id)),
131 pending_search: Default::default(),
132 match_ranges: Default::default(),
133 active_query: None,
134 search_id: 0,
135 search_history: SearchHistory::default(),
136 no_results: None,
137 }
138 }
139
140 fn clone(&self, cx: &mut ModelContext<Self>) -> Model<Self> {
141 cx.build_model(|cx| Self {
142 project: self.project.clone(),
143 excerpts: self
144 .excerpts
145 .update(cx, |excerpts, cx| cx.build_model(|cx| excerpts.clone(cx))),
146 pending_search: Default::default(),
147 match_ranges: self.match_ranges.clone(),
148 active_query: self.active_query.clone(),
149 search_id: self.search_id,
150 search_history: self.search_history.clone(),
151 no_results: self.no_results.clone(),
152 })
153 }
154
155 fn search(&mut self, query: SearchQuery, cx: &mut ModelContext<Self>) {
156 let search = self
157 .project
158 .update(cx, |project, cx| project.search(query.clone(), cx));
159 self.search_id += 1;
160 self.search_history.add(query.as_str().to_string());
161 self.active_query = Some(query);
162 self.match_ranges.clear();
163 self.pending_search = Some(cx.spawn(|this, mut cx| async move {
164 let mut matches = search;
165 let this = this.upgrade()?;
166 this.update(&mut cx, |this, cx| {
167 this.match_ranges.clear();
168 this.excerpts.update(cx, |this, cx| this.clear(cx));
169 this.no_results = Some(true);
170 })
171 .ok()?;
172
173 while let Some((buffer, anchors)) = matches.next().await {
174 let mut ranges = this
175 .update(&mut cx, |this, cx| {
176 this.no_results = Some(false);
177 this.excerpts.update(cx, |excerpts, cx| {
178 excerpts.stream_excerpts_with_context_lines(buffer, anchors, 1, cx)
179 })
180 })
181 .ok()?;
182
183 while let Some(range) = ranges.next().await {
184 this.update(&mut cx, |this, _| this.match_ranges.push(range))
185 .ok()?;
186 }
187 this.update(&mut cx, |_, cx| cx.notify()).ok()?;
188 }
189
190 this.update(&mut cx, |this, cx| {
191 this.pending_search.take();
192 cx.notify();
193 })
194 .ok()?;
195
196 None
197 }));
198 cx.notify();
199 }
200
201 fn semantic_search(&mut self, inputs: &SearchInputs, cx: &mut ModelContext<Self>) {
202 let search = SemanticIndex::global(cx).map(|index| {
203 index.update(cx, |semantic_index, cx| {
204 semantic_index.search_project(
205 self.project.clone(),
206 inputs.as_str().to_owned(),
207 10,
208 inputs.files_to_include().to_vec(),
209 inputs.files_to_exclude().to_vec(),
210 cx,
211 )
212 })
213 });
214 self.search_id += 1;
215 self.match_ranges.clear();
216 self.search_history.add(inputs.as_str().to_string());
217 self.no_results = None;
218 self.pending_search = Some(cx.spawn(|this, mut cx| async move {
219 let results = search?.await.log_err()?;
220 let matches = results
221 .into_iter()
222 .map(|result| (result.buffer, vec![result.range.start..result.range.start]));
223
224 this.update(&mut cx, |this, cx| {
225 this.no_results = Some(true);
226 this.excerpts.update(cx, |excerpts, cx| {
227 excerpts.clear(cx);
228 });
229 })
230 .ok()?;
231 for (buffer, ranges) in matches {
232 let mut match_ranges = this
233 .update(&mut cx, |this, cx| {
234 this.no_results = Some(false);
235 this.excerpts.update(cx, |excerpts, cx| {
236 excerpts.stream_excerpts_with_context_lines(buffer, ranges, 3, cx)
237 })
238 })
239 .ok()?;
240 while let Some(match_range) = match_ranges.next().await {
241 this.update(&mut cx, |this, cx| {
242 this.match_ranges.push(match_range);
243 while let Ok(Some(match_range)) = match_ranges.try_next() {
244 this.match_ranges.push(match_range);
245 }
246 cx.notify();
247 })
248 .ok()?;
249 }
250 }
251
252 this.update(&mut cx, |this, cx| {
253 this.pending_search.take();
254 cx.notify();
255 })
256 .ok()?;
257
258 None
259 }));
260 cx.notify();
261 }
262}
263
264#[derive(Clone, Debug, PartialEq, Eq)]
265pub enum ViewEvent {
266 UpdateTab,
267 Activate,
268 EditorEvent(editor::EditorEvent),
269 Dismiss,
270}
271
272impl EventEmitter<ViewEvent> for ProjectSearchView {}
273
274impl Render for ProjectSearchView {
275 type Element = Div;
276 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
277 if self.has_matches() {
278 div()
279 .flex_1()
280 .size_full()
281 .child(self.results_editor.clone())
282 } else {
283 let model = self.model.read(cx);
284 let has_no_results = model.no_results.unwrap_or(false);
285 let is_search_underway = model.pending_search.is_some();
286 let major_text = if is_search_underway {
287 Label::new("Searching...")
288 } else if has_no_results {
289 Label::new("No results for a given query")
290 } else {
291 Label::new(format!("{} search all files", self.current_mode.label()))
292 };
293 let major_text = div().justify_center().max_w_96().child(major_text);
294 let middle_text = div()
295 .items_center()
296 .max_w_96()
297 .child(Label::new(self.landing_text_minor()).size(LabelSize::Small));
298 v_stack().flex_1().size_full().justify_center().child(
299 h_stack()
300 .size_full()
301 .justify_center()
302 .child(h_stack().flex_1())
303 .child(v_stack().child(major_text).child(middle_text))
304 .child(h_stack().flex_1()),
305 )
306 }
307 }
308}
309
310// impl Entity for ProjectSearchView {
311// type Event = ViewEvent;
312// }
313
314// impl View for ProjectSearchView {
315// fn ui_name() -> &'static str {
316// "ProjectSearchView"
317// }
318
319// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
320// let model = &self.model.read(cx);
321// if model.match_ranges.is_empty() {
322// enum Status {}
323
324// let theme = theme::current(cx).clone();
325
326// // If Search is Active -> Major: Searching..., Minor: None
327// // If Semantic -> Major: "Search using Natural Language", Minor: {Status}/n{ex...}/n{ex...}
328// // If Regex -> Major: "Search using Regex", Minor: {ex...}
329// // If Text -> Major: "Text search all files and folders", Minor: {...}
330
331// let current_mode = self.current_mode;
332// let mut major_text = if model.pending_search.is_some() {
333// Cow::Borrowed("Searching...")
334// } else if model.no_results.is_some_and(|v| v) {
335// Cow::Borrowed("No Results")
336// } else {
337// match current_mode {
338// SearchMode::Text => Cow::Borrowed("Text search all files and folders"),
339// SearchMode::Semantic => {
340// Cow::Borrowed("Search all code objects using Natural Language")
341// }
342// SearchMode::Regex => Cow::Borrowed("Regex search all files and folders"),
343// }
344// };
345
346// let mut show_minor_text = true;
347// let semantic_status = self.semantic_state.as_ref().and_then(|semantic| {
348// let status = semantic.index_status;
349// match status {
350// SemanticIndexStatus::NotAuthenticated => {
351// major_text = Cow::Borrowed("Not Authenticated");
352// show_minor_text = false;
353// Some(vec![
354// "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables."
355// .to_string(), "If you authenticated using the Assistant Panel, please restart Zed to Authenticate.".to_string()])
356// }
357// SemanticIndexStatus::Indexed => Some(vec!["Indexing complete".to_string()]),
358// SemanticIndexStatus::Indexing {
359// remaining_files,
360// rate_limit_expiry,
361// } => {
362// if remaining_files == 0 {
363// Some(vec![format!("Indexing...")])
364// } else {
365// if let Some(rate_limit_expiry) = rate_limit_expiry {
366// let remaining_seconds =
367// rate_limit_expiry.duration_since(Instant::now());
368// if remaining_seconds > Duration::from_secs(0) {
369// Some(vec![format!(
370// "Remaining files to index (rate limit resets in {}s): {}",
371// remaining_seconds.as_secs(),
372// remaining_files
373// )])
374// } else {
375// Some(vec![format!("Remaining files to index: {}", remaining_files)])
376// }
377// } else {
378// Some(vec![format!("Remaining files to index: {}", remaining_files)])
379// }
380// }
381// }
382// SemanticIndexStatus::NotIndexed => None,
383// }
384// });
385
386// let minor_text = if let Some(no_results) = model.no_results {
387// if model.pending_search.is_none() && no_results {
388// vec!["No results found in this project for the provided query".to_owned()]
389// } else {
390// vec![]
391// }
392// } else {
393// match current_mode {
394// SearchMode::Semantic => {
395// let mut minor_text: Vec<String> = Vec::new();
396// minor_text.push("".into());
397// if let Some(semantic_status) = semantic_status {
398// minor_text.extend(semantic_status);
399// }
400// if show_minor_text {
401// minor_text
402// .push("Simply explain the code you are looking to find.".into());
403// minor_text.push(
404// "ex. 'prompt user for permissions to index their project'".into(),
405// );
406// }
407// minor_text
408// }
409// _ => vec![
410// "".to_owned(),
411// "Include/exclude specific paths with the filter option.".to_owned(),
412// "Matching exact word and/or casing is available too.".to_owned(),
413// ],
414// }
415// };
416
417// MouseEventHandler::new::<Status, _>(0, cx, |_, _| {
418// Flex::column()
419// .with_child(Flex::column().contained().flex(1., true))
420// .with_child(
421// Flex::column()
422// .align_children_center()
423// .with_child(Label::new(
424// major_text,
425// theme.search.major_results_status.clone(),
426// ))
427// .with_children(
428// minor_text.into_iter().map(|x| {
429// Label::new(x, theme.search.minor_results_status.clone())
430// }),
431// )
432// .aligned()
433// .top()
434// .contained()
435// .flex(7., true),
436// )
437// .contained()
438// .with_background_color(theme.editor.background)
439// })
440// .on_down(MouseButton::Left, |_, _, cx| {
441// cx.focus_parent();
442// })
443// .into_any_named("project search view")
444// } else {
445// ChildView::new(&self.results_editor, cx)
446// .flex(1., true)
447// .into_any_named("project search view")
448// }
449// }
450
451// fn focus_in(&mut self, _: AnyView, cx: &mut ViewContext<Self>) {
452// let handle = cx.weak_handle();
453// cx.update_global(|state: &mut ActiveSearches, cx| {
454// state
455// .0
456// .insert(self.model.read(cx).project.downgrade(), handle)
457// });
458
459// cx.update_global(|state: &mut ActiveSettings, cx| {
460// state.0.insert(
461// self.model.read(cx).project.downgrade(),
462// self.current_settings(),
463// );
464// });
465
466// if cx.is_self_focused() {
467// if self.query_editor_was_focused {
468// cx.focus(&self.query_editor);
469// } else {
470// cx.focus(&self.results_editor);
471// }
472// }
473// }
474// }
475
476impl FocusableView for ProjectSearchView {
477 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
478 self.results_editor.focus_handle(cx)
479 }
480}
481
482impl Item for ProjectSearchView {
483 type Event = ViewEvent;
484 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
485 let query_text = self.query_editor.read(cx).text(cx);
486
487 query_text
488 .is_empty()
489 .not()
490 .then(|| query_text.into())
491 .or_else(|| Some("Project Search".into()))
492 }
493
494 fn act_as_type<'a>(
495 &'a self,
496 type_id: TypeId,
497 self_handle: &'a View<Self>,
498 _: &'a AppContext,
499 ) -> Option<AnyView> {
500 if type_id == TypeId::of::<Self>() {
501 Some(self_handle.clone().into())
502 } else if type_id == TypeId::of::<Editor>() {
503 Some(self.results_editor.clone().into())
504 } else {
505 None
506 }
507 }
508
509 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
510 self.results_editor
511 .update(cx, |editor, cx| editor.deactivated(cx));
512 }
513
514 fn tab_content(&self, _: Option<usize>, cx: &WindowContext<'_>) -> AnyElement {
515 let last_query: Option<SharedString> = self
516 .model
517 .read(cx)
518 .search_history
519 .current()
520 .as_ref()
521 .map(|query| {
522 let query_text = util::truncate_and_trailoff(query, MAX_TAB_TITLE_LEN);
523 query_text.into()
524 });
525 let tab_name = last_query
526 .filter(|query| !query.is_empty())
527 .unwrap_or_else(|| "Project search".into());
528 h_stack()
529 .child(IconElement::new(Icon::MagnifyingGlass))
530 .child(Label::new(tab_name))
531 .into_any()
532 }
533
534 fn for_each_project_item(
535 &self,
536 cx: &AppContext,
537 f: &mut dyn FnMut(EntityId, &dyn project::Item),
538 ) {
539 self.results_editor.for_each_project_item(cx, f)
540 }
541
542 fn is_singleton(&self, _: &AppContext) -> bool {
543 false
544 }
545
546 fn can_save(&self, _: &AppContext) -> bool {
547 true
548 }
549
550 fn is_dirty(&self, cx: &AppContext) -> bool {
551 self.results_editor.read(cx).is_dirty(cx)
552 }
553
554 fn has_conflict(&self, cx: &AppContext) -> bool {
555 self.results_editor.read(cx).has_conflict(cx)
556 }
557
558 fn save(
559 &mut self,
560 project: Model<Project>,
561 cx: &mut ViewContext<Self>,
562 ) -> Task<anyhow::Result<()>> {
563 self.results_editor
564 .update(cx, |editor, cx| editor.save(project, cx))
565 }
566
567 fn save_as(
568 &mut self,
569 _: Model<Project>,
570 _: PathBuf,
571 _: &mut ViewContext<Self>,
572 ) -> Task<anyhow::Result<()>> {
573 unreachable!("save_as should not have been called")
574 }
575
576 fn reload(
577 &mut self,
578 project: Model<Project>,
579 cx: &mut ViewContext<Self>,
580 ) -> Task<anyhow::Result<()>> {
581 self.results_editor
582 .update(cx, |editor, cx| editor.reload(project, cx))
583 }
584
585 fn clone_on_split(
586 &self,
587 _workspace_id: WorkspaceId,
588 cx: &mut ViewContext<Self>,
589 ) -> Option<View<Self>>
590 where
591 Self: Sized,
592 {
593 let model = self.model.update(cx, |model, cx| model.clone(cx));
594 Some(cx.build_view(|cx| Self::new(model, cx, None)))
595 }
596
597 fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
598 self.results_editor
599 .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
600 }
601
602 fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
603 self.results_editor.update(cx, |editor, _| {
604 editor.set_nav_history(Some(nav_history));
605 });
606 }
607
608 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
609 self.results_editor
610 .update(cx, |editor, cx| editor.navigate(data, cx))
611 }
612
613 fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
614 match event {
615 ViewEvent::UpdateTab => {
616 f(ItemEvent::UpdateBreadcrumbs);
617 f(ItemEvent::UpdateTab);
618 }
619 ViewEvent::EditorEvent(editor_event) => {
620 Editor::to_item_events(editor_event, f);
621 }
622 ViewEvent::Dismiss => f(ItemEvent::CloseItem),
623 _ => {}
624 }
625 }
626
627 fn breadcrumb_location(&self) -> ToolbarItemLocation {
628 if self.has_matches() {
629 ToolbarItemLocation::Secondary
630 } else {
631 ToolbarItemLocation::Hidden
632 }
633 }
634
635 fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
636 self.results_editor.breadcrumbs(theme, cx)
637 }
638
639 fn serialized_item_kind() -> Option<&'static str> {
640 None
641 }
642
643 fn deserialize(
644 _project: Model<Project>,
645 _workspace: WeakView<Workspace>,
646 _workspace_id: workspace::WorkspaceId,
647 _item_id: workspace::ItemId,
648 _cx: &mut ViewContext<Pane>,
649 ) -> Task<anyhow::Result<View<Self>>> {
650 unimplemented!()
651 }
652}
653
654impl ProjectSearchView {
655 fn toggle_filters(&mut self, cx: &mut ViewContext<Self>) {
656 self.filters_enabled = !self.filters_enabled;
657 cx.update_global(|state: &mut ActiveSettings, cx| {
658 state.0.insert(
659 self.model.read(cx).project.downgrade(),
660 self.current_settings(),
661 );
662 });
663 }
664
665 fn current_settings(&self) -> ProjectSearchSettings {
666 ProjectSearchSettings {
667 search_options: self.search_options,
668 filters_enabled: self.filters_enabled,
669 current_mode: self.current_mode,
670 }
671 }
672 fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) {
673 self.search_options.toggle(option);
674 cx.update_global(|state: &mut ActiveSettings, cx| {
675 state.0.insert(
676 self.model.read(cx).project.downgrade(),
677 self.current_settings(),
678 );
679 });
680 }
681
682 fn index_project(&mut self, cx: &mut ViewContext<Self>) {
683 if let Some(semantic_index) = SemanticIndex::global(cx) {
684 // Semantic search uses no options
685 self.search_options = SearchOptions::none();
686
687 let project = self.model.read(cx).project.clone();
688
689 semantic_index.update(cx, |semantic_index, cx| {
690 semantic_index
691 .index_project(project.clone(), cx)
692 .detach_and_log_err(cx);
693 });
694
695 self.semantic_state = Some(SemanticState {
696 index_status: semantic_index.read(cx).status(&project),
697 maintain_rate_limit: None,
698 _subscription: cx.observe(&semantic_index, Self::semantic_index_changed),
699 });
700 self.semantic_index_changed(semantic_index, cx);
701 }
702 }
703
704 fn semantic_index_changed(
705 &mut self,
706 semantic_index: Model<SemanticIndex>,
707 cx: &mut ViewContext<Self>,
708 ) {
709 let project = self.model.read(cx).project.clone();
710 if let Some(semantic_state) = self.semantic_state.as_mut() {
711 cx.notify();
712 semantic_state.index_status = semantic_index.read(cx).status(&project);
713 if let SemanticIndexStatus::Indexing {
714 rate_limit_expiry: Some(_),
715 ..
716 } = &semantic_state.index_status
717 {
718 if semantic_state.maintain_rate_limit.is_none() {
719 semantic_state.maintain_rate_limit =
720 Some(cx.spawn(|this, mut cx| async move {
721 loop {
722 cx.background_executor().timer(Duration::from_secs(1)).await;
723 this.update(&mut cx, |_, cx| cx.notify()).log_err();
724 }
725 }));
726 return;
727 }
728 } else {
729 semantic_state.maintain_rate_limit = None;
730 }
731 }
732 }
733
734 fn clear_search(&mut self, cx: &mut ViewContext<Self>) {
735 self.model.update(cx, |model, cx| {
736 model.pending_search = None;
737 model.no_results = None;
738 model.match_ranges.clear();
739
740 model.excerpts.update(cx, |excerpts, cx| {
741 excerpts.clear(cx);
742 });
743 });
744 }
745
746 fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
747 let previous_mode = self.current_mode;
748 if previous_mode == mode {
749 return;
750 }
751
752 self.clear_search(cx);
753 self.current_mode = mode;
754 self.active_match_index = None;
755
756 match mode {
757 SearchMode::Semantic => {
758 let has_permission = self.semantic_permissioned(cx);
759 self.active_match_index = None;
760 cx.spawn(|this, mut cx| async move {
761 let has_permission = has_permission.await?;
762
763 if !has_permission {
764 let answer = this.update(&mut cx, |this, cx| {
765 let project = this.model.read(cx).project.clone();
766 let project_name = project
767 .read(cx)
768 .worktree_root_names(cx)
769 .collect::<Vec<&str>>()
770 .join("/");
771 let is_plural =
772 project_name.chars().filter(|letter| *letter == '/').count() > 0;
773 let prompt_text = format!("Would you like to index the '{}' project{} for semantic search? This requires sending code to the OpenAI API", project_name,
774 if is_plural {
775 "s"
776 } else {""});
777 cx.prompt(
778 PromptLevel::Info,
779 prompt_text.as_str(),
780 &["Continue", "Cancel"],
781 )
782 })?;
783
784 if answer.await? == 0 {
785 this.update(&mut cx, |this, _| {
786 this.semantic_permissioned = Some(true);
787 })?;
788 } else {
789 this.update(&mut cx, |this, cx| {
790 this.semantic_permissioned = Some(false);
791 debug_assert_ne!(previous_mode, SearchMode::Semantic, "Tried to re-enable semantic search mode after user modal was rejected");
792 this.activate_search_mode(previous_mode, cx);
793 })?;
794 return anyhow::Ok(());
795 }
796 }
797
798 this.update(&mut cx, |this, cx| {
799 this.index_project(cx);
800 })?;
801
802 anyhow::Ok(())
803 }).detach_and_log_err(cx);
804 }
805 SearchMode::Regex | SearchMode::Text => {
806 self.semantic_state = None;
807 self.active_match_index = None;
808 self.search(cx);
809 }
810 }
811
812 cx.update_global(|state: &mut ActiveSettings, cx| {
813 state.0.insert(
814 self.model.read(cx).project.downgrade(),
815 self.current_settings(),
816 );
817 });
818
819 cx.notify();
820 }
821 fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
822 let model = self.model.read(cx);
823 if let Some(query) = model.active_query.as_ref() {
824 if model.match_ranges.is_empty() {
825 return;
826 }
827 if let Some(active_index) = self.active_match_index {
828 let query = query.clone().with_replacement(self.replacement(cx));
829 self.results_editor.replace(
830 &(Box::new(model.match_ranges[active_index].clone()) as _),
831 &query,
832 cx,
833 );
834 self.select_match(Direction::Next, cx)
835 }
836 }
837 }
838 pub fn replacement(&self, cx: &AppContext) -> String {
839 self.replacement_editor.read(cx).text(cx)
840 }
841 fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
842 let model = self.model.read(cx);
843 if let Some(query) = model.active_query.as_ref() {
844 if model.match_ranges.is_empty() {
845 return;
846 }
847 if self.active_match_index.is_some() {
848 let query = query.clone().with_replacement(self.replacement(cx));
849 let matches = model
850 .match_ranges
851 .iter()
852 .map(|item| Box::new(item.clone()) as _)
853 .collect::<Vec<_>>();
854 for item in matches {
855 self.results_editor.replace(&item, &query, cx);
856 }
857 }
858 }
859 }
860
861 fn new(
862 model: Model<ProjectSearch>,
863 cx: &mut ViewContext<Self>,
864 settings: Option<ProjectSearchSettings>,
865 ) -> Self {
866 let project;
867 let excerpts;
868 let mut replacement_text = None;
869 let mut query_text = String::new();
870
871 // Read in settings if available
872 let (mut options, current_mode, filters_enabled) = if let Some(settings) = settings {
873 (
874 settings.search_options,
875 settings.current_mode,
876 settings.filters_enabled,
877 )
878 } else {
879 (SearchOptions::NONE, Default::default(), false)
880 };
881
882 {
883 let model = model.read(cx);
884 project = model.project.clone();
885 excerpts = model.excerpts.clone();
886 if let Some(active_query) = model.active_query.as_ref() {
887 query_text = active_query.as_str().to_string();
888 replacement_text = active_query.replacement().map(ToOwned::to_owned);
889 options = SearchOptions::from_query(active_query);
890 }
891 }
892 cx.observe(&model, |this, _, cx| this.model_changed(cx))
893 .detach();
894
895 let query_editor = cx.build_view(|cx| {
896 let mut editor = Editor::single_line(cx);
897 editor.set_placeholder_text("Text search all files", cx);
898 editor.set_text(query_text, cx);
899 editor
900 });
901 // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes
902 cx.subscribe(&query_editor, |_, _, event: &EditorEvent, cx| {
903 cx.emit(ViewEvent::EditorEvent(event.clone()))
904 })
905 .detach();
906 let replacement_editor = cx.build_view(|cx| {
907 let mut editor = Editor::single_line(cx);
908 editor.set_placeholder_text("Replace in project..", cx);
909 if let Some(text) = replacement_text {
910 editor.set_text(text, cx);
911 }
912 editor
913 });
914 let results_editor = cx.build_view(|cx| {
915 let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), cx);
916 editor.set_searchable(false);
917 editor
918 });
919 cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
920 .detach();
921
922 cx.subscribe(&results_editor, |this, _, event: &EditorEvent, cx| {
923 if matches!(event, editor::EditorEvent::SelectionsChanged { .. }) {
924 this.update_match_index(cx);
925 }
926 // Reraise editor events for workspace item activation purposes
927 cx.emit(ViewEvent::EditorEvent(event.clone()));
928 })
929 .detach();
930
931 let included_files_editor = cx.build_view(|cx| {
932 let mut editor = Editor::single_line(cx);
933 editor.set_placeholder_text("Include: crates/**/*.toml", cx);
934
935 editor
936 });
937 // Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes
938 cx.subscribe(&included_files_editor, |_, _, event: &EditorEvent, cx| {
939 cx.emit(ViewEvent::EditorEvent(event.clone()))
940 })
941 .detach();
942
943 let excluded_files_editor = cx.build_view(|cx| {
944 let mut editor = Editor::single_line(cx);
945 editor.set_placeholder_text("Exclude: vendor/*, *.lock", cx);
946
947 editor
948 });
949 // Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes
950 cx.subscribe(&excluded_files_editor, |_, _, event: &EditorEvent, cx| {
951 cx.emit(ViewEvent::EditorEvent(event.clone()))
952 })
953 .detach();
954
955 // Check if Worktrees have all been previously indexed
956 let mut this = ProjectSearchView {
957 replacement_editor,
958 search_id: model.read(cx).search_id,
959 model,
960 query_editor,
961 results_editor,
962 semantic_state: None,
963 semantic_permissioned: None,
964 search_options: options,
965 panels_with_errors: HashSet::new(),
966 active_match_index: None,
967 query_editor_was_focused: false,
968 included_files_editor,
969 excluded_files_editor,
970 filters_enabled,
971 current_mode,
972 replace_enabled: false,
973 };
974 this.model_changed(cx);
975 this
976 }
977
978 fn semantic_permissioned(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
979 if let Some(value) = self.semantic_permissioned {
980 return Task::ready(Ok(value));
981 }
982
983 SemanticIndex::global(cx)
984 .map(|semantic| {
985 let project = self.model.read(cx).project.clone();
986 semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx))
987 })
988 .unwrap_or(Task::ready(Ok(false)))
989 }
990 pub fn new_search_in_directory(
991 workspace: &mut Workspace,
992 dir_entry: &Entry,
993 cx: &mut ViewContext<Workspace>,
994 ) {
995 if !dir_entry.is_dir() {
996 return;
997 }
998 let Some(filter_str) = dir_entry.path.to_str() else {
999 return;
1000 };
1001
1002 let model = cx.build_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
1003 let search = cx.build_view(|cx| ProjectSearchView::new(model, cx, None));
1004 workspace.add_item(Box::new(search.clone()), cx);
1005 search.update(cx, |search, cx| {
1006 search
1007 .included_files_editor
1008 .update(cx, |editor, cx| editor.set_text(filter_str, cx));
1009 search.filters_enabled = true;
1010 search.focus_query_editor(cx)
1011 });
1012 }
1013
1014 // Add another search tab to the workspace.
1015 fn deploy(
1016 workspace: &mut Workspace,
1017 _: &workspace::NewSearch,
1018 cx: &mut ViewContext<Workspace>,
1019 ) {
1020 // Clean up entries for dropped projects
1021 cx.update_global(|state: &mut ActiveSearches, _cx| {
1022 state.0.retain(|project, _| project.is_upgradable())
1023 });
1024
1025 let query = workspace.active_item(cx).and_then(|item| {
1026 let editor = item.act_as::<Editor>(cx)?;
1027 let query = editor.query_suggestion(cx);
1028 if query.is_empty() {
1029 None
1030 } else {
1031 Some(query)
1032 }
1033 });
1034
1035 let settings = cx
1036 .global::<ActiveSettings>()
1037 .0
1038 .get(&workspace.project().downgrade());
1039
1040 let settings = if let Some(settings) = settings {
1041 Some(settings.clone())
1042 } else {
1043 None
1044 };
1045
1046 let model = cx.build_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
1047 let search = cx.build_view(|cx| ProjectSearchView::new(model, cx, settings));
1048
1049 workspace.add_item(Box::new(search.clone()), cx);
1050
1051 search.update(cx, |search, cx| {
1052 if let Some(query) = query {
1053 search.set_query(&query, cx);
1054 }
1055 search.focus_query_editor(cx)
1056 });
1057 }
1058
1059 fn search(&mut self, cx: &mut ViewContext<Self>) {
1060 let mode = self.current_mode;
1061 match mode {
1062 SearchMode::Semantic => {
1063 if self.semantic_state.is_some() {
1064 if let Some(query) = self.build_search_query(cx) {
1065 self.model
1066 .update(cx, |model, cx| model.semantic_search(query.as_inner(), cx));
1067 }
1068 }
1069 }
1070
1071 _ => {
1072 if let Some(query) = self.build_search_query(cx) {
1073 self.model.update(cx, |model, cx| model.search(query, cx));
1074 }
1075 }
1076 }
1077 }
1078
1079 fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
1080 let text = self.query_editor.read(cx).text(cx);
1081 let included_files =
1082 match Self::parse_path_matches(&self.included_files_editor.read(cx).text(cx)) {
1083 Ok(included_files) => {
1084 self.panels_with_errors.remove(&InputPanel::Include);
1085 included_files
1086 }
1087 Err(_e) => {
1088 self.panels_with_errors.insert(InputPanel::Include);
1089 cx.notify();
1090 return None;
1091 }
1092 };
1093 let excluded_files =
1094 match Self::parse_path_matches(&self.excluded_files_editor.read(cx).text(cx)) {
1095 Ok(excluded_files) => {
1096 self.panels_with_errors.remove(&InputPanel::Exclude);
1097 excluded_files
1098 }
1099 Err(_e) => {
1100 self.panels_with_errors.insert(InputPanel::Exclude);
1101 cx.notify();
1102 return None;
1103 }
1104 };
1105 let current_mode = self.current_mode;
1106 match current_mode {
1107 SearchMode::Regex => {
1108 match SearchQuery::regex(
1109 text,
1110 self.search_options.contains(SearchOptions::WHOLE_WORD),
1111 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1112 self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
1113 included_files,
1114 excluded_files,
1115 ) {
1116 Ok(query) => {
1117 self.panels_with_errors.remove(&InputPanel::Query);
1118 Some(query)
1119 }
1120 Err(_e) => {
1121 self.panels_with_errors.insert(InputPanel::Query);
1122 cx.notify();
1123 None
1124 }
1125 }
1126 }
1127 _ => match SearchQuery::text(
1128 text,
1129 self.search_options.contains(SearchOptions::WHOLE_WORD),
1130 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1131 self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
1132 included_files,
1133 excluded_files,
1134 ) {
1135 Ok(query) => {
1136 self.panels_with_errors.remove(&InputPanel::Query);
1137 Some(query)
1138 }
1139 Err(_e) => {
1140 self.panels_with_errors.insert(InputPanel::Query);
1141 cx.notify();
1142 None
1143 }
1144 },
1145 }
1146 }
1147
1148 fn parse_path_matches(text: &str) -> anyhow::Result<Vec<PathMatcher>> {
1149 text.split(',')
1150 .map(str::trim)
1151 .filter(|maybe_glob_str| !maybe_glob_str.is_empty())
1152 .map(|maybe_glob_str| {
1153 PathMatcher::new(maybe_glob_str)
1154 .with_context(|| format!("parsing {maybe_glob_str} as path matcher"))
1155 })
1156 .collect()
1157 }
1158
1159 fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
1160 if let Some(index) = self.active_match_index {
1161 let match_ranges = self.model.read(cx).match_ranges.clone();
1162 let new_index = self.results_editor.update(cx, |editor, cx| {
1163 editor.match_index_for_direction(&match_ranges, index, direction, 1, cx)
1164 });
1165
1166 let range_to_select = match_ranges[new_index].clone();
1167 self.results_editor.update(cx, |editor, cx| {
1168 let range_to_select = editor.range_for_match(&range_to_select);
1169 editor.unfold_ranges([range_to_select.clone()], false, true, cx);
1170 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
1171 s.select_ranges([range_to_select])
1172 });
1173 });
1174 }
1175 }
1176
1177 fn focus_query_editor(&mut self, cx: &mut ViewContext<Self>) {
1178 self.query_editor.update(cx, |query_editor, cx| {
1179 query_editor.select_all(&SelectAll, cx);
1180 });
1181 self.query_editor_was_focused = true;
1182 let editor_handle = self.query_editor.focus_handle(cx);
1183 cx.focus(&editor_handle);
1184 }
1185
1186 fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
1187 self.query_editor
1188 .update(cx, |query_editor, cx| query_editor.set_text(query, cx));
1189 }
1190
1191 fn focus_results_editor(&mut self, cx: &mut ViewContext<Self>) {
1192 self.query_editor.update(cx, |query_editor, cx| {
1193 let cursor = query_editor.selections.newest_anchor().head();
1194 query_editor.change_selections(None, cx, |s| s.select_ranges([cursor.clone()..cursor]));
1195 });
1196 self.query_editor_was_focused = false;
1197 let results_handle = self.results_editor.focus_handle(cx);
1198 cx.focus(&results_handle);
1199 }
1200
1201 fn model_changed(&mut self, cx: &mut ViewContext<Self>) {
1202 let match_ranges = self.model.read(cx).match_ranges.clone();
1203 if match_ranges.is_empty() {
1204 self.active_match_index = None;
1205 } else {
1206 self.active_match_index = Some(0);
1207 self.update_match_index(cx);
1208 let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id);
1209 let is_new_search = self.search_id != prev_search_id;
1210 self.results_editor.update(cx, |editor, cx| {
1211 if is_new_search {
1212 let range_to_select = match_ranges
1213 .first()
1214 .clone()
1215 .map(|range| editor.range_for_match(range));
1216 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
1217 s.select_ranges(range_to_select)
1218 });
1219 }
1220 editor.highlight_background::<Self>(
1221 match_ranges,
1222 |theme| theme.search_match_background,
1223 cx,
1224 );
1225 });
1226 if is_new_search && self.query_editor.focus_handle(cx).is_focused(cx) {
1227 self.focus_results_editor(cx);
1228 }
1229 }
1230
1231 cx.emit(ViewEvent::UpdateTab);
1232 cx.notify();
1233 }
1234
1235 fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
1236 let results_editor = self.results_editor.read(cx);
1237 let new_index = active_match_index(
1238 &self.model.read(cx).match_ranges,
1239 &results_editor.selections.newest_anchor().head(),
1240 &results_editor.buffer().read(cx).snapshot(cx),
1241 );
1242 if self.active_match_index != new_index {
1243 self.active_match_index = new_index;
1244 cx.notify();
1245 }
1246 }
1247
1248 pub fn has_matches(&self) -> bool {
1249 self.active_match_index.is_some()
1250 }
1251
1252 fn landing_text_minor(&self) -> SharedString {
1253 match self.current_mode {
1254 SearchMode::Text | SearchMode::Regex => "Include/exclude specific paths with the filter option. Matching exact word and/or casing is available too.".into(),
1255 SearchMode::Semantic => ".Simply explain the code you are looking to find. ex. 'prompt user for permissions to index their project'".into()
1256 }
1257 }
1258}
1259
1260impl Default for ProjectSearchBar {
1261 fn default() -> Self {
1262 Self::new()
1263 }
1264}
1265
1266impl ProjectSearchBar {
1267 pub fn new() -> Self {
1268 Self {
1269 active_project_search: Default::default(),
1270 subscription: Default::default(),
1271 }
1272 }
1273 fn cycle_mode(&self, _: &CycleMode, cx: &mut ViewContext<Self>) {
1274 if let Some(view) = self.active_project_search.as_ref() {
1275 view.update(cx, |this, cx| {
1276 // todo: po: 2nd argument of `next_mode` should be `SemanticIndex::enabled(cx))`, but we need to flesh out port of semantic_index first.
1277 let new_mode = crate::mode::next_mode(&this.current_mode, false);
1278 this.activate_search_mode(new_mode, cx);
1279 let editor_handle = this.query_editor.focus_handle(cx);
1280 cx.focus(&editor_handle);
1281 });
1282 }
1283 }
1284 fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
1285 if let Some(search_view) = self.active_project_search.as_ref() {
1286 search_view.update(cx, |search_view, cx| {
1287 if !search_view
1288 .replacement_editor
1289 .focus_handle(cx)
1290 .is_focused(cx)
1291 {
1292 cx.stop_propagation();
1293 search_view.search(cx);
1294 }
1295 });
1296 }
1297 }
1298
1299 fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) {
1300 if let Some(search_view) = workspace
1301 .active_item(cx)
1302 .and_then(|item| item.downcast::<ProjectSearchView>())
1303 {
1304 let new_query = search_view.update(cx, |search_view, cx| {
1305 let new_query = search_view.build_search_query(cx);
1306 if new_query.is_some() {
1307 if let Some(old_query) = search_view.model.read(cx).active_query.clone() {
1308 search_view.query_editor.update(cx, |editor, cx| {
1309 editor.set_text(old_query.as_str(), cx);
1310 });
1311 search_view.search_options = SearchOptions::from_query(&old_query);
1312 }
1313 }
1314 new_query
1315 });
1316 if let Some(new_query) = new_query {
1317 let model = cx.build_model(|cx| {
1318 let mut model = ProjectSearch::new(workspace.project().clone(), cx);
1319 model.search(new_query, cx);
1320 model
1321 });
1322 workspace.add_item(
1323 Box::new(cx.build_view(|cx| ProjectSearchView::new(model, cx, None))),
1324 cx,
1325 );
1326 }
1327 }
1328 }
1329
1330 fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
1331 self.cycle_field(Direction::Next, cx);
1332 }
1333
1334 fn tab_previous(&mut self, _: &editor::TabPrev, cx: &mut ViewContext<Self>) {
1335 self.cycle_field(Direction::Prev, cx);
1336 }
1337
1338 fn cycle_field(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
1339 let active_project_search = match &self.active_project_search {
1340 Some(active_project_search) => active_project_search,
1341
1342 None => {
1343 return;
1344 }
1345 };
1346
1347 active_project_search.update(cx, |project_view, cx| {
1348 let mut views = vec![&project_view.query_editor];
1349 if project_view.filters_enabled {
1350 views.extend([
1351 &project_view.included_files_editor,
1352 &project_view.excluded_files_editor,
1353 ]);
1354 }
1355 if project_view.replace_enabled {
1356 views.push(&project_view.replacement_editor);
1357 }
1358 let current_index = match views
1359 .iter()
1360 .enumerate()
1361 .find(|(_, view)| view.focus_handle(cx).is_focused(cx))
1362 {
1363 Some((index, _)) => index,
1364
1365 None => {
1366 return;
1367 }
1368 };
1369
1370 let new_index = match direction {
1371 Direction::Next => (current_index + 1) % views.len(),
1372 Direction::Prev if current_index == 0 => views.len() - 1,
1373 Direction::Prev => (current_index - 1) % views.len(),
1374 };
1375 let next_focus_handle = views[new_index].focus_handle(cx);
1376 cx.focus(&next_focus_handle);
1377 cx.stop_propagation();
1378 });
1379 }
1380
1381 fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) -> bool {
1382 if let Some(search_view) = self.active_project_search.as_ref() {
1383 search_view.update(cx, |search_view, cx| {
1384 search_view.toggle_search_option(option, cx);
1385 search_view.search(cx);
1386 });
1387
1388 cx.notify();
1389 true
1390 } else {
1391 false
1392 }
1393 }
1394 fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
1395 if let Some(search) = &self.active_project_search {
1396 search.update(cx, |this, cx| {
1397 this.replace_enabled = !this.replace_enabled;
1398 let editor_to_focus = if !this.replace_enabled {
1399 this.query_editor.focus_handle(cx)
1400 } else {
1401 this.replacement_editor.focus_handle(cx)
1402 };
1403 cx.focus(&editor_to_focus);
1404 cx.notify();
1405 });
1406 }
1407 }
1408
1409 fn toggle_filters(&mut self, cx: &mut ViewContext<Self>) -> bool {
1410 if let Some(search_view) = self.active_project_search.as_ref() {
1411 search_view.update(cx, |search_view, cx| {
1412 search_view.toggle_filters(cx);
1413 search_view
1414 .included_files_editor
1415 .update(cx, |_, cx| cx.notify());
1416 search_view
1417 .excluded_files_editor
1418 .update(cx, |_, cx| cx.notify());
1419 cx.refresh();
1420 cx.notify();
1421 });
1422 cx.notify();
1423 true
1424 } else {
1425 false
1426 }
1427 }
1428
1429 fn activate_search_mode(&self, mode: SearchMode, cx: &mut ViewContext<Self>) {
1430 // Update Current Mode
1431 if let Some(search_view) = self.active_project_search.as_ref() {
1432 search_view.update(cx, |search_view, cx| {
1433 search_view.activate_search_mode(mode, cx);
1434 });
1435 cx.notify();
1436 }
1437 }
1438
1439 fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool {
1440 if let Some(search) = self.active_project_search.as_ref() {
1441 search.read(cx).search_options.contains(option)
1442 } else {
1443 false
1444 }
1445 }
1446
1447 fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
1448 if let Some(search_view) = self.active_project_search.as_ref() {
1449 search_view.update(cx, |search_view, cx| {
1450 let new_query = search_view.model.update(cx, |model, _| {
1451 if let Some(new_query) = model.search_history.next().map(str::to_string) {
1452 new_query
1453 } else {
1454 model.search_history.reset_selection();
1455 String::new()
1456 }
1457 });
1458 search_view.set_query(&new_query, cx);
1459 });
1460 }
1461 }
1462
1463 fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
1464 if let Some(search_view) = self.active_project_search.as_ref() {
1465 search_view.update(cx, |search_view, cx| {
1466 if search_view.query_editor.read(cx).text(cx).is_empty() {
1467 if let Some(new_query) = search_view
1468 .model
1469 .read(cx)
1470 .search_history
1471 .current()
1472 .map(str::to_string)
1473 {
1474 search_view.set_query(&new_query, cx);
1475 return;
1476 }
1477 }
1478
1479 if let Some(new_query) = search_view.model.update(cx, |model, _| {
1480 model.search_history.previous().map(str::to_string)
1481 }) {
1482 search_view.set_query(&new_query, cx);
1483 }
1484 });
1485 }
1486 }
1487 fn new_placeholder_text(&self, cx: &mut ViewContext<Self>) -> Option<String> {
1488 let previous_query_keystrokes = cx
1489 .bindings_for_action(&PreviousHistoryQuery {})
1490 .into_iter()
1491 .next()
1492 .map(|binding| {
1493 binding
1494 .keystrokes()
1495 .iter()
1496 .map(|k| k.to_string())
1497 .collect::<Vec<_>>()
1498 });
1499 let next_query_keystrokes = cx
1500 .bindings_for_action(&NextHistoryQuery {})
1501 .into_iter()
1502 .next()
1503 .map(|binding| {
1504 binding
1505 .keystrokes()
1506 .iter()
1507 .map(|k| k.to_string())
1508 .collect::<Vec<_>>()
1509 });
1510 let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
1511 (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => Some(format!(
1512 "Search ({}/{} for previous/next query)",
1513 previous_query_keystrokes.join(" "),
1514 next_query_keystrokes.join(" ")
1515 )),
1516 (None, Some(next_query_keystrokes)) => Some(format!(
1517 "Search ({} for next query)",
1518 next_query_keystrokes.join(" ")
1519 )),
1520 (Some(previous_query_keystrokes), None) => Some(format!(
1521 "Search ({} for previous query)",
1522 previous_query_keystrokes.join(" ")
1523 )),
1524 (None, None) => None,
1525 };
1526 new_placeholder_text
1527 }
1528}
1529
1530impl Render for ProjectSearchBar {
1531 type Element = Div;
1532
1533 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
1534 let Some(search) = self.active_project_search.clone() else {
1535 return div();
1536 };
1537 let mut key_context = KeyContext::default();
1538 key_context.add("ProjectSearchBar");
1539 if let Some(placeholder_text) = self.new_placeholder_text(cx) {
1540 search.update(cx, |search, cx| {
1541 search.query_editor.update(cx, |this, cx| {
1542 this.set_placeholder_text(placeholder_text, cx)
1543 })
1544 });
1545 }
1546 let search = search.read(cx);
1547
1548 let query_column = v_stack()
1549 //.flex_1()
1550 .child(
1551 h_stack()
1552 .min_w_80()
1553 .on_action(cx.listener(|this, action, cx| this.confirm(action, cx)))
1554 .on_action(
1555 cx.listener(|this, action, cx| this.previous_history_query(action, cx)),
1556 )
1557 .on_action(cx.listener(|this, action, cx| this.next_history_query(action, cx)))
1558 .child(IconElement::new(Icon::MagnifyingGlass))
1559 .child(search.query_editor.clone())
1560 .child(
1561 h_stack()
1562 .child(
1563 IconButton::new("project-search-filter-button", Icon::Filter)
1564 .tooltip(|cx| {
1565 Tooltip::for_action("Toggle filters", &ToggleFilters, cx)
1566 })
1567 .on_click(cx.listener(|this, _, cx| {
1568 this.toggle_filters(cx);
1569 }))
1570 .selected(
1571 self.active_project_search
1572 .as_ref()
1573 .map(|search| search.read(cx).filters_enabled)
1574 .unwrap_or_default(),
1575 ),
1576 )
1577 .child(
1578 IconButton::new(
1579 "project-search-case-sensitive",
1580 Icon::CaseSensitive,
1581 )
1582 .tooltip(|cx| {
1583 Tooltip::for_action(
1584 "Toggle case sensitive",
1585 &ToggleCaseSensitive,
1586 cx,
1587 )
1588 })
1589 .selected(self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx))
1590 .on_click(cx.listener(
1591 |this, _, cx| {
1592 this.toggle_search_option(
1593 SearchOptions::CASE_SENSITIVE,
1594 cx,
1595 );
1596 },
1597 )),
1598 )
1599 .child(
1600 IconButton::new("project-search-whole-word", Icon::WholeWord)
1601 .tooltip(|cx| {
1602 Tooltip::for_action(
1603 "Toggle whole word",
1604 &ToggleWholeWord,
1605 cx,
1606 )
1607 })
1608 .selected(self.is_option_enabled(SearchOptions::WHOLE_WORD, cx))
1609 .on_click(cx.listener(|this, _, cx| {
1610 this.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1611 })),
1612 ),
1613 )
1614 .border_2()
1615 .bg(white())
1616 .rounded_lg(),
1617 )
1618 .when(search.filters_enabled, |this| {
1619 this.child(
1620 h_stack()
1621 .mt_2()
1622 .flex_1()
1623 .justify_between()
1624 .child(
1625 h_stack()
1626 .flex_1()
1627 .border_1()
1628 .mr_2()
1629 .child(search.included_files_editor.clone()),
1630 )
1631 .child(
1632 h_stack()
1633 .flex_1()
1634 .border_1()
1635 .ml_2()
1636 .child(search.excluded_files_editor.clone()),
1637 ),
1638 )
1639 });
1640 let mode_column = v_stack().items_start().justify_start().child(
1641 h_stack()
1642 .child(
1643 h_stack()
1644 .child(
1645 Button::new("project-search-text-button", "Text")
1646 .selected(search.current_mode == SearchMode::Text)
1647 .on_click(cx.listener(|this, _, cx| {
1648 this.activate_search_mode(SearchMode::Text, cx)
1649 }))
1650 .tooltip(|cx| {
1651 Tooltip::for_action("Toggle text search", &ActivateTextMode, cx)
1652 }),
1653 )
1654 .child(
1655 Button::new("project-search-regex-button", "Regex")
1656 .selected(search.current_mode == SearchMode::Regex)
1657 .on_click(cx.listener(|this, _, cx| {
1658 this.activate_search_mode(SearchMode::Regex, cx)
1659 }))
1660 .tooltip(|cx| {
1661 Tooltip::for_action(
1662 "Toggle regular expression search",
1663 &ActivateRegexMode,
1664 cx,
1665 )
1666 }),
1667 ),
1668 )
1669 .child(
1670 IconButton::new("project-search-toggle-replace", Icon::Replace)
1671 .on_click(cx.listener(|this, _, cx| {
1672 this.toggle_replace(&ToggleReplace, cx);
1673 }))
1674 .tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)),
1675 ),
1676 );
1677 let replace_column = if search.replace_enabled {
1678 h_stack()
1679 .p_1()
1680 .flex_1()
1681 .border_2()
1682 .rounded_lg()
1683 .child(IconElement::new(Icon::Replace).size(ui::IconSize::Small))
1684 .child(search.replacement_editor.clone())
1685 } else {
1686 // Fill out the space if we don't have a replacement editor.
1687 h_stack().flex_1()
1688 };
1689 let actions_column = h_stack()
1690 .when(search.replace_enabled, |this| {
1691 this.children([
1692 IconButton::new("project-search-replace-next", Icon::ReplaceNext)
1693 .on_click(cx.listener(|this, _, cx| {
1694 if let Some(search) = this.active_project_search.as_ref() {
1695 search.update(cx, |this, cx| {
1696 this.replace_next(&ReplaceNext, cx);
1697 })
1698 }
1699 }))
1700 .tooltip(|cx| Tooltip::for_action("Replace next match", &ReplaceNext, cx)),
1701 IconButton::new("project-search-replace-all", Icon::ReplaceAll)
1702 .on_click(cx.listener(|this, _, cx| {
1703 if let Some(search) = this.active_project_search.as_ref() {
1704 search.update(cx, |this, cx| {
1705 this.replace_all(&ReplaceAll, cx);
1706 })
1707 }
1708 }))
1709 .tooltip(|cx| Tooltip::for_action("Replace all matches", &ReplaceAll, cx)),
1710 ])
1711 })
1712 .when_some(search.active_match_index, |mut this, index| {
1713 let index = index + 1;
1714 let match_quantity = search.model.read(cx).match_ranges.len();
1715 if match_quantity > 0 {
1716 debug_assert!(match_quantity >= index);
1717 this = this.child(Label::new(format!("{index}/{match_quantity}")))
1718 }
1719 this
1720 })
1721 .children([
1722 IconButton::new("project-search-prev-match", Icon::ChevronLeft)
1723 .disabled(search.active_match_index.is_none())
1724 .on_click(cx.listener(|this, _, cx| {
1725 if let Some(search) = this.active_project_search.as_ref() {
1726 search.update(cx, |this, cx| {
1727 this.select_match(Direction::Prev, cx);
1728 })
1729 }
1730 }))
1731 .tooltip(|cx| {
1732 Tooltip::for_action("Go to previous match", &SelectPrevMatch, cx)
1733 }),
1734 IconButton::new("project-search-next-match", Icon::ChevronRight)
1735 .disabled(search.active_match_index.is_none())
1736 .on_click(cx.listener(|this, _, cx| {
1737 if let Some(search) = this.active_project_search.as_ref() {
1738 search.update(cx, |this, cx| {
1739 this.select_match(Direction::Next, cx);
1740 })
1741 }
1742 }))
1743 .tooltip(|cx| Tooltip::for_action("Go to next match", &SelectNextMatch, cx)),
1744 ]);
1745 h_stack()
1746 .key_context(key_context)
1747 .size_full()
1748 .p_1()
1749 .m_2()
1750 .justify_between()
1751 .on_action(cx.listener(|this, _: &ToggleFilters, cx| {
1752 this.toggle_filters(cx);
1753 }))
1754 .on_action(cx.listener(|this, _: &ToggleWholeWord, cx| {
1755 this.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1756 }))
1757 .on_action(cx.listener(|this, _: &ToggleCaseSensitive, cx| {
1758 this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1759 }))
1760 .on_action(cx.listener(|this, action, cx| {
1761 this.toggle_replace(action, cx);
1762 }))
1763 .on_action(cx.listener(|this, _: &ActivateTextMode, cx| {
1764 this.activate_search_mode(SearchMode::Text, cx)
1765 }))
1766 .on_action(cx.listener(|this, _: &ActivateRegexMode, cx| {
1767 this.activate_search_mode(SearchMode::Regex, cx)
1768 }))
1769 .on_action(cx.listener(|this, action, cx| {
1770 if let Some(search) = this.active_project_search.as_ref() {
1771 search.update(cx, |this, cx| {
1772 this.replace_next(action, cx);
1773 })
1774 }
1775 }))
1776 .on_action(cx.listener(|this, action, cx| {
1777 if let Some(search) = this.active_project_search.as_ref() {
1778 search.update(cx, |this, cx| {
1779 this.replace_all(action, cx);
1780 })
1781 }
1782 }))
1783 .on_action(cx.listener(|this, action, cx| {
1784 this.tab(action, cx);
1785 }))
1786 .on_action(cx.listener(|this, action, cx| {
1787 this.tab_previous(action, cx);
1788 }))
1789 .on_action(cx.listener(|this, action, cx| {
1790 this.cycle_mode(action, cx);
1791 }))
1792 .child(query_column)
1793 .child(mode_column)
1794 .child(replace_column)
1795 .child(actions_column)
1796 }
1797}
1798// impl Entity for ProjectSearchBar {
1799// type Event = ();
1800// }
1801
1802// impl View for ProjectSearchBar {
1803// fn ui_name() -> &'static str {
1804// "ProjectSearchBar"
1805// }
1806
1807// fn update_keymap_context(
1808// &self,
1809// keymap: &mut gpui::keymap_matcher::KeymapContext,
1810// cx: &AppContext,
1811// ) {
1812// Self::reset_to_default_keymap_context(keymap);
1813// let in_replace = self
1814// .active_project_search
1815// .as_ref()
1816// .map(|search| {
1817// search
1818// .read(cx)
1819// .replacement_editor
1820// .read_with(cx, |_, cx| cx.is_self_focused())
1821// })
1822// .flatten()
1823// .unwrap_or(false);
1824// if in_replace {
1825// keymap.add_identifier("in_replace");
1826// }
1827// }
1828
1829// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1830// if let Some(_search) = self.active_project_search.as_ref() {
1831// let search = _search.read(cx);
1832// let theme = theme::current(cx).clone();
1833// let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) {
1834// theme.search.invalid_editor
1835// } else {
1836// theme.search.editor.input.container
1837// };
1838
1839// let search = _search.read(cx);
1840// let filter_button = render_option_button_icon(
1841// search.filters_enabled,
1842// "icons/filter.svg",
1843// 0,
1844// "Toggle filters",
1845// Box::new(ToggleFilters),
1846// move |_, this, cx| {
1847// this.toggle_filters(cx);
1848// },
1849// cx,
1850// );
1851
1852// let search = _search.read(cx);
1853// let is_semantic_available = SemanticIndex::enabled(cx);
1854// let is_semantic_disabled = search.semantic_state.is_none();
1855// let icon_style = theme.search.editor_icon.clone();
1856// let is_active = search.active_match_index.is_some();
1857
1858// let render_option_button_icon = |path, option, cx: &mut ViewContext<Self>| {
1859// crate::search_bar::render_option_button_icon(
1860// self.is_option_enabled(option, cx),
1861// path,
1862// option.bits as usize,
1863// format!("Toggle {}", option.label()),
1864// option.to_toggle_action(),
1865// move |_, this, cx| {
1866// this.toggle_search_option(option, cx);
1867// },
1868// cx,
1869// )
1870// };
1871// let case_sensitive = is_semantic_disabled.then(|| {
1872// render_option_button_icon(
1873// "icons/case_insensitive.svg",
1874// SearchOptions::CASE_SENSITIVE,
1875// cx,
1876// )
1877// });
1878
1879// let whole_word = is_semantic_disabled.then(|| {
1880// render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx)
1881// });
1882
1883// let include_ignored = is_semantic_disabled.then(|| {
1884// render_option_button_icon(
1885// "icons/file_icons/git.svg",
1886// SearchOptions::INCLUDE_IGNORED,
1887// cx,
1888// )
1889// });
1890
1891// let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
1892// let is_active = if let Some(search) = self.active_project_search.as_ref() {
1893// let search = search.read(cx);
1894// search.current_mode == mode
1895// } else {
1896// false
1897// };
1898// render_search_mode_button(
1899// mode,
1900// side,
1901// is_active,
1902// move |_, this, cx| {
1903// this.activate_search_mode(mode, cx);
1904// },
1905// cx,
1906// )
1907// };
1908
1909// let search = _search.read(cx);
1910
1911// let include_container_style =
1912// if search.panels_with_errors.contains(&InputPanel::Include) {
1913// theme.search.invalid_include_exclude_editor
1914// } else {
1915// theme.search.include_exclude_editor.input.container
1916// };
1917
1918// let exclude_container_style =
1919// if search.panels_with_errors.contains(&InputPanel::Exclude) {
1920// theme.search.invalid_include_exclude_editor
1921// } else {
1922// theme.search.include_exclude_editor.input.container
1923// };
1924
1925// let matches = search.active_match_index.map(|match_ix| {
1926// Label::new(
1927// format!(
1928// "{}/{}",
1929// match_ix + 1,
1930// search.model.read(cx).match_ranges.len()
1931// ),
1932// theme.search.match_index.text.clone(),
1933// )
1934// .contained()
1935// .with_style(theme.search.match_index.container)
1936// .aligned()
1937// });
1938// let should_show_replace_input = search.replace_enabled;
1939// let replacement = should_show_replace_input.then(|| {
1940// Flex::row()
1941// .with_child(
1942// Svg::for_style(theme.search.replace_icon.clone().icon)
1943// .contained()
1944// .with_style(theme.search.replace_icon.clone().container),
1945// )
1946// .with_child(ChildView::new(&search.replacement_editor, cx).flex(1., true))
1947// .align_children_center()
1948// .flex(1., true)
1949// .contained()
1950// .with_style(query_container_style)
1951// .constrained()
1952// .with_min_width(theme.search.editor.min_width)
1953// .with_max_width(theme.search.editor.max_width)
1954// .with_height(theme.search.search_bar_row_height)
1955// .flex(1., false)
1956// });
1957// let replace_all = should_show_replace_input.then(|| {
1958// super::replace_action(
1959// ReplaceAll,
1960// "Replace all",
1961// "icons/replace_all.svg",
1962// theme.tooltip.clone(),
1963// theme.search.action_button.clone(),
1964// )
1965// });
1966// let replace_next = should_show_replace_input.then(|| {
1967// super::replace_action(
1968// ReplaceNext,
1969// "Replace next",
1970// "icons/replace_next.svg",
1971// theme.tooltip.clone(),
1972// theme.search.action_button.clone(),
1973// )
1974// });
1975// let query_column = Flex::column()
1976// .with_spacing(theme.search.search_row_spacing)
1977// .with_child(
1978// Flex::row()
1979// .with_child(
1980// Svg::for_style(icon_style.icon)
1981// .contained()
1982// .with_style(icon_style.container),
1983// )
1984// .with_child(ChildView::new(&search.query_editor, cx).flex(1., true))
1985// .with_child(
1986// Flex::row()
1987// .with_child(filter_button)
1988// .with_children(case_sensitive)
1989// .with_children(whole_word)
1990// .flex(1., false)
1991// .constrained()
1992// .contained(),
1993// )
1994// .align_children_center()
1995// .contained()
1996// .with_style(query_container_style)
1997// .constrained()
1998// .with_min_width(theme.search.editor.min_width)
1999// .with_max_width(theme.search.editor.max_width)
2000// .with_height(theme.search.search_bar_row_height)
2001// .flex(1., false),
2002// )
2003// .with_children(search.filters_enabled.then(|| {
2004// Flex::row()
2005// .with_child(
2006// Flex::row()
2007// .with_child(
2008// ChildView::new(&search.included_files_editor, cx)
2009// .contained()
2010// .constrained()
2011// .with_height(theme.search.search_bar_row_height)
2012// .flex(1., true),
2013// )
2014// .with_children(include_ignored)
2015// .contained()
2016// .with_style(include_container_style)
2017// .constrained()
2018// .with_height(theme.search.search_bar_row_height)
2019// .flex(1., true),
2020// )
2021// .with_child(
2022// ChildView::new(&search.excluded_files_editor, cx)
2023// .contained()
2024// .with_style(exclude_container_style)
2025// .constrained()
2026// .with_height(theme.search.search_bar_row_height)
2027// .flex(1., true),
2028// )
2029// .constrained()
2030// .with_min_width(theme.search.editor.min_width)
2031// .with_max_width(theme.search.editor.max_width)
2032// .flex(1., false)
2033// }))
2034// .flex(1., false);
2035// let switches_column = Flex::row()
2036// .align_children_center()
2037// .with_child(super::toggle_replace_button(
2038// search.replace_enabled,
2039// theme.tooltip.clone(),
2040// theme.search.option_button_component.clone(),
2041// ))
2042// .constrained()
2043// .with_height(theme.search.search_bar_row_height)
2044// .contained()
2045// .with_style(theme.search.option_button_group);
2046// let mode_column =
2047// Flex::row()
2048// .with_child(search_button_for_mode(
2049// SearchMode::Text,
2050// Some(Side::Left),
2051// cx,
2052// ))
2053// .with_child(search_button_for_mode(
2054// SearchMode::Regex,
2055// if is_semantic_available {
2056// None
2057// } else {
2058// Some(Side::Right)
2059// },
2060// cx,
2061// ))
2062// .with_children(is_semantic_available.then(|| {
2063// search_button_for_mode(SearchMode::Semantic, Some(Side::Right), cx)
2064// }))
2065// .contained()
2066// .with_style(theme.search.modes_container);
2067
2068// let nav_button_for_direction = |label, direction, cx: &mut ViewContext<Self>| {
2069// render_nav_button(
2070// label,
2071// direction,
2072// is_active,
2073// move |_, this, cx| {
2074// if let Some(search) = this.active_project_search.as_ref() {
2075// search.update(cx, |search, cx| search.select_match(direction, cx));
2076// }
2077// },
2078// cx,
2079// )
2080// };
2081
2082// let nav_column = Flex::row()
2083// .with_children(replace_next)
2084// .with_children(replace_all)
2085// .with_child(Flex::row().with_children(matches))
2086// .with_child(nav_button_for_direction("<", Direction::Prev, cx))
2087// .with_child(nav_button_for_direction(">", Direction::Next, cx))
2088// .constrained()
2089// .with_height(theme.search.search_bar_row_height)
2090// .flex_float();
2091
2092// Flex::row()
2093// .with_child(query_column)
2094// .with_child(mode_column)
2095// .with_child(switches_column)
2096// .with_children(replacement)
2097// .with_child(nav_column)
2098// .contained()
2099// .with_style(theme.search.container)
2100// .into_any_named("project search")
2101// } else {
2102// Empty::new().into_any()
2103// }
2104// }
2105// }
2106
2107impl EventEmitter<ToolbarItemEvent> for ProjectSearchBar {}
2108
2109impl ToolbarItemView for ProjectSearchBar {
2110 fn set_active_pane_item(
2111 &mut self,
2112 active_pane_item: Option<&dyn ItemHandle>,
2113 cx: &mut ViewContext<Self>,
2114 ) -> ToolbarItemLocation {
2115 cx.notify();
2116 self.subscription = None;
2117 self.active_project_search = None;
2118 if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) {
2119 search.update(cx, |search, cx| {
2120 if search.current_mode == SearchMode::Semantic {
2121 search.index_project(cx);
2122 }
2123 });
2124
2125 self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
2126 self.active_project_search = Some(search);
2127 ToolbarItemLocation::PrimaryLeft {}
2128 } else {
2129 ToolbarItemLocation::Hidden
2130 }
2131 }
2132
2133 fn row_count(&self, cx: &WindowContext<'_>) -> usize {
2134 if let Some(search) = self.active_project_search.as_ref() {
2135 if search.read(cx).filters_enabled {
2136 return 2;
2137 }
2138 }
2139 1
2140 }
2141}
2142
2143// #[cfg(test)]
2144// pub mod tests {
2145// use super::*;
2146// use editor::DisplayPoint;
2147// use gpui::{color::Color, executor::Deterministic, TestAppContext};
2148// use project::FakeFs;
2149// use semantic_index::semantic_index_settings::SemanticIndexSettings;
2150// use serde_json::json;
2151// use settings::SettingsStore;
2152// use std::sync::Arc;
2153// use theme::ThemeSettings;
2154
2155// #[gpui::test]
2156// async fn test_project_search(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
2157// init_test(cx);
2158
2159// let fs = FakeFs::new(cx.background_executor());
2160// fs.insert_tree(
2161// "/dir",
2162// json!({
2163// "one.rs": "const ONE: usize = 1;",
2164// "two.rs": "const TWO: usize = one::ONE + one::ONE;",
2165// "three.rs": "const THREE: usize = one::ONE + two::TWO;",
2166// "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
2167// }),
2168// )
2169// .await;
2170// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2171// let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
2172// let search_view = cx
2173// .add_window(|cx| ProjectSearchView::new(search.clone(), cx, None))
2174// .root(cx);
2175
2176// search_view.update(cx, |search_view, cx| {
2177// search_view
2178// .query_editor
2179// .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
2180// search_view.search(cx);
2181// });
2182// deterministic.run_until_parked();
2183// search_view.update(cx, |search_view, cx| {
2184// assert_eq!(
2185// search_view
2186// .results_editor
2187// .update(cx, |editor, cx| editor.display_text(cx)),
2188// "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;"
2189// );
2190// assert_eq!(
2191// search_view
2192// .results_editor
2193// .update(cx, |editor, cx| editor.all_text_background_highlights(cx)),
2194// &[
2195// (
2196// DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35),
2197// Color::red()
2198// ),
2199// (
2200// DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40),
2201// Color::red()
2202// ),
2203// (
2204// DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9),
2205// Color::red()
2206// )
2207// ]
2208// );
2209// assert_eq!(search_view.active_match_index, Some(0));
2210// assert_eq!(
2211// search_view
2212// .results_editor
2213// .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2214// [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
2215// );
2216
2217// search_view.select_match(Direction::Next, cx);
2218// });
2219
2220// search_view.update(cx, |search_view, cx| {
2221// assert_eq!(search_view.active_match_index, Some(1));
2222// assert_eq!(
2223// search_view
2224// .results_editor
2225// .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2226// [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
2227// );
2228// search_view.select_match(Direction::Next, cx);
2229// });
2230
2231// search_view.update(cx, |search_view, cx| {
2232// assert_eq!(search_view.active_match_index, Some(2));
2233// assert_eq!(
2234// search_view
2235// .results_editor
2236// .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2237// [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
2238// );
2239// search_view.select_match(Direction::Next, cx);
2240// });
2241
2242// search_view.update(cx, |search_view, cx| {
2243// assert_eq!(search_view.active_match_index, Some(0));
2244// assert_eq!(
2245// search_view
2246// .results_editor
2247// .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2248// [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
2249// );
2250// search_view.select_match(Direction::Prev, cx);
2251// });
2252
2253// search_view.update(cx, |search_view, cx| {
2254// assert_eq!(search_view.active_match_index, Some(2));
2255// assert_eq!(
2256// search_view
2257// .results_editor
2258// .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2259// [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
2260// );
2261// search_view.select_match(Direction::Prev, cx);
2262// });
2263
2264// search_view.update(cx, |search_view, cx| {
2265// assert_eq!(search_view.active_match_index, Some(1));
2266// assert_eq!(
2267// search_view
2268// .results_editor
2269// .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2270// [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
2271// );
2272// });
2273// }
2274
2275// #[gpui::test]
2276// async fn test_project_search_focus(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
2277// init_test(cx);
2278
2279// let fs = FakeFs::new(cx.background_executor());
2280// fs.insert_tree(
2281// "/dir",
2282// json!({
2283// "one.rs": "const ONE: usize = 1;",
2284// "two.rs": "const TWO: usize = one::ONE + one::ONE;",
2285// "three.rs": "const THREE: usize = one::ONE + two::TWO;",
2286// "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
2287// }),
2288// )
2289// .await;
2290// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2291// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
2292// let workspace = window.root(cx);
2293
2294// let active_item = cx.read(|cx| {
2295// workspace
2296// .read(cx)
2297// .active_pane()
2298// .read(cx)
2299// .active_item()
2300// .and_then(|item| item.downcast::<ProjectSearchView>())
2301// });
2302// assert!(
2303// active_item.is_none(),
2304// "Expected no search panel to be active, but got: {active_item:?}"
2305// );
2306
2307// workspace.update(cx, |workspace, cx| {
2308// ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
2309// });
2310
2311// let Some(search_view) = cx.read(|cx| {
2312// workspace
2313// .read(cx)
2314// .active_pane()
2315// .read(cx)
2316// .active_item()
2317// .and_then(|item| item.downcast::<ProjectSearchView>())
2318// }) else {
2319// panic!("Search view expected to appear after new search event trigger")
2320// };
2321// let search_view_id = search_view.id();
2322
2323// cx.spawn(|mut cx| async move {
2324// window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
2325// })
2326// .detach();
2327// deterministic.run_until_parked();
2328// search_view.update(cx, |search_view, cx| {
2329// assert!(
2330// search_view.query_editor.is_focused(cx),
2331// "Empty search view should be focused after the toggle focus event: no results panel to focus on",
2332// );
2333// });
2334
2335// search_view.update(cx, |search_view, cx| {
2336// let query_editor = &search_view.query_editor;
2337// assert!(
2338// query_editor.is_focused(cx),
2339// "Search view should be focused after the new search view is activated",
2340// );
2341// let query_text = query_editor.read(cx).text(cx);
2342// assert!(
2343// query_text.is_empty(),
2344// "New search query should be empty but got '{query_text}'",
2345// );
2346// let results_text = search_view
2347// .results_editor
2348// .update(cx, |editor, cx| editor.display_text(cx));
2349// assert!(
2350// results_text.is_empty(),
2351// "Empty search view should have no results but got '{results_text}'"
2352// );
2353// });
2354
2355// search_view.update(cx, |search_view, cx| {
2356// search_view.query_editor.update(cx, |query_editor, cx| {
2357// query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx)
2358// });
2359// search_view.search(cx);
2360// });
2361// deterministic.run_until_parked();
2362// search_view.update(cx, |search_view, cx| {
2363// let results_text = search_view
2364// .results_editor
2365// .update(cx, |editor, cx| editor.display_text(cx));
2366// assert!(
2367// results_text.is_empty(),
2368// "Search view for mismatching query should have no results but got '{results_text}'"
2369// );
2370// assert!(
2371// search_view.query_editor.is_focused(cx),
2372// "Search view should be focused after mismatching query had been used in search",
2373// );
2374// });
2375// cx.spawn(
2376// |mut cx| async move { window.dispatch_action(search_view_id, &ToggleFocus, &mut cx) },
2377// )
2378// .detach();
2379// deterministic.run_until_parked();
2380// search_view.update(cx, |search_view, cx| {
2381// assert!(
2382// search_view.query_editor.is_focused(cx),
2383// "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on",
2384// );
2385// });
2386
2387// search_view.update(cx, |search_view, cx| {
2388// search_view
2389// .query_editor
2390// .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
2391// search_view.search(cx);
2392// });
2393// deterministic.run_until_parked();
2394// search_view.update(cx, |search_view, cx| {
2395// assert_eq!(
2396// search_view
2397// .results_editor
2398// .update(cx, |editor, cx| editor.display_text(cx)),
2399// "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
2400// "Search view results should match the query"
2401// );
2402// assert!(
2403// search_view.results_editor.is_focused(cx),
2404// "Search view with mismatching query should be focused after search results are available",
2405// );
2406// });
2407// cx.spawn(|mut cx| async move {
2408// window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
2409// })
2410// .detach();
2411// deterministic.run_until_parked();
2412// search_view.update(cx, |search_view, cx| {
2413// assert!(
2414// search_view.results_editor.is_focused(cx),
2415// "Search view with matching query should still have its results editor focused after the toggle focus event",
2416// );
2417// });
2418
2419// workspace.update(cx, |workspace, cx| {
2420// ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
2421// });
2422// deterministic.run_until_parked();
2423// let Some(search_view_2) = cx.read(|cx| {
2424// workspace
2425// .read(cx)
2426// .active_pane()
2427// .read(cx)
2428// .active_item()
2429// .and_then(|item| item.downcast::<ProjectSearchView>())
2430// }) else {
2431// panic!("Search view expected to appear after new search event trigger")
2432// };
2433// let search_view_id_2 = search_view_2.id();
2434// assert_ne!(
2435// search_view_2, search_view,
2436// "New search view should be open after `workspace::NewSearch` event"
2437// );
2438
2439// search_view.update(cx, |search_view, cx| {
2440// assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO", "First search view should not have an updated query");
2441// assert_eq!(
2442// search_view
2443// .results_editor
2444// .update(cx, |editor, cx| editor.display_text(cx)),
2445// "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
2446// "Results of the first search view should not update too"
2447// );
2448// assert!(
2449// !search_view.query_editor.is_focused(cx),
2450// "Focus should be moved away from the first search view"
2451// );
2452// });
2453
2454// search_view_2.update(cx, |search_view_2, cx| {
2455// assert_eq!(
2456// search_view_2.query_editor.read(cx).text(cx),
2457// "two",
2458// "New search view should get the query from the text cursor was at during the event spawn (first search view's first result)"
2459// );
2460// assert_eq!(
2461// search_view_2
2462// .results_editor
2463// .update(cx, |editor, cx| editor.display_text(cx)),
2464// "",
2465// "No search results should be in the 2nd view yet, as we did not spawn a search for it"
2466// );
2467// assert!(
2468// search_view_2.query_editor.is_focused(cx),
2469// "Focus should be moved into query editor fo the new window"
2470// );
2471// });
2472
2473// search_view_2.update(cx, |search_view_2, cx| {
2474// search_view_2
2475// .query_editor
2476// .update(cx, |query_editor, cx| query_editor.set_text("FOUR", cx));
2477// search_view_2.search(cx);
2478// });
2479// deterministic.run_until_parked();
2480// search_view_2.update(cx, |search_view_2, cx| {
2481// assert_eq!(
2482// search_view_2
2483// .results_editor
2484// .update(cx, |editor, cx| editor.display_text(cx)),
2485// "\n\nconst FOUR: usize = one::ONE + three::THREE;",
2486// "New search view with the updated query should have new search results"
2487// );
2488// assert!(
2489// search_view_2.results_editor.is_focused(cx),
2490// "Search view with mismatching query should be focused after search results are available",
2491// );
2492// });
2493
2494// cx.spawn(|mut cx| async move {
2495// window.dispatch_action(search_view_id_2, &ToggleFocus, &mut cx);
2496// })
2497// .detach();
2498// deterministic.run_until_parked();
2499// search_view_id_2.update(cx, |search_view_2, cx| {
2500// assert!(
2501// search_view_2.results_editor.is_focused(cx),
2502// "Search view with matching query should switch focus to the results editor after the toggle focus event",
2503// );
2504// });
2505// }
2506
2507// #[gpui::test]
2508// async fn test_new_project_search_in_directory(
2509// deterministic: Arc<Deterministic>,
2510// cx: &mut TestAppContext,
2511// ) {
2512// init_test(cx);
2513
2514// let fs = FakeFs::new(cx.background_executor());
2515// fs.insert_tree(
2516// "/dir",
2517// json!({
2518// "a": {
2519// "one.rs": "const ONE: usize = 1;",
2520// "two.rs": "const TWO: usize = one::ONE + one::ONE;",
2521// },
2522// "b": {
2523// "three.rs": "const THREE: usize = one::ONE + two::TWO;",
2524// "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
2525// },
2526// }),
2527// )
2528// .await;
2529// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2530// let worktree_id = project.read_with(cx, |project, cx| {
2531// project.worktrees(cx).next().unwrap().read(cx).id()
2532// });
2533// let workspace = cx
2534// .add_window(|cx| Workspace::test_new(project, cx))
2535// .root(cx);
2536
2537// let active_item = cx.read(|cx| {
2538// workspace
2539// .read(cx)
2540// .active_pane()
2541// .read(cx)
2542// .active_item()
2543// .and_then(|item| item.downcast::<ProjectSearchView>())
2544// });
2545// assert!(
2546// active_item.is_none(),
2547// "Expected no search panel to be active, but got: {active_item:?}"
2548// );
2549
2550// let one_file_entry = cx.update(|cx| {
2551// workspace
2552// .read(cx)
2553// .project()
2554// .read(cx)
2555// .entry_for_path(&(worktree_id, "a/one.rs").into(), cx)
2556// .expect("no entry for /a/one.rs file")
2557// });
2558// assert!(one_file_entry.is_file());
2559// workspace.update(cx, |workspace, cx| {
2560// ProjectSearchView::new_search_in_directory(workspace, &one_file_entry, cx)
2561// });
2562// let active_search_entry = cx.read(|cx| {
2563// workspace
2564// .read(cx)
2565// .active_pane()
2566// .read(cx)
2567// .active_item()
2568// .and_then(|item| item.downcast::<ProjectSearchView>())
2569// });
2570// assert!(
2571// active_search_entry.is_none(),
2572// "Expected no search panel to be active for file entry"
2573// );
2574
2575// let a_dir_entry = cx.update(|cx| {
2576// workspace
2577// .read(cx)
2578// .project()
2579// .read(cx)
2580// .entry_for_path(&(worktree_id, "a").into(), cx)
2581// .expect("no entry for /a/ directory")
2582// });
2583// assert!(a_dir_entry.is_dir());
2584// workspace.update(cx, |workspace, cx| {
2585// ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry, cx)
2586// });
2587
2588// let Some(search_view) = cx.read(|cx| {
2589// workspace
2590// .read(cx)
2591// .active_pane()
2592// .read(cx)
2593// .active_item()
2594// .and_then(|item| item.downcast::<ProjectSearchView>())
2595// }) else {
2596// panic!("Search view expected to appear after new search in directory event trigger")
2597// };
2598// deterministic.run_until_parked();
2599// search_view.update(cx, |search_view, cx| {
2600// assert!(
2601// search_view.query_editor.is_focused(cx),
2602// "On new search in directory, focus should be moved into query editor"
2603// );
2604// search_view.excluded_files_editor.update(cx, |editor, cx| {
2605// assert!(
2606// editor.display_text(cx).is_empty(),
2607// "New search in directory should not have any excluded files"
2608// );
2609// });
2610// search_view.included_files_editor.update(cx, |editor, cx| {
2611// assert_eq!(
2612// editor.display_text(cx),
2613// a_dir_entry.path.to_str().unwrap(),
2614// "New search in directory should have included dir entry path"
2615// );
2616// });
2617// });
2618
2619// search_view.update(cx, |search_view, cx| {
2620// search_view
2621// .query_editor
2622// .update(cx, |query_editor, cx| query_editor.set_text("const", cx));
2623// search_view.search(cx);
2624// });
2625// deterministic.run_until_parked();
2626// search_view.update(cx, |search_view, cx| {
2627// assert_eq!(
2628// search_view
2629// .results_editor
2630// .update(cx, |editor, cx| editor.display_text(cx)),
2631// "\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
2632// "New search in directory should have a filter that matches a certain directory"
2633// );
2634// });
2635// }
2636
2637// #[gpui::test]
2638// async fn test_search_query_history(cx: &mut TestAppContext) {
2639// init_test(cx);
2640
2641// let fs = FakeFs::new(cx.background_executor());
2642// fs.insert_tree(
2643// "/dir",
2644// json!({
2645// "one.rs": "const ONE: usize = 1;",
2646// "two.rs": "const TWO: usize = one::ONE + one::ONE;",
2647// "three.rs": "const THREE: usize = one::ONE + two::TWO;",
2648// "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
2649// }),
2650// )
2651// .await;
2652// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2653// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
2654// let workspace = window.root(cx);
2655// workspace.update(cx, |workspace, cx| {
2656// ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
2657// });
2658
2659// let search_view = cx.read(|cx| {
2660// workspace
2661// .read(cx)
2662// .active_pane()
2663// .read(cx)
2664// .active_item()
2665// .and_then(|item| item.downcast::<ProjectSearchView>())
2666// .expect("Search view expected to appear after new search event trigger")
2667// });
2668
2669// let search_bar = window.add_view(cx, |cx| {
2670// let mut search_bar = ProjectSearchBar::new();
2671// search_bar.set_active_pane_item(Some(&search_view), cx);
2672// // search_bar.show(cx);
2673// search_bar
2674// });
2675
2676// // Add 3 search items into the history + another unsubmitted one.
2677// search_view.update(cx, |search_view, cx| {
2678// search_view.search_options = SearchOptions::CASE_SENSITIVE;
2679// search_view
2680// .query_editor
2681// .update(cx, |query_editor, cx| query_editor.set_text("ONE", cx));
2682// search_view.search(cx);
2683// });
2684// cx.foreground().run_until_parked();
2685// search_view.update(cx, |search_view, cx| {
2686// search_view
2687// .query_editor
2688// .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
2689// search_view.search(cx);
2690// });
2691// cx.foreground().run_until_parked();
2692// search_view.update(cx, |search_view, cx| {
2693// search_view
2694// .query_editor
2695// .update(cx, |query_editor, cx| query_editor.set_text("THREE", cx));
2696// search_view.search(cx);
2697// });
2698// cx.foreground().run_until_parked();
2699// search_view.update(cx, |search_view, cx| {
2700// search_view.query_editor.update(cx, |query_editor, cx| {
2701// query_editor.set_text("JUST_TEXT_INPUT", cx)
2702// });
2703// });
2704// cx.foreground().run_until_parked();
2705
2706// // Ensure that the latest input with search settings is active.
2707// search_view.update(cx, |search_view, cx| {
2708// assert_eq!(
2709// search_view.query_editor.read(cx).text(cx),
2710// "JUST_TEXT_INPUT"
2711// );
2712// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2713// });
2714
2715// // Next history query after the latest should set the query to the empty string.
2716// search_bar.update(cx, |search_bar, cx| {
2717// search_bar.next_history_query(&NextHistoryQuery, cx);
2718// });
2719// search_view.update(cx, |search_view, cx| {
2720// assert_eq!(search_view.query_editor.read(cx).text(cx), "");
2721// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2722// });
2723// search_bar.update(cx, |search_bar, cx| {
2724// search_bar.next_history_query(&NextHistoryQuery, cx);
2725// });
2726// search_view.update(cx, |search_view, cx| {
2727// assert_eq!(search_view.query_editor.read(cx).text(cx), "");
2728// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2729// });
2730
2731// // First previous query for empty current query should set the query to the latest submitted one.
2732// search_bar.update(cx, |search_bar, cx| {
2733// search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2734// });
2735// search_view.update(cx, |search_view, cx| {
2736// assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
2737// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2738// });
2739
2740// // Further previous items should go over the history in reverse order.
2741// search_bar.update(cx, |search_bar, cx| {
2742// search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2743// });
2744// search_view.update(cx, |search_view, cx| {
2745// assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
2746// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2747// });
2748
2749// // Previous items should never go behind the first history item.
2750// search_bar.update(cx, |search_bar, cx| {
2751// search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2752// });
2753// search_view.update(cx, |search_view, cx| {
2754// assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
2755// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2756// });
2757// search_bar.update(cx, |search_bar, cx| {
2758// search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2759// });
2760// search_view.update(cx, |search_view, cx| {
2761// assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
2762// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2763// });
2764
2765// // Next items should go over the history in the original order.
2766// search_bar.update(cx, |search_bar, cx| {
2767// search_bar.next_history_query(&NextHistoryQuery, cx);
2768// });
2769// search_view.update(cx, |search_view, cx| {
2770// assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
2771// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2772// });
2773
2774// search_view.update(cx, |search_view, cx| {
2775// search_view
2776// .query_editor
2777// .update(cx, |query_editor, cx| query_editor.set_text("TWO_NEW", cx));
2778// search_view.search(cx);
2779// });
2780// cx.foreground().run_until_parked();
2781// search_view.update(cx, |search_view, cx| {
2782// assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
2783// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2784// });
2785
2786// // New search input should add another entry to history and move the selection to the end of the history.
2787// search_bar.update(cx, |search_bar, cx| {
2788// search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2789// });
2790// search_view.update(cx, |search_view, cx| {
2791// assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
2792// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2793// });
2794// search_bar.update(cx, |search_bar, cx| {
2795// search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2796// });
2797// search_view.update(cx, |search_view, cx| {
2798// assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
2799// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2800// });
2801// search_bar.update(cx, |search_bar, cx| {
2802// search_bar.next_history_query(&NextHistoryQuery, cx);
2803// });
2804// search_view.update(cx, |search_view, cx| {
2805// assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
2806// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2807// });
2808// search_bar.update(cx, |search_bar, cx| {
2809// search_bar.next_history_query(&NextHistoryQuery, cx);
2810// });
2811// search_view.update(cx, |search_view, cx| {
2812// assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
2813// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2814// });
2815// search_bar.update(cx, |search_bar, cx| {
2816// search_bar.next_history_query(&NextHistoryQuery, cx);
2817// });
2818// search_view.update(cx, |search_view, cx| {
2819// assert_eq!(search_view.query_editor.read(cx).text(cx), "");
2820// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2821// });
2822// }
2823
2824// pub fn init_test(cx: &mut TestAppContext) {
2825// cx.foreground().forbid_parking();
2826// let fonts = cx.font_cache();
2827// let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
2828// theme.search.match_background = Color::red();
2829
2830// cx.update(|cx| {
2831// cx.set_global(SettingsStore::test(cx));
2832// cx.set_global(ActiveSearches::default());
2833// settings::register::<SemanticIndexSettings>(cx);
2834
2835// theme::init((), cx);
2836// cx.update_global::<SettingsStore, _, _>(|store, _| {
2837// let mut settings = store.get::<ThemeSettings>(None).clone();
2838// settings.theme = Arc::new(theme);
2839// store.override_global(settings)
2840// });
2841
2842// language::init(cx);
2843// client::init_settings(cx);
2844// editor::init(cx);
2845// workspace::init_settings(cx);
2846// Project::init_settings(cx);
2847// super::init(cx);
2848// });
2849// }
2850// }