From 65934ae181ec16a1bda743b8612803780d316edb Mon Sep 17 00:00:00 2001 From: smit <0xtimsb@gmail.com> Date: Wed, 12 Feb 2025 06:47:08 +0530 Subject: [PATCH] migrator: In-memory migration and improved UX (#24621) This PR adds: - Support for deprecated keymap and settings (In-memory migration) - Migration prompt only shown in `settings.json` / `keymap.json`. Release Notes: - The migration banner will only appear in `settings.json` and `keymap.json` if you have deprecated settings or keybindings, allowing you to migrate them to work with the new version on Zed. --- crates/editor/src/editor.rs | 14 +- crates/settings/src/keymap_file.rs | 23 +- crates/settings/src/settings_file.rs | 36 +--- crates/zed/src/main.rs | 7 +- crates/zed/src/zed.rs | 182 ++++++++-------- crates/zed/src/zed/migrate.rs | 307 ++++++++++++++++++++++----- 6 files changed, 374 insertions(+), 195 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0ad3bed29ea236..406f193817e7ce 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -12827,11 +12827,17 @@ impl Editor { .and_then(|f| f.as_local()) } - fn target_file_abs_path(&self, cx: &mut Context) -> Option { + pub fn target_file_abs_path(&self, cx: &mut Context) -> Option { self.active_excerpt(cx).and_then(|(_, buffer, _)| { - let project_path = buffer.read(cx).project_path(cx)?; - let project = self.project.as_ref()?.read(cx); - project.absolute_path(&project_path, cx) + let buffer = buffer.read(cx); + if let Some(project_path) = buffer.project_path(cx) { + let project = self.project.as_ref()?.read(cx); + project.absolute_path(&project_path, cx) + } else { + buffer + .file() + .and_then(|file| file.as_local().map(|file| file.abs_path(cx))) + } }) } diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index c264b56666358f..10980bba426e4d 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -10,7 +10,7 @@ use schemars::{ schema::{ArrayValidation, InstanceType, Schema, SchemaObject, SubschemaValidation}, JsonSchema, }; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use serde_json::Value; use std::{any::TypeId, fmt::Write, rc::Rc, sync::Arc, sync::LazyLock}; use util::{asset_str, markdown::MarkdownString}; @@ -47,12 +47,12 @@ pub(crate) static KEY_BINDING_VALIDATORS: LazyLock); /// Keymap section which binds keystrokes to actions. -#[derive(Debug, Deserialize, Default, Clone, JsonSchema, Serialize)] +#[derive(Debug, Deserialize, Default, Clone, JsonSchema)] pub struct KeymapSection { /// Determines when these bindings are active. When just a name is provided, like `Editor` or /// `Workspace`, the bindings will be active in that context. Boolean expressions like `X && Y`, @@ -97,9 +97,9 @@ impl KeymapSection { /// Unlike the other json types involved in keymaps (including actions), this doc-comment will not /// be included in the generated JSON schema, as it manually defines its `JsonSchema` impl. The /// actual schema used for it is automatically generated in `KeymapFile::generate_json_schema`. -#[derive(Debug, Deserialize, Default, Clone, Serialize)] +#[derive(Debug, Deserialize, Default, Clone)] #[serde(transparent)] -pub struct KeymapAction(pub(crate) Value); +pub struct KeymapAction(Value); impl std::fmt::Display for KeymapAction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -133,11 +133,9 @@ impl JsonSchema for KeymapAction { pub enum KeymapFileLoadResult { Success { key_bindings: Vec, - keymap_file: KeymapFile, }, SomeFailedToLoad { key_bindings: Vec, - keymap_file: KeymapFile, error_message: MarkdownString, }, JsonParseFailure { @@ -152,7 +150,7 @@ impl KeymapFile { pub fn load_asset(asset_path: &str, cx: &App) -> anyhow::Result> { match Self::load(asset_str::(asset_path).as_ref(), cx) { - KeymapFileLoadResult::Success { key_bindings, .. } => Ok(key_bindings), + KeymapFileLoadResult::Success { key_bindings } => Ok(key_bindings), KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => Err(anyhow!( "Error loading built-in keymap \"{asset_path}\": {error_message}" )), @@ -202,7 +200,6 @@ impl KeymapFile { if content.is_empty() { return KeymapFileLoadResult::Success { key_bindings: Vec::new(), - keymap_file: KeymapFile(Vec::new()), }; } let keymap_file = match parse_json_with_comments::(content) { @@ -296,10 +293,7 @@ impl KeymapFile { } if errors.is_empty() { - KeymapFileLoadResult::Success { - key_bindings, - keymap_file, - } + KeymapFileLoadResult::Success { key_bindings } } else { let mut error_message = "Errors in user keymap file.\n".to_owned(); for (context, section_errors) in errors { @@ -317,7 +311,6 @@ impl KeymapFile { } KeymapFileLoadResult::SomeFailedToLoad { key_bindings, - keymap_file, error_message: MarkdownString(error_message), } } @@ -619,7 +612,7 @@ fn inline_code_string(text: &str) -> MarkdownString { #[cfg(test)] mod tests { - use super::KeymapFile; + use crate::KeymapFile; #[test] fn can_deserialize_keymap_with_trailing_comma() { diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index ba93391804b548..1277044a632217 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -1,7 +1,7 @@ use crate::{settings_store::SettingsStore, Settings}; use fs::Fs; use futures::{channel::mpsc, StreamExt}; -use gpui::{App, BackgroundExecutor, ReadGlobal, UpdateGlobal}; +use gpui::{App, BackgroundExecutor, ReadGlobal}; use std::{path::PathBuf, sync::Arc, time::Duration}; pub const EMPTY_THEME_NAME: &str = "empty-theme"; @@ -78,40 +78,6 @@ pub fn watch_config_file( rx } -pub fn handle_settings_file_changes( - mut user_settings_file_rx: mpsc::UnboundedReceiver, - cx: &mut App, - settings_changed: impl Fn(Result, &mut App) + 'static, -) { - let user_settings_content = cx - .background_executor() - .block(user_settings_file_rx.next()) - .unwrap(); - SettingsStore::update_global(cx, |store, cx| { - let result = store.set_user_settings(&user_settings_content, cx); - if let Err(err) = &result { - log::error!("Failed to load user settings: {err}"); - } - settings_changed(result, cx); - }); - cx.spawn(move |cx| async move { - while let Some(user_settings_content) = user_settings_file_rx.next().await { - let result = cx.update_global(|store: &mut SettingsStore, cx| { - let result = store.set_user_settings(&user_settings_content, cx); - if let Err(err) = &result { - log::error!("Failed to load user settings: {err}"); - } - settings_changed(result, cx); - cx.refresh_windows(); - }); - if result.is_err() { - break; // App dropped - } - } - }) - .detach(); -} - pub fn update_settings_file( fs: Arc, cx: &App, diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index cbd2519e602a36..e69226d97cd1a8 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -34,7 +34,7 @@ use project::project_settings::ProjectSettings; use recent_projects::{open_ssh_project, SshSettings}; use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use session::{AppSession, Session}; -use settings::{handle_settings_file_changes, watch_config_file, Settings, SettingsStore}; +use settings::{watch_config_file, Settings, SettingsStore}; use simplelog::ConfigBuilder; use std::{ env, @@ -52,8 +52,9 @@ use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN}; use workspace::{AppState, SerializedWorkspaceLocation, WorkspaceSettings, WorkspaceStore}; use zed::{ app_menus, build_window_options, derive_paths_with_position, handle_cli_connection, - handle_keymap_file_changes, handle_settings_changed, initialize_workspace, - inline_completion_registry, open_paths_with_positions, OpenListener, OpenRequest, + handle_keymap_file_changes, handle_settings_changed, handle_settings_file_changes, + initialize_workspace, inline_completion_registry, open_paths_with_positions, OpenListener, + OpenRequest, }; #[cfg(unix)] diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index e0e0cbaecfdaba..fe2e7107c5b939 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -21,14 +21,16 @@ use command_palette_hooks::CommandPaletteFilter; use editor::ProposedChangesEditorToolbar; use editor::{scroll::Autoscroll, Editor, MultiBuffer}; use feature_flags::{FeatureFlagAppExt, FeatureFlagViewExt, GitUiFeatureFlag}; -use fs::Fs; use futures::{channel::mpsc, select_biased, StreamExt}; use gpui::{ actions, point, px, Action, App, AppContext as _, AsyncApp, Context, DismissEvent, Element, Entity, Focusable, KeyBinding, MenuItem, ParentElement, PathPromptOptions, PromptLevel, - ReadGlobal, SharedString, Styled, Task, TitlebarOptions, Window, WindowKind, WindowOptions, + ReadGlobal, SharedString, Styled, Task, TitlebarOptions, UpdateGlobal, Window, WindowKind, + WindowOptions, }; use image_viewer::ImageInfo; +use migrate::{MigrationType, MigratorBanner, MigratorEvent, MigratorNotification}; +use migrator::{migrate_keymap, migrate_settings}; pub use open_listener::*; use outline_panel::OutlinePanel; use paths::{local_settings_file_relative_path, local_tasks_file_relative_path}; @@ -150,6 +152,7 @@ pub fn initialize_workspace( let workspace_handle = cx.entity().clone(); let center_pane = workspace.active_pane().clone(); initialize_pane(workspace, ¢er_pane, window, cx); + cx.subscribe_in(&workspace_handle, window, { move |workspace, _, event, window, cx| match event { workspace::Event::PaneAdded(pane) => { @@ -855,7 +858,6 @@ fn initialize_pane( toolbar.add_item(breadcrumbs, window, cx); let buffer_search_bar = cx.new(|cx| search::BufferSearchBar::new(window, cx)); toolbar.add_item(buffer_search_bar.clone(), window, cx); - let proposed_change_bar = cx.new(|_| ProposedChangesEditorToolbar::new()); toolbar.add_item(proposed_change_bar, window, cx); let quick_action_bar = @@ -869,6 +871,8 @@ fn initialize_pane( toolbar.add_item(lsp_log_item, window, cx); let syntax_tree_item = cx.new(|_| language_tools::SyntaxTreeToolbarItemView::new()); toolbar.add_item(syntax_tree_item, window, cx); + let migrator_banner = cx.new(|cx| MigratorBanner::new(workspace, cx)); + toolbar.add_item(migrator_banner, window, cx); }) }); } @@ -1097,6 +1101,68 @@ fn open_log_file(workspace: &mut Workspace, window: &mut Window, cx: &mut Contex .detach(); } +pub fn handle_settings_file_changes( + mut user_settings_file_rx: mpsc::UnboundedReceiver, + cx: &mut App, + settings_changed: impl Fn(Option, &mut App) + 'static, +) { + MigratorNotification::set_global(cx.new(|_| MigratorNotification), cx); + let content = cx + .background_executor() + .block(user_settings_file_rx.next()) + .unwrap(); + let user_settings_content = if let Ok(Some(migrated_content)) = migrate_settings(&content) { + migrated_content + } else { + content + }; + SettingsStore::update_global(cx, |store, cx| { + let result = store.set_user_settings(&user_settings_content, cx); + if let Err(err) = &result { + log::error!("Failed to load user settings: {err}"); + } + settings_changed(result.err(), cx); + }); + cx.spawn(move |cx| async move { + while let Some(content) = user_settings_file_rx.next().await { + let user_settings_content; + let content_migrated; + + if let Ok(Some(migrated_content)) = migrate_settings(&content) { + user_settings_content = migrated_content; + content_migrated = true; + } else { + user_settings_content = content; + content_migrated = false; + } + + cx.update(|cx| { + if let Some(notifier) = MigratorNotification::try_global(cx) { + notifier.update(cx, |_, cx| { + cx.emit(MigratorEvent::ContentChanged { + migration_type: MigrationType::Settings, + migrated: content_migrated, + }); + }); + } + }) + .ok(); + let result = cx.update_global(|store: &mut SettingsStore, cx| { + let result = store.set_user_settings(&user_settings_content, cx); + if let Err(err) = &result { + log::error!("Failed to load user settings: {err}"); + } + settings_changed(result.err(), cx); + cx.refresh_windows(); + }); + if result.is_err() { + break; // App dropped + } + } + }) + .detach(); +} + pub fn handle_keymap_file_changes( mut user_keymap_file_rx: mpsc::UnboundedReceiver, cx: &mut App, @@ -1137,47 +1203,46 @@ pub fn handle_keymap_file_changes( cx.spawn(move |cx| async move { let mut user_keymap_content = String::new(); + let mut content_migrated = false; loop { select_biased! { _ = base_keymap_rx.next() => {}, _ = keyboard_layout_rx.next() => {}, content = user_keymap_file_rx.next() => { if let Some(content) = content { - user_keymap_content = content; + if let Ok(Some(migrated_content)) = migrate_keymap(&content) { + user_keymap_content = migrated_content; + content_migrated = true; + } else { + user_keymap_content = content; + content_migrated = false; + } } } }; cx.update(|cx| { + if let Some(notifier) = MigratorNotification::try_global(cx) { + notifier.update(cx, |_, cx| { + cx.emit(MigratorEvent::ContentChanged { + migration_type: MigrationType::Keymap, + migrated: content_migrated, + }); + }); + } let load_result = KeymapFile::load(&user_keymap_content, cx); match load_result { - KeymapFileLoadResult::Success { - key_bindings, - keymap_file, - } => { + KeymapFileLoadResult::Success { key_bindings } => { reload_keymaps(cx, key_bindings); - dismiss_app_notification(¬ification_id, cx); - show_keymap_migration_notification_if_needed( - keymap_file, - notification_id.clone(), - cx, - ); + dismiss_app_notification(¬ification_id.clone(), cx); } KeymapFileLoadResult::SomeFailedToLoad { key_bindings, - keymap_file, error_message, } => { if !key_bindings.is_empty() { reload_keymaps(cx, key_bindings); } - dismiss_app_notification(¬ification_id, cx); - if !show_keymap_migration_notification_if_needed( - keymap_file, - notification_id.clone(), - cx, - ) { - show_keymap_file_load_error(notification_id.clone(), error_message, cx); - } + show_keymap_file_load_error(notification_id.clone(), error_message, cx); } KeymapFileLoadResult::JsonParseFailure { error } => { show_keymap_file_json_error(notification_id.clone(), &error, cx) @@ -1209,66 +1274,6 @@ fn show_keymap_file_json_error( }); } -fn show_keymap_migration_notification_if_needed( - keymap_file: KeymapFile, - notification_id: NotificationId, - cx: &mut App, -) -> bool { - if !migrate::should_migrate_keymap(keymap_file) { - return false; - } - let message = MarkdownString(format!( - "Keymap migration needed, as the format for some actions has changed. \ - You can migrate your keymap by clicking below. A backup will be created at {}.", - MarkdownString::inline_code(&paths::keymap_backup_file().to_string_lossy()) - )); - show_markdown_app_notification( - notification_id, - message, - "Backup and Migrate Keymap".into(), - move |_, cx| { - let fs = ::global(cx); - cx.spawn(move |weak_notification, mut cx| async move { - migrate::migrate_keymap(fs).await.ok(); - weak_notification - .update(&mut cx, |_, cx| { - cx.emit(DismissEvent); - }) - .ok(); - }) - .detach(); - }, - cx, - ); - return true; -} - -fn show_settings_migration_notification_if_needed( - notification_id: NotificationId, - settings: serde_json::Value, - cx: &mut App, -) { - if !migrate::should_migrate_settings(&settings) { - return; - } - let message = MarkdownString(format!( - "Settings migration needed, as the format for some settings has changed. \ - You can migrate your settings by clicking below. A backup will be created at {}.", - MarkdownString::inline_code(&paths::settings_backup_file().to_string_lossy()) - )); - show_markdown_app_notification( - notification_id, - message, - "Backup and Migrate Settings".into(), - move |_, cx| { - let fs = ::global(cx); - migrate::migrate_settings(fs, cx); - cx.emit(DismissEvent); - }, - cx, - ); -} - fn show_keymap_file_load_error( notification_id: NotificationId, error_message: MarkdownString, @@ -1363,12 +1368,12 @@ pub fn load_default_keymap(cx: &mut App) { } } -pub fn handle_settings_changed(result: Result, cx: &mut App) { +pub fn handle_settings_changed(error: Option, cx: &mut App) { struct SettingsParseErrorNotification; let id = NotificationId::unique::(); - match result { - Err(error) => { + match error { + Some(error) => { if let Some(InvalidSettingsError::LocalSettings { .. }) = error.downcast_ref::() { @@ -1387,9 +1392,8 @@ pub fn handle_settings_changed(result: Result, }) }); } - Ok(settings) => { + None => { dismiss_app_notification(&id, cx); - show_settings_migration_notification_if_needed(id, settings, cx); } } } @@ -1672,7 +1676,7 @@ mod tests { use language::{LanguageMatcher, LanguageRegistry}; use project::{project_settings::ProjectSettings, Project, ProjectPath, WorktreeSettings}; use serde_json::json; - use settings::{handle_settings_file_changes, watch_config_file, SettingsStore}; + use settings::{watch_config_file, SettingsStore}; use std::{ path::{Path, PathBuf}, time::Duration, diff --git a/crates/zed/src/zed/migrate.rs b/crates/zed/src/zed/migrate.rs index 9116899937b982..d4a0b54f5be2f7 100644 --- a/crates/zed/src/zed/migrate.rs +++ b/crates/zed/src/zed/migrate.rs @@ -1,63 +1,248 @@ -use std::sync::Arc; - -use anyhow::Context; +use anyhow::{Context as _, Result}; +use editor::Editor; use fs::Fs; +use markdown_preview::markdown_elements::ParsedMarkdown; +use markdown_preview::markdown_renderer::render_parsed_markdown; +use migrator::{migrate_keymap, migrate_settings}; use settings::{KeymapFile, SettingsStore}; +use util::markdown::MarkdownString; +use util::ResultExt; -pub fn should_migrate_settings(settings: &serde_json::Value) -> bool { - let Ok(old_text) = serde_json::to_string(settings) else { - return false; - }; - migrator::migrate_settings(&old_text) - .ok() - .flatten() - .is_some() +use std::sync::Arc; + +use gpui::{Entity, EventEmitter, Global, WeakEntity}; +use ui::prelude::*; +use workspace::item::ItemHandle; +use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace}; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum MigrationType { + Keymap, + Settings, } -pub fn migrate_settings(fs: Arc, cx: &mut gpui::App) { - cx.background_executor() - .spawn(async move { - let old_text = SettingsStore::load_settings(&fs).await?; - let Some(new_text) = migrator::migrate_settings(&old_text)? else { - return anyhow::Ok(()); - }; - let settings_path = paths::settings_file().as_path(); - if fs.is_file(settings_path).await { - fs.atomic_write(paths::settings_backup_file().to_path_buf(), old_text) - .await - .with_context(|| { - "Failed to create settings backup in home directory".to_string() - })?; - 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) - })?; +pub struct MigratorBanner { + migration_type: Option, + message: ParsedMarkdown, + workspace: WeakEntity, +} + +pub enum MigratorEvent { + ContentChanged { + migration_type: MigrationType, + migrated: bool, + }, +} + +pub struct MigratorNotification; + +impl EventEmitter for MigratorNotification {} + +impl MigratorNotification { + pub fn try_global(cx: &App) -> Option> { + cx.try_global::() + .map(|notifier| notifier.0.clone()) + } + + pub fn set_global(notifier: Entity, cx: &mut App) { + cx.set_global(GlobalMigratorNotification(notifier)); + } +} + +struct GlobalMigratorNotification(Entity); + +impl Global for GlobalMigratorNotification {} + +impl MigratorBanner { + pub fn new(workspace: &Workspace, cx: &mut Context<'_, Self>) -> Self { + if let Some(notifier) = MigratorNotification::try_global(cx) { + cx.subscribe( + ¬ifier, + move |migrator_banner, _, event: &MigratorEvent, cx| { + migrator_banner.handle_notification(event, cx); + }, + ) + .detach(); + } + Self { + migration_type: None, + message: ParsedMarkdown { children: vec![] }, + workspace: workspace.weak_handle(), + } + } + fn handle_notification(&mut self, event: &MigratorEvent, cx: &mut Context<'_, Self>) { + match event { + MigratorEvent::ContentChanged { + migration_type, + migrated, + } => { + if self.migration_type == Some(*migration_type) { + let location = if *migrated { + ToolbarItemLocation::Secondary + } else { + ToolbarItemLocation::Hidden + }; + cx.emit(ToolbarItemEvent::ChangeLocation(location)); + cx.notify(); + } } - Ok(()) - }) - .detach_and_log_err(cx); + } + } } -pub fn should_migrate_keymap(keymap_file: KeymapFile) -> bool { - let Ok(old_text) = serde_json::to_string(&keymap_file) else { - return false; - }; - migrator::migrate_keymap(&old_text).ok().flatten().is_some() +impl EventEmitter for MigratorBanner {} + +impl ToolbarItemView for MigratorBanner { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + window: &mut Window, + cx: &mut Context, + ) -> ToolbarItemLocation { + cx.notify(); + let Some(target) = active_pane_item + .and_then(|item| item.act_as::(cx)) + .and_then(|editor| editor.update(cx, |editor, cx| editor.target_file_abs_path(cx))) + else { + return ToolbarItemLocation::Hidden; + }; + + if &target == paths::keymap_file() { + self.migration_type = Some(MigrationType::Keymap); + let fs = ::global(cx); + let should_migrate = should_migrate_keymap(fs); + cx.spawn_in(window, |this, mut cx| async move { + if let Ok(true) = should_migrate.await { + this.update(&mut cx, |_, cx| { + cx.emit(ToolbarItemEvent::ChangeLocation( + ToolbarItemLocation::Secondary, + )); + cx.notify(); + }) + .log_err(); + } + }) + .detach(); + } else if &target == paths::settings_file() { + self.migration_type = Some(MigrationType::Settings); + let fs = ::global(cx); + let should_migrate = should_migrate_settings(fs); + cx.spawn_in(window, |this, mut cx| async move { + if let Ok(true) = should_migrate.await { + this.update(&mut cx, |_, cx| { + cx.emit(ToolbarItemEvent::ChangeLocation( + ToolbarItemLocation::Secondary, + )); + cx.notify(); + }) + .log_err(); + } + }) + .detach(); + } + + if let Some(migration_type) = self.migration_type { + cx.spawn_in(window, |this, mut cx| async move { + let message = MarkdownString(format!( + "Your {} require migration to support this version of Zed. A backup will be saved to {}.", + match migration_type { + MigrationType::Keymap => "keymap", + MigrationType::Settings => "settings", + }, + match migration_type { + MigrationType::Keymap => paths::keymap_backup_file().to_string_lossy(), + MigrationType::Settings => paths::settings_backup_file().to_string_lossy(), + }, + )); + let parsed_markdown = cx + .background_executor() + .spawn(async move { + let file_location_directory = None; + let language_registry = None; + markdown_preview::markdown_parser::parse_markdown( + &message.0, + file_location_directory, + language_registry, + ) + .await + }) + .await; + this + .update(&mut cx, |this, _| { + this.message = parsed_markdown; + }) + .log_err(); + }) + .detach(); + } + + return ToolbarItemLocation::Hidden; + } +} + +impl Render for MigratorBanner { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let migration_type = self.migration_type; + h_flex() + .py_1() + .px_2() + .justify_between() + .bg(cx.theme().status().info_background) + .rounded_md() + .gap_2() + .overflow_hidden() + .child( + render_parsed_markdown(&self.message, Some(self.workspace.clone()), window, cx) + .text_ellipsis(), + ) + .child( + Button::new( + SharedString::from("backup-and-migrate"), + "Backup and Migrate", + ) + .style(ButtonStyle::Filled) + .on_click(move |_, _, cx| { + let fs = ::global(cx); + match migration_type { + Some(MigrationType::Keymap) => { + cx.spawn( + move |_| async move { write_keymap_migration(&fs).await.ok() }, + ) + .detach(); + } + Some(MigrationType::Settings) => { + cx.spawn( + move |_| async move { write_settings_migration(&fs).await.ok() }, + ) + .detach(); + } + None => unreachable!(), + } + }), + ) + .into_any_element() + } } -pub async fn migrate_keymap(fs: Arc) -> anyhow::Result<()> { +async fn should_migrate_keymap(fs: Arc) -> Result { let old_text = KeymapFile::load_keymap_file(&fs).await?; - let Some(new_text) = migrator::migrate_keymap(&old_text)? else { + if let Ok(Some(_)) = migrate_keymap(&old_text) { + return Ok(true); + }; + Ok(false) +} + +async fn should_migrate_settings(fs: Arc) -> Result { + let old_text = SettingsStore::load_settings(&fs).await?; + if let Ok(Some(_)) = migrate_settings(&old_text) { + return Ok(true); + }; + Ok(false) +} + +async fn write_keymap_migration(fs: &Arc) -> Result<()> { + let old_text = KeymapFile::load_keymap_file(fs).await?; + let Ok(Some(new_text)) = migrate_keymap(&old_text) else { return Ok(()); }; let keymap_path = paths::keymap_file().as_path(); @@ -77,6 +262,30 @@ pub async fn migrate_keymap(fs: Arc) -> anyhow::Result<()> { .await .with_context(|| format!("Failed to write keymap to file {:?}", keymap_path))?; } + Ok(()) +} +async fn write_settings_migration(fs: &Arc) -> Result<()> { + let old_text = SettingsStore::load_settings(fs).await?; + let Ok(Some(new_text)) = migrate_settings(&old_text) else { + return Ok(()); + }; + let settings_path = paths::settings_file().as_path(); + if fs.is_file(settings_path).await { + fs.atomic_write(paths::settings_backup_file().to_path_buf(), old_text) + .await + .with_context(|| "Failed to create settings backup in home directory".to_string())?; + 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))?; + } Ok(()) }