Fix docs validation to detect unknown keys (#49660)

Ben Kunkle and Zed Zippy created

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>

Change summary

Cargo.lock                                        |   3 
crates/docs_preprocessor/Cargo.toml               |   3 
crates/docs_preprocessor/src/main.rs              | 129 +++
crates/json_schema_store/src/json_schema_store.rs |  48 
crates/settings/src/keymap_file.rs                |  10 
crates/settings/src/settings_store.rs             | 123 ++-
crates/zed/src/main.rs                            |  29 
docs/src/ai/agent-settings.md                     |  16 
docs/src/ai/edit-prediction.md                    |  40 
docs/src/ai/external-agents.md                    |   2 
docs/src/ai/llm-providers.md                      |   6 
docs/src/ai/tool-permissions.md                   |  16 
docs/src/debugger.md                              |   4 
docs/src/development/windows.md                   |   4 
docs/src/key-bindings.md                          |  18 
docs/src/languages/ansible.md                     |   2 
docs/src/languages/biome.md                       |   2 
docs/src/languages/cpp.md                         |   2 
docs/src/languages/go.md                          |   4 
docs/src/languages/json.md                        |   4 
docs/src/languages/lua.md                         |   8 
docs/src/languages/python.md                      |   2 
docs/src/languages/ruby.md                        |  12 
docs/src/languages/sql.md                         |   6 
docs/src/languages/svelte.md                      |   4 
docs/src/languages/tailwindcss.md                 |   2 
docs/src/languages/yaml.md                        |   2 
docs/src/reference/all-settings.md                | 499 +++++++++++-----
docs/src/snippets.md                              |   2 
docs/src/tasks.md                                 |  14 
docs/src/vim.md                                   |  20 
docs/src/visual-customization.md                  |  13 
32 files changed, 666 insertions(+), 383 deletions(-)

Detailed changes

Cargo.lock ๐Ÿ”—

@@ -4997,8 +4997,11 @@ name = "docs_preprocessor"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "collections",
+ "jsonschema",
  "mdbook",
  "regex",
+ "schemars",
  "serde",
  "serde_json",
  "settings",

crates/docs_preprocessor/Cargo.toml ๐Ÿ”—

@@ -7,10 +7,13 @@ license = "GPL-3.0-or-later"
 
 [dependencies]
 anyhow.workspace = true
+collections.workspace = true
+jsonschema.workspace = true
 # We are specifically pinning this version of mdbook, as later versions introduce issues with double-nested subdirectories.
 # Ask @maxdeviant about this before bumping.
 mdbook = "= 0.4.40"
 regex.workspace = true
+schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/docs_preprocessor/src/main.rs ๐Ÿ”—

@@ -3,7 +3,7 @@ use mdbook::BookItem;
 use mdbook::book::{Book, Chapter};
 use mdbook::preprocess::CmdPreprocessor;
 use regex::Regex;
-use settings::KeymapFile;
+use settings::{KeymapFile, SettingsStore};
 use std::borrow::Cow;
 use std::collections::{HashMap, HashSet};
 use std::io::{self, Read};
@@ -22,7 +22,7 @@ static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
     load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
 });
 
-static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(load_all_actions);
+static ALL_ACTIONS: LazyLock<ActionManifest> = LazyLock::new(load_all_actions);
 
 const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
 
