onboarding: Wire up settings import (#35366)

Ben Kunkle created

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

Cargo.lock                            |   5 +
crates/onboarding/Cargo.toml          |   4 +
crates/onboarding/src/editing_page.rs |  35 +++++---
crates/onboarding/src/onboarding.rs   | 115 ++++++++++++++++++++++++++++
crates/settings/src/settings_store.rs |   2 
crates/settings_ui/Cargo.toml         |   1 
crates/settings_ui/src/settings_ui.rs | 114 ----------------------------
7 files changed, 147 insertions(+), 129 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -10982,12 +10982,16 @@ dependencies = [
  "gpui",
  "language",
  "project",
+ "schemars",
+ "serde",
  "settings",
  "theme",
  "ui",
+ "util",
  "workspace",
  "workspace-hack",
  "zed_actions",
+ "zlog",
 ]
 
 [[package]]
@@ -14773,7 +14777,6 @@ dependencies = [
  "notifications",
  "paths",
  "project",
- "schemars",
  "search",
  "serde",
  "serde_json",

crates/onboarding/Cargo.toml 🔗

@@ -24,9 +24,13 @@ fs.workspace = true
 gpui.workspace = true
 language.workspace = true
 project.workspace = true
+schemars.workspace = true
+serde.workspace = true
 settings.workspace = true
 theme.workspace = true
 ui.workspace = true
+util.workspace = true
 workspace-hack.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
+zlog.workspace = true

crates/onboarding/src/editing_page.rs 🔗

@@ -1,16 +1,19 @@
 use editor::{EditorSettings, ShowMinimap};
 use fs::Fs;
-use gpui::{App, IntoElement, Pixels, Window};
+use gpui::{Action, App, IntoElement, Pixels, Window};
 use language::language_settings::AllLanguageSettings;
 use project::project_settings::ProjectSettings;
 use settings::{Settings as _, update_settings_file};
 use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
 use ui::{
-    ContextMenu, DropdownMenu, IconButton, Label, LabelCommon, LabelSize, NumericStepper,
-    ParentElement, SharedString, Styled, SwitchColor, SwitchField, ToggleButtonGroup,
-    ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, div, h_flex, px, v_flex,
+    Clickable, ContextMenu, DropdownMenu, IconButton, Label, LabelCommon, LabelSize,
+    NumericStepper, ParentElement, SharedString, Styled, SwitchColor, SwitchField,
+    ToggleButtonGroup, ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, div, h_flex, px,
+    v_flex,
 };
 
+use crate::{ImportCursorSettings, ImportVsCodeSettings};
+
 fn read_show_mini_map(cx: &App) -> ShowMinimap {
     editor::EditorSettings::get_global(cx).minimap.show
 }
@@ -110,14 +113,22 @@ pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl Int
         )
         .child(
             h_flex()
-                .child(IconButton::new(
-                    "import-vs-code-settings",
-                    ui::IconName::Code,
-                ))
-                .child(IconButton::new(
-                    "import-cursor-settings",
-                    ui::IconName::CursorIBeam,
-                )),
+                .child(
+                    IconButton::new("import-vs-code-settings", ui::IconName::Code).on_click(
+                        |_, window, cx| {
+                            window
+                                .dispatch_action(ImportVsCodeSettings::default().boxed_clone(), cx)
+                        },
+                    ),
+                )
+                .child(
+                    IconButton::new("import-cursor-settings", ui::IconName::CursorIBeam).on_click(
+                        |_, window, cx| {
+                            window
+                                .dispatch_action(ImportCursorSettings::default().boxed_clone(), cx)
+                        },
+                    ),
+                ),
         )
         .child(Label::new("Popular Settings").size(LabelSize::Large))
         .child(

crates/onboarding/src/onboarding.rs 🔗

@@ -4,10 +4,13 @@ use db::kvp::KEY_VALUE_STORE;
 use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
 use fs::Fs;
 use gpui::{
-    AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
-    IntoElement, Render, SharedString, Subscription, Task, WeakEntity, Window, actions,
+    Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter,
+    FocusHandle, Focusable, IntoElement, Render, SharedString, Subscription, Task, WeakEntity,
+    Window, actions,
 };
-use settings::{Settings, SettingsStore, update_settings_file};
+use schemars::JsonSchema;
+use serde::Deserialize;
+use settings::{Settings, SettingsStore, VsCodeSettingsSource, update_settings_file};
 use std::sync::Arc;
 use theme::{ThemeMode, ThemeSettings};
 use ui::{
@@ -30,6 +33,24 @@ impl FeatureFlag for OnBoardingFeatureFlag {
     const NAME: &'static str = "onboarding";
 }
 
+/// Imports settings from Visual Studio Code.
+#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
+#[action(namespace = zed)]
+#[serde(deny_unknown_fields)]
+pub struct ImportVsCodeSettings {
+    #[serde(default)]
+    pub skip_prompt: bool,
+}
+
+/// Imports settings from Cursor editor.
+#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
+#[action(namespace = zed)]
+#[serde(deny_unknown_fields)]
+pub struct ImportCursorSettings {
+    #[serde(default)]
+    pub skip_prompt: bool,
+}
+
 pub const FIRST_OPEN: &str = "first_open";
 
 actions!(
@@ -95,6 +116,43 @@ pub fn init(cx: &mut App) {
         });
     });
 
+    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
+        workspace.register_action(|_workspace, action: &ImportVsCodeSettings, window, cx| {
+            let fs = <dyn Fs>::global(cx);
+            let action = *action;
+
+            window
+                .spawn(cx, async move |cx: &mut AsyncWindowContext| {
+                    handle_import_vscode_settings(
+                        VsCodeSettingsSource::VsCode,
+                        action.skip_prompt,
+                        fs,
+                        cx,
+                    )
+                    .await
+                })
+                .detach();
+        });
+
+        workspace.register_action(|_workspace, action: &ImportCursorSettings, window, cx| {
+            let fs = <dyn Fs>::global(cx);
+            let action = *action;
+
+            window
+                .spawn(cx, async move |cx: &mut AsyncWindowContext| {
+                    handle_import_vscode_settings(
+                        VsCodeSettingsSource::Cursor,
+                        action.skip_prompt,
+                        fs,
+                        cx,
+                    )
+                    .await
+                })
+                .detach();
+        });
+    })
+    .detach();
+
     cx.observe_new::<Workspace>(|_, window, cx| {
         let Some(window) = window else {
             return;
@@ -371,3 +429,54 @@ impl Item for Onboarding {
         f(*event)
     }
 }
+
+pub async fn handle_import_vscode_settings(
+    source: VsCodeSettingsSource,
+    skip_prompt: bool,
+    fs: Arc<dyn Fs>,
+    cx: &mut AsyncWindowContext,
+) {
+    use util::truncate_and_remove_front;
+
+    let vscode_settings =
+        match settings::VsCodeSettings::load_user_settings(source, fs.clone()).await {
+            Ok(vscode_settings) => vscode_settings,
+            Err(err) => {
+                zlog::error!("{err}");
+                let _ = cx.prompt(
+                    gpui::PromptLevel::Info,
+                    &format!("Could not find or load a {source} settings file"),
+                    None,
+                    &["Ok"],
+                );
+                return;
+            }
+        };
+
+    if !skip_prompt {
+        let prompt = cx.prompt(
+            gpui::PromptLevel::Warning,
+            &format!(
+                "Importing {} settings may overwrite your existing settings. \
+                Will import settings from {}",
+                vscode_settings.source,
+                truncate_and_remove_front(&vscode_settings.path.to_string_lossy(), 128),
+            ),
+            None,
+            &["Ok", "Cancel"],
+        );
+        let result = cx.spawn(async move |_| prompt.await.ok()).await;
+        if result != Some(0) {
+            return;
+        }
+    };
+
+    cx.update(|_, cx| {
+        let source = vscode_settings.source;
+        let path = vscode_settings.path.clone();
+        cx.global::<SettingsStore>()
+            .import_vscode_settings(fs, vscode_settings);
+        zlog::info!("Imported {source} settings from {}", path.display());
+    })
+    .ok();
+}

crates/settings/src/settings_store.rs 🔗

@@ -532,7 +532,9 @@ impl SettingsStore {
             }))
             .ok();
     }
