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 editor.unfold_ranges([range_to_select.clone()], false, true, cx);
636 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
637 s.select_ranges([range_to_select])
638 });
639 });
640 }
641 }
642
643 fn focus_query_editor(&mut self, cx: &mut ViewContext<Self>) {
644 self.query_editor.update(cx, |query_editor, cx| {
645 query_editor.select_all(&SelectAll, cx);
646 });
647 self.query_editor_was_focused = true;
648 cx.focus(&self.query_editor);
649 }
650
651 fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
652 self.query_editor
653 .update(cx, |query_editor, cx| query_editor.set_text(query, cx));
654 }
655
656 fn focus_results_editor(&mut self, cx: &mut ViewContext<Self>) {
657 self.query_editor.update(cx, |query_editor, cx| {
658 let cursor = query_editor.selections.newest_anchor().head();
659 query_editor.change_selections(None, cx, |s| s.select_ranges([cursor.clone()..cursor]));
660 });
661 self.query_editor_was_focused = false;
662 cx.focus(&self.results_editor);
663 }
664
665 fn model_changed(&mut self, cx: &mut ViewContext<Self>) {
666 let match_ranges = self.model.read(cx).match_ranges.clone();
667 if match_ranges.is_empty() {
668 self.active_match_index = None;
669 } else {
670 self.active_match_index = Some(0);
671 self.update_match_index(cx);
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.search_options = SearchOptions::from_query(&old_query);
763 }
764 }
765 new_query
766 });
767 if let Some(new_query) = new_query {
768 let model = cx.add_model(|cx| {
769 let mut model = ProjectSearch::new(workspace.project().clone(), cx);
770 model.search(new_query, cx);
771 model
772 });
773 workspace.add_item(
774 Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
775 cx,
776 );
777 }
778 }
779 }
780
781 fn select_next_match(pane: &mut Pane, _: &SelectNextMatch, cx: &mut ViewContext<Pane>) {
782 if let Some(search_view) = pane
783 .active_item()
784 .and_then(|item| item.downcast::<ProjectSearchView>())
785 {
786 search_view.update(cx, |view, cx| view.select_match(Direction::Next, cx));
787 } else {
788 cx.propagate_action();
789 }
790 }
791
792 fn select_prev_match(pane: &mut Pane, _: &SelectPrevMatch, cx: &mut ViewContext<Pane>) {
793 if let Some(search_view) = pane
794 .active_item()
795 .and_then(|item| item.downcast::<ProjectSearchView>())
796 {
797 search_view.update(cx, |view, cx| view.select_match(Direction::Prev, cx));
798 } else {
799 cx.propagate_action();
800 }
801 }
802
803 fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
804 self.cycle_field(Direction::Next, cx);
805 }
806
807 fn tab_previous(&mut self, _: &editor::TabPrev, cx: &mut ViewContext<Self>) {
808 self.cycle_field(Direction::Prev, cx);
809 }
810
811 fn cycle_field(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
812 let active_project_search = match &self.active_project_search {
813 Some(active_project_search) => active_project_search,
814
815 None => {
816 cx.propagate_action();
817 return;
818 }
819 };
820
821 active_project_search.update(cx, |project_view, cx| {
822 let views = &[
823 &project_view.query_editor,
824 &project_view.included_files_editor,
825 &project_view.excluded_files_editor,
826 ];
827
828 let current_index = match views
829 .iter()
830 .enumerate()
831 .find(|(_, view)| view.is_focused(cx))
832 {
833 Some((index, _)) => index,
834
835 None => {
836 cx.propagate_action();
837 return;
838 }
839 };
840
841 let new_index = match direction {
842 Direction::Next => (current_index + 1) % views.len(),
843 Direction::Prev if current_index == 0 => views.len() - 1,
844 Direction::Prev => (current_index - 1) % views.len(),
845 };
846 cx.focus(views[new_index]);
847 });
848 }
849
850 fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) -> bool {
851 if let Some(search_view) = self.active_project_search.as_ref() {
852 search_view.update(cx, |search_view, cx| {
853 search_view.search_options.toggle(option);
854 search_view.search(cx);
855 });
856 cx.notify();
857 true
858 } else {
859 false
860 }
861 }
862
863 fn render_nav_button(
864 &self,
865 icon: &'static str,
866 direction: Direction,
867 cx: &mut ViewContext<Self>,
868 ) -> AnyElement<Self> {
869 let action: Box<dyn Action>;
870 let tooltip;
871 match direction {
872 Direction::Prev => {
873 action = Box::new(SelectPrevMatch);
874 tooltip = "Select Previous Match";
875 }
876 Direction::Next => {
877 action = Box::new(SelectNextMatch);
878 tooltip = "Select Next Match";
879 }
880 };
881 let tooltip_style = theme::current(cx).tooltip.clone();
882
883 enum NavButton {}
884 MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
885 let theme = theme::current(cx);
886 let style = theme.search.option_button.inactive_state().style_for(state);
887 Label::new(icon, style.text.clone())
888 .contained()
889 .with_style(style.container)
890 })
891 .on_click(MouseButton::Left, move |_, this, cx| {
892 if let Some(search) = this.active_project_search.as_ref() {
893 search.update(cx, |search, cx| search.select_match(direction, cx));
894 }
895 })
896 .with_cursor_style(CursorStyle::PointingHand)
897 .with_tooltip::<NavButton>(
898 direction as usize,
899 tooltip.to_string(),
900 Some(action),
901 tooltip_style,
902 cx,
903 )
904 .into_any()
905 }
906
907 fn render_option_button(
908 &self,
909 icon: &'static str,
910 option: SearchOptions,
911 cx: &mut ViewContext<Self>,
912 ) -> AnyElement<Self> {
913 let tooltip_style = theme::current(cx).tooltip.clone();
914 let is_active = self.is_option_enabled(option, cx);
915 MouseEventHandler::<Self, _>::new(option.bits as usize, cx, |state, cx| {
916 let theme = theme::current(cx);
917 let style = theme
918 .search
919 .option_button
920 .in_state(is_active)
921 .style_for(state);
922 Label::new(icon, style.text.clone())
923 .contained()
924 .with_style(style.container)
925 })
926 .on_click(MouseButton::Left, move |_, this, cx| {
927 this.toggle_search_option(option, cx);
928 })
929 .with_cursor_style(CursorStyle::PointingHand)
930 .with_tooltip::<Self>(
931 option.bits as usize,
932 format!("Toggle {}", option.label()),
933 Some(option.to_toggle_action()),
934 tooltip_style,
935 cx,
936 )
937 .into_any()
938 }
939
940 fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool {
941 if let Some(search) = self.active_project_search.as_ref() {
942 search.read(cx).search_options.contains(option)
943 } else {
944 false
945 }
946 }
947}
948
949impl Entity for ProjectSearchBar {
950 type Event = ();
951}
952
953impl View for ProjectSearchBar {
954 fn ui_name() -> &'static str {
955 "ProjectSearchBar"
956 }
957
958 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
959 if let Some(search) = self.active_project_search.as_ref() {
960 let search = search.read(cx);
961 let theme = theme::current(cx).clone();
962 let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) {
963 theme.search.invalid_editor
964 } else {
965 theme.search.editor.input.container
966 };
967 let include_container_style =
968 if search.panels_with_errors.contains(&InputPanel::Include) {
969 theme.search.invalid_include_exclude_editor
970 } else {
971 theme.search.include_exclude_editor.input.container
972 };
973 let exclude_container_style =
974 if search.panels_with_errors.contains(&InputPanel::Exclude) {
975 theme.search.invalid_include_exclude_editor
976 } else {
977 theme.search.include_exclude_editor.input.container
978 };
979
980 let included_files_view = ChildView::new(&search.included_files_editor, cx)
981 .aligned()
982 .left()
983 .flex(1.0, true);
984 let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx)
985 .aligned()
986 .right()
987 .flex(1.0, true);
988
989 let row_spacing = theme.workspace.toolbar.container.padding.bottom;
990
991 Flex::column()
992 .with_child(
993 Flex::row()
994 .with_child(
995 Flex::row()
996 .with_child(
997 ChildView::new(&search.query_editor, cx)
998 .aligned()
999 .left()
1000 .flex(1., true),
1001 )
1002 .with_children(search.active_match_index.map(|match_ix| {
1003 Label::new(
1004 format!(
1005 "{}/{}",
1006 match_ix + 1,
1007 search.model.read(cx).match_ranges.len()
1008 ),
1009 theme.search.match_index.text.clone(),
1010 )
1011 .contained()
1012 .with_style(theme.search.match_index.container)
1013 .aligned()
1014 }))
1015 .contained()
1016 .with_style(query_container_style)
1017 .aligned()
1018 .constrained()
1019 .with_min_width(theme.search.editor.min_width)
1020 .with_max_width(theme.search.editor.max_width)
1021 .flex(1., false),
1022 )
1023 .with_child(
1024 Flex::row()
1025 .with_child(self.render_nav_button("<", Direction::Prev, cx))
1026 .with_child(self.render_nav_button(">", Direction::Next, cx))
1027 .aligned(),
1028 )
1029 .with_child(
1030 Flex::row()
1031 .with_child(self.render_option_button(
1032 "Case",
1033 SearchOptions::CASE_SENSITIVE,
1034 cx,
1035 ))
1036 .with_child(self.render_option_button(
1037 "Word",
1038 SearchOptions::WHOLE_WORD,
1039 cx,
1040 ))
1041 .with_child(self.render_option_button(
1042 "Regex",
1043 SearchOptions::REGEX,
1044 cx,
1045 ))
1046 .contained()
1047 .with_style(theme.search.option_button_group)
1048 .aligned(),
1049 )
1050 .contained()
1051 .with_margin_bottom(row_spacing),
1052 )
1053 .with_child(
1054 Flex::row()
1055 .with_child(
1056 Flex::row()
1057 .with_child(included_files_view)
1058 .contained()
1059 .with_style(include_container_style)
1060 .aligned()
1061 .constrained()
1062 .with_min_width(theme.search.include_exclude_editor.min_width)
1063 .with_max_width(theme.search.include_exclude_editor.max_width)
1064 .flex(1., false),
1065 )
1066 .with_child(
1067 Flex::row()
1068 .with_child(excluded_files_view)
1069 .contained()
1070 .with_style(exclude_container_style)
1071 .aligned()
1072 .constrained()
1073 .with_min_width(theme.search.include_exclude_editor.min_width)
1074 .with_max_width(theme.search.include_exclude_editor.max_width)
1075 .flex(1., false),
1076 ),
1077 )
1078 .contained()
1079 .with_style(theme.search.container)
1080 .aligned()
1081 .left()
1082 .into_any_named("project search")
1083 } else {
1084 Empty::new().into_any()
1085 }
1086 }
1087}
1088
1089impl ToolbarItemView for ProjectSearchBar {
1090 fn set_active_pane_item(
1091 &mut self,
1092 active_pane_item: Option<&dyn ItemHandle>,
1093 cx: &mut ViewContext<Self>,
1094 ) -> ToolbarItemLocation {
1095 cx.notify();
1096 self.subscription = None;
1097 self.active_project_search = None;
1098 if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) {
1099 self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
1100 self.active_project_search = Some(search);
1101 ToolbarItemLocation::PrimaryLeft {
1102 flex: Some((1., false)),
1103 }
1104 } else {
1105 ToolbarItemLocation::Hidden
1106 }
1107 }
1108
1109 fn row_count(&self) -> usize {
1110 2
1111 }
1112}
1113
1114#[cfg(test)]
1115pub mod tests {
1116 use super::*;
1117 use editor::DisplayPoint;
1118 use gpui::{color::Color, executor::Deterministic, TestAppContext};
1119 use project::FakeFs;
1120 use serde_json::json;
1121 use settings::SettingsStore;
1122 use std::sync::Arc;
1123 use theme::ThemeSettings;
1124
1125 #[gpui::test]
1126 async fn test_project_search(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
1127 init_test(cx);
1128
1129 let fs = FakeFs::new(cx.background());
1130 fs.insert_tree(
1131 "/dir",
1132 json!({
1133 "one.rs": "const ONE: usize = 1;",
1134 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
1135 "three.rs": "const THREE: usize = one::ONE + two::TWO;",
1136 "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
1137 }),
1138 )
1139 .await;
1140 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
1141 let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
1142 let (_, search_view) = cx.add_window(|cx| ProjectSearchView::new(search.clone(), cx));
1143
1144 search_view.update(cx, |search_view, cx| {
1145 search_view
1146 .query_editor
1147 .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
1148 search_view.search(cx);
1149 });
1150 deterministic.run_until_parked();
1151 search_view.update(cx, |search_view, cx| {
1152 assert_eq!(
1153 search_view
1154 .results_editor
1155 .update(cx, |editor, cx| editor.display_text(cx)),
1156 "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;"
1157 );
1158 assert_eq!(
1159 search_view
1160 .results_editor
1161 .update(cx, |editor, cx| editor.all_background_highlights(cx)),
1162 &[
1163 (
1164 DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35),
1165 Color::red()
1166 ),
1167 (
1168 DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40),
1169 Color::red()
1170 ),
1171 (
1172 DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9),
1173 Color::red()
1174 )
1175 ]
1176 );
1177 assert_eq!(search_view.active_match_index, Some(0));
1178 assert_eq!(
1179 search_view
1180 .results_editor
1181 .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1182 [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
1183 );
1184
1185 search_view.select_match(Direction::Next, cx);
1186 });
1187
1188 search_view.update(cx, |search_view, cx| {
1189 assert_eq!(search_view.active_match_index, Some(1));
1190 assert_eq!(
1191 search_view
1192 .results_editor
1193 .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1194 [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
1195 );
1196 search_view.select_match(Direction::Next, cx);
1197 });
1198
1199 search_view.update(cx, |search_view, cx| {
1200 assert_eq!(search_view.active_match_index, Some(2));
1201 assert_eq!(
1202 search_view
1203 .results_editor
1204 .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1205 [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
1206 );
1207 search_view.select_match(Direction::Next, cx);
1208 });
1209
1210 search_view.update(cx, |search_view, cx| {
1211 assert_eq!(search_view.active_match_index, Some(0));
1212 assert_eq!(
1213 search_view
1214 .results_editor
1215 .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1216 [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
1217 );
1218 search_view.select_match(Direction::Prev, cx);
1219 });
1220
1221 search_view.update(cx, |search_view, cx| {
1222 assert_eq!(search_view.active_match_index, Some(2));
1223 assert_eq!(
1224 search_view
1225 .results_editor
1226 .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1227 [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
1228 );
1229 search_view.select_match(Direction::Prev, cx);
1230 });
1231
1232 search_view.update(cx, |search_view, cx| {
1233 assert_eq!(search_view.active_match_index, Some(1));
1234 assert_eq!(
1235 search_view
1236 .results_editor
1237 .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1238 [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
1239 );
1240 });
1241 }
1242
1243 #[gpui::test]
1244 async fn test_project_search_focus(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
1245 init_test(cx);
1246
1247 let fs = FakeFs::new(cx.background());
1248 fs.insert_tree(
1249 "/dir",
1250 json!({
1251 "one.rs": "const ONE: usize = 1;",
1252 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
1253 "three.rs": "const THREE: usize = one::ONE + two::TWO;",
1254 "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
1255 }),
1256 )
1257 .await;
1258 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
1259 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1260
1261 let active_item = cx.read(|cx| {
1262 workspace
1263 .read(cx)
1264 .active_pane()
1265 .read(cx)
1266 .active_item()
1267 .and_then(|item| item.downcast::<ProjectSearchView>())
1268 });
1269 assert!(
1270 active_item.is_none(),
1271 "Expected no search panel to be active, but got: {active_item:?}"
1272 );
1273
1274 workspace.update(cx, |workspace, cx| {
1275 ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
1276 });
1277
1278 let Some(search_view) = cx.read(|cx| {
1279 workspace
1280 .read(cx)
1281 .active_pane()
1282 .read(cx)
1283 .active_item()
1284 .and_then(|item| item.downcast::<ProjectSearchView>())
1285 }) else {
1286 panic!("Search view expected to appear after new search event trigger")
1287 };
1288 let search_view_id = search_view.id();
1289
1290 cx.spawn(
1291 |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
1292 )
1293 .detach();
1294 deterministic.run_until_parked();
1295 search_view.update(cx, |search_view, cx| {
1296 assert!(
1297 search_view.query_editor.is_focused(cx),
1298 "Empty search view should be focused after the toggle focus event: no results panel to focus on",
1299 );
1300 });
1301
1302 search_view.update(cx, |search_view, cx| {
1303 let query_editor = &search_view.query_editor;
1304 assert!(
1305 query_editor.is_focused(cx),
1306 "Search view should be focused after the new search view is activated",
1307 );
1308 let query_text = query_editor.read(cx).text(cx);
1309 assert!(
1310 query_text.is_empty(),
1311 "New search query should be empty but got '{query_text}'",
1312 );
1313 let results_text = search_view
1314 .results_editor
1315 .update(cx, |editor, cx| editor.display_text(cx));
1316 assert!(
1317 results_text.is_empty(),
1318 "Empty search view should have no results but got '{results_text}'"
1319 );
1320 });
1321
1322 search_view.update(cx, |search_view, cx| {
1323 search_view.query_editor.update(cx, |query_editor, cx| {
1324 query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx)
1325 });
1326 search_view.search(cx);
1327 });
1328 deterministic.run_until_parked();
1329 search_view.update(cx, |search_view, cx| {
1330 let results_text = search_view
1331 .results_editor
1332 .update(cx, |editor, cx| editor.display_text(cx));
1333 assert!(
1334 results_text.is_empty(),
1335 "Search view for mismatching query should have no results but got '{results_text}'"
1336 );
1337 assert!(
1338 search_view.query_editor.is_focused(cx),
1339 "Search view should be focused after mismatching query had been used in search",
1340 );
1341 });
1342 cx.spawn(
1343 |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
1344 )
1345 .detach();
1346 deterministic.run_until_parked();
1347 search_view.update(cx, |search_view, cx| {
1348 assert!(
1349 search_view.query_editor.is_focused(cx),
1350 "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on",
1351 );
1352 });
1353
1354 search_view.update(cx, |search_view, cx| {
1355 search_view
1356 .query_editor
1357 .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
1358 search_view.search(cx);
1359 });
1360 deterministic.run_until_parked();
1361 search_view.update(cx, |search_view, cx| {
1362 assert_eq!(
1363 search_view
1364 .results_editor
1365 .update(cx, |editor, cx| editor.display_text(cx)),
1366 "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
1367 "Search view results should match the query"
1368 );
1369 assert!(
1370 search_view.results_editor.is_focused(cx),
1371 "Search view with mismatching query should be focused after search results are available",
1372 );
1373 });
1374 cx.spawn(
1375 |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
1376 )
1377 .detach();
1378 deterministic.run_until_parked();
1379 search_view.update(cx, |search_view, cx| {
1380 assert!(
1381 search_view.results_editor.is_focused(cx),
1382 "Search view with matching query should still have its results editor focused after the toggle focus event",
1383 );
1384 });
1385
1386 workspace.update(cx, |workspace, cx| {
1387 ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
1388 });
1389 search_view.update(cx, |search_view, cx| {
1390 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");
1391 assert_eq!(
1392 search_view
1393 .results_editor
1394 .update(cx, |editor, cx| editor.display_text(cx)),
1395 "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
1396 "Results should be unchanged after search view 2nd open in a row"
1397 );
1398 assert!(
1399 search_view.query_editor.is_focused(cx),
1400 "Focus should be moved into query editor again after search view 2nd open in a row"
1401 );
1402 });
1403
1404 cx.spawn(
1405 |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
1406 )
1407 .detach();
1408 deterministic.run_until_parked();
1409 search_view.update(cx, |search_view, cx| {
1410 assert!(
1411 search_view.results_editor.is_focused(cx),
1412 "Search view with matching query should switch focus to the results editor after the toggle focus event",
1413 );
1414 });
1415 }
1416
1417 pub fn init_test(cx: &mut TestAppContext) {
1418 cx.foreground().forbid_parking();
1419 let fonts = cx.font_cache();
1420 let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
1421 theme.search.match_background = Color::red();
1422
1423 cx.update(|cx| {
1424 cx.set_global(SettingsStore::test(cx));
1425 cx.set_global(ActiveSearches::default());
1426
1427 theme::init((), cx);
1428 cx.update_global::<SettingsStore, _, _>(|store, _| {
1429 let mut settings = store.get::<ThemeSettings>(None).clone();
1430 settings.theme = Arc::new(theme);
1431 store.override_global(settings)
1432 });
1433
1434 language::init(cx);
1435 client::init_settings(cx);
1436 editor::init(cx);
1437 workspace::init_settings(cx);
1438 Project::init_settings(cx);
1439 super::init(cx);
1440 });
1441 }
1442}