@@ -68,7 +68,7 @@ enum PreprocessorError {
 
 impl PreprocessorError {
     fn new_for_not_found_action(action_name: String) -> Self {
-        for action in &*ALL_ACTIONS {
+        for action in &ALL_ACTIONS.actions {
             for alias in &action.deprecated_aliases {
                 if alias == action_name.as_str() {
                     return PreprocessorError::DeprecatedActionUsed {
@@ -256,13 +256,14 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Preproces
 
 fn find_action_by_name(name: &str) -> Option<&ActionDef> {
     ALL_ACTIONS
+        .actions
         .binary_search_by(|action| action.name.as_str().cmp(name))
         .ok()
-        .map(|index| &ALL_ACTIONS[index])
+        .map(|index| &ALL_ACTIONS.actions[index])
 }
 
 fn actions_available() -> bool {
-    !ALL_ACTIONS.is_empty()
+    !ALL_ACTIONS.actions.is_empty()
 }
 
 fn is_missing_action(name: &str) -> bool {
@@ -290,6 +291,15 @@ fn find_binding(os: &str, action: &str) -> Option<String> {
 }
 
 fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
+    let settings_schema = SettingsStore::json_schema(&Default::default());
+    let settings_validator = jsonschema::validator_for(&settings_schema)
+        .expect("failed to compile settings JSON schema");
+
+    let keymap_schema =
+        keymap_schema_for_actions(&ALL_ACTIONS.actions, &ALL_ACTIONS.schema_definitions);
+    let keymap_validator =
+        jsonschema::validator_for(&keymap_schema).expect("failed to compile keymap JSON schema");
+
     fn for_each_labeled_code_block_mut(
         book: &mut Book,
         errors: &mut HashSet<PreprocessorError>,
@@ -378,9 +388,15 @@ fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<Pre
                     snippet_json_fixed.insert(0, '{');
                     snippet_json_fixed.push_str("\n}");
                 }
-                settings::parse_json_with_comments::<settings::SettingsContent>(
-                    &snippet_json_fixed,
-                )?;
+                let value =
+                    settings::parse_json_with_comments::<serde_json::Value>(&snippet_json_fixed)?;
+                let validation_errors: Vec<String> = settings_validator
+                    .iter_errors(&value)
+                    .map(|err| err.to_string())
+                    .collect();
+                if !validation_errors.is_empty() {
+                    anyhow::bail!("{}", validation_errors.join("\n"));
+                }
             }
             "keymap" => {
                 if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
@@ -388,21 +404,14 @@ fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<Pre
                     snippet_json_fixed.push_str("\n]");
                 }
 
-                let keymap = settings::KeymapFile::parse(&snippet_json_fixed)
-                    .context("Failed to parse keymap JSON")?;
-                for section in keymap.sections() {
-                    for (_keystrokes, action) in section.bindings() {
-                        if let Some((action_name, _)) = settings::KeymapFile::parse_action(action)
-                            .map_err(|err| anyhow::format_err!(err))
-                            .context("Failed to parse action")?
-                        {
-                            anyhow::ensure!(
-                                !is_missing_action(action_name),
-                                "Action not found: {}",
-                                action_name
-                            );
-                        }
-                    }
+                let value =
+                    settings::parse_json_with_comments::<serde_json::Value>(&snippet_json_fixed)?;
+                let validation_errors: Vec<String> = keymap_validator
+                    .iter_errors(&value)
+                    .map(|err| err.to_string())
+                    .collect();
+                if !validation_errors.is_empty() {
+                    anyhow::bail!("{}", validation_errors.join("\n"));
                 }
             }
             "debug" => {
@@ -505,19 +514,30 @@ where
 struct ActionDef {
     name: String,
     human_name: String,
+    #[serde(default)]
+    schema: Option<serde_json::Value>,
     deprecated_aliases: Vec<String>,
+    #[serde(default)]
+    deprecation_message: Option<String>,
     #[serde(rename = "documentation")]
     docs: Option<String>,
 }
 
-fn load_all_actions() -> Vec<ActionDef> {
+#[derive(Debug, serde::Deserialize)]
+struct ActionManifest {
+    actions: Vec<ActionDef>,
+    #[serde(default)]
+    schema_definitions: serde_json::Map<String, serde_json::Value>,
+}
+
+fn load_all_actions() -> ActionManifest {
     let asset_path = concat!(env!("CARGO_MANIFEST_DIR"), "/actions.json");
     match std::fs::read_to_string(asset_path) {
         Ok(content) => {
-            let mut actions: Vec<ActionDef> =
+            let mut manifest: ActionManifest =
                 serde_json::from_str(&content).expect("Failed to parse actions.json");
-            actions.sort_by(|a, b| a.name.cmp(&b.name));
-            actions
+            manifest.actions.sort_by(|a, b| a.name.cmp(&b.name));
+            manifest
         }
         Err(err) => {
             if std::env::var("CI").is_ok() {
@@ -527,7 +547,10 @@ fn load_all_actions() -> Vec<ActionDef> {
                 "Warning: actions.json not found, action validation will be skipped: {}",
                 err
             );
-            Vec::new()
+            ActionManifest {
+                actions: Vec::new(),
+                schema_definitions: serde_json::Map::new(),
+            }
         }
     }
 }
@@ -661,7 +684,7 @@ fn title_regex() -> &'static Regex {
 }
 
 fn generate_big_table_of_actions() -> String {
-    let actions = &*ALL_ACTIONS;
+    let actions = &ALL_ACTIONS.actions;
     let mut output = String::new();
 
     let mut actions_sorted = actions.iter().collect::<Vec<_>>();
@@ -710,3 +733,51 @@ fn generate_big_table_of_actions() -> String {
 
     output
 }
+
+fn keymap_schema_for_actions(
+    actions: &[ActionDef],
+    schema_definitions: &serde_json::Map<String, serde_json::Value>,
+) -> serde_json::Value {
+    let mut generator = KeymapFile::action_schema_generator();
+
+    for (name, definition) in schema_definitions {
+        generator
+            .definitions_mut()
+            .insert(name.clone(), definition.clone());
+    }
+
+    let mut action_schemas = Vec::new();
+    let mut documentation = collections::HashMap::<&str, &str>::default();
+    let mut deprecations = collections::HashMap::<&str, &str>::default();
+    let mut deprecation_messages = collections::HashMap::<&str, &str>::default();
+
+    for action in actions {
+        let schema = action
+            .schema
+            .as_ref()
+            .and_then(|v| serde_json::from_value::<schemars::Schema>(v.clone()).ok());
+        action_schemas.push((action.name.as_str(), schema));
+        if let Some(doc) = &action.docs {
+            documentation.insert(action.name.as_str(), doc.as_str());
+        }
+        if let Some(msg) = &action.deprecation_message {
+            deprecation_messages.insert(action.name.as_str(), msg.as_str());
+        }
+        for alias in &action.deprecated_aliases {
+            deprecations.insert(alias.as_str(), action.name.as_str());
+            let alias_schema = action
+                .schema
+                .as_ref()
+                .and_then(|v| serde_json::from_value::<schemars::Schema>(v.clone()).ok());
+            action_schemas.push((alias.as_str(), alias_schema));
+        }
+    }
+
+    KeymapFile::generate_json_schema(
+        generator,
+        action_schemas,
+        &documentation,
+        &deprecations,
+        &deprecation_messages,
+    )
+}

crates/json_schema_store/src/json_schema_store.rs ๐Ÿ”—

@@ -330,15 +330,13 @@ async fn resolve_dynamic_schema(
                 let icon_theme_names = icon_theme_names.as_slice();
                 let theme_names = theme_names.as_slice();
 
-                cx.global::<settings::SettingsStore>().json_schema(
-                    &settings::SettingsJsonSchemaParams {
-                        language_names,
-                        font_names,
-                        theme_names,
-                        icon_theme_names,
-                        lsp_adapter_names: &lsp_adapter_names,
-                    },
-                )
+                settings::SettingsStore::json_schema(&settings::SettingsJsonSchemaParams {
+                    language_names,
+                    font_names,
+                    theme_names,
+                    icon_theme_names,
+                    lsp_adapter_names: &lsp_adapter_names,
+                })
             })
         }
         "project_settings" => {
@@ -348,25 +346,21 @@ async fn resolve_dynamic_schema(
                 .map(|adapter| adapter.name().to_string())
                 .collect::<Vec<_>>();
 
-            cx.update(|cx| {
-                let language_names = &languages
-                    .language_names()
-                    .into_iter()
-                    .map(|name| name.to_string())
-                    .collect::<Vec<_>>();
+            let language_names = &languages
+                .language_names()
+                .into_iter()
+                .map(|name| name.to_string())
+                .collect::<Vec<_>>();
 
-                cx.global::<settings::SettingsStore>().project_json_schema(
-                    &settings::SettingsJsonSchemaParams {
-                        language_names,
-                        lsp_adapter_names: &lsp_adapter_names,
-                        // These are not allowed in project-specific settings but
-                        // they're still fields required by the
-                        // `SettingsJsonSchemaParams` struct.
-                        font_names: &[],
-                        theme_names: &[],
-                        icon_theme_names: &[],
-                    },
-                )
+            settings::SettingsStore::project_json_schema(&settings::SettingsJsonSchemaParams {
+                language_names,
+                lsp_adapter_names: &lsp_adapter_names,
+                // These are not allowed in project-specific settings but
+                // they're still fields required by the
+                // `SettingsJsonSchemaParams` struct.
+                font_names: &[],
+                theme_names: &[],
+                icon_theme_names: &[],
             })
         }
         "debug_tasks" => {

crates/settings/src/keymap_file.rs ๐Ÿ”—

@@ -531,12 +531,12 @@ impl KeymapFile {
         None
     }
 
-    fn generate_json_schema(
+    pub fn generate_json_schema<'a>(
         mut generator: schemars::SchemaGenerator,
-        action_schemas: Vec<(&'static str, Option<schemars::Schema>)>,
-        action_documentation: &HashMap<&'static str, &'static str>,
-        deprecations: &HashMap<&'static str, &'static str>,
-        deprecation_messages: &HashMap<&'static str, &'static str>,
+        action_schemas: Vec<(&'a str, Option<schemars::Schema>)>,
+        action_documentation: &HashMap<&'a str, &'a str>,
+        deprecations: &HashMap<&'a str, &'a str>,
+        deprecation_messages: &HashMap<&'a str, &'a str>,
     ) -> serde_json::Value {
         fn add_deprecation(schema: &mut schemars::Schema, message: String) {
             schema.insert(

crates/settings/src/settings_store.rs ๐Ÿ”—

@@ -264,6 +264,7 @@ pub trait AnySettingValue: 'static + Send + Sync {
 }
 
 /// Parameters that are used when generating some JSON schemas at runtime.
+#[derive(Default)]
 pub struct SettingsJsonSchemaParams<'a> {
     pub language_names: &'a [String],
     pub font_names: &'a [String],
@@ -1047,13 +1048,15 @@ impl SettingsStore {
             .subschema_for::<LanguageSettingsContent>()
             .to_value();
 
-        replace_subschema::<LanguageToSettingsMap>(generator, || {
-            json_schema!({
-                "type": "object",
-                "errorMessage": "No language with this name is installed.",
-                "properties": params.language_names.iter().map(|name| (name.clone(), language_settings_content_ref.clone())).collect::<serde_json::Map<_, _>>()
-            })
-        });
+        if !params.language_names.is_empty() {
+            replace_subschema::<LanguageToSettingsMap>(generator, || {
+                json_schema!({
+                    "type": "object",
+                    "errorMessage": "No language with this name is installed.",
+                    "properties": params.language_names.iter().map(|name| (name.clone(), language_settings_content_ref.clone())).collect::<serde_json::Map<_, _>>()
+                })
+            });
+        }
 
         generator.subschema_for::<LspSettings>();
 
@@ -1063,46 +1066,48 @@ impl SettingsStore {
             .expect("LspSettings should be defined")
             .clone();
 
-        replace_subschema::<LspSettingsMap>(generator, || {
-            let mut lsp_properties = serde_json::Map::new();
+        if !params.lsp_adapter_names.is_empty() {
+            replace_subschema::<LspSettingsMap>(generator, || {
+                let mut lsp_properties = serde_json::Map::new();
 
-            for adapter_name in params.lsp_adapter_names {
-                let mut base_lsp_settings = lsp_settings_definition
-                    .as_object()
-                    .expect("LspSettings should be an object")
-                    .clone();
+                for adapter_name in params.lsp_adapter_names {
+                    let mut base_lsp_settings = lsp_settings_definition
+                        .as_object()
+                        .expect("LspSettings should be an object")
+                        .clone();
 
-                if let Some(properties) = base_lsp_settings.get_mut("properties") {
-                    if let Some(properties_object) = properties.as_object_mut() {
-                        properties_object.insert(
+                    if let Some(properties) = base_lsp_settings.get_mut("properties") {
+                        if let Some(properties_object) = properties.as_object_mut() {
+                            properties_object.insert(
                             "initialization_options".to_string(),
                             serde_json::json!({
                                 "$ref": format!("{LSP_SETTINGS_SCHEMA_URL_PREFIX}{adapter_name}/initialization_options")
                             }),
                         );
-                        properties_object.insert(
+                            properties_object.insert(
                             "settings".to_string(),
                             serde_json::json!({
                                 "$ref": format!("{LSP_SETTINGS_SCHEMA_URL_PREFIX}{adapter_name}/settings")
                             }),
                         );
+                        }
                     }
-                }
 
-                lsp_properties.insert(
-                    adapter_name.clone(),
-                    serde_json::Value::Object(base_lsp_settings),
-                );
-            }
+                    lsp_properties.insert(
+                        adapter_name.clone(),
+                        serde_json::Value::Object(base_lsp_settings),
+                    );
+                }
 
-            json_schema!({
-                "type": "object",
-                "properties": lsp_properties
-            })
-        });
+                json_schema!({
+                    "type": "object",
+                    "properties": lsp_properties
+                })
+            });
+        }
     }
 
-    pub fn json_schema(&self, params: &SettingsJsonSchemaParams) -> Value {
+    pub fn json_schema(params: &SettingsJsonSchemaParams) -> Value {
         let mut generator = schemars::generate::SchemaSettings::draft2019_09()
             .with_transform(DefaultDenyUnknownFields)
             .with_transform(AllowTrailingCommas)
@@ -1111,26 +1116,32 @@ impl SettingsStore {
         UserSettingsContent::json_schema(&mut generator);
         Self::configure_schema_generator(&mut generator, params);
 
-        replace_subschema::<FontFamilyName>(&mut generator, || {
-            json_schema!({
-                "type": "string",
-                "enum": params.font_names,
-            })
-        });
+        if !params.font_names.is_empty() {
+            replace_subschema::<FontFamilyName>(&mut generator, || {
+                json_schema!({
+                     "type": "string",
+                     "enum": params.font_names,
+                })
+            });
+        }
 
-        replace_subschema::<ThemeName>(&mut generator, || {
-            json_schema!({
-                "type": "string",
-                "enum": params.theme_names,
-            })
-        });
+        if !params.theme_names.is_empty() {
+            replace_subschema::<ThemeName>(&mut generator, || {
+                json_schema!({
+                    "type": "string",
+                    "enum": params.theme_names,
+                })
+            });
+        }
 
-        replace_subschema::<IconThemeName>(&mut generator, || {
-            json_schema!({
-                "type": "string",
-                "enum": params.icon_theme_names,
-            })
-        });
+        if !params.icon_theme_names.is_empty() {
+            replace_subschema::<IconThemeName>(&mut generator, || {
+                json_schema!({
+                    "type": "string",
+                    "enum": params.icon_theme_names,
+                })
+            });
+        }
 
         generator
             .root_schema_for::<UserSettingsContent>()
@@ -1139,7 +1150,7 @@ impl SettingsStore {
 
     /// Generate JSON schema for project settings, including only settings valid
     /// for project-level configurations.
-    pub fn project_json_schema(&self, params: &SettingsJsonSchemaParams) -> Value {
+    pub fn project_json_schema(params: &SettingsJsonSchemaParams) -> Value {
         let mut generator = schemars::generate::SchemaSettings::draft2019_09()
             .with_transform(DefaultDenyUnknownFields)
             .with_transform(AllowTrailingCommas)
@@ -2421,9 +2432,9 @@ mod tests {
 
     #[gpui::test]
     fn test_lsp_settings_schema_generation(cx: &mut App) {
-        let store = SettingsStore::test(cx);
+        SettingsStore::test(cx);
 
-        let schema = store.json_schema(&SettingsJsonSchemaParams {
+        let schema = SettingsStore::json_schema(&SettingsJsonSchemaParams {
             language_names: &["Rust".to_string(), "TypeScript".to_string()],
             font_names: &["Zed Mono".to_string()],
             theme_names: &["One Dark".into()],
@@ -2472,9 +2483,9 @@ mod tests {
 
     #[gpui::test]
     fn test_lsp_project_settings_schema_generation(cx: &mut App) {
-        let store = SettingsStore::test(cx);
+        SettingsStore::test(cx);
 
-        let schema = store.project_json_schema(&SettingsJsonSchemaParams {
+        let schema = SettingsStore::project_json_schema(&SettingsJsonSchemaParams {
             language_names: &["Rust".to_string(), "TypeScript".to_string()],
             font_names: &["Zed Mono".to_string()],
             theme_names: &["One Dark".into()],
@@ -2523,7 +2534,7 @@ mod tests {
 
     #[gpui::test]
     fn test_project_json_schema_differs_from_user_schema(cx: &mut App) {
-        let store = SettingsStore::test(cx);
+        SettingsStore::test(cx);
 
         let params = SettingsJsonSchemaParams {
             language_names: &["Rust".to_string()],
@@ -2533,8 +2544,8 @@ mod tests {
             lsp_adapter_names: &["rust-analyzer".to_string()],
         };
 
-        let user_schema = store.json_schema(&params);
-        let project_schema = store.project_json_schema(&params);
+        let user_schema = SettingsStore::json_schema(&params);
+        let project_schema = SettingsStore::project_json_schema(&params);
 
         assert_ne!(user_schema, project_schema);
 

crates/zed/src/main.rs ๐Ÿ”—

@@ -1756,23 +1756,40 @@ fn dump_all_gpui_actions() {
     struct ActionDef {
         name: &'static str,
         human_name: String,
+        schema: Option<serde_json::Value>,
         deprecated_aliases: &'static [&'static str],
+        deprecation_message: Option<&'static str>,
         documentation: Option<&'static str>,
     }
+    let mut generator = settings::KeymapFile::action_schema_generator();
     let mut actions = gpui::generate_list_of_all_registered_actions()
-        .map(|action| ActionDef {
-            name: action.name,
-            human_name: command_palette::humanize_action_name(action.name),
-            deprecated_aliases: action.deprecated_aliases,
-            documentation: action.documentation,
+        .map(|action| {
+            let schema = (action.json_schema)(&mut generator)
+                .map(|s| serde_json::to_value(s).expect("Failed to serialize action schema"));
+            ActionDef {
+                name: action.name,
+                human_name: command_palette::humanize_action_name(action.name),
+                schema,
+                deprecated_aliases: action.deprecated_aliases,
+                deprecation_message: action.deprecation_message,
+                documentation: action.documentation,
+            }
         })
         .collect::<Vec<ActionDef>>();
 
     actions.sort_by_key(|a| a.name);
 
+    let schema_definitions = serde_json::to_value(generator.definitions())
+        .expect("Failed to serialize schema definitions");
+
+    let output = serde_json::json!({
+        "actions": actions,
+        "schema_definitions": schema_definitions,
+    });
+
     io::Write::write(
         &mut std::io::stdout(),
-        serde_json::to_string_pretty(&actions).unwrap().as_bytes(),
+        serde_json::to_string_pretty(&output).unwrap().as_bytes(),
     )
     .unwrap();
 }

docs/src/ai/agent-settings.md ๐Ÿ”—

@@ -242,8 +242,20 @@ Patterns are **case-insensitive** by default. To make a pattern case-sensitive,
 
 ```json [settings]
 {
-  "pattern": "^Makefile$",
-  "case_sensitive": true
+  "agent": {
+    "tool_permissions": {
+      "tools": {
+        "edit_file": {
+          "always_deny": [
+            {
+              "pattern": "^Makefile$",
+              "case_sensitive": true
+            }
+          ]
+        }
+      }
+    }
+  }
 }
 ```
 

docs/src/ai/edit-prediction.md ๐Ÿ”—

@@ -18,9 +18,11 @@ Once signed in, predictions appear as you type.
 You can confirm that Zeta is properly configured either by verifying whether you have the following code in your settings file:
 
 ```json [settings]
-"edit_predictions": {
-  "provider": "zed"
-},
+{
+  "edit_predictions": {
+    "provider": "zed"
+  }
+}
 ```
 
 The Z icon in the status bar also indicates Zeta is active.
@@ -68,7 +70,7 @@ On Linux, `alt-tab` is often used by the window manager for switching windows, s
 
 By default, `tab` is used to accept edit predictions. You can use another keybinding by inserting this in your keymap:
 
-```json [settings]
+```json [keymap]
 {
   "context": "Editor && edit_prediction",
   "bindings": {
@@ -81,7 +83,7 @@ By default, `tab` is used to accept edit predictions. You can use another keybin
 When there's a [conflict with the `tab` key](#edit-predictions-conflict), Zed uses a different key context to accept keybindings (`edit_prediction_conflict`).
 If you want to use a different one, you can insert this in your keymap:
 
-```json [settings]
+```json [keymap]
 {
   "context": "Editor && edit_prediction_conflict",
   "bindings": {
@@ -95,7 +97,7 @@ If your keybinding contains a modifier (`ctrl` in the example above), it will al
 You can also bind this action to keybind without a modifier.
 In that case, Zed will use the default modifier (`alt`) to preview the edit prediction.
 
-```json [settings]
+```json [keymap]
 {
   "context": "Editor && edit_prediction_conflict",
   "bindings": {
@@ -108,7 +110,7 @@ In that case, Zed will use the default modifier (`alt`) to preview the edit pred
 
 To maintain the use of the modifier key for accepting predictions when there is a language server completions menu, but allow `tab` to accept predictions regardless of cursor position, you can specify the context further with `showing_completions`:
 
-```json [settings]
+```json [keymap]
 {
   "context": "Editor && edit_prediction_conflict && !showing_completions",
   "bindings": {
@@ -261,8 +263,8 @@ To not have predictions appear automatically as you type when working with a spe
 
 ```json [settings]
 {
-  "language": {
-    "python": {
+  "languages": {
+    "Python": {
       "show_edit_predictions": false
     }
   }
@@ -286,9 +288,11 @@ To disable edit predictions for specific directories or files, set this in your
 To completely turn off edit prediction across all providers, explicitly set the settings to `none`, like so:
 
 ```json [settings]
-"features": {
-  "edit_prediction_provider": "none"
-},
+{
+  "edit_predictions": {
+    "provider": "none"
+  }
+}
 ```
 
 ## Configuring Other Providers {#other-providers}
@@ -301,8 +305,8 @@ To use GitHub Copilot as your provider, set this in your settings file ([how to
 
 ```json [settings]
 {
-  "features": {
-    "edit_prediction_provider": "copilot"
+  "edit_predictions": {
+    "provider": "copilot"
   }
 }
 ```
@@ -372,8 +376,8 @@ After adding your API key, Mercury Coder will appear in the provider dropdown in
 
 ```json [settings]
 {
-  "features": {
-    "edit_prediction_provider": "mercury"
+  "edit_predictions": {
+    "provider": "mercury"
   }
 }
 ```
@@ -394,8 +398,8 @@ After adding your API key, Codestral will appear in the provider dropdown in the
 
 ```json [settings]
 {
-  "features": {
-    "edit_prediction_provider": "codestral"
+  "edit_predictions": {
+    "provider": "codestral"
   }
 }
 ```

docs/src/ai/external-agents.md ๐Ÿ”—

@@ -88,7 +88,7 @@ If you'd like to bind this to a keyboard shortcut, you can do so by editing your
 [
   {
     "bindings": {
-      "cmd-alt-c": ["agent::NewExternalAgentThread", { "agent": "claude" }]
+      "cmd-alt-c": ["agent::NewExternalAgentThread", { "agent": "claude_code" }]
     }
   }
 ]

docs/src/ai/llm-providers.md ๐Ÿ”—

@@ -48,7 +48,7 @@ Ensure your credentials have the following permissions set up:
 
 Your IAM policy should look similar to:
 
-```json [settings]
+```json
 {
   "Version": "2012-10-17",
   "Statement": [
@@ -214,7 +214,7 @@ Custom models will be listed in the model dropdown in the Agent Panel.
 
 You can configure a model to use [extended thinking](https://docs.anthropic.com/en/docs/about-claude/models/extended-thinking-models) (if it supports it) by changing the mode in your model's configuration to `thinking`, for example:
 
-```json [settings]
+```json
 {
   "name": "claude-sonnet-4-latest",
   "display_name": "claude-sonnet-4-thinking",
@@ -762,7 +762,7 @@ The Zed agent comes pre-configured with common Grok models. If you wish to use a
 You can use a custom API endpoint for different providers, as long as it's compatible with the provider's API structure.
 To do so, add the following to your settings file ([how to edit](../configuring-zed.md#settings-files)):
 
-```json [settings]
+```json
 {
   "language_models": {
     "some-provider": {

docs/src/ai/tool-permissions.md ๐Ÿ”—

@@ -92,8 +92,20 @@ For example, a tool called `create_issue` on a server called `github` would be `
 
 ```json [settings]
 {
-  "pattern": "your-regex-here",
-  "case_sensitive": false
+  "agent": {
+    "tool_permissions": {
+      "tools": {
+        "edit_file": {
+          "always_allow": [
+            {
+              "pattern": "your-regex-here",
+              "case_sensitive": false
+            }
+          ]
+        }
+      }
+    }
+  }
 }
 ```
 

docs/src/debugger.md ๐Ÿ”—

@@ -255,7 +255,7 @@ The settings for the debugger are grouped under the `debugger` key in `settings.
 
 - Description: Whether the button should be displayed in the debugger toolbar.
 - Default: `true`
-- Setting: `debugger.show_button`
+- Setting: `debugger.button`
 
 **Options**
 
@@ -264,7 +264,7 @@ The settings for the debugger are grouped under the `debugger` key in `settings.
 ```json [settings]
 {
   "debugger": {
-    "show_button": true
+    "button": true
   }
 }
 ```

docs/src/development/windows.md ๐Ÿ”—

@@ -23,7 +23,7 @@ Clone the [Zed repository](https://github.com/zed-industries/zed).
 
 If you cannot compile Zed, make sure a Visual Studio installation includes at least the following components:
 
-```json [settings]
+```json
 {
   "version": "1.0",
   "components": [
@@ -41,7 +41,7 @@ If you cannot compile Zed, make sure a Visual Studio installation includes at le
 
 If you are using Build Tools only, make sure these components are installed:
 
-```json [settings]
+```json
 {
   "version": "1.0",
   "components": [

docs/src/key-bindings.md ๐Ÿ”—

@@ -99,13 +99,15 @@ The keys can be any single Unicode codepoint that your keyboard generates (for e
 
 A few examples:
 
-```json [settings]
- "bindings": {
-   "cmd-k cmd-s": "zed::OpenKeymap", // matches โŒ˜-k then โŒ˜-s
-   "space e": "editor::Complete", // type space then e
-   "รง": "editor::Complete", // matches โŒฅ-c
-   "shift shift": "file_finder::Toggle", // matches pressing and releasing shift twice
- }
+```json [keymap]
+{
+  "bindings": {
+    "cmd-k cmd-s": "zed::OpenKeymap", // matches โŒ˜-k then โŒ˜-s
+    "space e": "editor::ShowCompletions", // type space then e
+    "รง": "editor::ShowCompletions", // matches โŒฅ-c
+    "shift shift": "file_finder::Toggle" // matches pressing and releasing shift twice
+  }
+}
 ```
 
 The `shift-` modifier can only be used in combination with a letter to indicate the uppercase version. For example, `shift-g` matches typing `G`. Although on many keyboards shift is used to type punctuation characters like `(`, the keypress is not considered to be modified, and so `shift-(` does not match.
@@ -289,7 +291,7 @@ If you're on Linux or Windows, you might find yourself wanting to forward key co
 
 For example, `ctrl-n` creates a new tab in Zed on Linux. If you want to send `ctrl-n` to the built-in terminal when it's focused, add the following to your keymap:
 
-```json [settings]
+```json [keymap]
 {
   "context": "Terminal",
   "bindings": {

docs/src/languages/ansible.md ๐Ÿ”—

@@ -76,7 +76,7 @@ If your inventory file is in the YAML format, you can either:
 
 By default, the following default config is passed to the Ansible language server. It conveniently mirrors the defaults set by [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig/blob/03bc581e05e81d33808b42b2d7e76d70adb3b595/lua/lspconfig/configs/ansiblels.lua) for the Ansible language server:
 
-```json [settings]
+```json
 {
   "ansible": {
     "ansible": {

docs/src/languages/biome.md ๐Ÿ”—

@@ -29,7 +29,7 @@ The Biome extension includes support for the following languages:
 
 By default, the `biome.json` file is required to be in the root of the workspace.
 
-```json [settings]
+```json
 {
   "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json"
 }

docs/src/languages/cpp.md ๐Ÿ”—

@@ -180,7 +180,7 @@ Allows switching between corresponding C++ source files (e.g., `.cpp`) and heade
 by running the command {#action editor::SwitchSourceHeader} from the command palette or by setting
 a keybinding for the `editor::SwitchSourceHeader` action.
 
-```json [settings]
+```json [keymap]
 {
   "context": "Editor",
   "bindings": {

docs/src/languages/go.md ๐Ÿ”—

@@ -46,7 +46,7 @@ If `gopls` is not found you will likely need to add `export PATH="$PATH:$HOME/go
 
 Zed sets the following initialization options for inlay hints:
 
-```json [settings]
+```json
 "hints": {
     "assignVariableTypes": true,
     "compositeLiteralFields": true,
@@ -62,7 +62,7 @@ to make the language server send back inlay hints when Zed has them enabled in t
 
 Use
 
-```json [settings]
+```json
 "lsp": {
     "gopls": {
         "initialization_options": {

docs/src/languages/json.md ๐Ÿ”—

@@ -21,7 +21,7 @@ If you use files with the `*.jsonc` extension when using `Format Document` or ha
 
 To workaround this behavior you can add the following to your `.prettierrc` configuration file:
 
-```json [settings]
+```json
 {
   "overrides": [
     {
@@ -45,7 +45,7 @@ To specify a schema inline with your JSON files, add a `$schema` top level key l
 
 For example to for a `.luarc.json` for use with [lua-language-server](https://github.com/LuaLS/lua-language-server/):
 
-```json [settings]
+```json
 {
   "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
   "runtime.version": "Lua 5.4"

docs/src/languages/lua.md ๐Ÿ”—

@@ -14,7 +14,7 @@ Lua support is available through the [Lua extension](https://github.com/zed-exte
 
 To configure LuaLS you can create a `.luarc.json` file in the root of your project.
 
-```json [settings]
+```json
 {
   "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
   "runtime.version": "Lua 5.4",
@@ -60,7 +60,7 @@ cd .. && git clone https://github.com/notpeter/playdate-luacats
 
 Then in your `.luarc.json`:
 
-```json [settings]
+```json
 {
   "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
   "runtime.version": "Lua 5.4",
@@ -96,6 +96,7 @@ To enable [Inlay Hints](../configuring-languages.md#inlay-hints) for LuaLS in Ze
 1. Configure inlay hints in Settings ({#kb zed::OpenSettings}) under Languages > Lua, or add to your settings file:
 
 ```json [settings]
+{
   "languages": {
     "Lua": {
       "inlay_hints": {
@@ -106,6 +107,7 @@ To enable [Inlay Hints](../configuring-languages.md#inlay-hints) for LuaLS in Ze
       }
     }
   }
+}
 ```
 
 2. Add `"hint.enable": true` to your `.luarc.json`.
@@ -116,7 +118,7 @@ To enable [Inlay Hints](../configuring-languages.md#inlay-hints) for LuaLS in Ze
 
 To enable auto-formatting with your LuaLS (provided by [CppCXY/EmmyLuaCodeStyle](https://github.com/CppCXY/EmmyLuaCodeStyle)) make sure you have `"format.enable": true,` in your .luarc.json:
 
-```json [settings]
+```json
 {
   "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
   "format.enable": true

docs/src/languages/python.md ๐Ÿ”—

@@ -153,7 +153,7 @@ basedpyright reads project-specific configuration from the `pyrightconfig.json`
 
 Here's an example `pyrightconfig.json` file that configures basedpyright to use the `strict` type-checking mode and not to issue diagnostics for any files in `__pycache__` directories:
 
-```json [settings]
+```json
 {
   "typeCheckingMode": "strict",
   "ignore": ["**/__pycache__"]

docs/src/languages/ruby.md ๐Ÿ”—

@@ -410,11 +410,13 @@ Configure formatting in Settings ({#kb zed::OpenSettings}) under Languages > HTM
 
 ```json [settings]
 {
-  "HTML+ERB": {
-    "formatter": {
-      "external": {
-        "command": "erb-formatter",
-        "arguments": ["--stdin-filename", "{buffer_path}"]
+  "languages": {
+    "HTML+ERB": {
+      "formatter": {
+        "external": {
+          "command": "erb-formatter",
+          "arguments": ["--stdin-filename", "{buffer_path}"]
+        }
       }
     }
   }

docs/src/languages/sql.md ๐Ÿ”—

@@ -49,7 +49,7 @@ You can add this to Zed project settings (`.zed/settings.json`) or via your Zed
 
 Sql-formatter also allows more precise control by providing [sql-formatter configuration options](https://github.com/sql-formatter-org/sql-formatter#configuration-options). To provide these, create a `.sql-formatter.json` file in your project:
 
-```json [settings]
+```json
 {
   "language": "postgresql",
   "tabWidth": 2,
@@ -61,6 +61,7 @@ Sql-formatter also allows more precise control by providing [sql-formatter confi
 When using a `.sql-formatter.json` file you can use a simplified Zed settings configuration:
 
 ```json [settings]
+{
   "languages": {
     "SQL": {
       "formatter": {
@@ -69,5 +70,6 @@ When using a `.sql-formatter.json` file you can use a simplified Zed settings co
         }
       }
     }
-  },
+  }
+}
 ```

docs/src/languages/svelte.md ๐Ÿ”—

@@ -14,7 +14,7 @@ Svelte support is available through the [Svelte extension](https://github.com/ze
 
 You can modify how certain styles, such as directives and modifiers, appear in attributes:
 
-```json [settings]
+```json
 "syntax": {
   // Styling for directives (e.g., `class:foo` or `on:click`) (the `on` or `class` part of the attribute).
   "attribute.function": {
@@ -31,7 +31,7 @@ You can modify how certain styles, such as directives and modifiers, appear in a
 
 When inlay hints is enabled in Zed, to make the language server send them back, Zed sets the following initialization options:
 
-```json [settings]
+```json
 "inlayHints": {
   "parameterNames": {
     "enabled": "all",

docs/src/languages/tailwindcss.md ๐Ÿ”—

@@ -69,7 +69,7 @@ The `tailwindcss-intellisense-css` language server serves as an alternative to t
 
 Zed supports Prettier out of the box, which means that if you have the [Tailwind CSS Prettier plugin](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) installed, adding it to your Prettier configuration will make it work automatically:
 
-```json [settings]
+```json
 // .prettierrc
 {
   "plugins": ["prettier-plugin-tailwindcss"]

docs/src/languages/yaml.md ๐Ÿ”—

@@ -43,7 +43,7 @@ By default, Zed uses Prettier for formatting YAML files.
 
 You can customize the formatting behavior of Prettier. For example to use single-quotes in yaml files add the following to your `.prettierrc` configuration file:
 
-```json [settings]
+```json
 {
   "overrides": [
     {

docs/src/reference/all-settings.md ๐Ÿ”—

@@ -423,9 +423,11 @@ A font size from `6` to `100` pixels (inclusive)
 - Default:
 
 ```json [settings]
-"centered_layout": {
-  "left_padding": 0.2,
-  "right_padding": 0.2,
+{
+  "centered_layout": {
+    "left_padding": 0.2,
+    "right_padding": 0.2
+  }
 }
 ```
 
@@ -613,13 +615,17 @@ List of `string` values
 1. Don't show edit predictions in comments:
 
 ```json [settings]
-"disabled_in": ["comment"]
+{
+  "edit_predictions_disabled_in": ["comment"]
+}
 ```
 
 2. Don't show edit predictions in strings and comments:
 
 ```json [settings]
-"disabled_in": ["comment", "string"]
+{
+  "edit_predictions_disabled_in": ["comment", "string"]
+}
 ```
 
 3. Only in Go, don't show edit predictions in strings and comments:
@@ -645,25 +651,33 @@ List of `string` values
 1. Don't highlight the current line:
 
 ```json [settings]
-"current_line_highlight": "none"
+{
+  "current_line_highlight": "none"
+}
 ```
 
 2. Highlight the gutter area:
 
 ```json [settings]
-"current_line_highlight": "gutter"
+{
+  "current_line_highlight": "gutter"
+}
 ```
 
 3. Highlight the editor area:
 
 ```json [settings]
-"current_line_highlight": "line"
+{
+  "current_line_highlight": "line"
+}
 ```
 
 4. Highlight the full line:
 
 ```json [settings]
-"current_line_highlight": "all"
+{
+  "current_line_highlight": "all"
+}
 ```
 
 ## Selection Highlight
@@ -699,25 +713,33 @@ List of `string` values
 1. A vertical bar:
 
 ```json [settings]
-"cursor_shape": "bar"
+{
+  "cursor_shape": "bar"
+}
 ```
 
 2. A block that surrounds the following character:
 
 ```json [settings]
-"cursor_shape": "block"
+{
+  "cursor_shape": "block"
+}
 ```
 
 3. An underline / underscore that runs along the following character:
 
 ```json [settings]
-"cursor_shape": "underline"
+{
+  "cursor_shape": "underline"
+}
 ```
 
 4. An box drawn around the following character:
 
 ```json [settings]
-"cursor_shape": "hollow"
+{
+  "cursor_shape": "hollow"
+}
 ```
 
 ## Gutter
@@ -757,19 +779,25 @@ List of `string` values
 1. Never hide the mouse cursor:
 
 ```json [settings]
-"hide_mouse": "never"
+{
+  "hide_mouse": "never"
+}
 ```
 
 2. Hide only when typing:
 
 ```json [settings]
-"hide_mouse": "on_typing"
+{
+  "hide_mouse": "on_typing"
+}
 ```
 
 3. Hide on both typing and cursor movement:
 
 ```json [settings]
-"hide_mouse": "on_typing_and_movement"
+{
+  "hide_mouse": "on_typing_and_movement"
+}
 ```
 
 ## Snippet Sort Order
@@ -783,25 +811,33 @@ List of `string` values
 1. Place snippets at the top of the completion list:
 
 ```json [settings]
-"snippet_sort_order": "top"
+{
+  "snippet_sort_order": "top"
+}
 ```
 
 2. Place snippets normally without any preference:
 
 ```json [settings]
-"snippet_sort_order": "inline"
+{
+  "snippet_sort_order": "inline"
+}
 ```
 
 3. Place snippets at the bottom of the completion list:
 
 ```json [settings]
-"snippet_sort_order": "bottom"
+{
+  "snippet_sort_order": "bottom"
+}
 ```
 
 4. Do not show snippets in the completion list at all:
 
 ```json [settings]
-"snippet_sort_order": "none"
+{
+  "snippet_sort_order": "none"
+}
 ```
 
 ## Editor Scrollbar
@@ -811,19 +847,21 @@ List of `string` values
 - Default:
 
 ```json [settings]
-"scrollbar": {
-  "show": "auto",
-  "cursors": true,
-  "git_diff": true,
-  "search_results": true,
-  "selected_text": true,
-  "selected_symbol": true,
-  "diagnostics": "all",
-  "axes": {
-    "horizontal": true,
-    "vertical": true,
-  },
-},
+{
+  "scrollbar": {
+    "show": "auto",
+    "cursors": true,
+    "git_diff": true,
+    "search_results": true,
+    "selected_text": true,
+    "selected_symbol": true,
+    "diagnostics": "all",
+    "axes": {
+      "horizontal": true,
+      "vertical": true
+    }
+  }
+}
 ```
 
 ### Show Mode
@@ -837,32 +875,40 @@ List of `string` values
 1. Show the scrollbar if there's important information or follow the system's configured behavior:
 
 ```json [settings]
-"scrollbar": {
-  "show": "auto"
+{
+  "scrollbar": {
+    "show": "auto"
+  }
 }
 ```
 
 2. Match the system's configured behavior:
 
 ```json [settings]
-"scrollbar": {
-  "show": "system"
+{
+  "scrollbar": {
+    "show": "system"
+  }
 }
 ```
 
 3. Always show the scrollbar:
 
 ```json [settings]
-"scrollbar": {
-  "show": "always"
+{
+  "scrollbar": {
+    "show": "always"
+  }
 }
 ```
 
 4. Never show the scrollbar:
 
 ```json [settings]
-"scrollbar": {
-  "show": "never"
+{
+  "scrollbar": {
+    "show": "never"
+  }
 }
 ```
 
@@ -940,7 +986,9 @@ Diagnostic indicators appear as colored marks showing errors, warnings, and othe
 
 ```json [settings]
 {
-  "show_diagnostics": "all"
+  "scrollbar": {
+    "diagnostics": "all"
+  }
 }
 ```
 
@@ -948,7 +996,9 @@ Diagnostic indicators appear as colored marks showing errors, warnings, and othe
 
 ```json [settings]
 {
-  "show_diagnostics": "off"
+  "scrollbar": {
+    "diagnostics": "none"
+  }
 }
 ```
 
@@ -956,7 +1006,9 @@ Diagnostic indicators appear as colored marks showing errors, warnings, and othe
 
 ```json [settings]
 {
-  "show_diagnostics": "error"
+  "scrollbar": {
+    "diagnostics": "error"
+  }
 }
 ```
 
@@ -964,7 +1016,9 @@ Diagnostic indicators appear as colored marks showing errors, warnings, and othe
 
 ```json [settings]
 {
-  "show_diagnostics": "warning"
+  "scrollbar": {
+    "diagnostics": "warning"
+  }
 }
 ```
 
@@ -972,7 +1026,9 @@ Diagnostic indicators appear as colored marks showing errors, warnings, and othe
 
 ```json [settings]
 {
-  "show_diagnostics": "info"
+  "scrollbar": {
+    "diagnostics": "information"
+  }
 }
 ```
 
@@ -983,11 +1039,13 @@ Diagnostic indicators appear as colored marks showing errors, warnings, and othe
 - Default:
 
 ```json [settings]
-"scrollbar": {
-  "axes": {
-    "horizontal": true,
-    "vertical": true,
-  },
+{
+  "scrollbar": {
+    "axes": {
+      "horizontal": true,
+      "vertical": true
+    }
+  }
 }
 ```
 
@@ -1040,7 +1098,9 @@ Diagnostic indicators appear as colored marks showing errors, warnings, and othe
 
 ```json [settings]
 {
-  "show": "always"
+  "minimap": {
+    "show": "always"
+  }
 }
 ```
 
@@ -1048,7 +1108,9 @@ Diagnostic indicators appear as colored marks showing errors, warnings, and othe
 
 ```json [settings]
 {
-  "show": "auto"
+  "minimap": {
+    "show": "auto"
+  }
 }
 ```
 
@@ -1056,7 +1118,9 @@ Diagnostic indicators appear as colored marks showing errors, warnings, and othe
 
 ```json [settings]
 {
-  "show": "never"
+  "minimap": {
+    "show": "never"
+  }
 }
 ```
 
@@ -1072,7 +1136,9 @@ Diagnostic indicators appear as colored marks showing errors, warnings, and othe
 
 ```json [settings]
 {
-  "thumb": "hover"
+  "minimap": {
+    "thumb": "hover"
+  }
 }
 ```
 
@@ -1080,7 +1146,9 @@ Diagnostic indicators appear as colored marks showing errors, warnings, and othe
 
 ```json [settings]
 {
-  "thumb": "always"
+  "minimap": {
+    "thumb": "always"
+  }
 }
 ```
 
@@ -1096,7 +1164,9 @@ Diagnostic indicators appear as colored marks showing errors, warnings, and othe
 
 ```json [settings]
 {
-  "thumb_border": "full"
+  "minimap": {
+    "thumb_border": "full"
+  }
 }
 ```
 
@@ -1104,7 +1174,9 @@ Diagnostic indicators appear as colored marks showing errors, warnings, and othe
 
 ```json [settings]
 {
-  "thumb_border": "left_open"
+  "minimap": {
+    "thumb_border": "left_open"
+  }
 }
 ```
 
@@ -1112,7 +1184,9 @@ Diagnostic indicators appear as colored marks showing errors, warnings, and othe
 
 ```json [settings]
 {
-  "thumb_border": "right_open"
+  "minimap": {
+    "thumb_border": "right_open"
+  }
 }
 ```
 
@@ -1120,7 +1194,9 @@ Diagnostic indicators appear as colored marks showing errors, warnings, and othe
 
 ```json [settings]
 {
-  "thumb_border": "left_only"
+  "minimap": {
+    "thumb_border": "left_only"
+  }
 }
 ```
 
@@ -1128,7 +1204,9 @@ Diagnostic indicators appear as colored marks showing errors, warnings, and othe
 
 ```json [settings]
 {
-  "thumb_border": "none"
+  "minimap": {
+    "thumb_border": "none"
+  }
 }
 ```
 
@@ -1197,10 +1275,12 @@ or
 - Default:
 
 ```json [settings]
-"tab_bar": {
-  "show": true,
-  "show_nav_history_buttons": true,
-  "show_tab_bar_buttons": true
+{
+  "tab_bar": {
+    "show": true,
+    "show_nav_history_buttons": true,
+    "show_tab_bar_buttons": true
+  }
 }
 ```
 
@@ -1241,14 +1321,16 @@ or
 - Default:
 
 ```json [settings]
-"tabs": {
-  "close_position": "right",
-  "file_icons": false,
-  "git_status": false,
-  "activate_on_close": "history",
-  "show_close_button": "hover",
-  "show_diagnostics": "off"
-},
+{
+  "tabs": {
+    "close_position": "right",
+    "file_icons": false,
+    "git_status": false,
+    "activate_on_close": "history",
+    "show_close_button": "hover",
+    "show_diagnostics": "off"
+  }
+}
 ```
 
 ### Close Position
@@ -1263,7 +1345,9 @@ or
 
 ```json [settings]
 {
-  "close_position": "right"
+  "tabs": {
+    "close_position": "right"
+  }
 }
 ```
 
@@ -1271,7 +1355,9 @@ or
 
 ```json [settings]
 {
-  "close_position": "left"
+  "tabs": {
+    "close_position": "left"
+  }
 }
 ```
 
@@ -1299,7 +1385,9 @@ or
 
 ```json [settings]
 {
-  "activate_on_close": "history"
+  "tabs": {
+    "activate_on_close": "history"
+  }
 }
 ```
 
@@ -1307,7 +1395,9 @@ or
 
 ```json [settings]
 {
-  "activate_on_close": "neighbour"
+  "tabs": {
+    "activate_on_close": "neighbour"
+  }
 }
 ```
 
@@ -1315,7 +1405,9 @@ or
 
 ```json [settings]
 {
-  "activate_on_close": "left_neighbour"
+  "tabs": {
+    "activate_on_close": "left_neighbour"
+  }
 }
 ```
 
@@ -1331,7 +1423,9 @@ or
 
 ```json [settings]
 {
-  "show_close_button": "hover"
+  "tabs": {
+    "show_close_button": "hover"
+  }
 }
 ```
 
@@ -1339,7 +1433,9 @@ or
 
 ```json [settings]
 {
-  "show_close_button": "always"
+  "tabs": {
+    "show_close_button": "always"
+  }
 }
 ```
 
@@ -1347,7 +1443,9 @@ or
 
 ```json [settings]
 {
-  "show_close_button": "hidden"
+  "tabs": {
+    "show_close_button": "hidden"
+  }
 }
 ```
 
@@ -1363,7 +1461,9 @@ or
 
 ```json [settings]
 {
-  "show_diagnostics": "off"
+  "tabs": {
+    "show_diagnostics": "off"
+  }
 }
 ```
 
@@ -1371,7 +1471,9 @@ or
 
 ```json [settings]
 {
-  "show_diagnostics": "errors"
+  "tabs": {
+    "show_diagnostics": "errors"
+  }
 }
 ```
 
@@ -1379,7 +1481,9 @@ or
 
 ```json [settings]
 {
-  "show_diagnostics": "all"
+  "tabs": {
+    "show_diagnostics": "all"
+  }
 }
 ```
 
@@ -1441,9 +1545,11 @@ When trusted, project settings are synchronized automatically, language and MCP
 - Default:
 
 ```json [settings]
-"drag_and_drop_selection": {
-  "enabled": true,
-  "delay": 300
+{
+  "drag_and_drop_selection": {
+    "enabled": true,
+    "delay": 300
+  }
 }
 ```
 
@@ -1454,13 +1560,15 @@ When trusted, project settings are synchronized automatically, language and MCP
 - Default:
 
 ```json [settings]
-"toolbar": {
-  "breadcrumbs": true,
-  "quick_actions": true,
-  "selections_menu": true,
-  "agent_review": true,
-  "code_actions": false
-},
+{
+  "toolbar": {
+    "breadcrumbs": true,
+    "quick_actions": true,
+    "selections_menu": true,
+    "agent_review": true,
+    "code_actions": false
+  }
+}
 ```
 
 **Options**
@@ -1534,11 +1642,13 @@ Positive `integer` value between 1 and 32. Values outside of this range will be
 - Default:
 
 ```json [settings]
-"status_bar": {
-  "active_language_button": true,
-  "cursor_position_button": true,
-  "line_endings_button": false
-},
+{
+  "status_bar": {
+    "active_language_button": true,
+    "cursor_position_button": true,
+    "line_endings_button": false
+  }
+}
 ```
 
 There is an experimental setting that completely hides the status bar. This causes major usability problems (you will be unable to use many of Zed's features), but is provided for those who value screen real-estate above all else.
@@ -1569,11 +1679,13 @@ Some options are passed via `initialization_options` to the language server. The
 For example to pass the `check` option to `rust-analyzer`, use the following configuration:
 
 ```json [settings]
-"lsp": {
-  "rust-analyzer": {
-    "initialization_options": {
-      "check": {
-        "command": "clippy" // rust-analyzer.check.command (default: "check")
+{
+  "lsp": {
+    "rust-analyzer": {
+      "initialization_options": {
+        "check": {
+          "command": "clippy" // rust-analyzer.check.command (default: "check")
+        }
       }
     }
   }
@@ -1583,11 +1695,13 @@ For example to pass the `check` option to `rust-analyzer`, use the following con
 While other options may be changed at a runtime and should be placed under `settings`:
 
 ```json [settings]
-"lsp": {
-  "yaml-language-server": {
-    "settings": {
-      "yaml": {
-        "keyOrdering": true // Enforces alphabetical ordering of keys in maps
+{
+  "lsp": {
+    "yaml-language-server": {
+      "settings": {
+        "yaml": {
+          "keyOrdering": true // Enforces alphabetical ordering of keys in maps
+        }
       }
     }
   }
@@ -1639,8 +1753,8 @@ While other options may be changed at a runtime and should be placed under `sett
 
 ```json [settings]
 {
-  "features": {
-    "edit_prediction_provider": "zed"
+  "edit_predictions": {
+    "provider": "zed"
   }
 }
 ```
@@ -1657,8 +1771,8 @@ While other options may be changed at a runtime and should be placed under `sett
 
 ```json [settings]
 {
-  "features": {
-    "edit_prediction_provider": "zed"
+  "edit_predictions": {
+    "provider": "zed"
   }
 }
 ```
@@ -1667,8 +1781,8 @@ While other options may be changed at a runtime and should be placed under `sett
 
 ```json [settings]
 {
-  "features": {
-    "edit_prediction_provider": "copilot"
+  "edit_predictions": {
+    "provider": "copilot"
   }
 }
 ```
@@ -1677,8 +1791,8 @@ While other options may be changed at a runtime and should be placed under `sett
 
 ```json [settings]
 {
-  "features": {
-    "edit_prediction_provider": "supermaven"
+  "edit_predictions": {
+    "provider": "supermaven"
   }
 }
 ```
@@ -1687,8 +1801,8 @@ While other options may be changed at a runtime and should be placed under `sett
 
 ```json [settings]
 {
-  "features": {
-    "edit_prediction_provider": "none"
+  "edit_predictions": {
+    "provider": "none"
   }
 }
 ```
@@ -1828,17 +1942,19 @@ The result is still `)))` and not `))))))`, which is what it would be by default
 - Default:
 
 ```json [settings]
-"file_scan_exclusions": [
-  "**/.git",
-  "**/.svn",
-  "**/.hg",
-  "**/.jj",
-  "**/CVS",
-  "**/.DS_Store",
-  "**/Thumbs.db",
-  "**/.classpath",
-  "**/.settings"
-],
+{
+  "file_scan_exclusions": [
+    "**/.git",
+    "**/.svn",
+    "**/.hg",
+    "**/.jj",
+    "**/CVS",
+    "**/.DS_Store",
+    "**/Thumbs.db",
+    "**/.classpath",
+    "**/.settings"
+  ]
+}
 ```
 
 Note, specifying `file_scan_exclusions` in settings.json will override the defaults (shown above). If you are looking to exclude additional items you will need to include all the default values in your settings.
@@ -1850,7 +1966,9 @@ Note, specifying `file_scan_exclusions` in settings.json will override the defau
 - Default:
 
 ```json [settings]
-"file_scan_inclusions": [".env*"],
+{
+  "file_scan_inclusions": [".env*"]
+}
 ```
 
 ## File Types
@@ -1860,9 +1978,16 @@ Note, specifying `file_scan_exclusions` in settings.json will override the defau
 - Default:
 
 ```json [settings]
-"file_types": {
-  "JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"],
-  "Shell Script": [".env.*"]
+{
+  "file_types": {
+    "JSONC": [
+      "**/.zed/**/*.json",
+      "**/zed/**/*.json",
+      "**/Zed/**/*.json",
+      "**/.vscode/**/*.json"
+    ],
+    "Shell Script": [".env.*"]
+  }
 }
 ```
 
@@ -1892,10 +2017,7 @@ To interpret all `.c` files as C++, files called `MyLockFile` as TOML and files
     "include_warnings": true,
     "inline": {
       "enabled": false
-    },
-    "update_with_cursor": false,
-    "primary_only": false,
-    "use_rendered": false
+    }
   }
 }
 ```
@@ -2345,11 +2467,13 @@ Example:
 - Default:
 
 ```json [settings]
-"icon_theme": {
-  "mode": "system",
-  "dark": "Zed (Default)",
-  "light": "Zed (Default)"
-},
+{
+  "icon_theme": {
+    "mode": "system",
+    "dark": "Zed (Default)",
+    "light": "Zed (Default)"
+  }
+}
 ```
 
 ### Mode
@@ -2364,7 +2488,11 @@ Example:
 
 ```json [settings]
 {
-  "mode": "dark"
+  "icon_theme": {
+    "mode": "dark",
+    "dark": "Zed (Default)",
+    "light": "Zed (Default)"
+  }
 }
 ```
 
@@ -2372,7 +2500,11 @@ Example:
 
 ```json [settings]
 {
-  "mode": "light"
+  "icon_theme": {
+    "mode": "light",
+    "dark": "Zed (Default)",
+    "light": "Zed (Default)"
+  }
 }
 ```
 
@@ -2380,7 +2512,11 @@ Example:
 
 ```json [settings]
 {
-  "mode": "system"
+  "icon_theme": {
+    "mode": "system",
+    "dark": "Zed (Default)",
+    "light": "Zed (Default)"
+  }
 }
 ```
 
@@ -2455,15 +2591,17 @@ Run the {#action icon_theme_selector::Toggle} action in the command palette to s
 - Default:
 
 ```json [settings]
-"inlay_hints": {
-  "enabled": false,
-  "show_type_hints": true,
-  "show_parameter_hints": true,
-  "show_other_hints": true,
-  "show_background": false,
-  "edit_debounce_ms": 700,
-  "scroll_debounce_ms": 50,
-  "toggle_on_modifiers_press": null
+{
+  "inlay_hints": {
+    "enabled": false,
+    "show_type_hints": true,
+    "show_parameter_hints": true,
+    "show_other_hints": true,
+    "show_background": false,
+    "edit_debounce_ms": 700,
+    "scroll_debounce_ms": 50,
+    "toggle_on_modifiers_press": null
+  }
 }
 ```
 
@@ -2488,13 +2626,15 @@ Settings-related hint updates are not debounced.
 All possible config values for `toggle_on_modifiers_press` are:
 
 ```json [settings]
-"inlay_hints": {
-  "toggle_on_modifiers_press": {
-    "control": true,
-    "shift": true,
-    "alt": true,
-    "platform": true,
-    "function": true
+{
+  "inlay_hints": {
+    "toggle_on_modifiers_press": {
+      "control": true,
+      "shift": true,
+      "alt": true,
+      "platform": true,
+      "function": true
+    }
   }
 }
 ```
@@ -2508,11 +2648,12 @@ Unspecified values have a `false` value, hints won't be toggled if all the modif
 - Default:
 
 ```json [settings]
-"journal": {
-  "path": "~",
-  "hour_format": "hour12"
+{
+  "journal": {
+    "path": "~",
+    "hour_format": "hour12"
+  }
 }
-
 ```
 
 ### Path
@@ -2537,7 +2678,9 @@ Unspecified values have a `false` value, hints won't be toggled if all the modif
 
 ```json [settings]
 {
-  "hour_format": "hour12"
+  "journal": {
+    "hour_format": "hour12"
+  }
 }
 ```
 
@@ -2545,7 +2688,9 @@ Unspecified values have a `false` value, hints won't be toggled if all the modif
 
 ```json [settings]
 {
-  "hour_format": "hour24"
+  "journal": {
+    "hour_format": "hour24"
+  }
 }
 ```
 
@@ -2578,14 +2723,16 @@ Unspecified values have a `false` value, hints won't be toggled if all the modif
 To override settings for a language, add an entry for that languages name to the `languages` value. Example:
 
 ```json [settings]
-"languages": {
-  "C": {
-    "format_on_save": "off",
-    "preferred_line_length": 64,
-    "soft_wrap": "preferred_line_length"
-  },
-  "JSON": {
-    "tab_size": 4
+{
+  "languages": {
+    "C": {
+      "format_on_save": "off",
+      "preferred_line_length": 64,
+      "soft_wrap": "preferred_line_length"
+    },
+    "JSON": {
+      "tab_size": 4
+    }
   }
 }
 ```

docs/src/snippets.md ๐Ÿ”—

@@ -11,7 +11,7 @@ The snippets are located in `~/.config/zed/snippets` directory to which you can
 
 ## Example configuration
 
-```json [settings]
+```json
 {
   // Each snippet must have a name and body, but the prefix and description are optional.
   // The prefix is used to trigger the snippet, but when omitted then the name is used.

docs/src/tasks.md ๐Ÿ”—

@@ -94,7 +94,7 @@ These variables allow you to pull information from the current editor and use it
 
 To use a variable in a task, prefix it with a dollar sign (`$`):
 
-```json [settings]
+```json [tasks]
 {
   "label": "echo current file's path",
   "command": "echo $ZED_FILE"
@@ -111,7 +111,7 @@ When working with paths containing spaces or other special characters, please en
 
 For example, instead of this (which will fail if the path has a space):
 
-```json [settings]
+```json [tasks]
 {
   "label": "stat current file",
   "command": "stat $ZED_FILE"
@@ -120,7 +120,7 @@ For example, instead of this (which will fail if the path has a space):
 
 Provide the following:
 
-```json [settings]
+```json [tasks]
 {
   "label": "stat current file",
   "command": "stat",
@@ -130,7 +130,7 @@ Provide the following:
 
 Or explicitly include escaped quotes like so:
 
-```json [settings]
+```json [tasks]
 {
   "label": "stat current file",
   "command": "stat \"$ZED_FILE\""
@@ -142,7 +142,7 @@ Or explicitly include escaped quotes like so:
 Task definitions with variables which are not present at the moment the task list is determined are filtered out.
 For example, the following task will appear in the spawn modal only if there is a text selection:
 
-```json [settings]
+```json [tasks]
 {
   "label": "selected text",
   "command": "echo \"$ZED_SELECTED_TEXT\""
@@ -151,7 +151,7 @@ For example, the following task will appear in the spawn modal only if there is
 
 Set default values to such variables to have such tasks always displayed:
 
-```json [settings]
+```json [tasks]
 {
   "label": "selected text with default",
   "command": "echo \"${ZED_SELECTED_TEXT:no text selected}\""
@@ -233,7 +233,7 @@ Zed supports overriding the default action for inline runnable indicators via wo
 
 To tag a task, add the runnable tag name to the `tags` field on the task template:
 
-```json [settings]
+```json [tasks]
 {
   "label": "echo current file's path",
   "command": "echo $ZED_FILE",

docs/src/vim.md ๐Ÿ”—

@@ -216,7 +216,7 @@ These text objects implement the behavior of the [mini.ai](https://github.com/ec
 
 To use these text objects, you need to add bindings to your keymap. Here's an example configuration that makes them available when using text object operators (`i` and `a`) or change-surrounds (`cs`):
 
-```json [settings]
+```json [keymap]
 {
   "context": "vim_operator == a || vim_operator == i || vim_operator == cs",
   "bindings": {
@@ -368,11 +368,11 @@ As any Zed command is available, you may find that it's helpful to remember mnem
 
 Zed's key bindings are evaluated only when the `"context"` property matches your location in the editor. For example, if you add key bindings to the `"Editor"` context, they will only work when you're editing a file. If you add key bindings to the `"Workspace"` context, they will work everywhere in Zed. Here's an example of a key binding that saves when you're editing a file:
 
-```json [settings]
+```json [keymap]
 {
   "context": "Editor",
   "bindings": {
-    "ctrl-s": "file::Save"
+    "ctrl-s": "workspace::Save"
   }
 }
 ```
@@ -449,7 +449,7 @@ By default, you can navigate between the different files open in the editor with
 
 But you cannot use the same shortcuts to move between all the editor docks (the terminal, project panel, assistant panel, ...). If you want to use the same shortcuts to navigate to the docks, you can add the following key bindings to your user keymap.
 
-```json [settings]
+```json [keymap]
 {
   "context": "Dock",
   "bindings": {
@@ -464,7 +464,7 @@ But you cannot use the same shortcuts to move between all the editor docks (the
 
 Subword motion, which allows you to navigate and select individual words in `camelCase` or `snake_case`, is not enabled by default. To enable it, add these bindings to your keymap.
 
-```json [settings]
+```json [keymap]
 {
   "context": "VimControl && !menu && vim_mode != operator",
   "bindings": {
@@ -481,7 +481,7 @@ Subword motion, which allows you to navigate and select individual words in `cam
 
 Vim mode comes with shortcuts to surround the selection in normal mode (`ys`), but it doesn't have a shortcut to add surrounds in visual mode. By default, `shift-s` substitutes the selection (erases the text and enters insert mode). To use `shift-s` to add surrounds in visual mode, you can add the following object to your keymap.
 
-```json [settings]
+```json [keymap]
 {
   "context": "vim_mode == visual",
   "bindings": {
@@ -492,7 +492,7 @@ Vim mode comes with shortcuts to surround the selection in normal mode (`ys`), b
 
 In non-modal text editors, cursor navigation typically wraps when moving past line ends. Zed, however, handles this behavior exactly like Vim by default: the cursor stops at line boundaries. If you prefer your cursor to wrap between lines, override these keybindings:
 
-```json [settings]
+```json [keymap]
 // In VimScript, this would look like this:
 // set whichwrap+=<,>,[,],h,l
 {
@@ -508,7 +508,7 @@ In non-modal text editors, cursor navigation typically wraps when moving past li
 
 The [Sneak motion](https://github.com/justinmk/vim-sneak) feature allows for quick navigation to any two-character sequence in your text. You can enable it by adding the following keybindings to your keymap. By default, the `s` key is mapped to `vim::Substitute`. Adding these bindings will override that behavior, so ensure this change aligns with your workflow preferences.
 
-```json [settings]
+```json [keymap]
 {
   "context": "vim_mode == normal || vim_mode == visual",
   "bindings": {
@@ -520,7 +520,7 @@ The [Sneak motion](https://github.com/justinmk/vim-sneak) feature allows for qui
 
 The [vim-exchange](https://github.com/tommcdo/vim-exchange) feature does not have a default binding for visual mode, as the `shift-x` binding conflicts with the default `shift-x` binding for visual mode (`vim::VisualDeleteLine`). To assign the default vim-exchange binding, add the following keybinding to your keymap:
 
-```json [settings]
+```json [keymap]
 {
   "context": "vim_mode == visual",
   "bindings": {
@@ -587,7 +587,7 @@ Here's an example of these settings changed:
     "use_system_clipboard": "never",
     "use_smartcase_find": true,
     "gdefault": true,
-    "relative_line_numbers": "enabled",
+    "toggle_relative_line_numbers": true,
     "highlight_on_yank_duration": 50,
     "custom_digraphs": {
       "fz": "๐ŸงŸโ€โ™€๏ธ"

docs/src/visual-customization.md ๐Ÿ”—

@@ -483,13 +483,13 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k
 ## Agent Panel
 
 ```json [settings]
+{
   "agent": {
-    "version": "2",
-    "enabled": true,        // Enable/disable the agent
-    "button": true,         // Show/hide the icon in the status bar
-    "dock": "right",        // Where to dock: left, right, bottom
-    "default_width": 640,   // Default width (left/right docked)
-    "default_height": 320,  // Default height (bottom docked)
+    "enabled": true, // Enable/disable the agent
+    "button": true, // Show/hide the icon in the status bar
+    "dock": "right", // Where to dock: left, right, bottom
+    "default_width": 640, // Default width (left/right docked)
+    "default_height": 320 // Default height (bottom docked)
   },
   // Controls the font size for agent responses in the agent panel.
   // If not specified, it falls back to the UI font size.
@@ -497,6 +497,7 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k
   // Controls the font size for the agent panel's message editor, user message,
   // and any other snippet of code.
   "agent_buffer_font_size": 12
+}
 ```
 
 See [Zed AI Documentation](./ai/overview.md) for additional non-visual AI settings.