From 7c3a21f732ebc6b0822debb585bb153ecba399f2 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 2 Oct 2025 15:07:26 -0500 Subject: [PATCH] JSON based migrations (#39398) Closes #ISSUE Adds the ability to create settings and keymap migrations by mutating `serde_json::Value`s instead of using tree-sitter queries. This (hopefully) will make complicated migrations far simpler to implement. Release Notes: - N/A *or* Added/Fixed/Improved ... --------- Co-authored-by: Smit Co-authored-by: Smit --- Cargo.lock | 4 +- crates/migrator/Cargo.toml | 3 + crates/migrator/src/migrator.rs | 94 +++++++++++++++++++++---------- crates/zed/src/zed.rs | 59 +++++++++++++------ tooling/workspace-hack/Cargo.toml | 2 - 5 files changed, 110 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 631b1cfb0b60241f065b10816f36e684495e8a83..4ef8495856f8281ce9dfb2093ff564e30b863e80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9688,6 +9688,9 @@ dependencies = [ "convert_case 0.8.0", "log", "pretty_assertions", + "serde_json", + "serde_json_lenient", + "settings", "streaming-iterator", "tree-sitter", "tree-sitter-json", @@ -19686,7 +19689,6 @@ dependencies = [ "core-foundation 0.9.4", "core-foundation-sys", "cranelift-codegen", - "crc32fast", "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", diff --git a/crates/migrator/Cargo.toml b/crates/migrator/Cargo.toml index b23a60e9e9f5699c0c6c95a732b9b8f2e0a9e130..e60233a06d379060537e104279d95b3f20383f0d 100644 --- a/crates/migrator/Cargo.toml +++ b/crates/migrator/Cargo.toml @@ -21,6 +21,9 @@ streaming-iterator.workspace = true tree-sitter-json.workspace = true tree-sitter.workspace = true workspace-hack.workspace = true +serde_json_lenient.workspace = true +serde_json.workspace = true +settings.workspace = true [dev-dependencies] pretty_assertions.workspace = true diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index 788af803f89b2bea6951b1ed8f5932ac7c9b4177..e6085f9c58a3867540c159530d1803c5ef5a5174 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -65,14 +65,40 @@ fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result Result> { +fn run_migrations(text: &str, migrations: &[MigrationType]) -> Result> { let mut current_text = text.to_string(); let mut result: Option = None; - for (patterns, query) in migrations.iter() { - if let Some(migrated_text) = migrate(¤t_text, patterns, query)? { + for migration in migrations.iter() { + let migrated_text = match migration { + MigrationType::TreeSitter(patterns, query) => migrate(¤t_text, patterns, query)?, + MigrationType::Json(callback) => { + let old_content: serde_json_lenient::Value = + settings::parse_json_with_comments(¤t_text)?; + let old_value = serde_json::to_value(&old_content).unwrap(); + let mut new_value = old_value.clone(); + callback(&mut new_value); + if new_value != old_value { + let mut current = current_text.clone(); + let mut edits = vec![]; + settings::update_value_in_json_text( + &mut current, + &mut vec![], + 2, + &old_value, + &new_value, + &mut edits, + ); + let mut migrated_text = current_text.clone(); + for (range, replacement) in edits.into_iter() { + migrated_text.replace_range(range, &replacement); + } + Some(migrated_text) + } else { + None + } + } + }; + if let Some(migrated_text) = migrated_text { current_text = migrated_text.clone(); result = Some(migrated_text); } @@ -81,24 +107,24 @@ fn run_migrations( } pub fn migrate_keymap(text: &str) -> Result> { - let migrations: &[(MigrationPatterns, &Query)] = &[ - ( + let migrations: &[MigrationType] = &[ + MigrationType::TreeSitter( migrations::m_2025_01_29::KEYMAP_PATTERNS, &KEYMAP_QUERY_2025_01_29, ), - ( + MigrationType::TreeSitter( migrations::m_2025_01_30::KEYMAP_PATTERNS, &KEYMAP_QUERY_2025_01_30, ), - ( + MigrationType::TreeSitter( migrations::m_2025_03_03::KEYMAP_PATTERNS, &KEYMAP_QUERY_2025_03_03, ), - ( + MigrationType::TreeSitter( migrations::m_2025_03_06::KEYMAP_PATTERNS, &KEYMAP_QUERY_2025_03_06, ), - ( + MigrationType::TreeSitter( migrations::m_2025_04_15::KEYMAP_PATTERNS, &KEYMAP_QUERY_2025_04_15, ), @@ -106,65 +132,71 @@ pub fn migrate_keymap(text: &str) -> Result> { run_migrations(text, migrations) } +enum MigrationType<'a> { + TreeSitter(MigrationPatterns, &'a Query), + #[allow(unused)] + Json(fn(&mut serde_json::Value)), +} + pub fn migrate_settings(text: &str) -> Result> { - let migrations: &[(MigrationPatterns, &Query)] = &[ - ( + let migrations: &[MigrationType] = &[ + MigrationType::TreeSitter( migrations::m_2025_01_02::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_01_02, ), - ( + MigrationType::TreeSitter( migrations::m_2025_01_29::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_01_29, ), - ( + MigrationType::TreeSitter( migrations::m_2025_01_30::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_01_30, ), - ( + MigrationType::TreeSitter( migrations::m_2025_03_29::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_03_29, ), - ( + MigrationType::TreeSitter( migrations::m_2025_04_15::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_04_15, ), - ( + MigrationType::TreeSitter( migrations::m_2025_04_21::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_04_21, ), - ( + MigrationType::TreeSitter( migrations::m_2025_04_23::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_04_23, ), - ( + MigrationType::TreeSitter( migrations::m_2025_05_05::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_05_05, ), - ( + MigrationType::TreeSitter( migrations::m_2025_05_08::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_05_08, ), - ( + MigrationType::TreeSitter( migrations::m_2025_05_29::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_05_29, ), - ( + MigrationType::TreeSitter( migrations::m_2025_06_16::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_06_16, ), - ( + MigrationType::TreeSitter( migrations::m_2025_06_25::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_06_25, ), - ( + MigrationType::TreeSitter( migrations::m_2025_06_27::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_06_27, ), - ( + MigrationType::TreeSitter( migrations::m_2025_07_08::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_07_08, ), - ( + MigrationType::TreeSitter( migrations::m_2025_10_01::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_10_01, ), @@ -323,7 +355,7 @@ mod tests { } fn assert_migrate_settings_with_migrations( - migrations: &[(MigrationPatterns, &Query)], + migrations: &[MigrationType], input: &str, output: Option<&str>, ) { @@ -919,7 +951,7 @@ mod tests { #[test] fn test_mcp_settings_migration() { assert_migrate_settings_with_migrations( - &[( + &[MigrationType::TreeSitter( migrations::m_2025_06_16::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_06_16, )], @@ -1108,7 +1140,7 @@ mod tests { } }"#; assert_migrate_settings_with_migrations( - &[( + &[MigrationType::TreeSitter( migrations::m_2025_06_16::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_06_16, )], diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 2e26c1a0fec23dbcb220e610fec98c6fb1ce4b80..2d5e89edb0a5862057a54bc53ad9f8c5ac4b070c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1231,31 +1231,54 @@ pub fn handle_settings_file_changes( MigrationNotification::set_global(cx.new(|_| MigrationNotification), cx); // Helper function to process settings content - let process_settings = - move |content: String, is_user: bool, store: &mut SettingsStore, cx: &mut App| -> bool { - // Apply migrations to both user and global settings - let (processed_content, content_migrated) = - if let Ok(Some(migrated_content)) = migrate_settings(&content) { + let process_settings = move |content: String, + is_user: bool, + store: &mut SettingsStore, + cx: &mut App| + -> bool { + let id = NotificationId::Named("failed-to-migrate-settings".into()); + // Apply migrations to both user and global settings + let (processed_content, content_migrated) = match migrate_settings(&content) { + Ok(result) => { + dismiss_app_notification(&id, cx); + if let Some(migrated_content) = result { (migrated_content, true) } else { (content, false) - }; + } + } + Err(err) => { + show_app_notification(id, cx, move |cx| { + cx.new(|cx| { + MessageNotification::new(format!("Failed to migrate settings\n{err}"), cx) + .primary_message("Open Settings File") + .primary_icon(IconName::Settings) + .primary_on_click(|window, cx| { + window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx); + cx.emit(DismissEvent); + }) + }) + }); + // notify user here + (content, false) + } + }; - let result = if is_user { - store.set_user_settings(&processed_content, cx) - } else { - store.set_global_settings(&processed_content, cx) - }; + let result = if is_user { + store.set_user_settings(&processed_content, cx) + } else { + store.set_global_settings(&processed_content, cx) + }; - if let Err(err) = &result { - let settings_type = if is_user { "user" } else { "global" }; - log::error!("Failed to load {} settings: {err}", settings_type); - } + if let Err(err) = &result { + let settings_type = if is_user { "user" } else { "global" }; + log::error!("Failed to load {} settings: {err}", settings_type); + } - settings_changed(result.err(), cx); + settings_changed(result.err(), cx); - content_migrated - }; + content_migrated + }; // Initial load of both settings files let global_content = cx diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index 589a94fe809bda63be6979bfb769e64d15e86553..b2ce439f30680abdd6817391183a5a8caa34958e 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -46,7 +46,6 @@ clap = { version = "4", features = ["cargo", "derive", "string", "wrap_help"] } clap_builder = { version = "4", default-features = false, features = ["cargo", "color", "std", "string", "suggestions", "usage", "wrap_help"] } concurrent-queue = { version = "2" } cranelift-codegen = { version = "0.116", default-features = false, features = ["host-arch", "incremental-cache", "std", "timing", "unwind"] } -crc32fast = { version = "1" } crossbeam-channel = { version = "0.5" } crossbeam-epoch = { version = "0.9" } crossbeam-utils = { version = "0.8" } @@ -177,7 +176,6 @@ clap = { version = "4", features = ["cargo", "derive", "string", "wrap_help"] } clap_builder = { version = "4", default-features = false, features = ["cargo", "color", "std", "string", "suggestions", "usage", "wrap_help"] } concurrent-queue = { version = "2" } cranelift-codegen = { version = "0.116", default-features = false, features = ["host-arch", "incremental-cache", "std", "timing", "unwind"] } -crc32fast = { version = "1" } crossbeam-channel = { version = "0.5" } crossbeam-epoch = { version = "0.9" } crossbeam-utils = { version = "0.8" }