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