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