Cargo.lock 🔗
@@ -10929,6 +10929,7 @@ dependencies = [
"language",
"language_model",
"menu",
+ "notifications",
"project",
"schemars",
"serde",
Ben Kunkle created
Co-Authored-By: Danilo <danilo@zed.dev>
Co-Authored-By: Anthony <anthony@zed.dev>
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(-)
@@ -10929,6 +10929,7 @@ dependencies = [
"language",
"language_model",
"menu",
+ "notifications",
"project",
"schemars",
"serde",
@@ -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
@@ -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))
}
@@ -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 = <dyn 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 = <dyn 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<Workspace>,
source: VsCodeSettingsSource,
skip_prompt: bool,
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 path = vscode_settings.path.clone();
- cx.global::<SettingsStore>()
+ let result_channel = cx
+ .global::<SettingsStore>()
.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<R>(cx: &mut App, f: impl FnOnce(&mut Self, &mut App) -> R) -> R {
+ cx.update_default_global(f)
+ }
}
impl workspace::SerializableItem for Onboarding {
@@ -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<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
.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
}
}