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:
parent
e1d0e3fc34
commit
06226e1cbd
5 changed files with 192 additions and 90 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -10929,6 +10929,7 @@ dependencies = [
|
||||||
"language",
|
"language",
|
||||||
"language_model",
|
"language_model",
|
||||||
"menu",
|
"menu",
|
||||||
|
"notifications",
|
||||||
"project",
|
"project",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,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 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
|
||||||
.ok();
|
}) 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<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 {
|
||||||
|
|
|
@ -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,39 +535,60 @@ 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 old_text = Self::load_settings(&fs).await?;
|
let res = async move {
|
||||||
let new_text = cx.read_global(|store: &SettingsStore, _cx| {
|
let old_text = Self::load_settings(&fs).await?;
|
||||||
store.get_vscode_edits(old_text, &vscode_settings)
|
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 settings_path = paths::settings_file().as_path();
|
||||||
let resolved_path =
|
if fs.is_file(settings_path).await {
|
||||||
fs.canonicalize(settings_path).await.with_context(|| {
|
let resolved_path =
|
||||||
format!("Failed to canonicalize settings path {:?}", settings_path)
|
fs.canonicalize(settings_path).await.with_context(|| {
|
||||||
})?;
|
format!(
|
||||||
|
"Failed to canonicalize settings path {:?}",
|
||||||
|
settings_path
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
fs.atomic_write(resolved_path.clone(), new_text)
|
fs.atomic_write(resolved_path.clone(), new_text)
|
||||||
.await
|
.await
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!("Failed to write settings to file {:?}", resolved_path)
|
format!("Failed to write settings to file {:?}", resolved_path)
|
||||||
})?;
|
})?;
|
||||||
} else {
|
} else {
|
||||||
fs.atomic_write(settings_path.to_path_buf(), new_text)
|
fs.atomic_write(settings_path.to_path_buf(), new_text)
|
||||||
.await
|
.await
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!("Failed to write settings to file {:?}", settings_path)
|
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()
|
.boxed_local()
|
||||||
}))
|
}))
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
|
rx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue