1use crate::{
2 SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
3 ToggleWholeWord,
4};
5use anyhow::Result;
6use collections::HashMap;
7use editor::{
8 items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer,
9 SelectAll, MAX_TAB_TITLE_LEN,
10};
11use futures::StreamExt;
12use globset::{Glob, GlobMatcher};
13use gpui::{
14 actions,
15 elements::*,
16 platform::{CursorStyle, MouseButton},
17 Action, AnyElement, AnyViewHandle, AppContext, Entity, ModelContext, ModelHandle, Subscription,
18 Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
19};
20use menu::Confirm;
21use project::{search::SearchQuery, Project};
22use smallvec::SmallVec;
23use std::{
24 any::{Any, TypeId},
25 borrow::Cow,
26 collections::HashSet,
27 mem,
28 ops::Range,
29 path::PathBuf,
30 sync::Arc,
31};
32use util::ResultExt as _;
33use workspace::{
34 item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
35 searchable::{Direction, SearchableItem, SearchableItemHandle},
36 ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
37};
38
39actions!(project_search, [SearchInNew, ToggleFocus, NextField]);
40
41#[derive(Default)]
42struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
43
44pub fn init(cx: &mut AppContext) {
45 cx.set_global(ActiveSearches::default());
46 cx.add_action(ProjectSearchView::deploy);
47 cx.add_action(ProjectSearchView::move_focus_to_results);
48 cx.add_action(ProjectSearchBar::search);
49 cx.add_action(ProjectSearchBar::search_in_new);
50 cx.add_action(ProjectSearchBar::select_next_match);
51 cx.add_action(ProjectSearchBar::select_prev_match);
52 cx.capture_action(ProjectSearchBar::tab);
53 cx.capture_action(ProjectSearchBar::tab_previous);
54 add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
55 add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
56 add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
57}
58
59fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut AppContext) {
60 cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
61 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<ProjectSearchBar>() {
62 if search_bar.update(cx, |search_bar, cx| {
63 search_bar.toggle_search_option(option, cx)
64 }) {
65 return;
66 }
67 }
68 cx.propagate_action();
69 });
70}
71
72struct ProjectSearch {
73 project: ModelHandle<Project>,
74 excerpts: ModelHandle<MultiBuffer>,
75 pending_search: Option<Task<Option<()>>>,
76 match_ranges: Vec<Range<Anchor>>,
77 active_query: Option<SearchQuery>,
78 search_id: usize,
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: ModelHandle<ProjectSearch>,
90 query_editor: ViewHandle<Editor>,
91 results_editor: ViewHandle<Editor>,
92 case_sensitive: bool,
93 whole_word: bool,
94 regex: bool,
95 panels_with_errors: HashSet<InputPanel>,
96 active_match_index: Option<usize>,
97 search_id: usize,
98 query_editor_was_focused: bool,
99 included_files_editor: ViewHandle<Editor>,
100 excluded_files_editor: ViewHandle<Editor>,
101}
102
103pub struct ProjectSearchBar {
104 active_project_search: Option<ViewHandle<ProjectSearchView>>,
105 subscription: Option<Subscription>,
106}
107
108impl Entity for ProjectSearch {
109 type Event = ();
110}
111
112impl ProjectSearch {
113 fn new(project: ModelHandle<Project>, cx: &mut ModelContext<Self>) -> Self {
114 let replica_id = project.read(cx).replica_id();
115 Self {
116 project,
117 excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)),
118 pending_search: Default::default(),
119 match_ranges: Default::default(),
120 active_query: None,
121 search_id: 0,
122 }
123 }
124
125 fn clone(&self, cx: &mut ModelContext<Self>) -> ModelHandle<Self> {
126 cx.add_model(|cx| Self {
127 project: self.project.clone(),
128 excerpts: self
129 .excerpts
130 .update(cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))),
131 pending_search: Default::default(),
132 match_ranges: self.match_ranges.clone(),
133 active_query: self.active_query.clone(),
134 search_id: self.search_id,
135 })
136 }
137
138 fn search(&mut self, query: SearchQuery, cx: &mut ModelContext<Self>) {
139 let search = self
140 .project
141 .update(cx, |project, cx| project.search(query.clone(), cx));
142 self.search_id += 1;
143 self.active_query = Some(query);
144 self.match_ranges.clear();
145 self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
146 let matches = search.await.log_err()?;
147 let this = this.upgrade(&cx)?;
148 let mut matches = matches.into_iter().collect::<Vec<_>>();
149 let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| {
150 this.match_ranges.clear();
151 matches.sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path()));
152 this.excerpts.update(cx, |excerpts, cx| {
153 excerpts.clear(cx);
154 excerpts.stream_excerpts_with_context_lines(matches, 1, cx)
155 })
156 });
157
158 while let Some(match_range) = match_ranges.next().await {
159 this.update(&mut cx, |this, cx| {
160 this.match_ranges.push(match_range);
161 while let Ok(Some(match_range)) = match_ranges.try_next() {
162 this.match_ranges.push(match_range);
163 }
164 cx.notify();
165 });
166 }
167
168 this.update(&mut cx, |this, cx| {
169 this.pending_search.take();
170 cx.notify();
171 });
172
173 None
174 }));
175 cx.notify();
176 }
177}
178
179pub enum ViewEvent {
180 UpdateTab,
181 Activate,
182 EditorEvent(editor::Event),
183}
184
185impl Entity for ProjectSearchView {
186 type Event = ViewEvent;
187}
188
189impl View for ProjectSearchView {
190 fn ui_name() -> &'static str {
191 "ProjectSearchView"
192 }
193
194 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
195 let model = &self.model.read(cx);
196 if model.match_ranges.is_empty() {
197 enum Status {}
198
199 let theme = theme::current(cx).clone();
200 let text = if self.query_editor.read(cx).text(cx).is_empty() {
201 ""
202 } else if model.pending_search.is_some() {
203 "Searching..."
204 } else {
205 "No results"
206 };
207 MouseEventHandler::<Status, _>::new(0, cx, |_, _| {
208 Label::new(text, theme.search.results_status.clone())
209 .aligned()
210 .contained()
211 .with_background_color(theme.editor.background)
212 .flex(1., true)
213 })
214 .on_down(MouseButton::Left, |_, _, cx| {
215 cx.focus_parent();
216 })
217 .into_any_named("project search view")
218 } else {
219 ChildView::new(&self.results_editor, cx)
220 .flex(1., true)
221 .into_any_named("project search view")
222 }
223 }
224
225 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
226 let handle = cx.weak_handle();
227 cx.update_global(|state: &mut ActiveSearches, cx| {
228 state
229 .0
230 .insert(self.model.read(cx).project.downgrade(), handle)
231 });
232
233 if cx.is_self_focused() {
234 if self.query_editor_was_focused {
235 cx.focus(&self.query_editor);
236 } else {
237 cx.focus(&self.results_editor);
238 }
239 }
240 }
241}
242
243impl Item for ProjectSearchView {
244 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
245 Some(self.query_editor.read(cx).text(cx).into())
246 }
247
248 fn act_as_type<'a>(
249 &'a self,
250 type_id: TypeId,
251 self_handle: &'a ViewHandle<Self>,
252 _: &'a AppContext,
253 ) -> Option<&'a AnyViewHandle> {
254 if type_id == TypeId::of::<Self>() {
255 Some(self_handle)
256 } else if type_id == TypeId::of::<Editor>() {
257 Some(&self.results_editor)
258 } else {
259 None
260 }
261 }
262
263 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
264 self.results_editor
265 .update(cx, |editor, cx| editor.deactivated(cx));
266 }
267
268 fn tab_content<T: View>(
269 &self,
270 _detail: Option<usize>,
271 tab_theme: &theme::Tab,
272 cx: &AppContext,
273 ) -> AnyElement<T> {
274 Flex::row()
275 .with_child(
276 Svg::new("icons/magnifying_glass_12.svg")
277 .with_color(tab_theme.label.text.color)
278 .constrained()
279 .with_width(tab_theme.type_icon_width)
280 .aligned()
281 .contained()
282 .with_margin_right(tab_theme.spacing),
283 )
284 .with_children(self.model.read(cx).active_query.as_ref().map(|query| {
285 let query_text = util::truncate_and_trailoff(query.as_str(), MAX_TAB_TITLE_LEN);
286
287 Label::new(query_text, tab_theme.label.clone()).aligned()
288 }))
289 .into_any()
290 }
291
292 fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
293 self.results_editor.for_each_project_item(cx, f)
294 }
295
296 fn is_singleton(&self, _: &AppContext) -> bool {
297 false
298 }
299
300 fn can_save(&self, _: &AppContext) -> bool {
301 true
302 }
303
304 fn is_dirty(&self, cx: &AppContext) -> bool {
305 self.results_editor.read(cx).is_dirty(cx)
306 }
307
308 fn has_conflict(&self, cx: &AppContext) -> bool {
309 self.results_editor.read(cx).has_conflict(cx)
310 }
311
312 fn save(
313 &mut self,
314 project: ModelHandle<Project>,
315 cx: &mut ViewContext<Self>,
316 ) -> Task<anyhow::Result<()>> {
317 self.results_editor
318 .update(cx, |editor, cx| editor.save(project, cx))
319 }
320
321 fn save_as(
322 &mut self,
323 _: ModelHandle<Project>,
324 _: PathBuf,
325 _: &mut ViewContext<Self>,
326 ) -> Task<anyhow::Result<()>> {
327 unreachable!("save_as should not have been called")
328 }
329
330 fn reload(
331 &mut self,
332 project: ModelHandle<Project>,
333 cx: &mut ViewContext<Self>,
334 ) -> Task<anyhow::Result<()>> {
335 self.results_editor
336 .update(cx, |editor, cx| editor.reload(project, cx))
337 }
338
339 fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self>
340 where
341 Self: Sized,
342 {
343 let model = self.model.update(cx, |model, cx| model.clone(cx));
344 Some(Self::new(model, cx))
345 }
346
347 fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
348 self.results_editor
349 .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
350 }
351
352 fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
353 self.results_editor.update(cx, |editor, _| {
354 editor.set_nav_history(Some(nav_history));
355 });
356 }
357
358 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
359 self.results_editor
360 .update(cx, |editor, cx| editor.navigate(data, cx))
361 }
362
363 fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
364 match event {
365 ViewEvent::UpdateTab => {
366 smallvec::smallvec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab]
367 }
368 ViewEvent::EditorEvent(editor_event) => Editor::to_item_events(editor_event),
369 _ => SmallVec::new(),
370 }
371 }
372
373 fn breadcrumb_location(&self) -> ToolbarItemLocation {
374 if self.has_matches() {
375 ToolbarItemLocation::Secondary
376 } else {
377 ToolbarItemLocation::Hidden
378 }
379 }
380
381 fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
382 self.results_editor.breadcrumbs(theme, cx)
383 }
384
385 fn serialized_item_kind() -> Option<&'static str> {
386 None
387 }
388
389 fn deserialize(
390 _project: ModelHandle<Project>,
391 _workspace: WeakViewHandle<Workspace>,
392 _workspace_id: workspace::WorkspaceId,
393 _item_id: workspace::ItemId,
394 _cx: &mut ViewContext<Pane>,
395 ) -> Task<anyhow::Result<ViewHandle<Self>>> {
396 unimplemented!()
397 }
398}
399
400impl ProjectSearchView {
401 fn new(model: ModelHandle<ProjectSearch>, cx: &mut ViewContext<Self>) -> Self {
402 let project;
403 let excerpts;
404 let mut query_text = String::new();
405 let mut regex = false;
406 let mut case_sensitive = false;
407 let mut whole_word = false;
408
409 {
410 let model = model.read(cx);
411 project = model.project.clone();
412 excerpts = model.excerpts.clone();
413 if let Some(active_query) = model.active_query.as_ref() {
414 query_text = active_query.as_str().to_string();
415 regex = active_query.is_regex();
416 case_sensitive = active_query.case_sensitive();
417 whole_word = active_query.whole_word();
418 }
419 }
420 cx.observe(&model, |this, _, cx| this.model_changed(cx))
421 .detach();
422
423 let query_editor = cx.add_view(|cx| {
424 let mut editor = Editor::single_line(
425 Some(Arc::new(|theme| theme.search.editor.input.clone())),
426 cx,
427 );
428 editor.set_text(query_text, cx);
429 editor
430 });
431 // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes
432 cx.subscribe(&query_editor, |_, _, event, cx| {
433 cx.emit(ViewEvent::EditorEvent(event.clone()))
434 })
435 .detach();
436
437 let results_editor = cx.add_view(|cx| {
438 let mut editor = Editor::for_multibuffer(excerpts, Some(project), cx);
439 editor.set_searchable(false);
440 editor
441 });
442 cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
443 .detach();
444
445 cx.subscribe(&results_editor, |this, _, event, cx| {
446 if matches!(event, editor::Event::SelectionsChanged { .. }) {
447 this.update_match_index(cx);
448 }
449 // Reraise editor events for workspace item activation purposes
450 cx.emit(ViewEvent::EditorEvent(event.clone()));
451 })
452 .detach();
453
454 let included_files_editor = cx.add_view(|cx| {
455 let mut editor = Editor::single_line(
456 Some(Arc::new(|theme| {
457 theme.search.include_exclude_editor.input.clone()
458 })),
459 cx,
460 );
461 editor.set_placeholder_text("Include: crates/**/*.toml", cx);
462
463 editor
464 });
465 // Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes
466 cx.subscribe(&included_files_editor, |_, _, event, cx| {
467 cx.emit(ViewEvent::EditorEvent(event.clone()))
468 })
469 .detach();
470
471 let excluded_files_editor = cx.add_view(|cx| {
472 let mut editor = Editor::single_line(
473 Some(Arc::new(|theme| {
474 theme.search.include_exclude_editor.input.clone()
475 })),
476 cx,
477 );
478 editor.set_placeholder_text("Exclude: vendor/*, *.lock", cx);
479
480 editor
481 });
482 // Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes
483 cx.subscribe(&excluded_files_editor, |_, _, event, cx| {
484 cx.emit(ViewEvent::EditorEvent(event.clone()))
485 })
486 .detach();
487
488 let mut this = ProjectSearchView {
489 search_id: model.read(cx).search_id,
490 model,
491 query_editor,
492 results_editor,
493 case_sensitive,
494 whole_word,
495 regex,
496 panels_with_errors: HashSet::new(),
497 active_match_index: None,
498 query_editor_was_focused: false,
499 included_files_editor,
500 excluded_files_editor,
501 };
502 this.model_changed(cx);
503 this
504 }
505
506 // Re-activate the most recently activated search or the most recent if it has been closed.
507 // If no search exists in the workspace, create a new one.
508 fn deploy(
509 workspace: &mut Workspace,
510 _: &workspace::NewSearch,
511 cx: &mut ViewContext<Workspace>,
512 ) {
513 // Clean up entries for dropped projects
514 cx.update_global(|state: &mut ActiveSearches, cx| {
515 state.0.retain(|project, _| project.is_upgradable(cx))
516 });
517
518 let active_search = cx
519 .global::<ActiveSearches>()
520 .0
521 .get(&workspace.project().downgrade());
522
523 let existing = active_search
524 .and_then(|active_search| {
525 workspace
526 .items_of_type::<ProjectSearchView>(cx)
527 .find(|search| search == active_search)
528 })
529 .or_else(|| workspace.item_of_type::<ProjectSearchView>(cx));
530
531 let query = workspace.active_item(cx).and_then(|item| {
532 let editor = item.act_as::<Editor>(cx)?;
533 let query = editor.query_suggestion(cx);
534 if query.is_empty() {
535 None
536 } else {
537 Some(query)
538 }
539 });
540
541 let search = if let Some(existing) = existing {
542 workspace.activate_item(&existing, cx);
543 existing
544 } else {
545 let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
546 let view = cx.add_view(|cx| ProjectSearchView::new(model, cx));
547 workspace.add_item(Box::new(view.clone()), cx);
548 view
549 };
550
551 search.update(cx, |search, cx| {
552 if let Some(query) = query {
553 search.set_query(&query, cx);
554 }
555 search.focus_query_editor(cx)
556 });
557 }
558
559 fn search(&mut self, cx: &mut ViewContext<Self>) {
560 if let Some(query) = self.build_search_query(cx) {
561 self.model.update(cx, |model, cx| model.search(query, cx));
562 }
563 }
564
565 fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
566 let text = self.query_editor.read(cx).text(cx);
567 let included_files =
568 match Self::load_glob_set(&self.included_files_editor.read(cx).text(cx)) {
569 Ok(included_files) => {
570 self.panels_with_errors.remove(&InputPanel::Include);
571 included_files
572 }
573 Err(_e) => {
574 self.panels_with_errors.insert(InputPanel::Include);
575 cx.notify();
576 return None;
577 }
578 };
579 let excluded_files =
580 match Self::load_glob_set(&self.excluded_files_editor.read(cx).text(cx)) {
581 Ok(excluded_files) => {
582 self.panels_with_errors.remove(&InputPanel::Exclude);
583 excluded_files
584 }
585 Err(_e) => {
586 self.panels_with_errors.insert(InputPanel::Exclude);
587 cx.notify();
588 return None;
589 }
590 };
591 if self.regex {
592 match SearchQuery::regex(
593 text,
594 self.whole_word,
595 self.case_sensitive,
596 included_files,
597 excluded_files,
598 ) {
599 Ok(query) => {
600 self.panels_with_errors.remove(&InputPanel::Query);
601 Some(query)
602 }
603 Err(_e) => {
604 self.panels_with_errors.insert(InputPanel::Query);
605 cx.notify();
606 None
607 }
608 }
609 } else {
610 Some(SearchQuery::text(
611 text,
612 self.whole_word,
613 self.case_sensitive,
614 included_files,
615 excluded_files,
616 ))
617 }
618 }
619
620 fn load_glob_set(text: &str) -> Result<Vec<GlobMatcher>> {
621 text.split(',')
622 .map(str::trim)
623 .filter(|glob_str| !glob_str.is_empty())
624 .map(|glob_str| anyhow::Ok(Glob::new(glob_str)?.compile_matcher()))
625 .collect()
626 }
627
628 fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
629 if let Some(index) = self.active_match_index {
630 let match_ranges = self.model.read(cx).match_ranges.clone();
631 let new_index = self.results_editor.update(cx, |editor, cx| {
632 editor.match_index_for_direction(&match_ranges, index, direction, cx)
633 });
634
635 let range_to_select = match_ranges[new_index].clone();
636 self.results_editor.update(cx, |editor, cx| {
637 editor.unfold_ranges([range_to_select.clone()], false, true, cx);
638 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
639 s.select_ranges([range_to_select])
640 });
641 });
642 }
643 }
644
645 fn focus_query_editor(&mut self, cx: &mut ViewContext<Self>) {
646 self.query_editor.update(cx, |query_editor, cx| {
647 query_editor.select_all(&SelectAll, cx);
648 });
649 self.query_editor_was_focused = true;
650 cx.focus(&self.query_editor);
651 }
652
653 fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
654 self.query_editor
655 .update(cx, |query_editor, cx| query_editor.set_text(query, cx));
656 }
657
658 fn focus_results_editor(&mut self, cx: &mut ViewContext<Self>) {
659 self.query_editor.update(cx, |query_editor, cx| {
660 let cursor = query_editor.selections.newest_anchor().head();
661 query_editor.change_selections(None, cx, |s| s.select_ranges([cursor.clone()..cursor]));
662 });
663 self.query_editor_was_focused = false;
664 cx.focus(&self.results_editor);
665 }
666
667 fn model_changed(&mut self, cx: &mut ViewContext<Self>) {
668 let match_ranges = self.model.read(cx).match_ranges.clone();
669 if match_ranges.is_empty() {
670 self.active_match_index = None;
671 } else {
672 let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id);
673 let is_new_search = self.search_id != prev_search_id;
674 self.results_editor.update(cx, |editor, cx| {
675 if is_new_search {
676 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
677 s.select_ranges(match_ranges.first().cloned())
678 });
679 }
680 editor.highlight_background::<Self>(
681 match_ranges,
682 |theme| theme.search.match_background,
683 cx,
684 );
685 });
686 if is_new_search && self.query_editor.is_focused(cx) {
687 self.focus_results_editor(cx);
688 }
689 }
690
691 cx.emit(ViewEvent::UpdateTab);
692 cx.notify();
693 }
694
695 fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
696 let results_editor = self.results_editor.read(cx);
697 let new_index = active_match_index(
698 &self.model.read(cx).match_ranges,
699 &results_editor.selections.newest_anchor().head(),
700 &results_editor.buffer().read(cx).snapshot(cx),
701 );
702 if self.active_match_index != new_index {
703 self.active_match_index = new_index;
704 cx.notify();
705 }
706 }
707
708 pub fn has_matches(&self) -> bool {
709 self.active_match_index.is_some()
710 }
711
712 fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
713 if let Some(search_view) = pane
714 .active_item()
715 .and_then(|item| item.downcast::<ProjectSearchView>())
716 {
717 search_view.update(cx, |search_view, cx| {
718 if !search_view.results_editor.is_focused(cx)
719 && !search_view.model.read(cx).match_ranges.is_empty()
720 {
721 return search_view.focus_results_editor(cx);
722 }
723 });
724 }
725
726 cx.propagate_action();
727 }
728}
729
730impl Default for ProjectSearchBar {
731 fn default() -> Self {
732 Self::new()
733 }
734}
735
736impl ProjectSearchBar {
737 pub fn new() -> Self {
738 Self {
739 active_project_search: Default::default(),
740 subscription: Default::default(),
741 }
742 }
743
744 fn search(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
745 if let Some(search_view) = self.active_project_search.as_ref() {
746 search_view.update(cx, |search_view, cx| search_view.search(cx));
747 }
748 }
749
750 fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) {
751 if let Some(search_view) = workspace
752 .active_item(cx)
753 .and_then(|item| item.downcast::<ProjectSearchView>())
754 {
755 let new_query = search_view.update(cx, |search_view, cx| {
756 let new_query = search_view.build_search_query(cx);
757 if new_query.is_some() {
758 if let Some(old_query) = search_view.model.read(cx).active_query.clone() {
759 search_view.query_editor.update(cx, |editor, cx| {
760 editor.set_text(old_query.as_str(), cx);
761 });
762 search_view.regex = old_query.is_regex();
763 search_view.whole_word = old_query.whole_word();
764 search_view.case_sensitive = old_query.case_sensitive();
765 }
766 }
767 new_query
768 });
769 if let Some(new_query) = new_query {
770 let model = cx.add_model(|cx| {
771 let mut model = ProjectSearch::new(workspace.project().clone(), cx);
772 model.search(new_query, cx);
773 model
774 });
775 workspace.add_item(
776 Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
777 cx,
778 );
779 }
780 }
781 }
782
783 fn select_next_match(pane: &mut Pane, _: &SelectNextMatch, cx: &mut ViewContext<Pane>) {
784 if let Some(search_view) = pane
785 .active_item()
786 .and_then(|item| item.downcast::<ProjectSearchView>())
787 {
788 search_view.update(cx, |view, cx| view.select_match(Direction::Next, cx));
789 } else {
790 cx.propagate_action();
791 }
792 }
793
794 fn select_prev_match(pane: &mut Pane, _: &SelectPrevMatch, cx: &mut ViewContext<Pane>) {
795 if let Some(search_view) = pane
796 .active_item()
797 .and_then(|item| item.downcast::<ProjectSearchView>())
798 {
799 search_view.update(cx, |view, cx| view.select_match(Direction::Prev, cx));
800 } else {
801 cx.propagate_action();
802 }
803 }
804
805 fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
806 self.cycle_field(Direction::Next, cx);
807 }
808
809 fn tab_previous(&mut self, _: &editor::TabPrev, cx: &mut ViewContext<Self>) {
810 self.cycle_field(Direction::Prev, cx);
811 }
812
813 fn cycle_field(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
814 let active_project_search = match &self.active_project_search {
815 Some(active_project_search) => active_project_search,
816
817 None => {
818 cx.propagate_action();
819 return;
820 }
821 };
822
823 active_project_search.update(cx, |project_view, cx| {
824 let views = &[
825 &project_view.query_editor,
826 &project_view.included_files_editor,
827 &project_view.excluded_files_editor,
828 ];
829
830 let current_index = match views
831 .iter()
832 .enumerate()
833 .find(|(_, view)| view.is_focused(cx))
834 {
835 Some((index, _)) => index,
836
837 None => {
838 cx.propagate_action();
839 return;
840 }
841 };
842
843 let new_index = match direction {
844 Direction::Next => (current_index + 1) % views.len(),
845 Direction::Prev if current_index == 0 => views.len() - 1,
846 Direction::Prev => (current_index - 1) % views.len(),
847 };
848 cx.focus(views[new_index]);
849 });
850 }
851
852 fn toggle_search_option(&mut self, option: SearchOption, cx: &mut ViewContext<Self>) -> bool {
853 if let Some(search_view) = self.active_project_search.as_ref() {
854 search_view.update(cx, |search_view, cx| {
855 let value = match option {
856 SearchOption::WholeWord => &mut search_view.whole_word,
857 SearchOption::CaseSensitive => &mut search_view.case_sensitive,
858 SearchOption::Regex => &mut search_view.regex,
859 };
860 *value = !*value;
861 search_view.search(cx);
862 });
863 cx.notify();
864 true
865 } else {
866 false
867 }
868 }
869
870 fn render_nav_button(
871 &self,
872 icon: &'static str,
873 direction: Direction,
874 cx: &mut ViewContext<Self>,
875 ) -> AnyElement<Self> {
876 let action: Box<dyn Action>;
877 let tooltip;
878 match direction {
879 Direction::Prev => {
880 action = Box::new(SelectPrevMatch);
881 tooltip = "Select Previous Match";
882 }
883 Direction::Next => {
884 action = Box::new(SelectNextMatch);
885 tooltip = "Select Next Match";
886 }
887 };
888 let tooltip_style = theme::current(cx).tooltip.clone();
889
890 enum NavButton {}
891 MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
892 let theme = theme::current(cx);
893 let style = theme.search.option_button.style_for(state, false);
894 Label::new(icon, style.text.clone())
895 .contained()
896 .with_style(style.container)
897 })
898 .on_click(MouseButton::Left, move |_, this, cx| {
899 if let Some(search) = this.active_project_search.as_ref() {
900 search.update(cx, |search, cx| search.select_match(direction, cx));
901 }
902 })
903 .with_cursor_style(CursorStyle::PointingHand)
904 .with_tooltip::<NavButton>(
905 direction as usize,
906 tooltip.to_string(),
907 Some(action),
908 tooltip_style,
909 cx,
910 )
911 .into_any()
912 }
913
914 fn render_option_button(
915 &self,
916 icon: &'static str,
917 option: SearchOption,
918 cx: &mut ViewContext<Self>,
919 ) -> AnyElement<Self> {
920 let tooltip_style = theme::current(cx).tooltip.clone();
921 let is_active = self.is_option_enabled(option, cx);
922 MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
923 let theme = theme::current(cx);
924 let style = theme.search.option_button.style_for(state, is_active);
925 Label::new(icon, style.text.clone())
926 .contained()
927 .with_style(style.container)
928 })
929 .on_click(MouseButton::Left, move |_, this, cx| {
930 this.toggle_search_option(option, cx);
931 })
932 .with_cursor_style(CursorStyle::PointingHand)
933 .with_tooltip::<Self>(
934 option as usize,
935 format!("Toggle {}", option.label()),
936 Some(option.to_toggle_action()),
937 tooltip_style,
938 cx,
939 )
940 .into_any()
941 }
942
943 fn is_option_enabled(&self, option: SearchOption, cx: &AppContext) -> bool {
944 if let Some(search) = self.active_project_search.as_ref() {
945 let search = search.read(cx);
946 match option {
947 SearchOption::WholeWord => search.whole_word,
948 SearchOption::CaseSensitive => search.case_sensitive,
949 SearchOption::Regex => search.regex,
950 }
951 } else {
952 false
953 }
954 }
955}
956
957impl Entity for ProjectSearchBar {
958 type Event = ();
959}
960
961impl View for ProjectSearchBar {
962 fn ui_name() -> &'static str {
963 "ProjectSearchBar"
964 }
965
966 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
967 if let Some(search) = self.active_project_search.as_ref() {
968 let search = search.read(cx);
969 let theme = theme::current(cx).clone();
970 let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) {
971 theme.search.invalid_editor
972 } else {
973 theme.search.editor.input.container
974 };
975 let include_container_style =
976 if search.panels_with_errors.contains(&InputPanel::Include) {
977 theme.search.invalid_include_exclude_editor
978 } else {
979 theme.search.include_exclude_editor.input.container
980 };
981 let exclude_container_style =
982 if search.panels_with_errors.contains(&InputPanel::Exclude) {
983 theme.search.invalid_include_exclude_editor
984 } else {
985 theme.search.include_exclude_editor.input.container
986 };
987
988 let included_files_view = ChildView::new(&search.included_files_editor, cx)
989 .aligned()
990 .left()
991 .flex(1.0, true);
992 let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx)
993 .aligned()
994 .right()
995 .flex(1.0, true);
996
997 let row_spacing = theme.workspace.toolbar.container.padding.bottom;
998
999 Flex::column()
1000 .with_child(
1001 Flex::row()
1002 .with_child(
1003 Flex::row()
1004 .with_child(
1005 ChildView::new(&search.query_editor, cx)
1006 .aligned()
1007 .left()
1008 .flex(1., true),
1009 )
1010 .with_children(search.active_match_index.map(|match_ix| {
1011 Label::new(
1012 format!(
1013 "{}/{}",
1014 match_ix + 1,
1015 search.model.read(cx).match_ranges.len()
1016 ),
1017 theme.search.match_index.text.clone(),
1018 )
1019 .contained()
1020 .with_style(theme.search.match_index.container)
1021 .aligned()
1022 }))
1023 .contained()
1024 .with_style(query_container_style)
1025 .aligned()
1026 .constrained()
1027 .with_min_width(theme.search.editor.min_width)
1028 .with_max_width(theme.search.editor.max_width)
1029 .flex(1., false),
1030 )
1031 .with_child(
1032 Flex::row()
1033 .with_child(self.render_nav_button("<", Direction::Prev, cx))
1034 .with_child(self.render_nav_button(">", Direction::Next, cx))
1035 .aligned(),
1036 )
1037 .with_child(
1038 Flex::row()
1039 .with_child(self.render_option_button(
1040 "Case",
1041 SearchOption::CaseSensitive,
1042 cx,
1043 ))
1044 .with_child(self.render_option_button(
1045 "Word",
1046 SearchOption::WholeWord,
1047 cx,
1048 ))
1049 .with_child(self.render_option_button(
1050 "Regex",
1051 SearchOption::Regex,
1052 cx,
1053 ))
1054 .contained()
1055 .with_style(theme.search.option_button_group)
1056 .aligned(),
1057 )
1058 .contained()
1059 .with_margin_bottom(row_spacing),
1060 )
1061 .with_child(
1062 Flex::row()
1063 .with_child(
1064 Flex::row()
1065 .with_child(included_files_view)
1066 .contained()
1067 .with_style(include_container_style)
1068 .aligned()
1069 .constrained()
1070 .with_min_width(theme.search.include_exclude_editor.min_width)
1071 .with_max_width(theme.search.include_exclude_editor.max_width)
1072 .flex(1., false),
1073 )
1074 .with_child(
1075 Flex::row()
1076 .with_child(excluded_files_view)
1077 .contained()
1078 .with_style(exclude_container_style)
1079 .aligned()
1080 .constrained()
1081 .with_min_width(theme.search.include_exclude_editor.min_width)
1082 .with_max_width(theme.search.include_exclude_editor.max_width)
1083 .flex(1., false),
1084 ),
1085 )
1086 .contained()
1087 .with_style(theme.search.container)
1088 .aligned()
1089 .left()
1090 .into_any_named("project search")
1091 } else {
1092 Empty::new().into_any()
1093 }
1094 }
1095}
1096
1097impl ToolbarItemView for ProjectSearchBar {
1098 fn set_active_pane_item(
1099 &mut self,
1100 active_pane_item: Option<&dyn ItemHandle>,
1101 cx: &mut ViewContext<Self>,
1102 ) -> ToolbarItemLocation {
1103 cx.notify();
1104 self.subscription = None;
1105 self.active_project_search = None;
1106 if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) {
1107 self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
1108 self.active_project_search = Some(search);
1109 ToolbarItemLocation::PrimaryLeft {
1110 flex: Some((1., false)),
1111 }
1112 } else {
1113 ToolbarItemLocation::Hidden
1114 }
1115 }
1116
1117 fn row_count(&self) -> usize {
1118 2
1119 }
1120}
1121
1122#[cfg(test)]
1123pub mod tests {
1124 use super::*;
1125 use editor::DisplayPoint;
1126 use gpui::{color::Color, executor::Deterministic, TestAppContext};
1127 use project::FakeFs;
1128 use serde_json::json;
1129 use settings::SettingsStore;
1130 use std::sync::Arc;
1131 use theme::ThemeSettings;
1132
1133 #[gpui::test]
1134 async fn test_project_search(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
1135 init_test(cx);
1136
1137 let fs = FakeFs::new(cx.background());
1138 fs.insert_tree(
1139 "/dir",
1140 json!({
1141 "one.rs": "const ONE: usize = 1;",
1142 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
1143 "three.rs": "const THREE: usize = one::ONE + two::TWO;",
1144 "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
1145 }),
1146 )
1147 .await;
1148 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
1149 let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
1150 let (_, search_view) = cx.add_window(|cx| ProjectSearchView::new(search.clone(), cx));
1151
1152 search_view.update(cx, |search_view, cx| {
1153 search_view
1154 .query_editor
1155 .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
1156 search_view.search(cx);
1157 });
1158 deterministic.run_until_parked();
1159 search_view.update(cx, |search_view, cx| {
1160 assert_eq!(
1161 search_view
1162 .results_editor
1163 .update(cx, |editor, cx| editor.display_text(cx)),
1164 "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;"
1165 );
1166 assert_eq!(
1167 search_view
1168 .results_editor
1169 .update(cx, |editor, cx| editor.all_background_highlights(cx)),
1170 &[
1171 (
1172 DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35),
1173 Color::red()
1174 ),
1175 (
1176 DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40),
1177 Color::red()
1178 ),
1179 (
1180 DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9),
1181 Color::red()
1182 )
1183 ]
1184 );
1185 assert_eq!(search_view.active_match_index, Some(0));
1186 assert_eq!(
1187 search_view
1188 .results_editor
1189 .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1190 [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
1191 );
1192
1193 search_view.select_match(Direction::Next, cx);
1194 });
1195
1196 search_view.update(cx, |search_view, cx| {
1197 assert_eq!(search_view.active_match_index, Some(1));
1198 assert_eq!(
1199 search_view
1200 .results_editor
1201 .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1202 [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
1203 );
1204 search_view.select_match(Direction::Next, cx);
1205 });
1206
1207 search_view.update(cx, |search_view, cx| {
1208 assert_eq!(search_view.active_match_index, Some(2));
1209 assert_eq!(
1210 search_view
1211 .results_editor
1212 .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1213 [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
1214 );
1215 search_view.select_match(Direction::Next, cx);
1216 });
1217
1218 search_view.update(cx, |search_view, cx| {
1219 assert_eq!(search_view.active_match_index, Some(0));
1220 assert_eq!(
1221 search_view
1222 .results_editor
1223 .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1224 [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
1225 );
1226 search_view.select_match(Direction::Prev, cx);
1227 });
1228
1229 search_view.update(cx, |search_view, cx| {
1230 assert_eq!(search_view.active_match_index, Some(2));
1231 assert_eq!(
1232 search_view
1233 .results_editor
1234 .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1235 [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
1236 );
1237 search_view.select_match(Direction::Prev, cx);
1238 });
1239
1240 search_view.update(cx, |search_view, cx| {
1241 assert_eq!(search_view.active_match_index, Some(1));
1242 assert_eq!(
1243 search_view
1244 .results_editor
1245 .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1246 [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
1247 );
1248 });
1249 }
1250
1251 #[gpui::test]
1252 async fn test_project_search_focus(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
1253 init_test(cx);
1254
1255 let fs = FakeFs::new(cx.background());
1256 fs.insert_tree(
1257 "/dir",
1258 json!({
1259 "one.rs": "const ONE: usize = 1;",
1260 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
1261 "three.rs": "const THREE: usize = one::ONE + two::TWO;",
1262 "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
1263 }),
1264 )
1265 .await;
1266 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
1267 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1268
1269 let active_item = cx.read(|cx| {
1270 workspace
1271 .read(cx)
1272 .active_pane()
1273 .read(cx)
1274 .active_item()
1275 .and_then(|item| item.downcast::<ProjectSearchView>())
1276 });
1277 assert!(
1278 active_item.is_none(),
1279 "Expected no search panel to be active, but got: {active_item:?}"
1280 );
1281
1282 workspace.update(cx, |workspace, cx| {
1283 ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
1284 });
1285
1286 let Some(search_view) = cx.read(|cx| {
1287 workspace
1288 .read(cx)
1289 .active_pane()
1290 .read(cx)
1291 .active_item()
1292 .and_then(|item| item.downcast::<ProjectSearchView>())
1293 }) else {
1294 panic!("Search view expected to appear after new search event trigger")
1295 };
1296 let search_view_id = search_view.id();
1297
1298 cx.spawn(
1299 |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
1300 )
1301 .detach();
1302 deterministic.run_until_parked();
1303 search_view.update(cx, |search_view, cx| {
1304 assert!(
1305 search_view.query_editor.is_focused(cx),
1306 "Empty search view should be focused after the toggle focus event: no results panel to focus on",
1307 );
1308 });
1309
1310 search_view.update(cx, |search_view, cx| {
1311 let query_editor = &search_view.query_editor;
1312 assert!(
1313 query_editor.is_focused(cx),
1314 "Search view should be focused after the new search view is activated",
1315 );
1316 let query_text = query_editor.read(cx).text(cx);
1317 assert!(
1318 query_text.is_empty(),
1319 "New search query should be empty but got '{query_text}'",
1320 );
1321 let results_text = search_view
1322 .results_editor
1323 .update(cx, |editor, cx| editor.display_text(cx));
1324 assert!(
1325 results_text.is_empty(),
1326 "Empty search view should have no results but got '{results_text}'"
1327 );
1328 });
1329
1330 search_view.update(cx, |search_view, cx| {
1331 search_view.query_editor.update(cx, |query_editor, cx| {
1332 query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx)
1333 });
1334 search_view.search(cx);
1335 });
1336 deterministic.run_until_parked();
1337 search_view.update(cx, |search_view, cx| {
1338 let results_text = search_view
1339 .results_editor
1340 .update(cx, |editor, cx| editor.display_text(cx));
1341 assert!(
1342 results_text.is_empty(),
1343 "Search view for mismatching query should have no results but got '{results_text}'"
1344 );
1345 assert!(
1346 search_view.query_editor.is_focused(cx),
1347 "Search view should be focused after mismatching query had been used in search",
1348 );
1349 });
1350 cx.spawn(
1351 |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
1352 )
1353 .detach();
1354 deterministic.run_until_parked();
1355 search_view.update(cx, |search_view, cx| {
1356 assert!(
1357 search_view.query_editor.is_focused(cx),
1358 "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on",
1359 );
1360 });
1361
1362 search_view.update(cx, |search_view, cx| {
1363 search_view
1364 .query_editor
1365 .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
1366 search_view.search(cx);
1367 });
1368 deterministic.run_until_parked();
1369 search_view.update(cx, |search_view, cx| {
1370 assert_eq!(
1371 search_view
1372 .results_editor
1373 .update(cx, |editor, cx| editor.display_text(cx)),
1374 "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
1375 "Search view results should match the query"
1376 );
1377 assert!(
1378 search_view.results_editor.is_focused(cx),
1379 "Search view with mismatching query should be focused after search results are available",
1380 );
1381 });
1382 cx.spawn(
1383 |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
1384 )
1385 .detach();
1386 deterministic.run_until_parked();
1387 search_view.update(cx, |search_view, cx| {
1388 assert!(
1389 search_view.results_editor.is_focused(cx),
1390 "Search view with matching query should still have its results editor focused after the toggle focus event",
1391 );
1392 });
1393
1394 workspace.update(cx, |workspace, cx| {
1395 ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
1396 });
1397 search_view.update(cx, |search_view, cx| {
1398 assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row");
1399 assert_eq!(
1400 search_view
1401 .results_editor
1402 .update(cx, |editor, cx| editor.display_text(cx)),
1403 "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
1404 "Results should be unchanged after search view 2nd open in a row"
1405 );
1406 assert!(
1407 search_view.query_editor.is_focused(cx),
1408 "Focus should be moved into query editor again after search view 2nd open in a row"
1409 );
1410 });
1411
1412 cx.spawn(
1413 |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
1414 )
1415 .detach();
1416 deterministic.run_until_parked();
1417 search_view.update(cx, |search_view, cx| {
1418 assert!(
1419 search_view.results_editor.is_focused(cx),
1420 "Search view with matching query should switch focus to the results editor after the toggle focus event",
1421 );
1422 });
1423 }
1424
1425 pub fn init_test(cx: &mut TestAppContext) {
1426 cx.foreground().forbid_parking();
1427 let fonts = cx.font_cache();
1428 let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
1429 theme.search.match_background = Color::red();
1430
1431 cx.update(|cx| {
1432 cx.set_global(SettingsStore::test(cx));
1433 cx.set_global(ActiveSearches::default());
1434
1435 theme::init((), cx);
1436 cx.update_global::<SettingsStore, _, _>(|store, _| {
1437 let mut settings = store.get::<ThemeSettings>(None).clone();
1438 settings.theme = Arc::new(theme);
1439 store.override_global(settings)
1440 });
1441
1442 language::init(cx);
1443 client::init_settings(cx);
1444 editor::init(cx);
1445 workspace::init_settings(cx);
1446 Project::init_settings(cx);
1447 super::init(cx);
1448 });
1449 }
1450}