JSON based migrations (#39398)

Ben Kunkle , Smit , and Smit created

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 <heysmitbarmase@gmail.com>
Co-authored-by: Smit <smit@zed.dev>

Change summary

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(-)

Detailed changes

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",

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

crates/migrator/src/migrator.rs 🔗

@@ -65,14 +65,40 @@ fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Opt
     }
 }
 
-fn run_migrations(
-    text: &str,
-    migrations: &[(MigrationPatterns, &Query)],
-) -> Result<Option<String>> {
+fn run_migrations(text: &str, migrations: &[MigrationType]) -> Result<Option<String>> {
     let mut current_text = text.to_string();
     let mut result: Option<String> = None;
-    for (patterns, query) in migrations.iter() {
-        if let Some(migrated_text) = migrate(&current_text, patterns, query)? {
+    for migration in migrations.iter() {
+        let migrated_text = match migration {
+            MigrationType::TreeSitter(patterns, query) => migrate(&current_text, patterns, query)?,
+            MigrationType::Json(callback) => {
+                let old_content: serde_json_lenient::Value =
+                    settings::parse_json_with_comments(&current_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<Option<String>> {
-    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<Option<String>> {
     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<Option<String>> {
-    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,
             )],

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

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" }