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