From 06226e1cbd00eac371cc5d9c291a551881317744 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 4 Aug 2025 19:01:53 -0500 Subject: [PATCH] onboarding: Show indication that settings have already been imported (#35615) Co-Authored-By: Danilo Co-Authored-By: Anthony Release Notes: - N/A --- Cargo.lock | 1 + crates/onboarding/Cargo.toml | 1 + crates/onboarding/src/editing_page.rs | 128 ++++++++++++++------------ crates/onboarding/src/onboarding.rs | 79 ++++++++++++++-- crates/settings/src/settings_store.rs | 73 ++++++++++----- 5 files changed, 192 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 69386b3020..2ef41eafc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10929,6 +10929,7 @@ dependencies = [ "language", "language_model", "menu", + "notifications", "project", "schemars", "serde", diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 8f684dd1b8..b3056ff39e 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -30,6 +30,7 @@ itertools.workspace = true language.workspace = true language_model.workspace = true menu.workspace = true +notifications.workspace = true project.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 20ef17c7aa..a5e3a6bf05 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -12,7 +12,7 @@ use ui::{ ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*, }; -use crate::{ImportCursorSettings, ImportVsCodeSettings}; +use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState}; fn read_show_mini_map(cx: &App) -> ShowMinimap { editor::EditorSettings::get_global(cx).minimap.show @@ -165,7 +165,71 @@ fn write_format_on_save(format_on_save: bool, cx: &mut App) { }); } -fn render_import_settings_section() -> impl IntoElement { +fn render_setting_import_button( + label: SharedString, + icon_name: IconName, + action: &dyn Action, + imported: bool, +) -> impl IntoElement { + let action = action.boxed_clone(); + h_flex().w_full().child( + ButtonLike::new(label.clone()) + .full_width() + .style(ButtonStyle::Outlined) + .size(ButtonSize::Large) + .child( + h_flex() + .w_full() + .justify_between() + .child( + h_flex() + .gap_1p5() + .px_1() + .child( + Icon::new(icon_name) + .color(Color::Muted) + .size(IconSize::XSmall), + ) + .child(Label::new(label)), + ) + .when(imported, |this| { + this.child( + h_flex() + .gap_1p5() + .child( + Icon::new(IconName::Check) + .color(Color::Success) + .size(IconSize::XSmall), + ) + .child(Label::new("Imported").size(LabelSize::Small)), + ) + }), + ) + .on_click(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)), + ) +} + +fn render_import_settings_section(cx: &App) -> impl IntoElement { + let import_state = SettingsImportState::global(cx); + let imports: [(SharedString, IconName, &dyn Action, bool); 2] = [ + ( + "VS Code".into(), + IconName::EditorVsCode, + &ImportVsCodeSettings { skip_prompt: false }, + import_state.vscode, + ), + ( + "Cursor".into(), + IconName::EditorCursor, + &ImportCursorSettings { skip_prompt: false }, + import_state.cursor, + ), + ]; + + let [vscode, cursor] = imports.map(|(label, icon_name, action, imported)| { + render_setting_import_button(label, icon_name, action, imported) + }); + v_flex() .gap_4() .child( @@ -176,63 +240,7 @@ fn render_import_settings_section() -> impl IntoElement { .color(Color::Muted), ), ) - .child( - h_flex() - .w_full() - .gap_4() - .child( - h_flex().w_full().child( - ButtonLike::new("import_vs_code") - .full_width() - .style(ButtonStyle::Outlined) - .size(ButtonSize::Large) - .child( - h_flex() - .w_full() - .gap_1p5() - .px_1() - .child( - Icon::new(IconName::EditorVsCode) - .color(Color::Muted) - .size(IconSize::XSmall), - ) - .child(Label::new("VS Code")), - ) - .on_click(|_, window, cx| { - window.dispatch_action( - ImportVsCodeSettings::default().boxed_clone(), - cx, - ) - }), - ), - ) - .child( - h_flex().w_full().child( - ButtonLike::new("import_cursor") - .full_width() - .style(ButtonStyle::Outlined) - .size(ButtonSize::Large) - .child( - h_flex() - .w_full() - .gap_1p5() - .px_1() - .child( - Icon::new(IconName::EditorCursor) - .color(Color::Muted) - .size(IconSize::XSmall), - ) - .child(Label::new("Cursor")), - ) - .on_click(|_, window, cx| { - window.dispatch_action( - ImportCursorSettings::default().boxed_clone(), - cx, - ) - }), - ), - ), - ) + .child(h_flex().w_full().gap_4().child(vscode).child(cursor)) } fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl IntoElement { @@ -457,6 +465,6 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement { v_flex() .gap_4() - .child(render_import_settings_section()) + .child(render_import_settings_section(cx)) .child(render_popular_settings_section(window, cx)) } diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index a79d1d5aef..42e75ac2f8 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -6,9 +6,10 @@ use feature_flags::{FeatureFlag, FeatureFlagViewExt as _}; use fs::Fs; use gpui::{ Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter, - FocusHandle, Focusable, IntoElement, KeyContext, Render, SharedString, Subscription, Task, - WeakEntity, Window, actions, + FocusHandle, Focusable, Global, IntoElement, KeyContext, Render, SharedString, Subscription, + Task, WeakEntity, Window, actions, }; +use notifications::status_toast::{StatusToast, ToastIcon}; use schemars::JsonSchema; use serde::Deserialize; use settings::{SettingsStore, VsCodeSettingsSource}; @@ -137,9 +138,12 @@ pub fn init(cx: &mut App) { let fs = ::global(cx); let action = *action; + let workspace = cx.weak_entity(); + window .spawn(cx, async move |cx: &mut AsyncWindowContext| { handle_import_vscode_settings( + workspace, VsCodeSettingsSource::VsCode, action.skip_prompt, fs, @@ -154,9 +158,12 @@ pub fn init(cx: &mut App) { let fs = ::global(cx); let action = *action; + let workspace = cx.weak_entity(); + window .spawn(cx, async move |cx: &mut AsyncWindowContext| { handle_import_vscode_settings( + workspace, VsCodeSettingsSource::Cursor, action.skip_prompt, fs, @@ -555,6 +562,7 @@ impl Item for Onboarding { } pub async fn handle_import_vscode_settings( + workspace: WeakEntity, source: VsCodeSettingsSource, skip_prompt: bool, fs: Arc, @@ -595,14 +603,73 @@ pub async fn handle_import_vscode_settings( } }; - cx.update(|_, cx| { + let Ok(result_channel) = cx.update(|_, cx| { let source = vscode_settings.source; let path = vscode_settings.path.clone(); - cx.global::() + let result_channel = cx + .global::() .import_vscode_settings(fs, vscode_settings); zlog::info!("Imported {source} settings from {}", path.display()); - }) - .ok(); + result_channel + }) else { + return; + }; + + let result = result_channel.await; + workspace + .update_in(cx, |workspace, _, cx| match result { + Ok(_) => { + let confirmation_toast = StatusToast::new( + format!("Your {} settings were successfully imported.", source), + cx, + |this, _| { + this.icon(ToastIcon::new(IconName::Check).color(Color::Success)) + .dismiss_button(true) + }, + ); + SettingsImportState::update(cx, |state, _| match source { + VsCodeSettingsSource::VsCode => { + state.vscode = true; + } + VsCodeSettingsSource::Cursor => { + state.cursor = true; + } + }); + workspace.toggle_status_toast(confirmation_toast, cx); + } + Err(_) => { + let error_toast = StatusToast::new( + "Failed to import settings. See log for details", + cx, + |this, _| { + this.icon(ToastIcon::new(IconName::X).color(Color::Error)) + .action("Open Log", |window, cx| { + window.dispatch_action(workspace::OpenLog.boxed_clone(), cx) + }) + .dismiss_button(true) + }, + ); + workspace.toggle_status_toast(error_toast, cx); + } + }) + .ok(); +} + +#[derive(Default, Copy, Clone)] +pub struct SettingsImportState { + pub cursor: bool, + pub vscode: bool, +} + +impl Global for SettingsImportState {} + +impl SettingsImportState { + pub fn global(cx: &App) -> Self { + cx.try_global().cloned().unwrap_or_default() + } + pub fn update(cx: &mut App, f: impl FnOnce(&mut Self, &mut App) -> R) -> R { + cx.update_default_global(f) + } } impl workspace::SerializableItem for Onboarding { diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 7f6437dac8..bc42d2c886 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -2,7 +2,11 @@ use anyhow::{Context as _, Result}; use collections::{BTreeMap, HashMap, btree_map, hash_map}; use ec4rs::{ConfigParser, PropertiesSource, Section}; use fs::Fs; -use futures::{FutureExt, StreamExt, channel::mpsc, future::LocalBoxFuture}; +use futures::{ + FutureExt, StreamExt, + channel::{mpsc, oneshot}, + future::LocalBoxFuture, +}; use gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal}; use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; @@ -531,39 +535,60 @@ impl SettingsStore { .ok(); } - pub fn import_vscode_settings(&self, fs: Arc, vscode_settings: VsCodeSettings) { + pub fn import_vscode_settings( + &self, + fs: Arc, + vscode_settings: VsCodeSettings, + ) -> oneshot::Receiver> { + let (tx, rx) = oneshot::channel::>(); self.setting_file_updates_tx .unbounded_send(Box::new(move |cx: AsyncApp| { async move { - let old_text = Self::load_settings(&fs).await?; - let new_text = cx.read_global(|store: &SettingsStore, _cx| { - store.get_vscode_edits(old_text, &vscode_settings) - })?; - let settings_path = paths::settings_file().as_path(); - if fs.is_file(settings_path).await { - let resolved_path = - fs.canonicalize(settings_path).await.with_context(|| { - format!("Failed to canonicalize settings path {:?}", settings_path) - })?; + let res = async move { + let old_text = Self::load_settings(&fs).await?; + let new_text = cx.read_global(|store: &SettingsStore, _cx| { + store.get_vscode_edits(old_text, &vscode_settings) + })?; + let settings_path = paths::settings_file().as_path(); + if fs.is_file(settings_path).await { + let resolved_path = + fs.canonicalize(settings_path).await.with_context(|| { + format!( + "Failed to canonicalize settings path {:?}", + settings_path + ) + })?; - fs.atomic_write(resolved_path.clone(), new_text) - .await - .with_context(|| { - format!("Failed to write settings to file {:?}", resolved_path) - })?; - } else { - fs.atomic_write(settings_path.to_path_buf(), new_text) - .await - .with_context(|| { - format!("Failed to write settings to file {:?}", settings_path) - })?; + fs.atomic_write(resolved_path.clone(), new_text) + .await + .with_context(|| { + format!("Failed to write settings to file {:?}", resolved_path) + })?; + } else { + fs.atomic_write(settings_path.to_path_buf(), new_text) + .await + .with_context(|| { + format!("Failed to write settings to file {:?}", settings_path) + })?; + } + + anyhow::Ok(()) } + .await; - anyhow::Ok(()) + let new_res = match &res { + Ok(_) => anyhow::Ok(()), + Err(e) => Err(anyhow::anyhow!("Failed to write settings to file {:?}", e)), + }; + + _ = tx.send(new_res); + res } .boxed_local() })) .ok(); + + rx } }