+}
 
+impl SettingsStore {
     /// Updates the value of a setting in a JSON file, returning the new text
     /// for that JSON file.
     pub fn new_text_for_update<T: Settings>(

crates/settings_ui/Cargo.toml 🔗

@@ -30,7 +30,6 @@ menu.workspace = true
 notifications.workspace = true
 paths.workspace = true
 project.workspace = true
-schemars.workspace = true
 search.workspace = true
 serde.workspace = true
 serde_json.workspace = true

crates/settings_ui/src/settings_ui.rs 🔗

@@ -1,20 +1,12 @@
 mod appearance_settings_controls;
 
 use std::any::TypeId;
-use std::sync::Arc;
 
 use command_palette_hooks::CommandPaletteFilter;
 use editor::EditorSettingsControls;
 use feature_flags::{FeatureFlag, FeatureFlagViewExt};
-use fs::Fs;
-use gpui::{
-    Action, App, AsyncWindowContext, Entity, EventEmitter, FocusHandle, Focusable, Task, actions,
-};
-use schemars::JsonSchema;
-use serde::Deserialize;
-use settings::{SettingsStore, VsCodeSettingsSource};
+use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, actions};
 use ui::prelude::*;
-use util::truncate_and_remove_front;
 use workspace::item::{Item, ItemEvent};
 use workspace::{Workspace, with_active_or_new_workspace};
 
@@ -29,23 +21,6 @@ impl FeatureFlag for SettingsUiFeatureFlag {
     const NAME: &'static str = "settings-ui";
 }
 
-/// Imports settings from Visual Studio Code.
-#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
-#[action(namespace = zed)]
-#[serde(deny_unknown_fields)]
-pub struct ImportVsCodeSettings {
-    #[serde(default)]
-    pub skip_prompt: bool,
-}
-
-/// Imports settings from Cursor editor.
-#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
-#[action(namespace = zed)]
-#[serde(deny_unknown_fields)]
-pub struct ImportCursorSettings {
-    #[serde(default)]
-    pub skip_prompt: bool,
-}
 actions!(
     zed,
     [
@@ -72,45 +47,11 @@ pub fn init(cx: &mut App) {
         });
     });
 
