onboarding: Show indication that settings have already been imported (#35615)

Co-Authored-By: Danilo <danilo@zed.dev>
Co-Authored-By: Anthony <anthony@zed.dev>

Release Notes:

- N/A
This commit is contained in:
Ben Kunkle 2025-08-04 19:01:53 -05:00 committed by GitHub
parent e1d0e3fc34
commit 06226e1cbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 192 additions and 90 deletions

1
Cargo.lock generated
View file

@ -10929,6 +10929,7 @@ dependencies = [
"language", "language",
"language_model", "language_model",
"menu", "menu",
"notifications",
"project", "project",
"schemars", "schemars",
"serde", "serde",

View file

@ -30,6 +30,7 @@ itertools.workspace = true
language.workspace = true language.workspace = true
language_model.workspace = true language_model.workspace = true
menu.workspace = true menu.workspace = true
notifications.workspace = true
project.workspace = true project.workspace = true
schemars.workspace = true schemars.workspace = true
serde.workspace = true serde.workspace = true

View file

@ -12,7 +12,7 @@ use ui::{
ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*, ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*,
}; };
use crate::{ImportCursorSettings, ImportVsCodeSettings}; use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState};
fn read_show_mini_map(cx: &App) -> ShowMinimap { fn read_show_mini_map(cx: &App) -> ShowMinimap {
editor::EditorSettings::get_global(cx).minimap.show 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() v_flex()
.gap_4() .gap_4()
.child( .child(
@ -176,63 +240,7 @@ fn render_import_settings_section() -> impl IntoElement {
.color(Color::Muted), .color(Color::Muted),
), ),
) )
.child( .child(h_flex().w_full().gap_4().child(vscode).child(cursor))
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,
)
}),
),
),
)
} }
fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl IntoElement { 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 { pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex() v_flex()
.gap_4() .gap_4()
.child(render_import_settings_section()) .child(render_import_settings_section(cx))
.child(render_popular_settings_section(window, cx)) .child(render_popular_settings_section(window, cx))
} }

View file

@ -6,9 +6,10 @@ use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter, Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter,
FocusHandle, Focusable, IntoElement, KeyContext, Render, SharedString, Subscription, Task, FocusHandle, Focusable, Global, IntoElement, KeyContext, Render, SharedString, Subscription,
WeakEntity, Window, actions, Task, WeakEntity, Window, actions,
}; };
use notifications::status_toast::{StatusToast, ToastIcon};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use settings::{SettingsStore, VsCodeSettingsSource}; use settings::{SettingsStore, VsCodeSettingsSource};
@ -137,9 +138,12 @@ pub fn init(cx: &mut App) {
let fs = <dyn Fs>::global(cx); let fs = <dyn Fs>::global(cx);
let action = *action; let action = *action;
let workspace = cx.weak_entity();
window window
.spawn(cx, async move |cx: &mut AsyncWindowContext| { .spawn(cx, async move |cx: &mut AsyncWindowContext| {
handle_import_vscode_settings( handle_import_vscode_settings(
workspace,
VsCodeSettingsSource::VsCode, VsCodeSettingsSource::VsCode,
action.skip_prompt, action.skip_prompt,
fs, fs,
@ -154,9 +158,12 @@ pub fn init(cx: &mut App) {
let fs = <dyn Fs>::global(cx); let fs = <dyn Fs>::global(cx);
let action = *action; let action = *action;
let workspace = cx.weak_entity();
window window
.spawn(cx, async move |cx: &mut AsyncWindowContext| { .spawn(cx, async move |cx: &mut AsyncWindowContext| {
handle_import_vscode_settings( handle_import_vscode_settings(
workspace,
VsCodeSettingsSource::Cursor, VsCodeSettingsSource::Cursor,
action.skip_prompt, action.skip_prompt,
fs, fs,
@ -555,6 +562,7 @@ impl Item for Onboarding {
} }
pub async fn handle_import_vscode_settings( pub async fn handle_import_vscode_settings(
workspace: WeakEntity<Workspace>,
source: VsCodeSettingsSource, source: VsCodeSettingsSource,
skip_prompt: bool, skip_prompt: bool,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
@ -595,16 +603,75 @@ pub async fn handle_import_vscode_settings(
} }
}; };
cx.update(|_, cx| { let Ok(result_channel) = cx.update(|_, cx| {
let source = vscode_settings.source; let source = vscode_settings.source;
let path = vscode_settings.path.clone(); let path = vscode_settings.path.clone();
cx.global::<SettingsStore>() let result_channel = cx
.global::<SettingsStore>()
.import_vscode_settings(fs, vscode_settings); .import_vscode_settings(fs, vscode_settings);
zlog::info!("Imported {source} settings from {}", path.display()); zlog::info!("Imported {source} settings from {}", path.display());
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(); .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<R>(cx: &mut App, f: impl FnOnce(&mut Self, &mut App) -> R) -> R {
cx.update_default_global(f)
}
}
impl workspace::SerializableItem for Onboarding { impl workspace::SerializableItem for Onboarding {
fn serialized_item_kind() -> &'static str { fn serialized_item_kind() -> &'static str {
"OnboardingPage" "OnboardingPage"

View file

@ -2,7 +2,11 @@ use anyhow::{Context as _, Result};
use collections::{BTreeMap, HashMap, btree_map, hash_map}; use collections::{BTreeMap, HashMap, btree_map, hash_map};
use ec4rs::{ConfigParser, PropertiesSource, Section}; use ec4rs::{ConfigParser, PropertiesSource, Section};
use fs::Fs; 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 gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal};
use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name};
@ -531,10 +535,16 @@ impl SettingsStore {
.ok(); .ok();
} }
pub fn import_vscode_settings(&self, fs: Arc<dyn Fs>, vscode_settings: VsCodeSettings) { pub fn import_vscode_settings(
&self,
fs: Arc<dyn Fs>,
vscode_settings: VsCodeSettings,
) -> oneshot::Receiver<Result<()>> {
let (tx, rx) = oneshot::channel::<Result<()>>();
self.setting_file_updates_tx self.setting_file_updates_tx
.unbounded_send(Box::new(move |cx: AsyncApp| { .unbounded_send(Box::new(move |cx: AsyncApp| {
async move { async move {
let res = async move {
let old_text = Self::load_settings(&fs).await?; let old_text = Self::load_settings(&fs).await?;
let new_text = cx.read_global(|store: &SettingsStore, _cx| { let new_text = cx.read_global(|store: &SettingsStore, _cx| {
store.get_vscode_edits(old_text, &vscode_settings) store.get_vscode_edits(old_text, &vscode_settings)
@ -543,7 +553,10 @@ impl SettingsStore {
if fs.is_file(settings_path).await { if fs.is_file(settings_path).await {
let resolved_path = let resolved_path =
fs.canonicalize(settings_path).await.with_context(|| { fs.canonicalize(settings_path).await.with_context(|| {
format!("Failed to canonicalize settings path {:?}", settings_path) format!(
"Failed to canonicalize settings path {:?}",
settings_path
)
})?; })?;
fs.atomic_write(resolved_path.clone(), new_text) fs.atomic_write(resolved_path.clone(), new_text)
@ -561,9 +574,21 @@ impl SettingsStore {
anyhow::Ok(()) anyhow::Ok(())
} }
.await;
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() .boxed_local()
})) }))
.ok(); .ok();
rx
} }
} }