recent_projects.rs

  1pub mod disconnected_overlay;
  2mod remote_servers;
  3mod ssh_config;
  4mod ssh_connections;
  5
  6pub use ssh_connections::{is_connecting_over_ssh, open_ssh_project};
  7
  8use disconnected_overlay::DisconnectedOverlay;
  9use fuzzy::{StringMatch, StringMatchCandidate};
 10use gpui::{
 11    Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
 12    Subscription, Task, WeakEntity, Window,
 13};
 14use ordered_float::OrderedFloat;
 15use picker::{
 16    Picker, PickerDelegate,
 17    highlighted_match_with_paths::{HighlightedMatch, HighlightedMatchWithPaths},
 18};
 19pub use remote_servers::RemoteServerProjects;
 20use settings::Settings;
 21pub use ssh_connections::SshSettings;
 22use std::{
 23    path::{Path, PathBuf},
 24    sync::Arc,
 25};
 26use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container};
 27use util::{ResultExt, paths::PathExt};
 28use workspace::{
 29    CloseIntent, HistoryManager, ModalView, OpenOptions, SerializedWorkspaceLocation, WORKSPACE_DB,
 30    Workspace, WorkspaceId, with_active_or_new_workspace,
 31};
 32use zed_actions::{OpenRecent, OpenRemote};
 33
 34pub fn init(cx: &mut App) {
 35    SshSettings::register(cx);
 36    cx.on_action(|open_recent: &OpenRecent, cx| {
 37        let create_new_window = open_recent.create_new_window;
 38        with_active_or_new_workspace(cx, move |workspace, window, cx| {
 39            let Some(recent_projects) = workspace.active_modal::<RecentProjects>(cx) else {
 40                RecentProjects::open(workspace, create_new_window, window, cx);
 41                return;
 42            };
 43
 44            recent_projects.update(cx, |recent_projects, cx| {
 45                recent_projects
 46                    .picker
 47                    .update(cx, |picker, cx| picker.cycle_selection(window, cx))
 48            });
 49        });
 50    });
 51    cx.on_action(|open_remote: &OpenRemote, cx| {
 52        let from_existing_connection = open_remote.from_existing_connection;
 53        with_active_or_new_workspace(cx, move |workspace, window, cx| {
 54            if from_existing_connection {
 55                cx.propagate();
 56                return;
 57            }
 58            let handle = cx.entity().downgrade();
 59            let fs = workspace.project().read(cx).fs().clone();
 60            workspace.toggle_modal(window, cx, |window, cx| {
 61                RemoteServerProjects::new(fs, window, cx, handle)
 62            })
 63        });
 64    });
 65
 66    cx.observe_new(DisconnectedOverlay::register).detach();
 67}
 68
 69pub struct RecentProjects {
 70    pub picker: Entity<Picker<RecentProjectsDelegate>>,
 71    rem_width: f32,
 72    _subscription: Subscription,
 73}
 74
 75impl ModalView for RecentProjects {}
 76
 77impl RecentProjects {
 78    fn new(
 79        delegate: RecentProjectsDelegate,
 80        rem_width: f32,
 81        window: &mut Window,
 82        cx: &mut Context<Self>,
 83    ) -> Self {
 84        let picker = cx.new(|cx| {
 85            // We want to use a list when we render paths, because the items can have different heights (multiple paths).
 86            if delegate.render_paths {
 87                Picker::list(delegate, window, cx)
 88            } else {
 89                Picker::uniform_list(delegate, window, cx)
 90            }
 91        });
 92        let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
 93        // We do not want to block the UI on a potentially lengthy call to DB, so we're gonna swap
 94        // out workspace locations once the future runs to completion.
 95        cx.spawn_in(window, async move |this, cx| {
 96            let workspaces = WORKSPACE_DB
 97                .recent_workspaces_on_disk()
 98                .await
 99                .log_err()
100                .unwrap_or_default();
101            this.update_in(cx, move |this, window, cx| {
102                this.picker.update(cx, move |picker, cx| {
103                    picker.delegate.set_workspaces(workspaces);
104                    picker.update_matches(picker.query(cx), window, cx)
105                })
106            })
107            .ok()
108        })
109        .detach();
110        Self {
111            picker,
112            rem_width,
113            _subscription,
114        }
115    }
116
117    pub fn open(
118        workspace: &mut Workspace,
119        create_new_window: bool,
120        window: &mut Window,
121        cx: &mut Context<Workspace>,
122    ) {
123        let weak = cx.entity().downgrade();
124        workspace.toggle_modal(window, cx, |window, cx| {
125            let delegate = RecentProjectsDelegate::new(weak, create_new_window, true);
126
127            Self::new(delegate, 34., window, cx)
128        })
129    }
130}
131
132impl EventEmitter<DismissEvent> for RecentProjects {}
133
134impl Focusable for RecentProjects {
135    fn focus_handle(&self, cx: &App) -> FocusHandle {
136        self.picker.focus_handle(cx)
137    }
138}
139
140impl Render for RecentProjects {
141    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
142        v_flex()
143            .w(rems(self.rem_width))
144            .child(self.picker.clone())
145            .on_mouse_down_out(cx.listener(|this, _, window, cx| {
146                this.picker.update(cx, |this, cx| {
147                    this.cancel(&Default::default(), window, cx);
148                })
149            }))
150    }
151}
152
153pub struct RecentProjectsDelegate {
154    workspace: WeakEntity<Workspace>,
155    workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>,
156    selected_match_index: usize,
157    matches: Vec<StringMatch>,
158    render_paths: bool,
159    create_new_window: bool,
160    // Flag to reset index when there is a new query vs not reset index when user delete an item
161    reset_selected_match_index: bool,
162    has_any_non_local_projects: bool,
163}
164
165impl RecentProjectsDelegate {
166    fn new(workspace: WeakEntity<Workspace>, create_new_window: bool, render_paths: bool) -> Self {
167        Self {
168            workspace,
169            workspaces: Vec::new(),
170            selected_match_index: 0,
171            matches: Default::default(),
172            create_new_window,
173            render_paths,
174            reset_selected_match_index: true,
175            has_any_non_local_projects: false,
176        }
177    }
178
179    pub fn set_workspaces(&mut self, workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>) {
180        self.workspaces = workspaces;
181        self.has_any_non_local_projects = !self
182            .workspaces
183            .iter()
184            .all(|(_, location)| matches!(location, SerializedWorkspaceLocation::Local(_, _)));
185    }
186}
187impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
188impl PickerDelegate for RecentProjectsDelegate {
189    type ListItem = ListItem;
190
191    fn placeholder_text(&self, window: &mut Window, _: &mut App) -> Arc<str> {
192        let (create_window, reuse_window) = if self.create_new_window {
193            (
194                window.keystroke_text_for(&menu::Confirm),
195                window.keystroke_text_for(&menu::SecondaryConfirm),
196            )
197        } else {
198            (
199                window.keystroke_text_for(&menu::SecondaryConfirm),
200                window.keystroke_text_for(&menu::Confirm),
201            )
202        };
203        Arc::from(format!(
204            "{reuse_window} reuses this window, {create_window} opens a new one",
205        ))
206    }
207
208    fn match_count(&self) -> usize {
209        self.matches.len()
210    }
211
212    fn selected_index(&self) -> usize {
213        self.selected_match_index
214    }
215
216    fn set_selected_index(
217        &mut self,
218        ix: usize,
219        _window: &mut Window,
220        _cx: &mut Context<Picker<Self>>,
221    ) {
222        self.selected_match_index = ix;
223    }
224
225    fn update_matches(
226        &mut self,
227        query: String,
228        _: &mut Window,
229        cx: &mut Context<Picker<Self>>,
230    ) -> gpui::Task<()> {
231        let query = query.trim_start();
232        let smart_case = query.chars().any(|c| c.is_uppercase());
233        let candidates = self
234            .workspaces
235            .iter()
236            .enumerate()
237            .filter(|(_, (id, _))| !self.is_current_workspace(*id, cx))
238            .map(|(id, (_, location))| {
239                let combined_string = location
240                    .sorted_paths()
241                    .iter()
242                    .map(|path| path.compact().to_string_lossy().into_owned())
243                    .collect::<Vec<_>>()
244                    .join("");
245
246                StringMatchCandidate::new(id, &combined_string)
247            })
248            .collect::<Vec<_>>();
249        self.matches = smol::block_on(fuzzy::match_strings(
250            candidates.as_slice(),
251            query,
252            smart_case,
253            100,
254            &Default::default(),
255            cx.background_executor().clone(),
256        ));
257        self.matches.sort_unstable_by_key(|m| m.candidate_id);
258
259        if self.reset_selected_match_index {
260            self.selected_match_index = self
261                .matches
262                .iter()
263                .enumerate()
264                .rev()
265                .max_by_key(|(_, m)| OrderedFloat(m.score))
266                .map(|(ix, _)| ix)
267                .unwrap_or(0);
268        }
269        self.reset_selected_match_index = true;
270        Task::ready(())
271    }
272
273    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
274        if let Some((selected_match, workspace)) = self
275            .matches
276            .get(self.selected_index())
277            .zip(self.workspace.upgrade())
278        {
279            let (candidate_workspace_id, candidate_workspace_location) =
280                &self.workspaces[selected_match.candidate_id];
281            let replace_current_window = if self.create_new_window {
282                secondary
283            } else {
284                !secondary
285            };
286            workspace
287                .update(cx, |workspace, cx| {
288                    if workspace.database_id() == Some(*candidate_workspace_id) {
289                        Task::ready(Ok(()))
290                    } else {
291                        match candidate_workspace_location {
292                            SerializedWorkspaceLocation::Local(paths, _) => {
293                                let paths = paths.paths().to_vec();
294                                if replace_current_window {
295                                    cx.spawn_in(window, async move |workspace, cx| {
296                                        let continue_replacing = workspace
297                                            .update_in(cx, |workspace, window, cx| {
298                                                workspace.prepare_to_close(
299                                                    CloseIntent::ReplaceWindow,
300                                                    window,
301                                                    cx,
302                                                )
303                                            })?
304                                            .await?;
305                                        if continue_replacing {
306                                            workspace
307                                                .update_in(cx, |workspace, window, cx| {
308                                                    workspace.open_workspace_for_paths(
309                                                        true, paths, window, cx,
310                                                    )
311                                                })?
312                                                .await
313                                        } else {
314                                            Ok(())
315                                        }
316                                    })
317                                } else {
318                                    workspace.open_workspace_for_paths(false, paths, window, cx)
319                                }
320                            }
321                            SerializedWorkspaceLocation::Ssh(ssh_project) => {
322                                let app_state = workspace.app_state().clone();
323
324                                let replace_window = if replace_current_window {
325                                    window.window_handle().downcast::<Workspace>()
326                                } else {
327                                    None
328                                };
329
330                                let open_options = OpenOptions {
331                                    replace_window,
332                                    ..Default::default()
333                                };
334
335                                let connection_options = SshSettings::get_global(cx)
336                                    .connection_options_for(
337                                        ssh_project.host.clone(),
338                                        ssh_project.port,
339                                        ssh_project.user.clone(),
340                                    );
341
342                                let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
343
344                                cx.spawn_in(window, async move |_, cx| {
345                                    open_ssh_project(
346                                        connection_options,
347                                        paths,
348                                        app_state,
349                                        open_options,
350                                        cx,
351                                    )
352                                    .await
353                                })
354                            }
355                        }
356                    }
357                })
358                .detach_and_log_err(cx);
359            cx.emit(DismissEvent);
360        }
361    }
362
363    fn dismissed(&mut self, _window: &mut Window, _: &mut Context<Picker<Self>>) {}
364
365    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
366        let text = if self.workspaces.is_empty() {
367            "Recently opened projects will show up here".into()
368        } else {
369            "No matches".into()
370        };
371        Some(text)
372    }
373
374    fn render_match(
375        &self,
376        ix: usize,
377        selected: bool,
378        window: &mut Window,
379        cx: &mut Context<Picker<Self>>,
380    ) -> Option<Self::ListItem> {
381        let hit = self.matches.get(ix)?;
382
383        let (_, location) = self.workspaces.get(hit.candidate_id)?;
384
385        let mut path_start_offset = 0;
386
387        let (match_labels, paths): (Vec<_>, Vec<_>) = location
388            .sorted_paths()
389            .iter()
390            .map(|p| p.compact())
391            .map(|path| {
392                let highlighted_text =
393                    highlights_for_path(path.as_ref(), &hit.positions, path_start_offset);
394
395                path_start_offset += highlighted_text.1.char_count;
396                highlighted_text
397            })
398            .unzip();
399
400        let highlighted_match = HighlightedMatchWithPaths {
401            match_label: HighlightedMatch::join(match_labels.into_iter().flatten(), ", "),
402            paths,
403        };
404
405        Some(
406            ListItem::new(ix)
407                .toggle_state(selected)
408                .inset(true)
409                .spacing(ListItemSpacing::Sparse)
410                .child(
411                    h_flex()
412                        .flex_grow()
413                        .gap_3()
414                        .when(self.has_any_non_local_projects, |this| {
415                            this.child(match location {
416                                SerializedWorkspaceLocation::Local(_, _) => {
417                                    Icon::new(IconName::Screen)
418                                        .color(Color::Muted)
419                                        .into_any_element()
420                                }
421                                SerializedWorkspaceLocation::Ssh(_) => Icon::new(IconName::Server)
422                                    .color(Color::Muted)
423                                    .into_any_element(),
424                            })
425                        })
426                        .child({
427                            let mut highlighted = highlighted_match.clone();
428                            if !self.render_paths {
429                                highlighted.paths.clear();
430                            }
431                            highlighted.render(window, cx)
432                        }),
433                )
434                .map(|el| {
435                    let delete_button = div()
436                        .child(
437                            IconButton::new("delete", IconName::Close)
438                                .icon_size(IconSize::Small)
439                                .on_click(cx.listener(move |this, _event, window, cx| {
440                                    cx.stop_propagation();
441                                    window.prevent_default();
442
443                                    this.delegate.delete_recent_project(ix, window, cx)
444                                }))
445                                .tooltip(Tooltip::text("Delete from Recent Projects...")),
446                        )
447                        .into_any_element();
448
449                    if self.selected_index() == ix {
450                        el.end_slot::<AnyElement>(delete_button)
451                    } else {
452                        el.end_hover_slot::<AnyElement>(delete_button)
453                    }
454                })
455                .tooltip(move |_, cx| {
456                    let tooltip_highlighted_location = highlighted_match.clone();
457                    cx.new(|_| MatchTooltip {
458                        highlighted_location: tooltip_highlighted_location,
459                    })
460                    .into()
461                }),
462        )
463    }
464
465    fn render_footer(
466        &self,
467        window: &mut Window,
468        cx: &mut Context<Picker<Self>>,
469    ) -> Option<AnyElement> {
470        Some(
471            h_flex()
472                .w_full()
473                .p_2()
474                .gap_2()
475                .justify_end()
476                .border_t_1()
477                .border_color(cx.theme().colors().border_variant)
478                .child(
479                    Button::new("remote", "Open Remote Folder")
480                        .key_binding(KeyBinding::for_action(
481                            &OpenRemote {
482                                from_existing_connection: false,
483                            },
484                            window,
485                            cx,
486                        ))
487                        .on_click(|_, window, cx| {
488                            window.dispatch_action(
489                                OpenRemote {
490                                    from_existing_connection: false,
491                                }
492                                .boxed_clone(),
493                                cx,
494                            )
495                        }),
496                )
497                .child(
498                    Button::new("local", "Open Local Folder")
499                        .key_binding(KeyBinding::for_action(&workspace::Open, window, cx))
500                        .on_click(|_, window, cx| {
501                            window.dispatch_action(workspace::Open.boxed_clone(), cx)
502                        }),
503                )
504                .into_any(),
505        )
506    }
507}
508
509// Compute the highlighted text for the name and path
510fn highlights_for_path(
511    path: &Path,
512    match_positions: &Vec<usize>,
513    path_start_offset: usize,
514) -> (Option<HighlightedMatch>, HighlightedMatch) {
515    let path_string = path.to_string_lossy();
516    let path_char_count = path_string.chars().count();
517    // Get the subset of match highlight positions that line up with the given path.
518    // Also adjusts them to start at the path start
519    let path_positions = match_positions
520        .iter()
521        .copied()
522        .skip_while(|position| *position < path_start_offset)
523        .take_while(|position| *position < path_start_offset + path_char_count)
524        .map(|position| position - path_start_offset)
525        .collect::<Vec<_>>();
526
527    // Again subset the highlight positions to just those that line up with the file_name
528    // again adjusted to the start of the file_name
529    let file_name_text_and_positions = path.file_name().map(|file_name| {
530        let text = file_name.to_string_lossy();
531        let char_count = text.chars().count();
532        let file_name_start = path_char_count - char_count;
533        let highlight_positions = path_positions
534            .iter()
535            .copied()
536            .skip_while(|position| *position < file_name_start)
537            .take_while(|position| *position < file_name_start + char_count)
538            .map(|position| position - file_name_start)
539            .collect::<Vec<_>>();
540        HighlightedMatch {
541            text: text.to_string(),
542            highlight_positions,
543            char_count,
544            color: Color::Default,
545        }
546    });
547
548    (
549        file_name_text_and_positions,
550        HighlightedMatch {
551            text: path_string.to_string(),
552            highlight_positions: path_positions,
553            char_count: path_char_count,
554            color: Color::Default,
555        },
556    )
557}
558impl RecentProjectsDelegate {
559    fn delete_recent_project(
560        &self,
561        ix: usize,
562        window: &mut Window,
563        cx: &mut Context<Picker<Self>>,
564    ) {
565        if let Some(selected_match) = self.matches.get(ix) {
566            let (workspace_id, _) = self.workspaces[selected_match.candidate_id];
567            cx.spawn_in(window, async move |this, cx| {
568                let _ = WORKSPACE_DB.delete_workspace_by_id(workspace_id).await;
569                let workspaces = WORKSPACE_DB
570                    .recent_workspaces_on_disk()
571                    .await
572                    .unwrap_or_default();
573                this.update_in(cx, move |picker, window, cx| {
574                    picker.delegate.set_workspaces(workspaces);
575                    picker
576                        .delegate
577                        .set_selected_index(ix.saturating_sub(1), window, cx);
578                    picker.delegate.reset_selected_match_index = false;
579                    picker.update_matches(picker.query(cx), window, cx);
580                    // After deleting a project, we want to update the history manager to reflect the change.
581                    // But we do not emit a update event when user opens a project, because it's handled in `workspace::load_workspace`.
582                    if let Some(history_manager) = HistoryManager::global(cx) {
583                        history_manager
584                            .update(cx, |this, cx| this.delete_history(workspace_id, cx));
585                    }
586                })
587            })
588            .detach();
589        }
590    }
591
592    fn is_current_workspace(
593        &self,
594        workspace_id: WorkspaceId,
595        cx: &mut Context<Picker<Self>>,
596    ) -> bool {
597        if let Some(workspace) = self.workspace.upgrade() {
598            let workspace = workspace.read(cx);
599            if Some(workspace_id) == workspace.database_id() {
600                return true;
601            }
602        }
603
604        false
605    }
606}
607struct MatchTooltip {
608    highlighted_location: HighlightedMatchWithPaths,
609}
610
611impl Render for MatchTooltip {
612    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
613        tooltip_container(window, cx, |div, _, _| {
614            self.highlighted_location.render_paths_children(div)
615        })
616    }
617}
618
619#[cfg(test)]
620mod tests {
621    use std::path::PathBuf;
622
623    use dap::debugger_settings::DebuggerSettings;
624    use editor::Editor;
625    use gpui::{TestAppContext, UpdateGlobal, WindowHandle};
626    use project::{Project, project_settings::ProjectSettings};
627    use serde_json::json;
628    use settings::SettingsStore;
629    use util::path;
630    use workspace::{AppState, open_paths};
631
632    use super::*;
633
634    #[gpui::test]
635    async fn test_prompts_on_dirty_before_submit(cx: &mut TestAppContext) {
636        let app_state = init_test(cx);
637
638        cx.update(|cx| {
639            SettingsStore::update_global(cx, |store, cx| {
640                store.update_user_settings::<ProjectSettings>(cx, |settings| {
641                    settings.session.restore_unsaved_buffers = false
642                });
643            });
644        });
645
646        app_state
647            .fs
648            .as_fake()
649            .insert_tree(
650                path!("/dir"),
651                json!({
652                    "main.ts": "a"
653                }),
654            )
655            .await;
656        cx.update(|cx| {
657            open_paths(
658                &[PathBuf::from(path!("/dir/main.ts"))],
659                app_state,
660                workspace::OpenOptions::default(),
661                cx,
662            )
663        })
664        .await
665        .unwrap();
666        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
667
668        let workspace = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
669        workspace
670            .update(cx, |workspace, _, _| assert!(!workspace.is_edited()))
671            .unwrap();
672
673        let editor = workspace
674            .read_with(cx, |workspace, cx| {
675                workspace
676                    .active_item(cx)
677                    .unwrap()
678                    .downcast::<Editor>()
679                    .unwrap()
680            })
681            .unwrap();
682        workspace
683            .update(cx, |_, window, cx| {
684                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
685            })
686            .unwrap();
687        workspace
688            .update(cx, |workspace, _, _| assert!(workspace.is_edited(), "After inserting more text into the editor without saving, we should have a dirty project"))
689            .unwrap();
690
691        let recent_projects_picker = open_recent_projects(&workspace, cx);
692        workspace
693            .update(cx, |_, _, cx| {
694                recent_projects_picker.update(cx, |picker, cx| {
695                    assert_eq!(picker.query(cx), "");
696                    let delegate = &mut picker.delegate;
697                    delegate.matches = vec![StringMatch {
698                        candidate_id: 0,
699                        score: 1.0,
700                        positions: Vec::new(),
701                        string: "fake candidate".to_string(),
702                    }];
703                    delegate.set_workspaces(vec![(
704                        WorkspaceId::default(),
705                        SerializedWorkspaceLocation::from_local_paths(vec![path!("/test/path/")]),
706                    )]);
707                });
708            })
709            .unwrap();
710
711        assert!(
712            !cx.has_pending_prompt(),
713            "Should have no pending prompt on dirty project before opening the new recent project"
714        );
715        cx.dispatch_action(*workspace, menu::Confirm);
716        workspace
717            .update(cx, |workspace, _, cx| {
718                assert!(
719                    workspace.active_modal::<RecentProjects>(cx).is_none(),
720                    "Should remove the modal after selecting new recent project"
721                )
722            })
723            .unwrap();
724        assert!(
725            cx.has_pending_prompt(),
726            "Dirty workspace should prompt before opening the new recent project"
727        );
728        cx.simulate_prompt_answer("Cancel");
729        assert!(
730            !cx.has_pending_prompt(),
731            "Should have no pending prompt after cancelling"
732        );
733        workspace
734            .update(cx, |workspace, _, _| {
735                assert!(
736                    workspace.is_edited(),
737                    "Should be in the same dirty project after cancelling"
738                )
739            })
740            .unwrap();
741    }
742
743    fn open_recent_projects(
744        workspace: &WindowHandle<Workspace>,
745        cx: &mut TestAppContext,
746    ) -> Entity<Picker<RecentProjectsDelegate>> {
747        cx.dispatch_action(
748            (*workspace).into(),
749            OpenRecent {
750                create_new_window: false,
751            },
752        );
753        workspace
754            .update(cx, |workspace, _, cx| {
755                workspace
756                    .active_modal::<RecentProjects>(cx)
757                    .unwrap()
758                    .read(cx)
759                    .picker
760                    .clone()
761            })
762            .unwrap()
763    }
764
765    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
766        cx.update(|cx| {
767            let state = AppState::test(cx);
768            language::init(cx);
769            crate::init(cx);
770            editor::init(cx);
771            workspace::init_settings(cx);
772            DebuggerSettings::register(cx);
773            Project::init_settings(cx);
774            state
775        })
776    }
777}