-    cx.observe_new(|workspace: &mut Workspace, window, cx| {
+    cx.observe_new(|_workspace: &mut Workspace, window, cx| {
         let Some(window) = window else {
             return;
         };
 
-        workspace.register_action(|_workspace, action: &ImportVsCodeSettings, window, cx| {
-            let fs = <dyn Fs>::global(cx);
-            let action = *action;
-
-            window
-                .spawn(cx, async move |cx: &mut AsyncWindowContext| {
-                    handle_import_vscode_settings(
-                        VsCodeSettingsSource::VsCode,
-                        action.skip_prompt,
-                        fs,
-                        cx,
-                    )
-                    .await
-                })
-                .detach();
-        });
-
-        workspace.register_action(|_workspace, action: &ImportCursorSettings, window, cx| {
-            let fs = <dyn Fs>::global(cx);
-            let action = *action;
-
-            window
-                .spawn(cx, async move |cx: &mut AsyncWindowContext| {
-                    handle_import_vscode_settings(
-                        VsCodeSettingsSource::Cursor,
-                        action.skip_prompt,
-                        fs,
-                        cx,
-                    )
-                    .await
-                })
-                .detach();
-        });
-
         let settings_ui_actions = [TypeId::of::<OpenSettingsEditor>()];
 
         CommandPaletteFilter::update_global(cx, |filter, _cx| {
@@ -138,57 +79,6 @@ pub fn init(cx: &mut App) {
     keybindings::init(cx);
 }
 
-async fn handle_import_vscode_settings(
-    source: VsCodeSettingsSource,
-    skip_prompt: bool,
-    fs: Arc<dyn Fs>,
-    cx: &mut AsyncWindowContext,
-) {
-    let vscode_settings =
-        match settings::VsCodeSettings::load_user_settings(source, fs.clone()).await {
-            Ok(vscode_settings) => vscode_settings,
-            Err(err) => {
-                log::error!("{err}");
-                let _ = cx.prompt(
-                    gpui::PromptLevel::Info,
-                    &format!("Could not find or load a {source} settings file"),
-                    None,
-                    &["Ok"],
-                );
-                return;
-            }
-        };
-
-    let prompt = if skip_prompt {
-        Task::ready(Some(0))
-    } else {
-        let prompt = cx.prompt(
-            gpui::PromptLevel::Warning,
-            &format!(
-                "Importing {} settings may overwrite your existing settings. \
-                Will import settings from {}",
-                vscode_settings.source,
-                truncate_and_remove_front(&vscode_settings.path.to_string_lossy(), 128),
-            ),
-            None,
-            &["Ok", "Cancel"],
-        );
-        cx.spawn(async move |_| prompt.await.ok())
-    };
-    if prompt.await != Some(0) {
-        return;
-    }
-
-    cx.update(|_, cx| {
-        let source = vscode_settings.source;
-        let path = vscode_settings.path.clone();
-        cx.global::<SettingsStore>()
-            .import_vscode_settings(fs, vscode_settings);
-        log::info!("Imported {source} settings from {}", path.display());
-    })
-    .ok();
-}
-
 pub struct SettingsPage {
     focus_handle: FocusHandle,
 }